Golang standard library had a logger package called log. But structural logging has become increasingly important for go developers and various 3rd party logging libraries have been created and used by people. For example, logrus has been used in 100,000 different libraries. Standard logging library with support for structured logging library log/slog is published with Go 1.21 version.
As the Trendyol Indexing team, we use zap as a logger library in our golang projects. Since we were curious about the performance of the log/slog library, I started researching the log/slog library during special interest time sessions. In light of the information we have obtained, we will try to find the answer to the question of whether we should change our logger library to log/slog.
In this article, I will provide information on the features, advantages, and disadvantages of the log/slog library.
Table Of Contents
log/slog Features
- Handler Options
- Add Custom Field
- Child Logger
- Hiding Sensitive Data
- Use slog as a Frontend
- Use slog as a Default Logger
You can look examples on my github repo.
GitHub - henesgokdag/golang-logger-compare
*Contribute to henesgokdag/golang-logger-compare development by creating an account on GitHub.*github.com
log/slog Features
Handler Options
The library is divided into frontend as Logger and backend interface as Handler. The log/slog feature comes with two different Handlers that are used to format the logs. One of these Handlers is a text handler, and the other is a JSON handler. Additionally, you can create your custom handler by using the Handler interface to implement it. Also, golang has published a guide on how to write log/slog handlers. You can reach it from here.
type Handler interface { Enabled(context.Context, Level) bool Handle(context.Context, Record) error WithAttrs(attrs []Attr) Handler WithGroup(name string) Handler }
Json Handler
Output
We didn’t see debug message in logs because we set log level to info.
{“time”:”2023–09–02T10:06:23.682743+03:00",”level”:”INFO”,”msg”:”This is an Info message”} {“time”:”2023–09–02T10:06:23.682747+03:00",”level”:”WARN”,”msg”:”This is a Warning message”} {“time”:”2023–09–02T10:06:23.68275+03:00",”level”:”ERROR”,”msg”:”This is an Error message”}
TextHandler
Output
time=2023–09–02T10:06:19.906+03:00 level=INFO msg=”This is an Info message” time=2023–09–02T10:06:19.906+03:00 level=WARN msg=”This is a Warning message” time=2023–09–02T10:06:19.906+03:00 level=ERROR msg=”This is an Error message”
Add Custom Field
You can add custom fields to your log. If we want to add a custom field to a log, there are two ways to do it.
Output
{“time”:”2023–09–02T10:09:25.279457+03:00",”level”:”INFO”,”msg”:”hello”,”test”:1} {“time”:”2023–09–02T10:09:25.280265+03:00",”level”:”INFO”,”msg”:”This is an Info message”,”test”:1}
Child Logger
If you need to choose specific attributes for all logs, you can utilize a child logger. These loggers are useful because they establish a new logging context inherited from their parent logger while also allowing for the inclusion of extra fields. In Slog, you can create child loggers by using the Logger.With() method. This method receives one or multiple key/value pairs, and generates a new Logger that includes the designated attributes.
Output
{“time”:”2023–09–02T10:09:25.280315+03:00",”level”:”INFO”,”msg”:”image upload successful”,”program_info”:{“pid”:92036,”go_version”:”go1.21rc4"},”image_id”:”39ud88"} {“time”:”2023–09–02T10:09:25.28032+03:00",”level”:”WARN”,”msg”:”storage is 90% full”,”program_info”:{“pid”:92036,”go_version”:”go1.21rc4"},”available_space”:”900.1 mb”} {"time":"2023–09–02T10:09:25.280315+03:00","level":"INFO","msg":"image upload successful","program_info":{"pid":92036,"go_version":"go1.21rc4"},"image_id":"39ud88"} {"time":"2023–09–02T10:09:25.28032+03:00","level":"WARN","msg":"storage is 90% full","program_info":{"pid":92036,"go_version":"go1.21rc4"},"available_space":"900.1 mb"}
Hiding Sensitive Data
The LogValuer interface lets you choose the type of output you want to generate, instead of logging certain fields. This interface can be used to hiding sensitive data. By doing so, you can prevent personal details like name, surname, and password from being displayed in the logs.
type LogValuer interface { LogValue() Value }
Example
Output
{“time”:”2023–09–14T10:04:51.272601+03:00",”level”:”INFO”,”msg”:”info”,”user”:”user-12234"}
Use slog as a Frontend
I mentioned above that the library uses the logger interface for the frontend and the handler interface for the backend. That way, existing logging packages can talk to a common backend, so the packages that use them can interoperate without having to be rewritten. Handlers are written or in progress for many common logging packages, including Zap, logr and hclog.
Output
{“level”:”info”,”ts”:1694675091.2726688,”msg”:”Using Slog frontend with Zap backend!”,”process_id”:71993}
Use slog as a Default Logger
Also, you can use slog as a default logger.
Output
{“time”:”2023–09–02T10:09:25.280487+03:00",”level”:”INFO”,”msg”:”Hello from old logger”}
Benchmark Results
I used 5 different logger libraries in benchmark tests and I did 2 different tests with struct and without struct.
With Struct Benchmark Result
I used the following struct in the tests.
package util
type Person struct { Name string Age int Address string }
type Car struct { Brand string Model string Year int }
type ExampleStruct struct { PeopleList []Person CarList []Car Field1 int64 Field2 string Field3 float64 Field4 bool Field5 []byte Field6 map[string]int Field7 [1000]int Field8 []*ExampleStruct Field9 interface{} Field10 int }
func getExampleData() ExampleStruct { people := []Person{ {Name: "John", Age: 30, Address: "London"}, {Name: "Michael", Age: 25, Address: "Manchester"}, {Name: "Jane", Age: 28, Address: "Liverpool"}, {Name: "Jane", Age: 28, Address: "Liverpool"}, {Name: "Jane", Age: 28, Address: "Liverpool"}, {Name: "Jane", Age: 28, Address: "Liverpool"}, {Name: "Jane", Age: 28, Address: "Liverpool"}, }
cars := []Car{ {Brand: "Toyota", Model: "Corolla", Year: 2020}, {Brand: "Honda", Model: "Civic", Year: 2019}, {Brand: "Honda", Model: "Civic", Year: 2019}, {Brand: "Honda", Model: "Civic", Year: 2019}, {Brand: "Honda", Model: "Civic", Year: 2019}, {Brand: "Honda", Model: "Civic", Year: 2019}, {Brand: "Honda", Model: "Civic", Year: 2019}, {Brand: "Honda", Model: "Civic", Year: 2019}, }
return ExampleStruct{ PeopleList: people, CarList: cars, Field1: 1234567890, Field2: "This is a huge struct example", Field3: 3.141592653589793, Field4: true, Field5: []byte{1, 2, 3, 4, 5}, Field6: map[string]int{"a": 1, "b": 2, "c": 3}, Field7: [1000]int{}, // Initializing an array with 1000 elements Field8: []*ExampleStruct{}, Field9: nil, Field10: 1, } }
Without Struct Benchmark Results
I used the following model in the tests.
logger.Debug("This is a Debug message") logger.Info("This is an Info message") logger.Warn("This is a Warning message") logger.Error("This is an Error message")
You can also take a look at the benchmark made by the Zap library here.
Pros
- A more performant handler can be written because it has its own interface.
- log/slog golang’s stlib.
- In benchmarks, log/slog is more performant than apex and logrus libraries.
- SlogJsonHandler’s number of allocations per operation value in tests with struct is much lower than other libraries.
Cons
- Extremely slow compared to Zap in benchmarks tests without structs. (This is very big problem because google said “We found that over 95% of calls to logging methods pass five or fewer attributes.”)
Conclucion
As a result, if you are going to start a new project, you can try the log/slog package or if you are using logrus, etc. as a log package in an existing project, you can consider converting to log/slog. Since we have zap in our current projects, which is more performant, I will not recommend a change to my team. Maybe in the future, I can recommend a structure where we use zap as a backend with a more performant handler.
I would like to thank the indexing team members for making the article better with their feedback.