GitHub - samber/slog-multi: 🚨 Design workflows of slog handlers: pipeline, middleware, fanout, routing, failover, load balancing... (original) (raw)

slog-multi: Advanced Handler Composition for Go's Structured Logging (pipelining, fanout, routing, failover...)

tag Go Version GoDoc Build Status Go report Coverage 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

Middlewares:

See also:

HTTP middlewares:

Loggers:

Log sinks:

πŸš€ Installation

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

Broadcast: slogmulti.Fanout()

Distribute logs to multiple slog.Handler in parallel for maximum throughput and redundancy.

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:

{ "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.

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:

Failover: slogmulti.Failover()

Ensure logging reliability by trying multiple handlers in order until one succeeds. Perfect for high-availability scenarios.

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:

Load balancing: slogmulti.Pool()

Distribute logging load across multiple handlers using round-robin with randomization to increase throughput and provide redundancy.

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:

Recover errors: slogmulti.RecoverHandlerError()

Gracefully handle logging failures without crashing the application. Catches both panics and errors from handlers.

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.

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:

{ "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:

πŸ”§ Advanced Patterns

Custom middleware

Middleware must match the following prototype:

type Middleware func(slog.Handler) slog.Handler

The example above uses:

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.

mdw := slogmulti.NewHandleInlineHandler( // simulate "Handle()" method func(ctx context.Context, groups []string, attrs []slog.Attr, record slog.Record) error { // Custom logic here // [...] return nil }, )

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

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

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

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

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.

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

Error Handling

Security and Privacy

Monitoring and Observability

🀝 Contributing

Don't hesitate ;)

Install some dev dependencies

make tools

Run tests

make test

or

make watch-test

πŸ‘€ Contributors

Contributors

πŸ’« Show your support

If this project helped you, please give it a ⭐️ on GitHub!

GitHub Sponsors

πŸ“ License

Copyright Β© 2023 Samuel Berthe.

This project is MIT licensed.