pax_global_header 0000666 0000000 0000000 00000000064 15140164271 0014513 g ustar 00root root 0000000 0000000 52 comment=3aa309b63ba6ea3d9ae1db696ea4a78787cd1b8b slog-multi-1.7.1/ 0000775 0000000 0000000 00000000000 15140164271 0013615 5 ustar 00root root 0000000 0000000 slog-multi-1.7.1/.github/ 0000775 0000000 0000000 00000000000 15140164271 0015155 5 ustar 00root root 0000000 0000000 slog-multi-1.7.1/.github/FUNDING.yml 0000664 0000000 0000000 00000000021 15140164271 0016763 0 ustar 00root root 0000000 0000000 github: [samber] slog-multi-1.7.1/.github/dependabot.yml 0000664 0000000 0000000 00000000305 15140164271 0020003 0 ustar 00root root 0000000 0000000 version: 2 updates: - package-ecosystem: github-actions directory: / schedule: interval: monthly - package-ecosystem: gomod directory: / schedule: interval: monthly slog-multi-1.7.1/.github/workflows/ 0000775 0000000 0000000 00000000000 15140164271 0017212 5 ustar 00root root 0000000 0000000 slog-multi-1.7.1/.github/workflows/lint.yml 0000664 0000000 0000000 00000000676 15140164271 0020714 0 ustar 00root root 0000000 0000000 name: Lint on: push: pull_request: jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/setup-go@v6 with: go-version: 1.21 stable: false - uses: actions/checkout@v6 - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: args: --timeout 120s --max-same-issues 50 - name: Bearer uses: bearer/bearer-action@v2 slog-multi-1.7.1/.github/workflows/release.yml 0000664 0000000 0000000 00000002521 15140164271 0021355 0 ustar 00root root 0000000 0000000 name: Release on: workflow_dispatch: inputs: semver: type: string description: 'Semver (eg: v1.2.3)' required: true jobs: release: if: github.triggering_actor == 'samber' runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 with: go-version: 1.21 stable: false - name: Test run: make test # remove tests in order to clean dependencies - name: Remove xxx_test.go files run: rm -rf *_test.go ./examples ./images # cleanup test dependencies - name: Cleanup dependencies run: go mod tidy - name: List files run: tree -Cfi - name: Write new go.mod into logs run: cat go.mod - name: Write new go.sum into logs run: cat go.sum - name: Create tag run: | git config --global user.name '${{ github.triggering_actor }}' git config --global user.email "${{ github.triggering_actor}}@users.noreply.github.com" git add . git commit --allow-empty -m 'bump ${{ inputs.semver }}' git tag ${{ inputs.semver }} git push origin ${{ inputs.semver }} - name: Release uses: softprops/action-gh-release@v2 with: name: ${{ inputs.semver }} tag_name: ${{ inputs.semver }} slog-multi-1.7.1/.github/workflows/test.yml 0000664 0000000 0000000 00000001407 15140164271 0020716 0 ustar 00root root 0000000 0000000 name: Tests on: push: tags: branches: pull_request: jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: go: - '1.21' - '1.22' - '1.23' - '1.24' - '1.x' steps: - uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 with: go-version: ${{ matrix.go }} stable: false - name: Build run: make build - name: Test run: make test - name: Test run: make coverage - name: Codecov uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} file: ./cover.out flags: unittests verbose: true if: matrix.go == '1.21' slog-multi-1.7.1/.gitignore 0000664 0000000 0000000 00000001355 15140164271 0015611 0 ustar 00root root 0000000 0000000 # Created by https://www.toptal.com/developers/gitignore/api/go # Edit at https://www.toptal.com/developers/gitignore?templates=go ### Go ### # If you prefer the allow list template instead of the deny list, see community template: # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ # Go workspace file ### Go Patch ### /vendor/ /Godeps/ # End of https://www.toptal.com/developers/gitignore/api/go cover.out cover.html .vscode .idea/ slog-multi-1.7.1/LICENSE 0000664 0000000 0000000 00000002056 15140164271 0014625 0 ustar 00root root 0000000 0000000 MIT License Copyright (c) 2023 Samuel Berthe Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. slog-multi-1.7.1/Makefile 0000664 0000000 0000000 00000002010 15140164271 0015246 0 ustar 00root root 0000000 0000000 build: go build -v ./... test: go test -race -v ./... watch-test: reflex -t 50ms -s -- sh -c 'gotest -race -v ./...' bench: go test -benchmem -count 3 -bench ./... watch-bench: reflex -t 50ms -s -- sh -c 'go test -benchmem -count 3 -bench ./...' coverage: go test -v -coverprofile=cover.out -covermode=atomic ./... go tool cover -html=cover.out -o cover.html tools: go install github.com/cespare/reflex@latest go install github.com/rakyll/gotest@latest go install github.com/psampaz/go-mod-outdated@latest go install github.com/jondot/goweight@latest go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest go get -t -u golang.org/x/tools/cmd/cover go install github.com/sonatype-nexus-community/nancy@latest go mod tidy lint: golangci-lint run --timeout 60s --max-same-issues 50 ./... lint-fix: golangci-lint run --timeout 60s --max-same-issues 50 --fix ./... audit: go list -json -m all | nancy sleuth outdated: go list -u -m -json all | go-mod-outdated -update -direct weight: goweight slog-multi-1.7.1/README.md 0000664 0000000 0000000 00000057435 15140164271 0015112 0 ustar 00root root 0000000 0000000 # slog-multi: Advanced Handler Composition for Go's Structured Logging (pipelining, fanout, routing, failover...) [](https://github.com/samber/slog-multi/releases)  [](https://pkg.go.dev/github.com/samber/slog-multi)  [](https://goreportcard.com/report/github.com/samber/slog-multi) [](https://codecov.io/gh/samber/slog-multi) [](https://github.com/samber/slog-multi/graphs/contributors) [](./LICENSE) **slog-multi** provides advanced composition patterns for Go's structured logging (`slog`). It enables you to build sophisticated logging workflows by combining multiple handlers with different strategies for distribution, routing, transformation, and error handling. ## π― Features - **π Fanout**: Distribute logs to multiple handlers in parallel - **π£οΈ Router**: Conditionally route logs based on custom criteria - **π― First Match**: Route logs to the first matching handler only - **π Failover**: High-availability logging with automatic fallback - **βοΈ Load Balancing**: Distribute load across multiple handlers - **π Pipeline**: Transform and filter logs with middleware chains - **π‘οΈ Error Recovery**: Graceful handling of logging failures Middlewares: - **β‘ Inline Handlers**: Quick implementation of custom handlers - **π§ Inline Middleware**: Rapid development of transformation logic
**See also:** - [slog-multi](https://github.com/samber/slog-multi): `slog.Handler` chaining, fanout, routing, failover, load balancing... - [slog-formatter](https://github.com/samber/slog-formatter): `slog` attribute formatting - [slog-sampling](https://github.com/samber/slog-sampling): `slog` sampling policy - [slog-mock](https://github.com/samber/slog-mock): `slog.Handler` for test purposes **HTTP middlewares:** - [slog-gin](https://github.com/samber/slog-gin): Gin middleware for `slog` logger - [slog-echo](https://github.com/samber/slog-echo): Echo middleware for `slog` logger - [slog-fiber](https://github.com/samber/slog-fiber): Fiber middleware for `slog` logger - [slog-chi](https://github.com/samber/slog-chi): Chi middleware for `slog` logger - [slog-http](https://github.com/samber/slog-http): `net/http` middleware for `slog` logger **Loggers:** - [slog-zap](https://github.com/samber/slog-zap): A `slog` handler for `Zap` - [slog-zerolog](https://github.com/samber/slog-zerolog): A `slog` handler for `Zerolog` - [slog-logrus](https://github.com/samber/slog-logrus): A `slog` handler for `Logrus` **Log sinks:** - [slog-datadog](https://github.com/samber/slog-datadog): A `slog` handler for `Datadog` - [slog-betterstack](https://github.com/samber/slog-betterstack): A `slog` handler for `Betterstack` - [slog-rollbar](https://github.com/samber/slog-rollbar): A `slog` handler for `Rollbar` - [slog-loki](https://github.com/samber/slog-loki): A `slog` handler for `Loki` - [slog-sentry](https://github.com/samber/slog-sentry): A `slog` handler for `Sentry` - [slog-syslog](https://github.com/samber/slog-syslog): A `slog` handler for `Syslog` - [slog-logstash](https://github.com/samber/slog-logstash): A `slog` handler for `Logstash` - [slog-fluentd](https://github.com/samber/slog-fluentd): A `slog` handler for `Fluentd` - [slog-graylog](https://github.com/samber/slog-graylog): A `slog` handler for `Graylog` - [slog-quickwit](https://github.com/samber/slog-quickwit): A `slog` handler for `Quickwit` - [slog-slack](https://github.com/samber/slog-slack): A `slog` handler for `Slack` - [slog-telegram](https://github.com/samber/slog-telegram): A `slog` handler for `Telegram` - [slog-mattermost](https://github.com/samber/slog-mattermost): A `slog` handler for `Mattermost` - [slog-microsoft-teams](https://github.com/samber/slog-microsoft-teams): A `slog` handler for `Microsoft Teams` - [slog-webhook](https://github.com/samber/slog-webhook): A `slog` handler for `Webhook` - [slog-kafka](https://github.com/samber/slog-kafka): A `slog` handler for `Kafka` - [slog-nats](https://github.com/samber/slog-nats): A `slog` handler for `NATS` - [slog-parquet](https://github.com/samber/slog-parquet): A `slog` handler for `Parquet` + `Object Storage` - [slog-channel](https://github.com/samber/slog-channel): A `slog` handler for Go channels ## π Installation ```sh go get github.com/samber/slog-multi ``` **Compatibility**: go >= 1.21 No breaking changes will be made to exported APIs before v2.0.0. > [!WARNING] > Use this library carefully, log processing can be very costly (!) > > Excessive logging βwith multiple processing steps and destinationsβ can introduce significant overhead, which is generally undesirable in performance-critical paths. Logging is always expensive, and sometimes, metrics or a sampling strategy are cheaper. The library itself does not generate extra load. ## π‘ Usage GoDoc: [https://pkg.go.dev/github.com/samber/slog-multi](https://pkg.go.dev/github.com/samber/slog-multi) ### Broadcast: `slogmulti.Fanout()` Distribute logs to multiple `slog.Handler` in parallel for maximum throughput and redundancy. ```go import ( "net" slogmulti "github.com/samber/slog-multi" "log/slog" "os" "time" ) func main() { logstash, _ := net.Dial("tcp", "logstash.acme:4242") // use github.com/netbrain/goautosocket for auto-reconnect datadogHandler := slogdatadog.NewDatadogHandler(slogdatadog.Option{ APIKey: "your-api-key", Service: "my-service", }) stderr := os.Stderr logger := slog.New( slogmulti.Fanout( slog.NewJSONHandler(logstash, &slog.HandlerOptions{}), // pass to first handler: logstash over tcp slog.NewTextHandler(stderr, &slog.HandlerOptions{}), // then to second handler: stderr datadogHandler, // ... ), ) logger. With( slog.Group("user", slog.String("id", "user-123"), slog.Time("created_at", time.Now()), ), ). With("environment", "dev"). With("error", fmt.Errorf("an error")). Error("A message") } ``` Stderr output: ``` time=2023-04-10T14:00:0.000000+00:00 level=ERROR msg="A message" user.id=user-123 user.created_at=2023-04-10T14:00:0.000000+00:00 environment=dev error="an error" ``` Netcat output: ```json { "time":"2023-04-10T14:00:0.000000+00:00", "level":"ERROR", "msg":"A message", "user":{ "id":"user-123", "created_at":"2023-04-10T14:00:0.000000+00:00" }, "environment":"dev", "error":"an error" } ``` ### Routing: `slogmulti.Router()` Distribute logs to all matching `slog.Handler` based on custom criteria like log level, attributes, or business logic. ```go import ( "context" slogmulti "github.com/samber/slog-multi" slogslack "github.com/samber/slog-slack" "log/slog" "os" ) func main() { slackChannelUS := slogslack.Option{Level: slog.LevelError, WebhookURL: "xxx", Channel: "supervision-us"}.NewSlackHandler() slackChannelEU := slogslack.Option{Level: slog.LevelError, WebhookURL: "xxx", Channel: "supervision-eu"}.NewSlackHandler() slackChannelAPAC := slogslack.Option{Level: slog.LevelError, WebhookURL: "xxx", Channel: "supervision-apac"}.NewSlackHandler() consoleHandler := slog.NewTextHandler(os.Stderr, nil) logger := slog.New( slogmulti.Router(). Add(slackChannelUS, recordMatchRegion("us")). Add(slackChannelEU, recordMatchRegion("eu")). Add(slackChannelAPAC, recordMatchRegion("apac")). Add(consoleHandler, slogmulti.LevelIs(slog.LevelInfo, slog.LevelDebug)). Handler(), ) logger. With("region", "us"). With("pool", "us-east-1"). Error("Server desynchronized") } func recordMatchRegion(region string) func(ctx context.Context, r slog.Record) bool { return func(ctx context.Context, r slog.Record) bool { ok := false r.Attrs(func(attr slog.Attr) bool { if attr.Key == "region" && attr.Value.Kind() == slog.KindString && attr.Value.String() == region { ok = true return false } return true }) return ok } } ``` **Use Cases:** - Environment-specific logging (dev vs prod) - Level-based routing (errors to Slack, info to console) - Business logic routing (user actions vs system events) ### First Match Routing: `Router().FirstMatch()` Route logs to the **first matching handler only**, unlike regular routing which sends to all matching handlers. Perfect for priority-based routing where you want exactly one handler to receive each log. ```go import ( slogmulti "github.com/samber/slog-multi" slogslack "github.com/samber/slog-slack" "log/slog" ) func main() { queryChannel := slogslack.Option{Level: slog.LevelDebug, WebhookURL: "xxx", Channel: "db-queries"}.NewSlackHandler() requestChannel := slogslack.Option{Level: slog.LevelError, WebhookURL: "xxx", Channel: "service-requests"}.NewSlackHandler() influxdbChannel := slogslack.Option{Level: slog.LevelInfo, WebhookURL: "xxx", Channel: "influxdb-metrics"}.NewSlackHandler() fallbackChannel := slogslack.Option{Level: slog.LevelError, WebhookURL: "xxx", Channel: "logs"}.NewSlackHandler() logger := slog.New( slogmulti.Router(). Add(queryChannel, slogmulti.AttrKindIs("query", slog.KindString, "args", slog.KindAny)). Add(requestChannel, slogmulti.AttrKindIs("method", slog.KindString, "body", slog.KindAny)). Add(influxdbChannel, slogmulti.AttrValueIs("scope", "influx")). Add(fallbackChannel). // Catch-all for everything else FirstMatch(). // β Enable first-match routing Handler(), ) // Goes to queryChannel only (stops at first match) logger.Debug("Executing SQL query", "query", "SELECT * FROM users WHERE id = ?", "args", []int{1}) // Goes to requestChannel only (stops at first match) logger.Error("Incoming request failed", "method", "POST", "body", "{'name':'test'}") // Goes to fallbackChannel (no other handlers matched) logger.Error("An unexpected error occurred") } ``` #### Built-in Predicates **Level predicates:** - `LevelIs(levels ...slog.Level)` - Match specific log levels - `LevelIsNot(levels ...slog.Level)` - Exclude specific log levels **Message predicates:** - `MessageIs(msg string)` - Exact message match - `MessageIsNot(msg string)` - Message doesn't match - `MessageContains(part string)` - Message contains substring - `MessageNotContains(part string)` - Message doesn't contain substring **Attribute predicates:** - `AttrValueIs(key, value, ...)` - Check attributes have exact values - `AttrKindIs(key, kind, ...)` - Check attributes have specific types ### Failover: `slogmulti.Failover()` Ensure logging reliability by trying multiple handlers in order until one succeeds. Perfect for high-availability scenarios. ```go import ( "net" slogmulti "github.com/samber/slog-multi" "log/slog" "os" "time" ) func main() { // Create connections to multiple log servers // ncat -l 1000 -k // ncat -l 1001 -k // ncat -l 1002 -k // List AZs - use github.com/netbrain/goautosocket for auto-reconnect logstash1, _ := net.Dial("tcp", "logstash.eu-west-3a.internal:1000") logstash2, _ := net.Dial("tcp", "logstash.eu-west-3b.internal:1000") logstash3, _ := net.Dial("tcp", "logstash.eu-west-3c.internal:1000") logger := slog.New( slogmulti.Failover()( slog.HandlerOptions{}.NewJSONHandler(logstash1, nil), // Primary slog.HandlerOptions{}.NewJSONHandler(logstash2, nil), // Secondary slog.HandlerOptions{}.NewJSONHandler(logstash3, nil), // Tertiary ), ) logger. With( slog.Group("user", slog.String("id", "user-123"), slog.Time("created_at", time.Now()), ), ). With("environment", "dev"). With("error", fmt.Errorf("an error")). Error("A message") } ``` **Use Cases:** - High-availability logging infrastructure - Disaster recovery scenarios - Multi-region deployments ### Load balancing: `slogmulti.Pool()` Distribute logging load across multiple handlers using round-robin with randomization to increase throughput and provide redundancy. ```go import ( "net" slogmulti "github.com/samber/slog-multi" "log/slog" "os" "time" ) func main() { // Create multiple log servers // ncat -l 1000 -k // ncat -l 1001 -k // ncat -l 1002 -k // List AZs - use github.com/netbrain/goautosocket for auto-reconnect logstash1, _ := net.Dial("tcp", "logstash.eu-west-3a.internal:1000") logstash2, _ := net.Dial("tcp", "logstash.eu-west-3b.internal:1000") logstash3, _ := net.Dial("tcp", "logstash.eu-west-3c.internal:1000") logger := slog.New( slogmulti.Pool()( // A random handler will be picked for each log slog.HandlerOptions{}.NewJSONHandler(logstash1, nil), slog.HandlerOptions{}.NewJSONHandler(logstash2, nil), slog.HandlerOptions{}.NewJSONHandler(logstash3, nil), ), ) // High-volume logging for i := 0; i < 1000; i++ { logger. With( slog.Group("user", slog.String("id", "user-123"), slog.Time("created_at", time.Now()), ), ). With("environment", "dev"). With("error", fmt.Errorf("an error")). Error("A message") } } ``` **Use Cases:** - High-throughput logging scenarios - Distributed logging infrastructure - Performance optimization ### Recover errors: `slogmulti.RecoverHandlerError()` Gracefully handle logging failures without crashing the application. Catches both panics and errors from handlers. ```go import ( "context" slogformatter "github.com/samber/slog-formatter" slogmulti "github.com/samber/slog-multi" "log/slog" "os" ) recovery := slogmulti.RecoverHandlerError( func(ctx context.Context, record slog.Record, err error) { // will be called only if subsequent handlers fail or return an error log.Println(err.Error()) }, ) sink := NewSinkHandler(...) logger := slog.New( slogmulti. Pipe(recovery). Handler(sink), ) err := fmt.Errorf("an error") logger.Error("a message", slog.Any("very_private_data", "abcd"), slog.Any("user", user), slog.Any("err", err)) // outputs: // time=2023-04-10T14:00:0.000000+00:00 level=ERROR msg="a message" error.message="an error" error.type="*errors.errorString" user="John doe" very_private_data="********" ``` ### Pipelining: `slogmulti.Pipe()` Transform and filter logs using middleware chains. Perfect for data privacy, formatting, and cross-cutting concerns. ```go import ( "context" slogmulti "github.com/samber/slog-multi" "log/slog" "os" "time" ) func main() { // First middleware: format Go `error` type into an structured object {error: "*myCustomErrorType", message: "could not reach https://a.b/c"} errorFormattingMiddleware := slogmulti.NewHandleInlineMiddleware(func(ctx context.Context, record slog.Record, next func(context.Context, slog.Record) error) error { record.Attrs(func(attr slog.Attr) bool { if attr.Key == "error" && attr.Value.Kind() == slog.KindAny { if err, ok := attr.Value.Any().(error); ok { record.AddAttrs( slog.String("error_type", "error"), slog.String("error_message", err.Error()), ) } } return true }) return next(ctx, record) }) // Second middleware: remove PII gdprMiddleware := slogmulti.NewHandleInlineMiddleware(func(ctx context.Context, record slog.Record, next func(context.Context, slog.Record) error) error { record.Attrs(func(attr slog.Attr) bool { if attr.Key == "email" || attr.Key == "phone" || attr.Key == "created_at" { record.AddAttrs(slog.String(attr.Key, "*********")) } return true }) return next(ctx, record) }) // Final handler sink := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{}) logger := slog.New( slogmulti. Pipe(errorFormattingMiddleware). Pipe(gdprMiddleware). // ... Handler(sink), ) logger. With( slog.Group("user", slog.String("id", "user-123"), slog.String("email", "user-123"), slog.Time("created_at", time.Now()), ), ). With("environment", "dev"). Error("A message", slog.String("foo", "bar"), slog.Any("error", fmt.Errorf("an error")), ) } ``` Stderr output: ```json { "time":"2023-04-10T14:00:0.000000+00:00", "level":"ERROR", "msg":"A message", "user":{ "email":"*******", "phone":"*******", "created_at":"*******" }, "environment":"dev", "foo":"bar", "error":{ "type":"*myCustomErrorType", "message":"an error" } } ``` **Use Cases:** - Data privacy and GDPR compliance - Error formatting and standardization - Log enrichment and transformation - Performance monitoring and metrics ## π§ Advanced Patterns ### Custom middleware Middleware must match the following prototype: ```go type Middleware func(slog.Handler) slog.Handler ``` The example above uses: - a custom middleware, [see here](./examples/pipe/gdpr.go) - an inline middleware, [see here](./examples/pipe/errors.go) > **Note**: `WithAttrs` and `WithGroup` methods of custom middleware must return a new instance, not `this`. #### Inline handler Inline handlers provide shortcuts to implement `slog.Handler` without creating full struct implementations. ```go mdw := slogmulti.NewHandleInlineHandler( // simulate "Handle()" method func(ctx context.Context, groups []string, attrs []slog.Attr, record slog.Record) error { // Custom logic here // [...] return nil }, ) ``` ```go mdw := slogmulti.NewInlineHandler( // simulate "Enabled()" method func(ctx context.Context, groups []string, attrs []slog.Attr, level slog.Level) bool { // Custom logic here // [...] return true }, // simulate "Handle()" method func(ctx context.Context, groups []string, attrs []slog.Attr, record slog.Record) error { // Custom logic here // [...] return nil }, ) ``` #### Inline middleware Inline middleware provides shortcuts to implement middleware functions that hook specific methods. #### Hook `Enabled()` Method ```go middleware := slogmulti.NewEnabledInlineMiddleware(func(ctx context.Context, level slog.Level, next func(context.Context, slog.Level) bool) bool{ // Custom logic before calling next if level == slog.LevelDebug { return false // Skip debug logs } return next(ctx, level) }) ``` #### Hook `Handle()` Method ```go middleware := slogmulti.NewHandleInlineMiddleware(func(ctx context.Context, record slog.Record, next func(context.Context, slog.Record) error) error { // Add timestamp to all logs record.AddAttrs(slog.Time("logged_at", time.Now())) return next(ctx, record) }) ``` #### Hook `WithAttrs()` Method ```go mdw := slogmulti.NewWithAttrsInlineMiddleware(func(attrs []slog.Attr, next func([]slog.Attr) slog.Handler) slog.Handler{ // Filter out sensitive attributes filtered := make([]slog.Attr, 0, len(attrs)) for _, attr := range attrs { if attr.Key != "password" && attr.Key != "token" { filtered = append(filtered, attr) } } return next(attrs) }) ``` #### Hook `WithGroup()` Method ```go mdw := slogmulti.NewWithGroupInlineMiddleware(func(name string, next func(string) slog.Handler) slog.Handler{ // Add prefix to group names prefixedName := "app." + name return next(name) }) ``` #### Complete Inline Middleware > **Warning**: You should implement your own middleware for complex scenarios. ```go mdw := slogmulti.NewInlineMiddleware( func(ctx context.Context, level slog.Level, next func(context.Context, slog.Level) bool) bool{ // Custom logic here // [...] return next(ctx, level) }, func(ctx context.Context, record slog.Record, next func(context.Context, slog.Record) error) error{ // Custom logic here // [...] return next(ctx, record) }, func(attrs []slog.Attr, next func([]slog.Attr) slog.Handler) slog.Handler{ // Custom logic here // [...] return next(attrs) }, func(name string, next func(string) slog.Handler) slog.Handler{ // Custom logic here // [...] return next(name) }, ) ``` ## π‘ Best Practices ### Performance Considerations - **Use Fanout sparingly**: Broadcasting to many handlers can impact performance - **Implement sampling**: For high-volume logs, consider sampling strategies - **Monitor handler performance**: Some handlers (like network-based ones) can be slow - **Use buffering**: Consider buffering for network-based handlers ### Error Handling - **Always use error recovery**: Wrap handlers with `RecoverHandlerError` - **Implement fallbacks**: Use failover patterns for critical logging - **Monitor logging failures**: Track when logging fails to identify issues ### Security and Privacy - **Redact sensitive data**: Use middleware to remove PII and secrets - **Validate log content**: Ensure logs don't contain sensitive information - **Use secure connections**: For network-based handlers, use TLS ### Monitoring and Observability - **Add correlation IDs**: Include request IDs in logs for tracing - **Structured logging**: Use slog's structured logging features consistently - **Log levels**: Use appropriate log levels for different types of information ## π€ Contributing - Ping me on twitter [@samuelberthe](https://twitter.com/samuelberthe) (DMs, mentions, whatever :)) - Fork the [project](https://github.com/samber/slog-multi) - Fix [open issues](https://github.com/samber/slog-multi/issues) or request new features Don't hesitate ;) ```bash # Install some dev dependencies make tools # Run tests make test # or make watch-test ``` ## π€ Contributors  ## π« Show your support If this project helped you, please give it a βοΈ on GitHub! [](https://github.com/sponsors/samber) ## π License Copyright Β© 2023 [Samuel Berthe](https://github.com/samber). This project is [MIT](./LICENSE) licensed. slog-multi-1.7.1/error.go 0000664 0000000 0000000 00000000423 15140164271 0015274 0 ustar 00root root 0000000 0000000 package slogmulti import "fmt" func try(callback func() error) (err error) { defer func() { if r := recover(); r != nil { if e, ok := r.(error); ok { err = e } else { err = fmt.Errorf("unexpected error: %+v", r) } } }() err = callback() return } slog-multi-1.7.1/failover.go 0000664 0000000 0000000 00000011000 15140164271 0015743 0 ustar 00root root 0000000 0000000 package slogmulti import ( "context" "log/slog" "github.com/samber/lo" ) // Ensure FailoverHandler implements the slog.Handler interface at compile time var _ slog.Handler = (*FailoverHandler)(nil) // FailoverHandler implements a high-availability logging pattern. // It attempts to forward log records to handlers in order until one succeeds. // This is useful for scenarios where you want primary and backup logging destinations. // // @TODO: implement round robin strategy for load balancing across multiple handlers type FailoverHandler struct { // handlers contains the list of slog.Handler instances in priority order // The first handler that successfully processes a record will be used handlers []slog.Handler } // Failover creates a failover handler factory function. // This function returns a closure that can be used to create failover handlers // with different sets of handlers. // // Example usage: // // handler := slogmulti.Failover()( // primaryHandler, // First choice // secondaryHandler, // Fallback if primary fails // backupHandler, // Last resort // ) // logger := slog.New(handler) // // Returns: // // A function that creates FailoverHandler instances with the provided handlers func Failover() func(...slog.Handler) slog.Handler { return func(handlers ...slog.Handler) slog.Handler { return &FailoverHandler{ handlers: handlers, } } } // Enabled checks if any of the underlying handlers are enabled for the given log level. // This method implements the slog.Handler interface requirement. // // The handler is considered enabled if at least one of its child handlers // is enabled for the specified level. This ensures that if any handler // can process the log, the failover handler will attempt to distribute it. // // Args: // // ctx: The context for the logging operation // l: The log level to check // // Returns: // // true if at least one handler is enabled for the level, false otherwise func (h *FailoverHandler) Enabled(ctx context.Context, l slog.Level) bool { for i := range h.handlers { if h.handlers[i].Enabled(ctx, l) { return true } } return false } // Handle attempts to process a log record using handlers in priority order. // This method implements the slog.Handler interface requirement. // // This implements a "fail-fast" strategy where the first successful handler // prevents further attempts, making it efficient for high-availability scenarios. // // Args: // // ctx: The context for the logging operation // r: The log record to process // // Returns: // // nil if any handler successfully processed the record, or the last error encountered func (h *FailoverHandler) Handle(ctx context.Context, r slog.Record) error { var err error for i := range h.handlers { if h.handlers[i].Enabled(ctx, r.Level) { err = try(func() error { return h.handlers[i].Handle(ctx, r.Clone()) }) if err == nil { return nil } } } return err } // WithAttrs creates a new FailoverHandler with additional attributes added to all child handlers. // This method implements the slog.Handler interface requirement. // // The method creates new handler instances for each child handler with the additional // attributes, ensuring that the attributes are properly propagated to all handlers // in the failover chain. // // Args: // // attrs: The attributes to add to all handlers // // Returns: // // A new FailoverHandler with the attributes added to all child handlers func (h *FailoverHandler) WithAttrs(attrs []slog.Attr) slog.Handler { handers := lo.Map(h.handlers, func(h slog.Handler, _ int) slog.Handler { return h.WithAttrs(attrs) }) return Failover()(handers...) } // WithGroup creates a new FailoverHandler with a group name applied to all child handlers. // This method implements the slog.Handler interface requirement. // // The method follows the same pattern as the standard slog implementation: // - If the group name is empty, returns the original handler unchanged // - Otherwise, creates new handler instances for each child handler with the group name // // Args: // // name: The group name to apply to all handlers // // Returns: // // A new FailoverHandler with the group name applied to all child handlers, // or the original handler if the group name is empty func (h *FailoverHandler) WithGroup(name string) slog.Handler { // https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247 if name == "" { return h } handers := lo.Map(h.handlers, func(h slog.Handler, _ int) slog.Handler { return h.WithGroup(name) }) return Failover()(handers...) } slog-multi-1.7.1/firstmatch.go 0000664 0000000 0000000 00000005766 15140164271 0016326 0 ustar 00root root 0000000 0000000 package slogmulti import ( "context" "log/slog" "slices" "github.com/samber/lo" ) // Ensure FirstMatchHandler implements the slog.Handler interface at compile time var _ slog.Handler = (*FirstMatchHandler)(nil) type FirstMatchHandler struct { handlers []*RoutableHandler } func FirstMatch(handlers ...*RoutableHandler) *FirstMatchHandler { return &FirstMatchHandler{handlers: lo.Map(handlers, func(h *RoutableHandler, _ int) *RoutableHandler { return &RoutableHandler{ predicates: h.predicates, handler: h.handler, groups: slices.Clone(h.groups), attrs: slices.Clone(h.attrs), skipPredicates: true, // prevent double matching } })} } // Enabled checks if any of the underlying handlers are enabled for the given log level. // This method implements the slog.Handler interface requirement. // See FanoutHandler.WithAttrs for details. func (h *FirstMatchHandler) Enabled(ctx context.Context, l slog.Level) bool { for i := range h.handlers { if h.handlers[i].Enabled(ctx, l) { return true } } return false } // Handle distributes a log record to the first matching handler. // This method implements the slog.Handler interface requirement. // // The method: // 1. Iterates through each child handler. // 2. Checks if the handler's predicates match the record. // 3. If a match is found, it checks if the handler is enabled for the record's level. // 4. If enabled, it forwards the record to that handler and returns. // 5. If no handlers match, it returns nil. func (h *FirstMatchHandler) Handle(ctx context.Context, r slog.Record) error { for i := range h.handlers { record, ok := h.handlers[i].isMatch(ctx, r) if ok { if h.handlers[i].Enabled(ctx, record.Level) { return try(func() error { return h.handlers[i].Handle(ctx, r) }) } return nil // Handler matched but is not enabled; do not proceed further } } return nil } // WithAttrs creates a new FirstMatchHandler with additional attributes added to all child handlers. // This method implements the slog.Handler interface requirement. // See FanoutHandler.WithAttrs for details. func (h *FirstMatchHandler) WithAttrs(attrs []slog.Attr) slog.Handler { handlers := lo.Map(h.handlers, func(h *RoutableHandler, _ int) *RoutableHandler { return h.WithAttrs(slices.Clone(attrs)).(*RoutableHandler) }) return newFirstMatch(handlers...) } // WithGroup creates a new FirstMatchHandler with a group name applied to all child handlers. // This method implements the slog.Handler interface requirement. // See FanoutHandler.WithGroup for details. func (h *FirstMatchHandler) WithGroup(name string) slog.Handler { // https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247 if name == "" { return h } handlers := lo.Map(h.handlers, func(h *RoutableHandler, _ int) *RoutableHandler { return h.WithGroup(name).(*RoutableHandler) }) return newFirstMatch(handlers...) } func newFirstMatch(handlers ...*RoutableHandler) *FirstMatchHandler { return &FirstMatchHandler{handlers: handlers} } slog-multi-1.7.1/go.mod 0000664 0000000 0000000 00000000256 15140164271 0014726 0 ustar 00root root 0000000 0000000 module github.com/samber/slog-multi go 1.21 require ( github.com/samber/lo v1.52.0 github.com/samber/slog-common v0.20.0 ) require golang.org/x/text v0.22.0 // indirect slog-multi-1.7.1/go.sum 0000664 0000000 0000000 00000000757 15140164271 0014761 0 ustar 00root root 0000000 0000000 github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/samber/slog-common v0.20.0 h1:WaLnm/aCvBJSk5nR5aXZTFBaV0B47A+AEaEOiZDeUnc= github.com/samber/slog-common v0.20.0/go.mod h1:+Ozat1jgnnE59UAlmNX1IF3IByHsODnnwf9jUcBZ+m8= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= slog-multi-1.7.1/go.work 0000664 0000000 0000000 00000000161 15140164271 0015124 0 ustar 00root root 0000000 0000000 go 1.21 use ( . ./examples/failover ./examples/fanout ./examples/pipe ./examples/pool ./examples/router ) slog-multi-1.7.1/go.work.sum 0000664 0000000 0000000 00000005776 15140164271 0015750 0 ustar 00root root 0000000 0000000 github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/samber/slog-common v0.18.1 h1:c0EipD/nVY9HG5shgm/XAs67mgpWDMF+MmtptdJNCkQ= github.com/samber/slog-common v0.18.1/go.mod h1:QNZiNGKakvrfbJ2YglQXLCZauzkI9xZBjOhWFKS3IKk= github.com/samber/slog-common v0.20.0 h1:WaLnm/aCvBJSk5nR5aXZTFBaV0B47A+AEaEOiZDeUnc= github.com/samber/slog-common v0.20.0/go.mod h1:+Ozat1jgnnE59UAlmNX1IF3IByHsODnnwf9jUcBZ+m8= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= slog-multi-1.7.1/handler_inline.go 0000664 0000000 0000000 00000004051 15140164271 0017117 0 ustar 00root root 0000000 0000000 package slogmulti import ( "context" "log/slog" ) // NewInlineHandler is a shortcut to a handler that implements all methods. func NewInlineHandler( enabledFunc func(ctx context.Context, groups []string, attrs []slog.Attr, level slog.Level) bool, handleFunc func(ctx context.Context, groups []string, attrs []slog.Attr, record slog.Record) error, ) slog.Handler { if enabledFunc == nil { panic("slog-multi: enabledFunc is required") } if handleFunc == nil { panic("slog-multi: handleFunc is required") } return &InlineHandler{ groups: []string{}, attrs: []slog.Attr{}, enabledFunc: enabledFunc, handleFunc: handleFunc, } } var _ slog.Handler = (*InlineHandler)(nil) type InlineHandler struct { groups []string attrs []slog.Attr enabledFunc func(ctx context.Context, groups []string, attrs []slog.Attr, level slog.Level) bool handleFunc func(ctx context.Context, groups []string, attrs []slog.Attr, record slog.Record) error } // Implements slog.Handler func (h *InlineHandler) Enabled(ctx context.Context, level slog.Level) bool { return h.enabledFunc(ctx, h.groups, h.attrs, level) } // Implements slog.Handler func (h *InlineHandler) Handle(ctx context.Context, record slog.Record) error { return h.handleFunc(ctx, h.groups, h.attrs, record) } // Implements slog.Handler func (h *InlineHandler) WithAttrs(attrs []slog.Attr) slog.Handler { newAttrs := []slog.Attr{} newAttrs = append(newAttrs, h.attrs...) newAttrs = append(newAttrs, attrs...) return &InlineHandler{ groups: h.groups, attrs: newAttrs, enabledFunc: h.enabledFunc, handleFunc: h.handleFunc, } } // Implements slog.Handler func (h *InlineHandler) WithGroup(name string) slog.Handler { // https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247 if name == "" { return h } newGroups := []string{} newGroups = append(newGroups, h.groups...) newGroups = append(newGroups, name) return &InlineHandler{ groups: newGroups, attrs: h.attrs, enabledFunc: h.enabledFunc, handleFunc: h.handleFunc, } } slog-multi-1.7.1/handler_inline_handle.go 0000664 0000000 0000000 00000003221 15140164271 0020430 0 ustar 00root root 0000000 0000000 package slogmulti import ( "context" "log/slog" ) // NewHandleInlineHandler is a shortcut to a middleware that implements only the `Handle` method. func NewHandleInlineHandler(handleFunc func(ctx context.Context, groups []string, attrs []slog.Attr, record slog.Record) error) slog.Handler { return &HandleInlineHandler{ groups: []string{}, attrs: []slog.Attr{}, handleFunc: handleFunc, } } var _ slog.Handler = (*HandleInlineHandler)(nil) type HandleInlineHandler struct { groups []string attrs []slog.Attr handleFunc func(ctx context.Context, groups []string, attrs []slog.Attr, record slog.Record) error } // Implements slog.Handler func (h *HandleInlineHandler) Enabled(ctx context.Context, level slog.Level) bool { return true } // Implements slog.Handler func (h *HandleInlineHandler) Handle(ctx context.Context, record slog.Record) error { return h.handleFunc(ctx, h.groups, h.attrs, record) } // Implements slog.Handler func (h *HandleInlineHandler) WithAttrs(attrs []slog.Attr) slog.Handler { newAttrs := []slog.Attr{} newAttrs = append(newAttrs, h.attrs...) newAttrs = append(newAttrs, attrs...) return &HandleInlineHandler{ groups: h.groups, attrs: newAttrs, handleFunc: h.handleFunc, } } // Implements slog.Handler func (h *HandleInlineHandler) WithGroup(name string) slog.Handler { // https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247 if name == "" { return h } newGroups := []string{} newGroups = append(newGroups, h.groups...) newGroups = append(newGroups, name) return &HandleInlineHandler{ groups: newGroups, attrs: h.attrs, handleFunc: h.handleFunc, } } slog-multi-1.7.1/middleware.go 0000664 0000000 0000000 00000001513 15140164271 0016261 0 ustar 00root root 0000000 0000000 package slogmulti import ( "log/slog" ) // Middleware is a function type that transforms one slog.Handler into another. // It follows the standard middleware pattern where a function takes a handler // and returns a new handler that wraps the original with additional functionality. // // Middleware functions can be used to: // - Transform log records (e.g., add timestamps, modify levels) // - Filter records based on conditions // - Add context or attributes to records // - Implement cross-cutting concerns like error recovery or sampling // // Example usage: // // gdprMiddleware := NewGDPRMiddleware() // sink := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{}) // // logger := slog.New( // slogmulti. // Pipe(gdprMiddleware). // // ... // Handler(sink), // ) type Middleware func(slog.Handler) slog.Handler slog-multi-1.7.1/middleware_inline.go 0000664 0000000 0000000 00000005047 15140164271 0017625 0 ustar 00root root 0000000 0000000 package slogmulti import ( "context" "log/slog" ) // NewInlineMiddleware is a shortcut to a middleware that implements all methods. func NewInlineMiddleware( enabledFunc func(ctx context.Context, level slog.Level, next func(context.Context, slog.Level) bool) bool, handleFunc func(ctx context.Context, record slog.Record, next func(context.Context, slog.Record) error) error, withAttrsFunc func(attrs []slog.Attr, next func([]slog.Attr) slog.Handler) slog.Handler, withGroupFunc func(name string, next func(string) slog.Handler) slog.Handler, ) Middleware { return func(next slog.Handler) slog.Handler { if next == nil { panic("slog-multi: next is required") } if enabledFunc == nil { panic("slog-multi: enabledFunc is required") } if handleFunc == nil { panic("slog-multi: handleFunc is required") } if withAttrsFunc == nil { panic("slog-multi: withAttrsFunc is required") } if withGroupFunc == nil { panic("slog-multi: withGroupFunc is required") } return &InlineMiddleware{ next: next, enabledFunc: enabledFunc, handleFunc: handleFunc, withAttrsFunc: withAttrsFunc, withGroupFunc: withGroupFunc, } } } var _ slog.Handler = (*InlineMiddleware)(nil) type InlineMiddleware struct { next slog.Handler enabledFunc func(ctx context.Context, level slog.Level, next func(context.Context, slog.Level) bool) bool handleFunc func(ctx context.Context, record slog.Record, next func(context.Context, slog.Record) error) error withAttrsFunc func(attrs []slog.Attr, next func([]slog.Attr) slog.Handler) slog.Handler withGroupFunc func(name string, next func(string) slog.Handler) slog.Handler } // Implements slog.Handler func (h *InlineMiddleware) Enabled(ctx context.Context, level slog.Level) bool { return h.enabledFunc(ctx, level, h.next.Enabled) } // Implements slog.Handler func (h *InlineMiddleware) Handle(ctx context.Context, record slog.Record) error { return h.handleFunc(ctx, record, h.next.Handle) } // Implements slog.Handler func (h *InlineMiddleware) WithAttrs(attrs []slog.Attr) slog.Handler { return NewInlineMiddleware( h.enabledFunc, h.handleFunc, h.withAttrsFunc, h.withGroupFunc, )(h.withAttrsFunc(attrs, h.next.WithAttrs)) } // Implements slog.Handler func (h *InlineMiddleware) WithGroup(name string) slog.Handler { // https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247 if name == "" { return h } return NewInlineMiddleware( h.enabledFunc, h.handleFunc, h.withAttrsFunc, h.withGroupFunc, )(h.withGroupFunc(name, h.next.WithGroup)) } slog-multi-1.7.1/middleware_inline_enabled.go 0000664 0000000 0000000 00000003151 15140164271 0021271 0 ustar 00root root 0000000 0000000 package slogmulti import ( "context" "log/slog" ) // NewEnabledInlineMiddleware is shortcut to a middleware that implements only the `Enable` method. func NewEnabledInlineMiddleware(enabledFunc func(ctx context.Context, level slog.Level, next func(context.Context, slog.Level) bool) bool) Middleware { return func(next slog.Handler) slog.Handler { if enabledFunc == nil { panic("slog-multi: enabledFunc is required") } if next == nil { panic("slog-multi: next is required") } return &EnabledInlineMiddleware{ next: next, enabledFunc: enabledFunc, } } } var _ slog.Handler = (*EnabledInlineMiddleware)(nil) type EnabledInlineMiddleware struct { next slog.Handler // enableFunc func(context.Context, slog.Level) bool enabledFunc func(context.Context, slog.Level, func(context.Context, slog.Level) bool) bool } // Implements slog.Handler func (h *EnabledInlineMiddleware) Enabled(ctx context.Context, level slog.Level) bool { return h.enabledFunc(ctx, level, h.next.Enabled) } // Implements slog.Handler func (h *EnabledInlineMiddleware) Handle(ctx context.Context, record slog.Record) error { return h.next.Handle(ctx, record) } // Implements slog.Handler func (h *EnabledInlineMiddleware) WithAttrs(attrs []slog.Attr) slog.Handler { return NewEnabledInlineMiddleware(h.enabledFunc)(h.next.WithAttrs(attrs)) } // Implements slog.Handler func (h *EnabledInlineMiddleware) WithGroup(name string) slog.Handler { // https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247 if name == "" { return h } return NewEnabledInlineMiddleware(h.enabledFunc)(h.next.WithGroup(name)) } slog-multi-1.7.1/middleware_inline_handle.go 0000664 0000000 0000000 00000003077 15140164271 0021141 0 ustar 00root root 0000000 0000000 package slogmulti import ( "context" "log/slog" ) // NewHandleInlineMiddleware is a shortcut to a middleware that implements only the `Handle` method. func NewHandleInlineMiddleware(handleFunc func(ctx context.Context, record slog.Record, next func(context.Context, slog.Record) error) error) Middleware { return func(next slog.Handler) slog.Handler { if next == nil { panic("slog-multi: next is required") } if handleFunc == nil { panic("slog-multi: handleFunc is required") } return &HandleInlineMiddleware{ next: next, handleFunc: handleFunc, } } } var _ slog.Handler = (*HandleInlineMiddleware)(nil) type HandleInlineMiddleware struct { next slog.Handler handleFunc func(ctx context.Context, record slog.Record, next func(context.Context, slog.Record) error) error } // Implements slog.Handler func (h *HandleInlineMiddleware) Enabled(ctx context.Context, level slog.Level) bool { return h.next.Enabled(ctx, level) } // Implements slog.Handler func (h *HandleInlineMiddleware) Handle(ctx context.Context, record slog.Record) error { return h.handleFunc(ctx, record, h.next.Handle) } // Implements slog.Handler func (h *HandleInlineMiddleware) WithAttrs(attrs []slog.Attr) slog.Handler { return NewHandleInlineMiddleware(h.handleFunc)(h.next.WithAttrs(attrs)) } // Implements slog.Handler func (h *HandleInlineMiddleware) WithGroup(name string) slog.Handler { // https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247 if name == "" { return h } return NewHandleInlineMiddleware(h.handleFunc)(h.next.WithGroup(name)) } slog-multi-1.7.1/middleware_inline_with_attrs.go 0000664 0000000 0000000 00000003122 15140164271 0022065 0 ustar 00root root 0000000 0000000 package slogmulti import ( "context" "log/slog" ) // NewWithAttrsInlineMiddleware is a shortcut to a middleware that implements only the `WithAttrs` method. func NewWithAttrsInlineMiddleware(withAttrsFunc func(attrs []slog.Attr, next func([]slog.Attr) slog.Handler) slog.Handler) Middleware { return func(next slog.Handler) slog.Handler { if withAttrsFunc == nil { panic("slog-multi: withAttrsFunc is required") } if next == nil { panic("slog-multi: next is required") } return &WithAttrsInlineMiddleware{ next: next, withAttrsFunc: withAttrsFunc, } } } var _ slog.Handler = (*WithAttrsInlineMiddleware)(nil) type WithAttrsInlineMiddleware struct { next slog.Handler withAttrsFunc func(attrs []slog.Attr, next func([]slog.Attr) slog.Handler) slog.Handler } // Implements slog.Handler func (h *WithAttrsInlineMiddleware) Enabled(ctx context.Context, level slog.Level) bool { return h.next.Enabled(ctx, level) } // Implements slog.Handler func (h *WithAttrsInlineMiddleware) Handle(ctx context.Context, record slog.Record) error { return h.next.Handle(ctx, record) } // Implements slog.Handler func (h *WithAttrsInlineMiddleware) WithAttrs(attrs []slog.Attr) slog.Handler { return NewWithAttrsInlineMiddleware(h.withAttrsFunc)(h.withAttrsFunc(attrs, h.next.WithAttrs)) } // Implements slog.Handler func (h *WithAttrsInlineMiddleware) WithGroup(name string) slog.Handler { // https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247 if name == "" { return h } return NewWithAttrsInlineMiddleware(h.withAttrsFunc)(h.next.WithGroup(name)) } slog-multi-1.7.1/middleware_inline_with_group.go 0000664 0000000 0000000 00000003074 15140164271 0022072 0 ustar 00root root 0000000 0000000 package slogmulti import ( "context" "log/slog" ) // NewWithGroupInlineMiddleware is a shortcut to a middleware that implements only the `WithAttrs` method. func NewWithGroupInlineMiddleware(withGroupFunc func(name string, next func(string) slog.Handler) slog.Handler) Middleware { return func(next slog.Handler) slog.Handler { if next == nil { panic("slog-multi: next is required") } if withGroupFunc == nil { panic("slog-multi: withGroupFunc is required") } return &WithGroupInlineMiddleware{ next: next, withGroupFunc: withGroupFunc, } } } var _ slog.Handler = (*WithGroupInlineMiddleware)(nil) type WithGroupInlineMiddleware struct { next slog.Handler withGroupFunc func(name string, next func(string) slog.Handler) slog.Handler } // Implements slog.Handler func (h *WithGroupInlineMiddleware) Enabled(ctx context.Context, level slog.Level) bool { return h.next.Enabled(ctx, level) } // Implements slog.Handler func (h *WithGroupInlineMiddleware) Handle(ctx context.Context, record slog.Record) error { return h.next.Handle(ctx, record) } // Implements slog.Handler func (h *WithGroupInlineMiddleware) WithAttrs(attrs []slog.Attr) slog.Handler { return NewWithGroupInlineMiddleware(h.withGroupFunc)(h.next.WithAttrs(attrs)) } // Implements slog.Handler func (h *WithGroupInlineMiddleware) WithGroup(name string) slog.Handler { // https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247 if name == "" { return h } return NewWithGroupInlineMiddleware(h.withGroupFunc)(h.withGroupFunc(name, h.next.WithGroup)) } slog-multi-1.7.1/multi.go 0000664 0000000 0000000 00000012213 15140164271 0015275 0 ustar 00root root 0000000 0000000 package slogmulti import ( "context" "errors" "log/slog" "slices" "github.com/samber/lo" ) // Ensure FanoutHandler implements the slog.Handler interface at compile time var _ slog.Handler = (*FanoutHandler)(nil) // FanoutHandler distributes log records to multiple slog.Handler instances in parallel. // It implements the slog.Handler interface and forwards all logging operations to all // registered handlers that are enabled for the given log level. type FanoutHandler struct { // handlers contains the list of slog.Handler instances to which log records will be distributed handlers []slog.Handler } // Fanout creates a new FanoutHandler that distributes records to multiple slog.Handler instances. // If exactly one handler is provided, it returns that handler unmodified. // If you pass a FanoutHandler as an argument, its handlers are flattened into the new FanoutHandler. // This function is the primary entry point for creating a multi-handler setup. // // Example usage: // // handler := slogmulti.Fanout( // slog.NewJSONHandler(os.Stdout, nil), // slogdatadog.NewDatadogHandler(...), // ) // logger := slog.New(handler) // // Args: // // handlers: Variable number of slog.Handler instances to distribute logs to // // Returns: // // A slog.Handler that forwards all operations to the provided handlers func Fanout(handlers ...slog.Handler) slog.Handler { var flat []slog.Handler for _, handler := range handlers { if fan, ok := handler.(*FanoutHandler); ok { flat = append(flat, fan.handlers...) } else { flat = append(flat, handler) } } if len(flat) == 1 { return flat[0] } return &FanoutHandler{ handlers: flat, } } // Enabled checks if any of the underlying handlers are enabled for the given log level. // This method implements the slog.Handler interface requirement. // // The handler is considered enabled if at least one of its child handlers // is enabled for the specified level. This ensures that if any handler // can process the log, the fanout handler will attempt to distribute it. // // Args: // // ctx: The context for the logging operation // l: The log level to check // // Returns: // // true if at least one handler is enabled for the level, false otherwise func (h *FanoutHandler) Enabled(ctx context.Context, l slog.Level) bool { for i := range h.handlers { if h.handlers[i].Enabled(ctx, l) { return true } } return false } // Handle distributes a log record to all enabled handlers. // This method implements the slog.Handler interface requirement. // // The method: // 1. Iterates through all registered handlers // 2. Checks if each handler is enabled for the record's level // 3. For enabled handlers, calls their Handle method with a cloned record // 4. Collects any errors that occur during handling // 5. Returns a combined error if any handlers failed // // Note: Each handler receives a cloned record to prevent interference between handlers. // This ensures that one handler cannot modify the record for other handlers. // // Args: // // ctx: The context for the logging operation // r: The log record to distribute // // Returns: // // An error if any handler failed to process the record, nil otherwise func (h *FanoutHandler) Handle(ctx context.Context, r slog.Record) error { var errs []error for i := range h.handlers { if h.handlers[i].Enabled(ctx, r.Level) { err := try(func() error { return h.handlers[i].Handle(ctx, r.Clone()) }) if err != nil { errs = append(errs, err) } } } // If errs is empty, or contains only nil errors, this returns nil return errors.Join(errs...) } // WithAttrs creates a new FanoutHandler with additional attributes added to all child handlers. // This method implements the slog.Handler interface requirement. // // The method creates new handler instances for each child handler with the additional // attributes, ensuring that the attributes are properly propagated to all handlers // in the fanout chain. // // Args: // // attrs: The attributes to add to all handlers // // Returns: // // A new FanoutHandler with the attributes added to all child handlers func (h *FanoutHandler) WithAttrs(attrs []slog.Attr) slog.Handler { handlers := lo.Map(h.handlers, func(h slog.Handler, _ int) slog.Handler { return h.WithAttrs(slices.Clone(attrs)) }) return Fanout(handlers...) } // WithGroup creates a new FanoutHandler with a group name applied to all child handlers. // This method implements the slog.Handler interface requirement. // // The method follows the same pattern as the standard slog implementation: // - If the group name is empty, returns the original handler unchanged // - Otherwise, creates new handler instances for each child handler with the group name // // Args: // // name: The group name to apply to all handlers // // Returns: // // A new FanoutHandler with the group name applied to all child handlers, // or the original handler if the group name is empty func (h *FanoutHandler) WithGroup(name string) slog.Handler { // https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247 if name == "" { return h } handlers := lo.Map(h.handlers, func(h slog.Handler, _ int) slog.Handler { return h.WithGroup(name) }) return Fanout(handlers...) } slog-multi-1.7.1/pipe.go 0000664 0000000 0000000 00000005070 15140164271 0015103 0 ustar 00root root 0000000 0000000 package slogmulti import ( "log/slog" ) // PipeBuilder provides a fluent API for building middleware chains. // It allows you to compose multiple middleware functions that will be applied // to log records in the order they are added (last-in, first-out). type PipeBuilder struct { // middlewares contains the list of middleware functions to be applied // The middlewares are applied in reverse order (LIFO) when building the final handler middlewares []Middleware } // Pipe creates a new PipeBuilder with the provided middleware functions. // This function is the entry point for building middleware chains. // // Middleware functions are applied in reverse order (last-in, first-out), // which means the last middleware added will be the first one applied to incoming records. // This allows for intuitive composition where you can think of the chain as // "transform A, then transform B, then send to handler". // // Example usage: // // handler := slogmulti.Pipe( // RewriteLevel(slog.LevelWarn, slog.LevelInfo), // RewriteMessage("prefix: %s"), // RedactPII(), // ).Handler(finalHandler) // // Args: // // middlewares: Variable number of middleware functions to chain together // // Returns: // // A new PipeBuilder instance ready for further configuration func Pipe(middlewares ...Middleware) *PipeBuilder { return &PipeBuilder{middlewares: middlewares} } // Pipe adds an additional middleware to the chain. // This method provides a fluent API for building middleware chains incrementally. // // Args: // // middleware: The middleware function to add to the chain // // Returns: // // The PipeBuilder instance for method chaining func (h *PipeBuilder) Pipe(middleware Middleware) *PipeBuilder { h.middlewares = append(h.middlewares, middleware) return h } // Handler creates a slog.Handler by applying all middleware to the provided handler. // This method finalizes the middleware chain and returns a handler that can be used with slog.New(). // // This LIFO approach ensures that the middleware chain is applied in the intuitive order: // the first middleware in the chain is applied first to incoming records. // // Args: // // handler: The final slog.Handler that will receive the transformed records // // Returns: // // A slog.Handler that applies all middleware transformations before forwarding to the final handler func (h *PipeBuilder) Handler(handler slog.Handler) slog.Handler { for len(h.middlewares) > 0 { middleware := h.middlewares[len(h.middlewares)-1] h.middlewares = h.middlewares[0 : len(h.middlewares)-1] handler = middleware(handler) } return handler } slog-multi-1.7.1/pool.go 0000664 0000000 0000000 00000012026 15140164271 0015116 0 ustar 00root root 0000000 0000000 package slogmulti import ( "context" "math/rand" "time" "log/slog" "github.com/samber/lo" ) // Ensure PoolHandler implements the slog.Handler interface at compile time var _ slog.Handler = (*PoolHandler)(nil) // PoolHandler implements a load balancing strategy for logging handlers. // It distributes log records across multiple handlers using a round-robin approach // with randomization to ensure even distribution and prevent hot-spotting. type PoolHandler struct { // randSource provides a thread-safe random number generator for load balancing randSource rand.Source // handlers contains the list of slog.Handler instances to distribute records across handlers []slog.Handler } // Pool creates a load balancing handler factory function. // This function returns a closure that can be used to create pool handlers // with different sets of handlers for load balancing. // // The pool uses a round-robin strategy with randomization to distribute // log records evenly across all available handlers. This is useful for: // - Increasing logging throughput by parallelizing handler operations // - Providing redundancy by having multiple handlers process the same records // - Load balancing across multiple logging destinations // // Example usage: // // handler := slogmulti.Pool()( // handler1, // Will receive ~33% of records // handler2, // Will receive ~33% of records // handler3, // Will receive ~33% of records // ) // logger := slog.New(handler) // // Returns: // // A function that creates PoolHandler instances with the provided handlers func Pool() func(...slog.Handler) slog.Handler { return func(handlers ...slog.Handler) slog.Handler { return &PoolHandler{ randSource: rand.NewSource(time.Now().UnixNano()), handlers: handlers, } } } // Enabled checks if any of the underlying handlers are enabled for the given log level. // This method implements the slog.Handler interface requirement. // // The handler is considered enabled if at least one of its child handlers // is enabled for the specified level. This ensures that if any handler // can process the log, the pool handler will attempt to distribute it. // // Args: // // ctx: The context for the logging operation // l: The log level to check // // Returns: // // true if at least one handler is enabled for the level, false otherwise func (h *PoolHandler) Enabled(ctx context.Context, l slog.Level) bool { for i := range h.handlers { if h.handlers[i].Enabled(ctx, l) { return true } } return false } // Handle distributes a log record to a handler selected using round-robin with randomization. // This method implements the slog.Handler interface requirement. // // This approach ensures even distribution of load while providing fault tolerance // through the failover behavior when a handler is unavailable. // // Args: // // ctx: The context for the logging operation // r: The log record to distribute // // Returns: // // nil if any handler successfully processed the record, or the last error encountered func (h *PoolHandler) Handle(ctx context.Context, r slog.Record) error { if len(h.handlers) == 0 { return nil } // round robin with randomization rand := h.randSource.Int63() % int64(len(h.handlers)) handlers := append(h.handlers[rand:], h.handlers[:rand]...) var err error for i := range handlers { if handlers[i].Enabled(ctx, r.Level) { err = try(func() error { return handlers[i].Handle(ctx, r.Clone()) }) if err == nil { return nil } } } return err } // WithAttrs creates a new PoolHandler with additional attributes added to all child handlers. // This method implements the slog.Handler interface requirement. // // The method creates new handler instances for each child handler with the additional // attributes, ensuring that the attributes are properly propagated to all handlers // in the pool. // // Args: // // attrs: The attributes to add to all handlers // // Returns: // // A new PoolHandler with the attributes added to all child handlers func (h *PoolHandler) WithAttrs(attrs []slog.Attr) slog.Handler { handers := lo.Map(h.handlers, func(h slog.Handler, _ int) slog.Handler { return h.WithAttrs(attrs) }) return Pool()(handers...) } // WithGroup creates a new PoolHandler with a group name applied to all child handlers. // This method implements the slog.Handler interface requirement. // // The method follows the same pattern as the standard slog implementation: // - If the group name is empty, returns the original handler unchanged // - Otherwise, creates new handler instances for each child handler with the group name // // Args: // // name: The group name to apply to all handlers // // Returns: // // A new PoolHandler with the group name applied to all child handlers, // or the original handler if the group name is empty func (h *PoolHandler) WithGroup(name string) slog.Handler { // https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247 if name == "" { return h } handers := lo.Map(h.handlers, func(h slog.Handler, _ int) slog.Handler { return h.WithGroup(name) }) return Pool()(handers...) } slog-multi-1.7.1/recover.go 0000664 0000000 0000000 00000011214 15140164271 0015610 0 ustar 00root root 0000000 0000000 package slogmulti import ( "context" "fmt" "log/slog" ) // RecoveryFunc is a callback function that handles errors and panics from logging handlers. // It receives the context, the log record that caused the error, and the error itself. // This function can be used to log the error, send alerts, or perform any other // error handling logic without affecting the main application flow. type RecoveryFunc func(ctx context.Context, record slog.Record, err error) // Ensure HandlerErrorRecovery implements the slog.Handler interface at compile time var _ slog.Handler = (*HandlerErrorRecovery)(nil) // HandlerErrorRecovery wraps a slog.Handler to provide panic and error recovery. // It catches both panics and errors from the underlying handler and calls // a recovery function to handle them gracefully. type HandlerErrorRecovery struct { // recovery is the function called when an error or panic occurs recovery RecoveryFunc // handler is the underlying slog.Handler that this recovery wrapper protects handler slog.Handler } // RecoverHandlerError creates a middleware that adds error recovery to a slog.Handler. // This function returns a closure that can be used to wrap handlers with recovery logic. // // The recovery handler provides fault tolerance by: // 1. Catching panics from the underlying handler // 2. Catching errors returned by the underlying handler // 3. Calling the recovery function with the error details // 4. Propagating the original error to maintain logging semantics // // Example usage: // // recovery := slogmulti.RecoverHandlerError(func(ctx context.Context, record slog.Record, err error) { // fmt.Printf("Logging error: %v\n", err) // }) // safeHandler := recovery(riskyHandler) // logger := slog.New(safeHandler) // // Args: // // recovery: The function to call when an error or panic occurs // // Returns: // // A function that wraps handlers with recovery logic func RecoverHandlerError(recovery RecoveryFunc) func(slog.Handler) slog.Handler { return func(handler slog.Handler) slog.Handler { return &HandlerErrorRecovery{ recovery: recovery, handler: handler, } } } // Enabled checks if the underlying handler is enabled for the given log level. // This method implements the slog.Handler interface requirement. // // Args: // // ctx: The context for the logging operation // l: The log level to check // // Returns: // // true if the underlying handler is enabled for the level, false otherwise func (h *HandlerErrorRecovery) Enabled(ctx context.Context, l slog.Level) bool { return h.handler.Enabled(ctx, l) } // Handle processes a log record with error recovery. // This method implements the slog.Handler interface requirement. // // This ensures that logging errors don't crash the application while still // allowing the error to be handled appropriately by the calling code. // // Args: // // ctx: The context for the logging operation // record: The log record to process // // Returns: // // The error from the underlying handler (never nil if an error occurred) func (h *HandlerErrorRecovery) Handle(ctx context.Context, record slog.Record) error { defer func() { if r := recover(); r != nil { if e, ok := r.(error); ok { h.recovery(ctx, record, e) } else { h.recovery(ctx, record, fmt.Errorf("%+v", r)) } } }() err := h.handler.Handle(ctx, record) if err != nil { h.recovery(ctx, record, err) } // propagate error return err } // WithAttrs creates a new HandlerErrorRecovery with additional attributes. // This method implements the slog.Handler interface requirement. // // Args: // // attrs: The attributes to add to the underlying handler // // Returns: // // A new HandlerErrorRecovery with the additional attributes func (h *HandlerErrorRecovery) WithAttrs(attrs []slog.Attr) slog.Handler { return &HandlerErrorRecovery{ recovery: h.recovery, handler: h.handler.WithAttrs(attrs), } } // WithGroup creates a new HandlerErrorRecovery with a group name. // This method implements the slog.Handler interface requirement. // // The method follows the same pattern as the standard slog implementation: // - If the group name is empty, returns the original handler unchanged // - Otherwise, creates a new handler with the group name applied to the underlying handler // // Args: // // name: The group name to apply to the underlying handler // // Returns: // // A new HandlerErrorRecovery with the group name, or the original handler if the name is empty func (h *HandlerErrorRecovery) WithGroup(name string) slog.Handler { // https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247 if name == "" { return h } return &HandlerErrorRecovery{ recovery: h.recovery, handler: h.handler.WithGroup(name), } } slog-multi-1.7.1/router.go 0000664 0000000 0000000 00000014302 15140164271 0015464 0 ustar 00root root 0000000 0000000 package slogmulti import ( "context" "fmt" "log/slog" "slices" "github.com/samber/lo" slogcommon "github.com/samber/slog-common" ) type router struct { handlers []slog.Handler firstMatch bool } // Router creates a new router instance for building conditional log routing. // This function is the entry point for creating a routing configuration. // // Example usage: // // r := slogmulti.Router(). // Add(consoleHandler, slogmulti.LevelIs(slog.LevelInfo)). // Add(fileHandler, slogmulti.LevelIs(slog.LevelError)). // Handler() // // Returns: // // A new router instance ready for configuration func Router() *router { return &router{ handlers: []slog.Handler{}, firstMatch: false, } } // 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 // 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, predicates ...func(ctx context.Context, r slog.Record) bool) *router { return &router{ handlers: append( h.handlers, &RoutableHandler{ predicates: predicates, handler: handler, groups: []string{}, attrs: []slog.Attr{}, skipPredicates: false, }, ), firstMatch: h.firstMatch, } } // Handler creates a slog.Handler from the configured router. // This method finalizes the routing configuration and returns a handler // that can be used with slog.New(). // // Returns: // // A slog.Handler that implements the routing logic func (h *router) Handler() slog.Handler { if h.firstMatch { return FirstMatch(lo.Map(h.handlers, func(h slog.Handler, _ int) *RoutableHandler { rh, ok := h.(*RoutableHandler) if !ok { panic(fmt.Sprintf("expected *RoutableHandler, got %T", h)) } return &(*rh) })...) } else { return Fanout(h.handlers...) } } func (h *router) FirstMatch() *router { return &router{ handlers: h.handlers, firstMatch: true, } } // Ensure RoutableHandler implements the slog.Handler interface at compile time var _ slog.Handler = (*RoutableHandler)(nil) // RoutableHandler wraps a slog.Handler with conditional matching logic. // 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 { // 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 groups []string // attrs contains accumulated attributes that should be added to records attrs []slog.Attr // skipPredicates indicates the caller MUST call isMatch(ctx, record) and MUST NOT invoke the handler for a given record if isMatch returns false. skipPredicates bool } // Enabled checks if the underlying handler is enabled for the given log level. // This method implements the slog.Handler interface requirement. // // Args: // // ctx: The context for the logging operation // l: The log level to check // // Returns: // // true if the underlying handler is enabled for the level, false otherwise func (h *RoutableHandler) Enabled(ctx context.Context, l slog.Level) bool { return h.handler.Enabled(ctx, l) } // Handle processes a log record if all predicates return true. // This method implements the slog.Handler interface requirement. // // Args: // // ctx: The context for the logging operation // r: The log record to process // // Returns: // // An error if the underlying handler failed to process the record, nil otherwise func (h *RoutableHandler) Handle(ctx context.Context, r slog.Record) error { if h.skipPredicates { return h.handler.Handle(ctx, r) } else { _, ok := h.isMatch(ctx, r) if ok { return h.handler.Handle(ctx, r) } } return nil } func (h *RoutableHandler) isMatch(ctx context.Context, r slog.Record) (slog.Record, bool) { clone := slog.NewRecord(r.Time, r.Level, r.Message, r.PC) clone.AddAttrs( slogcommon.AppendRecordAttrsToAttrs(h.attrs, h.groups, &r)..., ) for _, predicate := range h.predicates { if !predicate(ctx, clone) { return clone, false } } return clone, true } // WithAttrs creates a new RoutableHandler with additional attributes. // This method implements the slog.Handler interface requirement. // // The method properly handles attribute accumulation within the current group context, // ensuring that attributes are correctly applied to records when they are processed. // // Args: // // attrs: The attributes to add to the handler // // Returns: // // A new RoutableHandler with the additional attributes func (h *RoutableHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return &RoutableHandler{ predicates: h.predicates, handler: h.handler.WithAttrs(attrs), groups: slices.Clone(h.groups), attrs: slogcommon.AppendAttrsToGroup(h.groups, h.attrs, attrs...), skipPredicates: h.skipPredicates, } } // WithGroup creates a new RoutableHandler with a group name. // This method implements the slog.Handler interface requirement. // // The method follows the same pattern as the standard slog implementation: // - If the group name is empty, returns the original handler unchanged // - Otherwise, creates a new handler with the group name added to the group hierarchy // // Args: // // name: The group name to apply to the handler // // Returns: // // A new RoutableHandler with the group name, or the original handler if the name is empty func (h *RoutableHandler) WithGroup(name string) slog.Handler { // https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247 if name == "" { return h } return &RoutableHandler{ predicates: h.predicates, handler: h.handler.WithGroup(name), groups: append(slices.Clone(h.groups), name), attrs: h.attrs, skipPredicates: h.skipPredicates, } } slog-multi-1.7.1/router_predicate.go 0000664 0000000 0000000 00000014142 15140164271 0017506 0 ustar 00root root 0000000 0000000 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) } } // AttrValueIs returns a function that checks if the record has all specified attributes with exact values. // Example usage: // // r := slogmulti.Router(). // Add(consoleHandler, slogmulti.AttrValueIs("scope", "influx")). // Add(fileHandler, slogmulti.AttrValueIs("env", "production", "region", "us-east")). // Handler() // // Args: // // args: Pairs of attribute key (string) and expected value (any) // // Returns: // // A function that checks if the record has all specified attributes with exact values func AttrValueIs(args ...any) func(ctx context.Context, r slog.Record) bool { if len(args)%2 != 0 { panic("AttrValueIs requires key/value pairs") } m := map[string]any{} for i := 0; i < len(args); i += 2 { key, ok1 := args[i].(string) value := args[i+1] if !ok1 { panic("AttrValueIs requires string keys") } m[key] = value } return func(ctx context.Context, r slog.Record) bool { count := 0 r.Attrs(func(attr slog.Attr) bool { if v, ok := m[attr.Key]; ok && attr.Value.Any() == v { count++ if count == len(m) { return false // early exit } } return true }) return count == len(m) } } // AttrKindIs returns a function that checks if the record has an attribute with the given key and type. // Example usage: // // r := slogmulti.Router(). // Add(consoleHandler, slogmulti.AttrKindIs("user_id", slog.KindString)). // Add(fileHandler, slogmulti.AttrKindIs("user_id", slog.KindString)). // Handler() // // Args: // // key: The attribute key to check // ty: The attribute type to check // // Returns: // // A function that checks if the record has an attribute with the given key and type func AttrKindIs(args ...any) func(ctx context.Context, r slog.Record) bool { if len(args)%2 != 0 { panic("AttrKindIs requires key/kind pairs") } m := map[string]slog.Kind{} for i := 0; i < len(args); i += 2 { key, ok1 := args[i].(string) ty, ok2 := args[i+1].(slog.Kind) if !ok1 || !ok2 { panic("AttrKindIs requires string keys and slog.Kind values") } m[key] = ty } return func(ctx context.Context, r slog.Record) bool { count := 0 r.Attrs(func(attr slog.Attr) bool { if ty, ok := m[attr.Key]; ok && attr.Value.Kind() == ty { count++ if count == len(m) { return false // early exit } } return true }) return count == len(m) } }