Zerolog for Go: structured fields, context, and sampling

Use Zerolog typed events, logger context, stack capture, hooks, sampling, and request propagation, then verify JSON collection in Guance.

Best practices
Illustration of logs flowing through a collection pipeline

Zerolog is a structured logging library for Go with a chainable event API and JSON output. It is designed to limit allocations in common logging paths. That design can help a busy service, but it does not make every logging call allocation-free or establish that it is the fastest choice for every workload.

This guide covers typed fields, logger context, stack traces, hooks, sampling, request context, and collection in Guance. Start by making the records useful, then measure the cost of producing and delivering them.

Create and emit an event

In a Go module, add the package:

go get github.com/rs/zerolog

A complete main.go example is:

package main

import (
    "errors"

    "github.com/rs/zerolog/log"
)

func main() {
    log.Debug().Msg("Debug details")
    log.Info().Str("user_id", "USR-9").Msg("User signed in")
    log.Warn().Msg("Configuration missing; using default")
    err := errors.New("insufficient balance")
    log.Error().Err(err).Str("order_id", "A-1024").Msg("Payment declined")
}

The package-level logger writes JSON to stderr and includes a timestamp. An information event can look like:

{"level":"info","user_id":"USR-9","time":"2026-08-25T14:35:10+08:00","message":"User signed in"}

Finish a chain with Msg(), Msgf(), or Send() to emit it. Calling log.Info().Str(...) alone constructs an event without sending a record. Use typed methods such as Str, Int, Bool, Dur, Time, and Err to keep fields suitable for queries.

Zerolog's seven named severities are trace, debug, info, warn, error, fatal, and panic. Set zerolog.SetGlobalLevel(zerolog.InfoLevel) to suppress lower-severity events globally, or set a logger's threshold with Level. Configure a threshold explicitly instead of depending on an application's untouched defaults. The project documentation describes the API and encoding options.

Build a logger with stable context

For application code, construct a logger whose destination and fields are explicit:

package main

import (
    "os"

    "github.com/rs/zerolog"
)

func main() {
    logger := zerolog.New(os.Stdout).
        Level(zerolog.InfoLevel).
        With().Timestamp().
        Str("service", "order-api").
        Logger()

    logger.Info().Int("items", 3).Msg("Order accepted")
}

With() creates a context builder. Calling Logger() produces a logger carrying those fields on subsequent events. A newly constructed logger does not add a timestamp unless you request it, as this example does. Add stable service, version, and instance fields once; add request-specific values to a request logger.

Avoid colliding keys between bound context and individual events. Duplicate JSON keys can be interpreted differently by downstream parsers. Do not reuse or mutate a single in-progress event across goroutines, and ensure the selected io.Writer supports the concurrency used by the service.

Render readable output during development

As an alternative inside a function, with os, time, and zerolog imported:

logger := zerolog.New(zerolog.ConsoleWriter{
    Out: os.Stderr,
    TimeFormat: time.RFC3339,
}).Level(zerolog.DebugLevel).
    With().Timestamp().Caller().Logger()

logger.Debug().Msg("Development logger ready")

ConsoleWriter renders JSON events as readable text, with color when configured. That formatting adds work. For a structured collection path, send JSON directly so the collector can retain typed fields. Binary CBOR output is also available through the binary_log build tag, but requires a compatible consumer; it cannot be sent unchanged to a JSON-line parser.

Capture a stack only when one exists

Err(err) records an error value; ordinary Go errors do not automatically carry their creation stack. Zerolog's optional pkgerrors marshaler can extract stack data captured by github.com/pkg/errors:

go get github.com/pkg/errors
package main

import (
    "os"

    errors "github.com/pkg/errors"
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/pkgerrors"
)

func main() {
    zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
    logger := zerolog.New(os.Stdout).With().Timestamp().Logger()
    err := errors.New("payment provider unavailable")
    logger.Error().Stack().Err(err).Msg("Payment processing failed")
}

Configure the global marshaler during startup, before concurrent logging begins. Stack() requests stack serialization; it does not manufacture a historical stack for an error that never captured one. Confirm the actual error types and wrapping conventions used by dependencies. The stack marshaler documents the supported integration.

Stack collection and output add cost and can expose source paths or sensitive error text. Choose where they help an investigation rather than requiring them for every routine rejected request.

Add a hook for shared event behavior

With an existing logger, a hook can add a field at selected levels:

logger = logger.Hook(zerolog.HookFunc(
    func(e *zerolog.Event, level zerolog.Level, msg string) {
        if level >= zerolog.ErrorLevel {
            e.Str("alert_hint", "check-oncall")
        }
    },
))
logger.Error().Msg("Dependency unavailable")

The hook annotates the record; it does not send a notification. Keep hook work bounded and avoid network calls in the logging path. Verify the resulting event before using alert_hint in an alerting rule.

Sample a specific noisy stream

A basic sampler retains roughly one in every N events passed through that sampled logger:

sampled := logger.Sample(&zerolog.BasicSampler{N: 100})
for i := 0; i < 1000; i++ {
    sampled.Info().Int("sequence", i).Msg("Heartbeat")
}
logger.Error().Msg("Critical failure remains on the unsampled logger")

BasicSampler does not automatically exempt errors. Keep important failures and audit records on an appropriate unsampled path, or configure a level-aware sampler. Sampling individual records can remove part of a request's story; it is not equivalent to retaining all records from selected requests.

Test the retained count and query usefulness at representative volume. Costs depend on actual retained bytes and downstream storage rules, not just the sampler's N value. The sampling API gives the behavior to build on.

Carry a logger through context.Context

Bind a request identifier, then attach the logger to the request context. In a handler where r is the incoming *http.Request and logger is the application logger:

reqLogger := logger.With().Str("request_id", "req-abc-123").Logger()
ctx := reqLogger.WithContext(r.Context())
process(ctx)

A downstream function imports context and zerolog and uses the attached logger:

func process(ctx context.Context) {
    zerolog.Ctx(ctx).Info().Msg("Processing request")
}

Use a generated or validated identifier per real request; the fixed value above demonstrates propagation only. Every function must receive the derived context for the association to continue. Without an attached logger, zerolog.Ctx can return the configured default context logger or a disabled logger, so missing context may also mean missing output. Check the context API.

Verify the record after collection

Produce an information record, an error, and a record with known request context. Configure the DataKit log collector for the actual stdout, stderr, or file path. Parse time, map level to status, and verify how warn, fatal, and panic are represented.

For application performance monitoring (APM) correlation, emit the actual active trace identifier using the Guance association requirements. Compare the stored log with the same request's trace. A generated request ID is not a replacement for trace_id.

After enabling sampling, verify that the records required for incidents still arrive. When using multiple indexes or retention policies, confirm which events enter each destination and how the retained volume changes.

Common questions

Should I choose Zerolog or the standard library's slog?

slog offers a standard structured-logging API. Zerolog offers its typed chain API, sampling, and encoding choices. Choose from integration needs and equivalent workload tests rather than an unconditional performance ranking. If you use a Zerolog-backed slog.Handler, check that the installed version or selected adapter supports the fields and levels you need.

Can Zerolog output text?

Yes, through ConsoleWriter. The underlying event encoding is JSON by default, while the writer renders readable text. Keep JSON when the downstream parser expects structured records.

Do Fatal and Panic terminate the process?

A completed fatal event calls os.Exit(1); deferred functions do not run. A completed panic event panics, which unwinds the goroutine and can be recovered by an appropriate caller. An unrecovered panic terminates the process. Neither is a substitute for returning an ordinary request error, and abrupt exit can strand buffered output in a custom writer.

How should I rotate files?

Zerolog writes to an io.Writer; it does not own file rotation. Use a writer with an appropriate rotation policy or coordinate external rotation and reopening. copytruncate can lose events during its copy/truncate interval. For containers, a runtime-managed output path often avoids application-managed files, but its retention and collection still need testing.

Related reading

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting