Embracing Go 1.21.0's slog: A Unified Logging Interface with Benchmarks against zerolog and zap
The release of Go 1.21.0 marks an exciting moment in the Go community, with the introduction of a new logging package, log/slog
. This addition brings more choices to developers for structured logging and raises the question: How does it compare with existing solutions like zerolog
and uber-go/zap
?
In this article, we will highlight the slog
package and use it as a basis to explore a unified logging strategy using interfaces. Then, we'll benchmark slog
against zerolog
and zap
to see how it stacks up in terms of performance.
Introducing slog
in Go 1.21.0
The new slog
package comes as a part of the standard library, offering a zero-allocation logger optimized for performance and structured logging. Its interface and usage align with modern logging requirements, enabling developers to easily incorporate it into their existing applications.
The Importance of Interfaces in Logging
Interfaces in Go provide a way to specify the behavior that types must implement, without detailing how they should do so. When it comes to logging, interfaces enable us to:
Define a standard logging behavior across our application.
Achieve modularity and flexibility by allowing multiple logger implementations.
Make unit testing easier by mocking the logger during tests.
Defining a Logging Interface
Let's start by defining a simple logging interface:
// Logger defines the standard behavior for our loggers.
type Logger interface {
Debug(msg string, keysAndValues ...interface{})
Info(msg string, keysAndValues ...interface{})
Warn(msg string, keysAndValues ...interface{})
Error(msg string, keysAndValues ...interface{})
Fatal(msg string, keysAndValues ...interface{})
}
The interface specifies a set of methods, each corresponding to a log level. The keysAndValues
variadic parameter allows us to pass structured data to our logs.
Implementing the Logger with ZeroLog and Zap
zerolog
is a zero-allocation JSON logger for Go that is both fast and reliable. It's a great choice when you need performance and structured logging. Let's implement our logger interface using ZeroLog:
package logger
import (
"os"
"github.com/rs/zerolog"
)
type ZeroLogger struct {
log zerolog.Logger
}
func NewZeroLogger() Log {
zl := zerolog.New(os.Stdout).With().Timestamp().Logger()
return &ZeroLogger{log: zl}
}
func (zl *ZeroLogger) Debug(msg string, keysAndValues ...interface{}) {
zl.log.Debug().Fields(keysAndValues).Msg(msg)
}
func (zl *ZeroLogger) Info(msg string, keysAndValues ...interface{}) {
zl.log.Info().Fields(keysAndValues).Msg(msg)
}
func (zl *ZeroLogger) Warn(msg string, keysAndValues ...interface{}) {
zl.log.Warn().Fields(keysAndValues).Msg(msg)
}
func (zl *ZeroLogger) Error(msg string, keysAndValues ...interface{}) {
zl.log.Error().Fields(keysAndValues).Msg(msg)
}
func (zl *ZeroLogger) Fatal(msg string, keysAndValues ...interface{}) {
zl.log.Fatal().Fields(keysAndValues).Msg(msg)
}
package logger
import (
"go.uber.org/zap"
)
type ZapLogger struct {
log *zap.SugaredLogger
}
func NewZapLogger() Log {
logger, _ := zap.NewProduction()
sugar := logger.Sugar()
return &ZapLogger{log: sugar}
}
func (zl *ZapLogger) Debug(msg string, keysAndValues ...interface{}) {
zl.log.Debugw(msg, keysAndValues...)
}
func (zl *ZapLogger) Info(msg string, keysAndValues ...interface{}) {
zl.log.Infow(msg, keysAndValues...)
}
func (zl *ZapLogger) Warn(msg string, keysAndValues ...interface{}) {
zl.log.Warnw(msg, keysAndValues...)
}
func (zl *ZapLogger) Error(msg string, keysAndValues ...interface{}) {
zl.log.Errorw(msg, keysAndValues...)
}
func (zl *ZapLogger) Fatal(msg string, keysAndValues ...interface{}) {
zl.log.Fatalw(msg, keysAndValues...)
}
With this implementation, we've married the power and efficiency of ZeroLog with the flexibility of our defined logging interface.
Implementing a Unified Logging Interface with slog
Using interfaces to manage logging is a powerful pattern. Here's how we can include slog
into an interface-based logging strategy:
package logger
import (
"log"
"log/slog"
"os"
)
type SlogLogger struct {
log *slog.Logger
}
func NewSlogLogger() Log {
sl := slog.New(slog.NewJSONHandler(os.Stdout, nil))
return &SlogLogger{log: sl}
}
func (zl *SlogLogger) Debug(msg string, keysAndValues ...interface{}) {
zl.log.Debug(msg, keysAndValues...)
}
func (zl *SlogLogger) Info(msg string, keysAndValues ...interface{}) {
zl.log.Info(msg, keysAndValues...)
}
func (zl *SlogLogger) Warn(msg string, keysAndValues ...interface{}) {
zl.log.Warn(msg, keysAndValues...)
}
func (zl *SlogLogger) Error(msg string, keysAndValues ...interface{}) {
zl.log.Error(msg, keysAndValues...)
}
func (zl *SlogLogger) Fatal(msg string, keysAndValues ...interface{}) {
zl.log.Error(msg, keysAndValues...)
log.Fatal(msg)
}
Benchmarking slog
, zerolog
, and zap
A critical aspect of evaluating slog
is understanding its performance compared to existing solutions. We'll conduct a benchmark against zerolog
and zap
.
Benchmark Setup:
We'll define benchmarks for slog
, zerolog
, and zap
using the same logging interface, as shown earlier in the article.
package main
import (
"testing"
"github.com/dwarvesf/test-log/logger"
)
func BenchmarkZeroLog(b *testing.B) {
log := logger.NewZeroLogger()
benchmarkLogger(log, b)
}
func BenchmarkSlog(b *testing.B) {
log := logger.NewSlogLogger()
benchmarkLogger(log, b)
}
func BenchmarkZapLog(b *testing.B) {
log := logger.NewZapLogger()
benchmarkLogger(log, b)
}
func benchmarkLogger(log logger.Log, b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
log.Info("Benchmarking log performance", "iteration", i)
}
}
Benchmark Results:
After running the benchmarks, you may get results like:
goos: darwin
goarch: arm64
pkg: github.com/dwarvesf/test-log
BenchmarkZeroLog-8 6475746 174.4 ns/op 40 B/op 2 allocs/op
BenchmarkZeroLog-8 6915920 173.6 ns/op 40 B/op 2 allocs/op
BenchmarkZeroLog-8 6898648 173.7 ns/op 40 B/op 2 allocs/op
BenchmarkZeroLog-8 6909384 173.8 ns/op 40 B/op 2 allocs/op
BenchmarkZeroLog-8 6790539 175.2 ns/op 40 B/op 1 allocs/op
BenchmarkSlog-8 2070051 585.2 ns/op 40 B/op 1 allocs/op
BenchmarkSlog-8 2051962 580.0 ns/op 40 B/op 1 allocs/op
BenchmarkSlog-8 2061958 582.1 ns/op 40 B/op 1 allocs/op
BenchmarkSlog-8 2066503 580.5 ns/op 40 B/op 1 allocs/op
BenchmarkSlog-8 2067446 579.4 ns/op 40 B/op 1 allocs/op
BenchmarkZapLog-8 3109299 385.1 ns/op 168 B/op 3 allocs/op
BenchmarkZapLog-8 3103274 385.7 ns/op 168 B/op 3 allocs/op
BenchmarkZapLog-8 3100112 385.8 ns/op 168 B/op 3 allocs/op
BenchmarkZapLog-8 3109857 385.8 ns/op 168 B/op 3 allocs/op
BenchmarkZapLog-8 3101862 386.1 ns/op 168 B/op 3 allocs/op
PASS
ok github.com/dwarvesf/test-log 23.824s
We ran multiple iterations for each logging library. Here are the summarized results:
zerolog
:
Average Operation Time: 174.1 ns/op
Memory Allocated per Operation: 40 B/op
Number of Allocations per Operation: 1.8 allocs/op
slog
:
Average Operation Time: 581.4 ns/op
Memory Allocated per Operation: 40 B/op
Number of Allocations per Operation: 1 allocs/op
zap
:
Average Operation Time: 385.9 ns/op
Memory Allocated per Operation: 168 B/op
Number of Allocations per Operation: 3 allocs/op
Analysis:
Performance:
zerolog
emerges as the fastest logging library in terms of the operation time, followed closely byzap
. The new kid on the block,slog
, is somewhat behind in this metric.Memory Footprint: Both
slog
andzerolog
are efficient, allocating only 40 B/op. In comparison,zap
requires a significantly larger memory footprint, with 168 B/op.Allocations:
slog
has the fewest allocations per operation, closely followed byzerolog
.zap
requires the most, with 3 allocs/op.
Conclusion
While slog
might not be the fastest logger in the block, but it offers efficient memory usage and fewer allocations. This might make it a preferred choice for scenarios where memory efficiency is more critical than sheer speed.
zerolog
proves to be a strong performer, being both fast and memory-efficient. On the other hand, zap
, while being faster than slog
, requires more memory and makes more allocations.
The introduction of slog
in Go 1.21.0 invites developers to consider and balance their needs between speed, memory usage, and allocations. As always, the "best" logger depends on the specific use case and requirements of the application. This benchmark simply adds more data for making an informed decision in the evolving world of Go logging.
Contributing
At Dwarves, we encourage our people to read, write, share what we learn with others, and contributing to the Brainery is an important part of our learning culture. For visitors, you are welcome to read them, contribute to them, and suggest additions. We maintain a monthly pool of $1500 to reward contributors who support our journey of lifelong growth in knowledge and network.
Love what we are doing?
Check out our products
Hire us to build your software
Join us, we are also hiring
Visit our Discord Learning Site
Visit our GitHub
Subscribe to my newsletter
Read articles from Hieu Phan Quang directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by