Side note: Centralize your Go logs with Better Stack
Head over to Better Stack and start shipping logs from any Go logging framework—Slog, Zerolog, Zap, or others.
There's probably a 99% chance that if you're logging in Go, you're using a
third-party logging framework since the built-in log package lacks even the
most basic features required for production logging.
This recently changed with the release of Go 1.21 where the new log/slog package for structured, leveled, and context-aware logging was one of the major highlights.
Since the Go ecosystem has already spawned many several comprehensive logging solutions, you might be wondering whether the Slog package is the harbinger of obsolescence for its predecessors, or merely another versatile tool in your logging arsenal.
This article aims to uncover some insights that should help you clarify that question by comparing nine prominent Go logging packages.
Head over to Better Stack and start shipping logs from any Go logging framework—Slog, Zerolog, Zap, or others.
The following benchmark results were observed by running a subset of our custom benchmarking suite on my local machine:
| Package | Iterations | Time | Objects Allocated |
|---|---|---|---|
| Phuslu/log | 44559532 | 27 ns/op | 0 allocs/op |
| Zerolog | 42537776 | 30 ns/op | 0 allocs/op |
| Zap | 17901230 | 71 ns/op | 0 allocs/op |
| Zap (sugared) | 14898670 | 87 ns/op | 1 allocs/op |
| Slog (with Zap Backend) | 1005426 | 121.9 ns/op | 0 allocs/op |
| Logf | 6770377 | 163.5 ns/op | 0 allocs/op |
| Slog | 7105790 | 174 ns/op | 0 allocs/op |
| Apex/log | 138122 | 870 ns/op | 5 allocs/op |
| Logrus | 658953 | 2231 ns/op | 23 allocs/op |
| Log15 | 505850 | 21997 ns/op | 20 allocs/op |
See the full results here.
Zerolog is currently the fastest structured logging framework for Go programs. It offers excellent performance for high-load systems with minimal to zero heap allocations.
Here's the simplest example of using Zerolog to log a message:
The resulting log record will be in JSON format:
Zerolog can also be configured to produce binary logs in the
CBOR format (for reducing log storage requirements) but it
does not support any other log format. In development
environments where log readability is essential, you can use the ConsoleWriter
type to log in a human-friendly, colorized output:
The ability to add contextual properties to your log records in the form of
key/value pairs is fully supported. It also provides helpers for saving and
retrieving a logger from a context.Context instance making it easy to
contextualize your logging in the application.
Here's a basic example demonstrating this feature:
Zerolog offers other notable features, including log volume reduction through sampling, the ability to execute code before logging for transformation, transportation or filtration purposes, logging to multiple destinations, and automatic collection and formatting of stack traces when used with libraries like pkgerrors or xerrors.
See our complete guide to Zerolog to learn more and view more complex usage examples.
Zap grew out of Uber's frustration with the slow logging times recorded on their high-performance servers. It pioneered the minimal allocations approach that was eventually adopted and improved on by Zerolog, although the latter gave up some flexibility to achieve the performance gains.
Zap closely follows Zerolog in terms of performance and memory allocations, but outshines it when it comes to customization capabilities, allowing you to closely tailor the framework to your specific needs.
This high level of customization is achieved through the Zapcore package, which defines and implements the low-level interfaces on which Zap is built. By creating alternate implementations for these interfaces, you have the power to customize and extend Zap's behavior in any way you desire.
Getting started with logging in Zap is made easier with the availability of
development and production presets. The former logs at the DEBUG level using a
human-friendly format, while the production preset logs in JSON format at the
INFO level. Unlike most logging frameworks, a pre-configured global logger is
not automatically available. It needs to be explicitly initialized before use.
Zap also has a unique approach to its logging APIs. It provides a base Logger
type which provides type-safety and the best possible performance at the cost of
verbosity, and the SugaredLogger type which wraps the Logger functionality
in a slower but less verbose API. You can use both APIs in the same codebase and
convert between them freely as you wish.
Ensure to check out our comprehensive guide to Zap for more details.
Slog is the new built-in logging framework that
just landed in Go 1.21, residing at
log/slog. It aims to address the need for a high-performance, structured, and
leveled logging solution in the Go standard library. While it provides a
pre-configured global logger like the log package, the default output format
is not structured. However, creating a custom Logger instance allows you to
change this behavior easily.
To understand how Slog works, there are three key aspects:
Logger frontend that provides level methods such as Info(),
Warn(), and Error() for recording various events in the program.Logger is represented by the Record type.Handler type serves as the backend aspect of Slog. It determines how
the log records are formatted and processed.Slog provides two built-in handlers by default: TextHandler and JSONHandler.
The former produces Logfmt output, while the latter produces line-delimited
JSON. You can also use any alternative backends produced by the community or
create your own from scratch as you see fit.
You even also use existing third-party logging frameworks as backends for Slog
as long as they implement the Handler interface. Here's an example that uses
Zap's official slog.Handler implementation:
When it comes to logging APIs, Slog draws some inspiration from Zap. First, it allows you to provide alternating key/value pairs for contextual logging. Alternatively, you can opt for strongly typed attributes, which minimize allocations and locking but increase verbosity. You can see a comparison of the two approaches below:
Learn more: Logging in Go with Slog
Phuslu/log (hereafter referred to as Phuslog) is a high-performance logging framework that is highly inspired by Zerolog, even sharing a largely similar API:
However, it aims to improve logging flexibility in log formatting and output without compromising on performance. While Zerolog only offers JSON and CBOR output, Phuslog supports Logfmt and TSV (Tab-Separated Values) in addition to JSON:
The following writers are provided to help you easily transmit your logs wherever you want:
ConsoleWriter: Logs a colorized, semi-structured, and human-friendly format
to the underlying io.Writer.FileWriter: Logs to an auto-rotating file (by time or size).MultiLevelWriter: Dispatch logs to a multiple writersSyslogWriter: Writes logs to Syslog.JournalWriter: Logs to the Linux Systemd
journal.EventlogWriter: Logs to the Windows event log.AsyncWriter: Buffers logs to a data channel, and writes them asynchronously.It also includes helper functions for accessing the currently executing Goroutine ID, creating tracing IDs, and more. Learn more about Phuslog by checking out it's GitHub repo
Logrus is one of the early pioneers of
structured logging in Go, but it's now no longer being actively developed. It
preserved compatibility with the built-in log package but added log
levels, contextual logging, customizable output formats
and integration with popular logging tools.
By default, Logrus utilizes the TextFormatter type to generate semi-structured
and colorized output when logging to the terminal. However, it can be configured
to produce fully structured output in JSON or Logfmt.
Context-aware logging is possible by using the Fields type which is an alias
for map[string]interface{}. However, there is no type-safe option for
contextual logging and its usage of maps is
relatively inefficient.
Logrus also supports hooks, allowing customization of the logging process. These
hooks enable actions such as sending logs to a
log management service or transforming and
filtering logs before they reach their final destination. With over
80 hooks available, you have a
wide range of options but if none of them suit your needs, you can always
implement the Hook interface to address specific use cases.
Log15 is a logging framework that aims to produce logs that are both human-readable and machine-readable while providing a logging API that is easy to understand and use. Getting started with the framework is straightforward as it provides a pre-configured logger that is ready for use.
The log message is the first argument to the level methods, followed by
alternating key/value pairs for additional context. You can also create a new
logger with some pre-defined fields by using the New() method. An alternative
to variadic arguments for specifying contextual data is the Ctx type which,
like Logrus' Fields type, is an alias for map[string]inteface{}.
When it comes to log formatting, the Format interface is provided along with a
few implementations such as JsonFormat, LogFmtFormat, and TerminalFormat.
The job of determining how and where log records are stored is left to the
Handler interface, which provides a composable way to achieve a logging setup
that suits your application.
Some of the notable built-in Handler implementations include StreamHandler
which writes to any io.Writer, FilterHandler for filtering records,
ChannelHandler for writing logs to a channel, and MultiHandler for logging
to multiple destinations simultaneously.
Logf is a logging framework that's dedicated to producing structured logs in the Logfmt format. It differentiates itself by offering a minimal API, making it a suitable choice for those seeking simplicity and a lightweight solution.
Logf provides five level methods: Debug(), Info(), Warn(), Error(), and
Fatal(). Each one accepts the log message as the first argument, followed by
alternating key/value pairs for contextual logging. It also supports adding
default fields to all logs produced by a Logger instance.
The customization options are pretty minimal here. You can enable a colorized
output, set any io.Writer to be the destination of the logs, change the
timestamp format, set the default level, and enable the inclusion of the caller
in the output. If your requirements extend beyond these options, you may need to
explore other logging frameworks.
Apex is another well-regarded structured logging solution for Go. Although it has not seen recent updates, it remains a popular choice for logging in Go. Inspired by Logrus, Apex enhances the logging API in several ways:
WithField() method for attaching single key/value pairs to a
record, while WithFields() for adding multiple contextual pairs.Formatter and Hook interfaces, Apex introduces a
simpler Handler interface.Here's a basic example using Apex for logging:
Without configuration, Apex produces a semi-structured log output where
contextual fields are outputted in the Logfmt format, while the standard fields
are presented as is without a corresponding key. However, it's easy to switch to
fully structured format by importing either the json or logfmt handlers and
using them as follows:
The multi handler allows logging using multiple formats or sending logs to
different destinations simultaneously:
Apex places contextual fields in a fields object when serializing log entries to the JSON format. This approach ensures consistent schema, facilitating further processing of log entries.
You'll notice that Apex places contextual fields in a fields object when
serializing log entries to the JSON format. This approach ensures consistent
schema, facilitating further processing of log entries.
One notable feature of Apex is its tracing support, which allows for tracking
operations, their durations, and resulting errors without writing multiple
logging statements. By using the Trace() method with a deferred call, you can
conveniently generate logs that indicate the start and duration of an operation:
The above is a minimal example that makes an HTTP GET request to an API
endpoint. The deferred Trace() call in the example above generates two logs:
one indicates the start of an HTTP request and the other displays the duration
and any errors encountered.
Here's the output if the HTTP request succeeds:
If it fails, you will notice an error property in the second log entry:
Logr is an abstraction for Go programs that
offers a way to implement logging without being tied to a particular
implementation. It offers a Logger type that provides a simple API for
emitting logs, but the actual formatting and writing of the records is left to
types that implement LogSink interface.
The general idea is that application and library authors use methods on the
Logger type to write logging statements, but the actual logging functionality
is provided by any logging framework that implements the LogSink interface.
This makes the logging interface remains the same no matter what underlying
framework you choose.
Here's a simple example using a Zerolog implementation via go-logr/zerologr:
The Logger API provides only two methods for creating logs:
With the zerologr implementation, these methods map to Zerolog's info and
error levels directly. To log at other levels, you need to set the verbosity
level using the V() method and chain the Info() method to the resulting
logger:
The idea here is that the higher the number, the less severe the log entry.
Since zerolog.TraceLevel is the lowest severity in Zerolog, using a number
higher than 2 will not produce any output.
It may take a little experimenting to fully understand the ideas espoused by Logr, and with the release of Slog, it makes more sense to use its abstraction for new projects as it is sure to be more widely adopted.
Stop grepping through log files. Better Stack transforms your structured logs from Slog, Zerolog, or Zap into queryable dashboards—filter by any field, track error rates, and spot performance bottlenecks instantly.
The introduction of the log/slog package changes the game when it comes to
logging solutions in Go. By leaning on many of the prior innovations, it
succeeds in bringing a high-performance logging framework to the standard
library that should cover most needs. It is safe to say that it has now made
using libraries using older techniques like Apex, Log15, Logurs, and Logr mostly
obsolete.
However, libraries like Zerolog, Phuslu/log and Zap will definitely remain relevant for the foreseeable future due to their performance and unique features. As demonstrated above, you can even use them as alternate backends for Slog if its built-in handlers don't quite meet up to your expectations.
One knock against Slog is its overly convoluted logging API for guaranteeing strongly-typed fields. However, the ability to switch out the underlying framework without affecting the API is a massive plus that should make logging in the Go ecosystem more seamless and consistent.
Thanks for reading, and happy logging!
We use cookies to authenticate users, improve the product user experience, and for personalized ads. Learn more.