From 30bbae2990d32e367e1d5bd5b01b0180b9d45b4e Mon Sep 17 00:00:00 2001 From: Samuel Berthe Date: Mon, 1 Sep 2025 15:28:34 +0200 Subject: [PATCH 1/2] feat: adding some router predicates --- README.md | 2 +- router.go | 48 +++++++-------- router_predicate.go | 143 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+), 25 deletions(-) create mode 100644 router_predicate.go diff --git a/README.md b/README.md index 40849d8..b878fbc 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ func main() { Add(slackChannelUS, recordMatchRegion("us")). Add(slackChannelEU, recordMatchRegion("eu")). Add(slackChannelAPAC, recordMatchRegion("apac")). - Add(consoleHandler, slogmulti.Level(slog.LevelInfo)). + Add(consoleHandler, slogmulti.LevelIs(slog.LevelInfo, slog.LevelDebug)). Handler(), ) diff --git a/router.go b/router.go index e2a1d0b..86cfe10 100644 --- a/router.go +++ b/router.go @@ -19,8 +19,8 @@ type router struct { // Example usage: // // r := slogmulti.Router(). -// Add(consoleHandler, slogmulti.Level(slog.LevelInfo)). -// Add(fileHandler, slogmulti.Level(slog.LevelError)). +// Add(consoleHandler, slogmulti.MatchLevel(slog.LevelInfo)). +// Add(fileHandler, slogmulti.MatchLevel(slog.LevelError)). // Handler() // // Returns: @@ -32,26 +32,26 @@ func Router() *router { } } -// Add registers a new handler with optional matchers to the router. -// The handler will only process records if all provided matchers return true. +// Add registers a new handler with optional predicates to the router. +// The handler will only process records if all provided predicates return true. // // Args: // // handler: The slog.Handler to register -// matchers: Optional functions that determine if a record should be routed to this handler +// predicates: Optional functions that determine if a record should be routed to this handler // // Returns: // // The router instance for method chaining -func (h *router) Add(handler slog.Handler, matchers ...func(ctx context.Context, r slog.Record) bool) *router { +func (h *router) Add(handler slog.Handler, predicates ...func(ctx context.Context, r slog.Record) bool) *router { return &router{ handlers: append( h.handlers, &RoutableHandler{ - matchers: matchers, - handler: handler, - groups: []string{}, - attrs: []slog.Attr{}, + predicates: predicates, + handler: handler, + groups: []string{}, + attrs: []slog.Attr{}, }, ), } @@ -72,13 +72,13 @@ func (h *router) Handler() slog.Handler { var _ slog.Handler = (*RoutableHandler)(nil) // RoutableHandler wraps a slog.Handler with conditional matching logic. -// It only forwards records to the underlying handler if all matchers return true. +// It only forwards records to the underlying handler if all predicates return true. // This enables sophisticated routing scenarios like level-based or attribute-based routing. // // @TODO: implement round robin strategy for load balancing across multiple handlers type RoutableHandler struct { - // matchers contains functions that determine if a record should be processed - matchers []func(ctx context.Context, r slog.Record) bool + // predicates contains functions that determine if a record should be processed + predicates []func(ctx context.Context, r slog.Record) bool // handler is the underlying slog.Handler that processes matching records handler slog.Handler // groups tracks the current group hierarchy for proper attribute handling @@ -102,7 +102,7 @@ func (h *RoutableHandler) Enabled(ctx context.Context, l slog.Level) bool { return h.handler.Enabled(ctx, l) } -// Handle processes a log record if all matchers return true. +// Handle processes a log record if all predicates return true. // This method implements the slog.Handler interface requirement. // // Args: @@ -119,8 +119,8 @@ func (h *RoutableHandler) Handle(ctx context.Context, r slog.Record) error { slogcommon.AppendRecordAttrsToAttrs(h.attrs, h.groups, &r)..., ) - for _, matcher := range h.matchers { - if !matcher(ctx, clone) { + for _, predicate := range h.predicates { + if !predicate(ctx, clone) { return nil } } @@ -143,10 +143,10 @@ func (h *RoutableHandler) Handle(ctx context.Context, r slog.Record) error { // A new RoutableHandler with the additional attributes func (h *RoutableHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return &RoutableHandler{ - matchers: h.matchers, - handler: h.handler.WithAttrs(attrs), - groups: slices.Clone(h.groups), - attrs: slogcommon.AppendAttrsToGroup(h.groups, h.attrs, attrs...), + predicates: h.predicates, + handler: h.handler.WithAttrs(attrs), + groups: slices.Clone(h.groups), + attrs: slogcommon.AppendAttrsToGroup(h.groups, h.attrs, attrs...), } } @@ -171,9 +171,9 @@ func (h *RoutableHandler) WithGroup(name string) slog.Handler { } return &RoutableHandler{ - matchers: h.matchers, - handler: h.handler.WithGroup(name), - groups: append(slices.Clone(h.groups), name), - attrs: h.attrs, + predicates: h.predicates, + handler: h.handler.WithGroup(name), + groups: append(slices.Clone(h.groups), name), + attrs: h.attrs, } } diff --git a/router_predicate.go b/router_predicate.go new file mode 100644 index 0000000..93df86d --- /dev/null +++ b/router_predicate.go @@ -0,0 +1,143 @@ +package slogmulti + +import ( + "context" + "log/slog" + "strings" +) + +// LevelIs returns a function that checks if the record level is in the given levels. +// Example usage: +// +// r := slogmulti.Router(). +// Add(consoleHandler, slogmulti.LevelIs(slog.LevelInfo)). +// Add(fileHandler, slogmulti.LevelIs(slog.LevelError)). +// Handler() +// +// Args: +// +// levels: The levels to match +// +// Returns: +// +// A function that checks if the record level is in the given levels +func LevelIs(levels ...slog.Level) func(ctx context.Context, r slog.Record) bool { + return func(ctx context.Context, r slog.Record) bool { + for _, level := range levels { + if r.Level == level { + return true + } + } + return false + } +} + +// LevelIsNot returns a function that checks if the record level is not in the given levels. +// Example usage: +// +// r := slogmulti.Router(). +// Add(consoleHandler, slogmulti.LevelIsNot(slog.LevelInfo)). +// Add(fileHandler, slogmulti.LevelIsNot(slog.LevelError)). +// Handler() +// +// Args: +// +// levels: The levels to check +// +// Returns: +// +// A function that checks if the record level is not in the given levels +func LevelIsNot(levels ...slog.Level) func(ctx context.Context, r slog.Record) bool { + return func(ctx context.Context, r slog.Record) bool { + for _, level := range levels { + if r.Level == level { + return false + } + } + return true + } +} + +// MessageIs returns a function that checks if the record message is equal to the given message. +// Example usage: +// +// r := slogmulti.Router(). +// Add(consoleHandler, slogmulti.MessageIs("database error")). +// Add(fileHandler, slogmulti.MessageIs("database error")). +// Handler() +// +// Args: +// +// msg: The message to check +// +// Returns: +// +// A function that checks if the record message is equal to the given message +func MessageIs(msg string) func(ctx context.Context, r slog.Record) bool { + return func(ctx context.Context, r slog.Record) bool { + return r.Message == msg + } +} + +// MessageIsNot returns a function that checks if the record message is not equal to the given message. +// Example usage: +// +// r := slogmulti.Router(). +// Add(consoleHandler, slogmulti.MessageIsNot("database error")). +// Add(fileHandler, slogmulti.MessageIsNot("database error")). +// Handler() +// +// Args: +// +// msg: The message to check +// +// Returns: +// +// A function that checks if the record message is not equal to the given message +func MessageIsNot(msg string) func(ctx context.Context, r slog.Record) bool { + return func(ctx context.Context, r slog.Record) bool { + return r.Message != msg + } +} + +// MessageContains returns a function that checks if the record message contains the given part. +// Example usage: +// +// r := slogmulti.Router(). +// Add(consoleHandler, slogmulti.MessageContains("database error")). +// Add(fileHandler, slogmulti.MessageContains("database error")). +// Handler() +// +// Args: +// +// part: The part to check +// +// Returns: +// +// A function that checks if the record message contains the given part +func MessageContains(part string) func(ctx context.Context, r slog.Record) bool { + return func(ctx context.Context, r slog.Record) bool { + return strings.Contains(r.Message, part) + } +} + +// MessageNotContains returns a function that checks if the record message does not contain the given part. +// Example usage: +// +// r := slogmulti.Router(). +// Add(consoleHandler, slogmulti.MessageNotContains("database error")). +// Add(fileHandler, slogmulti.MessageNotContains("database error")). +// Handler() +// +// Args: +// +// part: The part to check +// +// Returns: +// +// A function that checks if the record message does not contain the given part +func MessageNotContains(part string) func(ctx context.Context, r slog.Record) bool { + return func(ctx context.Context, r slog.Record) bool { + return !strings.Contains(r.Message, part) + } +} From cac1e0d55e2c451a6e88b7dee4ec5da2e3a38e5a Mon Sep 17 00:00:00 2001 From: Samuel Berthe Date: Thu, 4 Sep 2025 10:39:04 +0200 Subject: [PATCH 2/2] Update router.go --- router.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/router.go b/router.go index 86cfe10..d655164 100644 --- a/router.go +++ b/router.go @@ -19,8 +19,8 @@ type router struct { // Example usage: // // r := slogmulti.Router(). -// Add(consoleHandler, slogmulti.MatchLevel(slog.LevelInfo)). -// Add(fileHandler, slogmulti.MatchLevel(slog.LevelError)). +// Add(consoleHandler, slogmulti.LevelIs(slog.LevelInfo)). +// Add(fileHandler, slogmulti.LevelIs(slog.LevelError)). // Handler() // // Returns: