Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 29 additions & 9 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import (
"fmt"
"io"
"log/slog"
"path/filepath"
"runtime"
"strings"
"sync"

"github.com/fogfish/logger/v3/internal/trie"
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}

Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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 {
Expand Down
79 changes: 79 additions & 0 deletions logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
2 changes: 1 addition & 1 deletion options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Loading