diff --git a/handler.go b/handler.go index 3710c06..1185306 100644 --- a/handler.go +++ b/handler.go @@ -15,8 +15,8 @@ import ( "fmt" "io" "log/slog" + "path/filepath" "runtime" - "strings" "sync" "github.com/fogfish/logger/v3/internal/trie" @@ -55,7 +55,11 @@ type modTrieHandler struct { trie *trie.Node } -func (h *modTrieHandler) Enabled(context.Context, slog.Level) bool { return true } +func (h *modTrieHandler) Enabled(ctx context.Context, level slog.Level) bool { + // For module-based filtering, we need to allow the handler to process the record + // and make the filtering decision in Handle() where we have access to the source path + return true +} func (h *modTrieHandler) Handle(ctx context.Context, r slog.Record) error { if r.PC == 0 { @@ -65,15 +69,21 @@ func (h *modTrieHandler) Handle(ctx context.Context, r slog.Record) error { fs := runtime.CallersFrames([]uintptr{r.PC}) f, _ := fs.Next() - parts := strings.Split(f.File, "go/src/") - path := parts[0] - if len(parts) > 1 { - path = parts[1] - } - + path := filepath.Dir(f.Function) _, n := h.trie.Lookup(path) - if len(n.Path) != 0 && n.Level <= r.Level { + // If a specific module rule is found, use that rule + if len(n.Path) != 0 { + if n.Level <= r.Level { + return h.Handler.Handle(ctx, r) + } + // Module rule found but level doesn't match, don't log + return nil + } + + // No specific module rule found, fall back to default handler behavior + // But we need to check if the underlying handler would accept this level + if h.Handler.Enabled(ctx, r.Level) { return h.Handler.Handle(ctx, r) } @@ -135,6 +145,11 @@ func (h *stdioHandler) Handle(ctx context.Context, r slog.Record) error { return err } + // If attrs is nil, it means the message was filtered out + if attrs == nil { + return nil + } + time := attrs["time"] level := attrs["level"] @@ -176,6 +191,11 @@ func (h *stdioHandler) computeAttrs( return nil, err } + // If buffer is empty, it means the message was filtered out by modTrieHandler + if h.b.Len() == 0 { + return nil, nil + } + var attrs map[string]any err := json.Unmarshal(h.b.Bytes(), &attrs) if err != nil { diff --git a/logger_test.go b/logger_test.go index e8aaf86..49beaf0 100644 --- a/logger_test.go +++ b/logger_test.go @@ -128,3 +128,82 @@ func TestJSONLogger(t *testing.T) { } }) } + +func TestStdioLoggerWithTrie(t *testing.T) { + b := &bytes.Buffer{} + log := slog.New(NewStdioHandler( + WithWriter(b), + WithLogLevel(INFO), // Default level is INFO + WithLogLevelForMod(map[string]slog.Level{ + "github.com/fogfish/logger": ERROR, // Only allow ERROR and above for this specific module + }), + )) + + t.Run("FilteredByModuleRule", func(t *testing.T) { + defer b.Reset() + + // This should be filtered out because logger module has ERROR level requirement + log.Info("this info message should be filtered by module rule") + txt := b.String() + // Buffer should be empty since message was filtered + if txt != "" { + t.Errorf("expected empty buffer, got: %s", txt) + } + }) + + t.Run("AllowedByModuleRule", func(t *testing.T) { + defer b.Reset() + + // This should pass through because it meets the module's ERROR requirement + log.Error("this error message should appear due to module rule") + txt := b.String() + if !strings.Contains(txt, "ERR") || + !strings.Contains(txt, "this error message should appear due to module rule") { + t.Errorf("unexpected log line %s", txt) + } + }) + + t.Run("DefaultBehaviorForUnspecifiedModule", func(t *testing.T) { + defer b.Reset() + + // Test needs to simulate logging from a different module path + // Since we can't easily change the source path in tests, let's create + // a logger that doesn't match any trie rules + logOther := slog.New(NewStdioHandler( + WithWriter(b), + WithLogLevel(INFO), // Default level is INFO + WithLogLevelForMod(map[string]slog.Level{ + "some/other/module": ERROR, // Rule for a different module + }), + )) + + // This should pass through because no specific rule exists and it meets default INFO level + logOther.Info("this info message should appear due to default level") + txt := b.String() + if !strings.Contains(txt, "INF") || + !strings.Contains(txt, "this info message should appear due to default level") { + t.Errorf("expected info message to appear with default behavior, got: %s", txt) + } + }) + + t.Run("FilteredByDefaultLevel", func(t *testing.T) { + defer b.Reset() + + // Test with a logger that has higher default level + logStrict := slog.New(NewStdioHandler( + WithWriter(b), + WithLogLevel(ERROR), // Default level is ERROR + WithLogLevelForMod(map[string]slog.Level{ + "some/other/module": DEBUG, // Rule for a different module (not this one) + }), + )) + + // This should be filtered out because it doesn't meet default ERROR level + logStrict.Info("this info message should be filtered by default level") + txt := b.String() + // Buffer should be empty since message was filtered by default level + if txt != "" { + t.Errorf("expected empty buffer due to default level filtering, got: %s", txt) + } + }) +} diff --git a/options.go b/options.go index 9acecfc..ae6f1ac 100644 --- a/options.go +++ b/options.go @@ -22,7 +22,7 @@ var ( Console = []Option{ func(o *opts) { o.attributes = append(o.attributes, attrLogLevel7Shorten(true)) }, WithTimeFormat("[15:04:05.000]"), - WithLogLevel(INFO), + WithLogLevel(NOTICE), WithLogLevelFromEnv(), WithSourceShorten(), WithLogLevelForModFromEnv(),