I was doing sharing session this week related to structured logging and how we can utilize it in my team at works. My team has already used Zerolog, but I guess it would be no harm if we update the knowledge regarding a new library more over, because it is Golang standard library which we don’t need to depend to a 3rd party library. Here’s the presentation I created.
Introduction
Golang has introduced their first structured logging library when they released go 1.20.x as an experimental library. Then they included it as their standard library in the release of 1.21.x. The API of the structured logging is quite easy to understand it is similar to the popular structured logging library out there like Zerolog or Logrus.
Code Example
Golang log/slog is integrated with log library as its default logger, but you can also create a new text/JSON logging handler or create your own handler by just implementing its interfaces. Below is the code example on how to use log/slog:
1
2
3
4
5
6
7
8
9
10
11
12
13
var (
textLogger *slog.Logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{}))
jsonLogger *slog.Logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{}))
)
funcmain() {
slog.Info("this is info using default logger")
textLogger.Warn("this is warning", slog.Int("response_code", http.StatusNotFound))
jsonLogger.Error("this is error",
slog.String("status", http.StatusText(http.StatusInternalServerError)),
slog.Bool("is_json", true),
)
}
slog.Info/Warn/Error means that you will send an info/warn/error level logs, followed with the message and the attributes (slog.String/Bool/Int). The output of code above will be like:
1
2
3
2009/11/10 23:00:00 INFO this is info using default logger
time=2009-11-10T23:00:00.000Z level=WARN msg="this is warning"response_code=404{"time":"2009-11-10T23:00:00Z","level":"ERROR","msg":"this is error","status":"Internal Server Error","is_json":true}
Allocations to be Concerned
The log/slog API is very flexible that you can put your log attributes as a key/pair variadic arguments, but please be aware that it might introduce extra allocations:
1
2
3
4
5
6
7
8
9
10
11
funcmain() {
slog.Info("❌ have more allocations since we don't define the type",
"key", "value",
"key2", time.Minute,
)
slog.LogAttrs(context.Background(), slog.LevelInfo,
"✅ avoid extra allocations",
slog.String("key", "value"),
slog.Duration("key2", time.Minute),
)
}
Output:
1
2
2009/11/10 23:00:00 INFO ❌ have more allocations since we don't define the typekey=value key2=1m0s
2009/11/10 23:00:00 INFO ✅ avoid extra allocations key=value key2=1m0s
Both code above will output the same thing, the only difference is you don’t create the attributes using slog.LogAttrs. Here’s the benchmark result:
1
2
3
4
5
6
7
8
9
benchstat keyval.txt attr.txt
name old time/op new time/op delta
Slog-10 1.39µs ± 1% 1.45µs ± 2% +4.37% (p=0.000 n=10+9)name old alloc/op new alloc/op delta
Slog-10 734B ± 2% 676B ± 1% -7.90% (p=0.000 n=10+9)name old allocs/op new allocs/op delta
Slog-10 7.00 ± 0% 5.00 ± 0% -28.57% (p=0.000 n=10+10)
Derive Logger from another Logger
You can also derive a logger from one logger and keep the atrributes, let’s say you have a complex nested struct where you want to keep atrributes from parent struct inside their child struct.
1
2
3
4
5
funcmain() {
logger2 := slog.With(slog.Int("num", 3))
logger2.Info("whenever logger2 is called, you can see the num attribute derived")
}
The output will be similar to this:
1
2009/11/10 23:00:00 INFO whenever logger2 is called, you can see the num attribute derived num=3
Logger Group
You can also group logger attributes so you could know the scope of those attributes
Send context in case your handler handles the context (tracing, metrics, etc)
Define the slog.Level
Define the message
Put contextual attributes
Group attributes together if needed
Use Case
Dynamic Log Level
Dynamic log level means you can change your log level at runtime level without needing any re-deployment
1
2
3
4
5
6
7
8
9
10
11
var (
level = new(slog.LevelVar)
logger = slog.New(slog.NewTextHandler(
os.Stdout, &slog.HandlerOptions{Level: level}))
)
funcmain() {
logger.Debug("won't be shown") // by default log level is INFO
level.Set(slog.LevelDebug) // change it to DEBUG
logger.Debug("shown")
}
You can manipulate the atrributes value by utilizing ReplaceAttr inside the slog.HandlerOptions struct.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var (
logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: func(g []string, a slog.Attr) slog.Attr {
if"password"== a.Key {
a.Value = slog.StringValue("[PII DATA]")
}
return a
}
}))
)
funcmain() {
logger.Info("password will be redacted",
slog.String("password", "P@$$w0rd?!"))
}
Output:
1
time=2009-11-10T23:00:00.000Z level=INFO msg="password will be redacted"password="[PII DATA]"
Custom Handler
By default, log/slog provides you with three handler, the default, text, and JSON. You can create your own custom handler by just implementing their interfaces.
1
2
3
4
5
6
7
8
type CustomHandler struct {
slog.Handler
}
func (c *CustomHandler) Handle(ctx context.Context, r slog.Record) error {
// handle context here for tracing, monitoring, logging, etc
return c.Handler.Handle(ctx, r)
}
Here’s the reference on how to do it. Perhaps I’ll cover this in another article.
Thank you for reading!
···
Love This Content?
Any kind of supports is greatly appreciated! Kindly support me via Bitcoin, Ko-fi, Trakteer, or just continue to read another content. You can write a response via Webmention and let me know the URL via Telegraph.