From 533188f1cf8acb1ae5a000b1f28acd7932a6886d Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Sun, 31 Aug 2025 19:42:02 +0300 Subject: [PATCH 1/3] fallback to default logger if mod-level rule is undefined --- handler.go | 30 +++++++++++++++++-- logger_test.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++ options.go | 2 +- 3 files changed, 108 insertions(+), 3 deletions(-) diff --git a/handler.go b/handler.go index 3710c06..fe8e987 100644 --- a/handler.go +++ b/handler.go @@ -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,6 +69,7 @@ func (h *modTrieHandler) Handle(ctx context.Context, r slog.Record) error { fs := runtime.CallersFrames([]uintptr{r.PC}) f, _ := fs.Next() + // TODO: "go/pkg/mod" parts := strings.Split(f.File, "go/src/") path := parts[0] if len(parts) > 1 { @@ -73,7 +78,18 @@ func (h *modTrieHandler) Handle(ctx context.Context, r slog.Record) error { _, 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 +151,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 +197,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(), From b610b9b403f0dcfcfb91a86967ad78a6749ea059 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Sun, 31 Aug 2025 19:51:53 +0300 Subject: [PATCH 2/3] debug github action --- handler.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/handler.go b/handler.go index fe8e987..e8f70a5 100644 --- a/handler.go +++ b/handler.go @@ -69,8 +69,12 @@ func (h *modTrieHandler) Handle(ctx context.Context, r slog.Record) error { fs := runtime.CallersFrames([]uintptr{r.PC}) f, _ := fs.Next() + fmt.Printf("==> %s\n", f.File) + + // .hm.rnnr.wrk.lggr.lggr/logger_test.go // TODO: "go/pkg/mod" parts := strings.Split(f.File, "go/src/") + fmt.Printf(" %v\n", parts) path := parts[0] if len(parts) > 1 { path = parts[1] From 0ba043821a4684087ee6a12b18a422c418778c33 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Sun, 31 Aug 2025 19:56:05 +0300 Subject: [PATCH 3/3] use function name in the call frame to deduct module --- handler.go | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/handler.go b/handler.go index e8f70a5..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" @@ -69,17 +69,7 @@ func (h *modTrieHandler) Handle(ctx context.Context, r slog.Record) error { fs := runtime.CallersFrames([]uintptr{r.PC}) f, _ := fs.Next() - fmt.Printf("==> %s\n", f.File) - - // .hm.rnnr.wrk.lggr.lggr/logger_test.go - // TODO: "go/pkg/mod" - parts := strings.Split(f.File, "go/src/") - fmt.Printf(" %v\n", parts) - path := parts[0] - if len(parts) > 1 { - path = parts[1] - } - + path := filepath.Dir(f.Function) _, n := h.trie.Lookup(path) // If a specific module rule is found, use that rule