π¨ Design workflows of slog handlers: pipeline, middleware, fanout, routing, failover, load balancing...
See the codeslog-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.
Middlewares:
See also:
slog.Handler chaining, fanout, routing, failover, load balancing...slog attribute formattingslog sampling policyslog.Handler for test purposesHTTP middlewares:
slog loggerslog loggerslog loggerslog loggernet/http middleware for slog loggerLoggers:
slog handler for Zapslog handler for Zerologslog handler for LogrusLog sinks:
slog handler for Datadogslog handler for Betterstackslog handler for Rollbarslog handler for Lokislog handler for Sentryslog handler for Syslogslog handler for Logstashslog handler for Fluentdslog handler for Graylogslog handler for Quickwitslog handler for Slackslog handler for Telegramslog handler for Mattermostslog handler for Microsoft Teamsslog handler for Webhookslog handler for Kafkaslog handler for NATSslog handler for Parquet + Object Storageslog handler for Go channelsgo 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.
GoDoc: https://pkg.go.dev/github.com/samber/slog-multi
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"
}
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:
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.
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")
}
Level predicates:
LevelIs(levels ...slog.Level) - Match specific log levelsLevelIsNot(levels ...slog.Level) - Exclude specific log levelsMessage predicates:
MessageIs(msg string) - Exact message matchMessageIsNot(msg string) - Message doesn't matchMessageContains(part string) - Message contains substringMessageNotContains(part string) - Message doesn't contain substringAttribute predicates:
AttrValueIs(key, value, ...) - Check attributes have exact valuesAttrKindIs(key, kind, ...) - Check attributes have specific typesslogmulti.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:
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:
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="********"
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:
Middleware must match the following prototype:
type Middleware func(slog.Handler) slog.Handler
The example above uses:
Note:
WithAttrsandWithGroupmethods of custom middleware must return a new instance, notthis.
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 provides shortcuts to implement middleware functions that hook specific methods.
Enabled() Methodmiddleware := 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)
})
Handle() Methodmiddleware := 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)
})
WithAttrs() Methodmdw := 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)
})
WithGroup() Methodmdw := slogmulti.NewWithGroupInlineMiddleware(func(name string, next func(string) slog.Handler) slog.Handler{
// Add prefix to group names
prefixedName := "app." + name
return next(name)
})
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)
},
)
RecoverHandlerErrorDon't hesitate ;)
# Install some dev dependencies
make tools
# Run tests
make test
# or
make watch-test
If this project helped you, please give it a βοΈ on GitHub!
Copyright Β© 2023 Samuel Berthe.
This project is MIT licensed.
610 followers Β· starred May 2024
258 followers Β· starred Nov 2023
471 followers Β· starred Aug 2023
427 followers Β· starred Feb 2025
Go
98.4%
Makefile
1.6%
π¨ Design workflows of slog handlers: pipeline, middleware, fanout, routing, failover, load balancing...
See the codeslog-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.
Middlewares:
See also:
slog.Handler chaining, fanout, routing, failover, load balancing...slog attribute formattingslog sampling policyslog.Handler for test purposesHTTP middlewares:
slog loggerslog loggerslog loggerslog loggernet/http middleware for slog loggerLoggers:
slog handler for Zapslog handler for Zerologslog handler for LogrusLog sinks:
slog handler for Datadogslog handler for Betterstackslog handler for Rollbarslog handler for Lokislog handler for Sentryslog handler for Syslogslog handler for Logstashslog handler for Fluentdslog handler for Graylogslog handler for Quickwitslog handler for Slackslog handler for Telegramslog handler for Mattermostslog handler for Microsoft Teamsslog handler for Webhookslog handler for Kafkaslog handler for NATSslog handler for Parquet + Object Storageslog handler for Go channelsgo 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.
GoDoc: https://pkg.go.dev/github.com/samber/slog-multi
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"
}
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:
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.
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")
}
Level predicates:
LevelIs(levels ...slog.Level) - Match specific log levelsLevelIsNot(levels ...slog.Level) - Exclude specific log levelsMessage predicates:
MessageIs(msg string) - Exact message matchMessageIsNot(msg string) - Message doesn't matchMessageContains(part string) - Message contains substringMessageNotContains(part string) - Message doesn't contain substringAttribute predicates:
AttrValueIs(key, value, ...) - Check attributes have exact valuesAttrKindIs(key, kind, ...) - Check attributes have specific typesslogmulti.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:
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:
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="********"
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:
Middleware must match the following prototype:
type Middleware func(slog.Handler) slog.Handler
The example above uses:
Note:
WithAttrsandWithGroupmethods of custom middleware must return a new instance, notthis.
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 provides shortcuts to implement middleware functions that hook specific methods.
Enabled() Methodmiddleware := 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)
})
Handle() Methodmiddleware := 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)
})
WithAttrs() Methodmdw := 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)
})
WithGroup() Methodmdw := slogmulti.NewWithGroupInlineMiddleware(func(name string, next func(string) slog.Handler) slog.Handler{
// Add prefix to group names
prefixedName := "app." + name
return next(name)
})
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)
},
)
RecoverHandlerErrorDon't hesitate ;)
# Install some dev dependencies
make tools
# Run tests
make test
# or
make watch-test
If this project helped you, please give it a βοΈ on GitHub!
Copyright Β© 2023 Samuel Berthe.
This project is MIT licensed.
610 followers Β· starred May 2024
258 followers Β· starred Nov 2023
471 followers Β· starred Aug 2023
427 followers Β· starred Feb 2025
Go
98.4%
Makefile
1.6%