slog: Handler chain, fanout, failover, load balancing...
Design workflows of slog handlers:
- fanout: distribute
log.Record
to multipleslog.Handler
in parallel - pipeline: rewrite
log.Record
on the fly (eg: for privacy reason) - failover: forward
log.Record
to the first availableslog.Handler
- load balancing: increase log bandwidth by sending
log.Record
to a pool ofslog.Handler
Here a simple workflow with both pipeline and fanout:
See also:
- slog-datadog: A
slog
handler forDatadog
- slog-logstash: A
slog
handler forLogstash
- slog-slack: A
slog
handler forSlack
- slog-loki: A
slog
handler forLoki
- slog-sentry: A
slog
handler forSentry
- slog-fluentd: A
slog
handler forFluentd
- slog-syslog: A
slog
handler forSyslog
🚀
Install
go get github.com/samber/slog-multi
Compatibility: go >= 1.20.1
This library is v0 and follows SemVer strictly. On slog
final release (go 1.21), this library will go v1.
No breaking changes will be made to exported APIs before v1.0.0.
💡
Usage
GoDoc: https://pkg.go.dev/github.com/samber/slog-multi
slogmulti.Fanout()
Broadcast: Distribute logs to multiple slog.Handler
in parallel.
import (
slogmulti "github.com/samber/slog-multi"
"golang.org/x/exp/slog"
)
func main() {
logstash, _ := net.Dial("tcp", "logstash.acme:4242") // use github.com/netbrain/goautosocket for auto-reconnect
stderr := os.Stderr
logger := slog.New(
slogmulti.Fanout(
slog.HandlerOptions{}.NewJSONHandler(logstash), // pass to first handler: logstash over tcp
slog.HandlerOptions{}.NewTextHandler(stderr), // then to second handler: stderr
// ...
),
)
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.Failover()
Failover: List multiple targets for a slog.Record
instead of retrying on the same unavailable log management system.
import (
"net"
slogmulti "github.com/samber/slog-multi"
"golang.org/x/exp/slog"
)
func main() {
// 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), // send to this instance first
slog.HandlerOptions{}.NewJSONHandler(logstash2), // then this instance in case of failure
slog.HandlerOptions{}.NewJSONHandler(logstash3), // and finally this instance in case of double failure
),
)
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")
}
slogmulti.Pool()
Load balancing: Increase log bandwidth by sending log.Record
to a pool of slog.Handler
.
import (
"net"
slogmulti "github.com/samber/slog-multi"
"golang.org/x/exp/slog"
)
func main() {
// 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
slog.HandlerOptions{}.NewJSONHandler(logstash1),
slog.HandlerOptions{}.NewJSONHandler(logstash2),
slog.HandlerOptions{}.NewJSONHandler(logstash3),
),
)
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")
}
slogmulti.Pipe()
Chaining: Rewrite log.Record
on the fly (eg: for privacy reason).
func main() {
// first middleware: format go `error` type into an object {error: "*myCustomErrorType", message: "could not reach https://a.b/c"}
errorFormattingMiddleware := slogmulti.NewHandleInlineMiddleware(errorFormattingMiddleware)
// second middleware: remove PII
gdprMiddleware := NewGDPRMiddleware()
// final handler
sink := slog.HandlerOptions{}.NewJSONHandler(os.Stderr)
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":{
"id":"*******",
"email":"*******",
"created_at":"*******"
},
"environment":"dev",
"foo":"bar",
"error":{
"type":"*myCustomErrorType",
"message":"an error"
}
}
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, instead of this
.
Inline middleware
An "inline middleware" (aka. lambda), is a shortcut to middleware implementation, that hooks a single method and proxies others.
// hook `logger.Enabled` method
mdw := slogmulti.NewEnabledInlineMiddleware(func(ctx context.Context, level slog.Level, next func(context.Context, slog.Level) bool) bool{
// [...]
return next(ctx, level)
})
// hook `logger.Handle` method
mdw := slogmulti.NewHandleInlineMiddleware(func(ctx context.Context, record slog.Record, next func(context.Context, slog.Record) error) error {
// [...]
return next(ctx, record)
})
// hook `logger.WithAttrs` method
mdw := slogmulti.NewWithAttrsInlineMiddleware(func(attrs []slog.Attr, next func([]slog.Attr) slog.Handler) slog.Handler{
// [...]
return next(attrs)
})
// hook `logger.WithGroup` method
mdw := slogmulti.NewWithGroupInlineMiddleware(func(name string, next func(string) slog.Handler) slog.Handler{
// [...]
return next(name)
})
A super inline middleware that hooks all methods.
Warning: you would rather implement your own middleware.
mdw := slogmulti.NewInlineMiddleware(
func(ctx context.Context, level slog.Level, next func(context.Context, slog.Level) bool) bool{
// [...]
return next(ctx, level)
},
func(ctx context.Context, record slog.Record, next func(context.Context, slog.Record) error) error{
// [...]
return next(ctx, record)
},
func(attrs []slog.Attr, next func([]slog.Attr) slog.Handler) slog.Handler{
// [...]
return next(attrs)
},
func(name string, next func(string) slog.Handler) slog.Handler{
// [...]
return next(name)
},
)
🤝
Contributing
- Ping me on twitter @samuelberthe (DMs, mentions, whatever :))
- Fork the project
- Fix open issues or request new features
Don't hesitate ;)
# Install some dev dependencies
make tools
# Run tests
make test
# or
make watch-test
👤
Contributors
💫
Show your support
Give a
📝
License
Copyright © 2023 Samuel Berthe.
This project is MIT licensed.
slog/multi: Enabled should look at handlers' Enabled
MultiHandler.Enabled should return true only if at least one of its contained handlers' Enabled methods returns true.
(Although that is only an optimization and does not affect correctness.)