Updated Nov 5, 20266 min read

Go Logging in 2026: slog, zap, zerolog and Structured Logs

How to do structured logging in Go: the standard library's log/slog, when zap or zerolog are worth adding, request-scoped fields, levels and redaction, and how logs connect to traces in production.

Logging in Go used to start with picking a library, because the standard log package only writes lines of text. Go 1.21 added log/slog, which gives the standard library structured logging with levels, key-value fields and JSON output, and it is the sensible default for new services.

Useful production logs need more than a logger: the fields that let you find one failing request, a consistent level policy, keeping secrets out of the output, and a link from a log line to the trace of the request it belongs to. This guide covers slog, when zap or zerolog are worth it, those practices, and how Encore (a tool that works alongside your framework) handles logging for Go services.

Go logging options at a glance

Encore rloglog/slogzapzerologlogrus
DependencyPart of the Encore SDKStandard librarygo.uber.org/zapgithub.com/rs/zerologgithub.com/sirupsen/logrus
Structured fieldsKey-value pairsKey-value pairs or slog.AttrTyped fieldsChained typed fieldsWithFields map
Trace correlationTrace ID and request context attachedManualManualManualManual
Performance focusGeneralGeneral, pluggable handlersVery low allocationZero allocationSlower, reflection-based
StatusActiveActiveActiveActiveMaintenance mode

Structured logs tied to traces with Encore

Encore

Encore is a tool that works alongside your framework for TypeScript and Go, with a platform that provisions and deploys into your own AWS or GCP account. Its logging package, encore.dev/rlog, writes structured logs and records each line in the trace of the request that produced it, alongside the API calls, database queries and Pub/Sub messages Encore traces on its own, as described in the Go logging docs.

The Infrastructure page in the Encore dashboard with a database's configuration panel open, showing fields for vCPUs, memory, storage, availability type, max connections and backups.
Each provisioned resource can be reviewed and its configuration edited per environment in the Encore dashboard. The screenshot shows a GCP Cloud SQL database.

Set up the app

Get started
brew install encoredev/tap/encore &&
encore app create
Copy

Run in your terminal to get started locally.

~
$ encore app create payments --example=go/hello-world$ cd payments && mkdir payment

There is no logger to construct or pass around, fields are alternating keys and values, errors go in as values, and rlog.With carries shared fields:

// payment/payment.go package payment import ( "context" "errors" "encore.dev/rlog" ) type ChargeParams struct { OrderID string `json:"order_id"` AmountMinor int64 `json:"amount_minor"` Currency string `json:"currency"` } //encore:api public method=POST path=/charges func Charge(ctx context.Context, p *ChargeParams) error { logger := rlog.With("order_id", p.OrderID) if err := authorize(p); err != nil { logger.Error("payment authorization failed", "event", "payment.failed", "err", err, "amount_minor", p.AmountMinor, "currency", p.Currency, ) return err } logger.Info("payment authorized", "event", "payment.authorized", "amount_minor", p.AmountMinor, ) return nil } func authorize(p *ChargeParams) error { if p.AmountMinor <= 0 { return errors.New("amount must be positive") } return nil }

Encore adds the service, endpoint, trace ID and authenticated user ID to every line, so the code does not repeat them. Under encore run, logs show in the terminal and inside each request's trace in the local development dashboard. Deployed on AWS, container logs go to CloudWatch organized by service, and on GCP to Cloud Logging. The minimum level is set with log_level in encore.app, and the default emits everything down to trace level, so Debug calls in hot paths also reach production unless you raise it.

An AI coding agent debugging a failed request can open its trace and read the log lines in context.

Log lines are also attached to the request's trace, as covered in the tracing docs, and in your cloud they go to CloudWatch on AWS or Cloud Logging on GCP, per the AWS and GCP infrastructure docs.

Best fit: Go teams who want structured logs that carry request and trace context on every line.

log/slog

slog has three parts: a Logger with methods per level, Handler implementations that format output, and attributes. The built-in TextHandler and JSONHandler cover most needs, and third-party handlers send output elsewhere. The Go blog introduction explains the design. A production setup writes JSON to stdout and adds request-scoped fields in middleware:

package main import ( "log/slog" "net/http" "os" ) func main() { logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelInfo, })) slog.SetDefault(logger) mux := http.NewServeMux() mux.HandleFunc("POST /charges", func(w http.ResponseWriter, r *http.Request) { log := logger.With("request_id", r.Header.Get("X-Request-ID")) log.Info("payment authorized", "order_id", r.URL.Query().Get("order")) w.WriteHeader(http.StatusNoContent) }) slog.Info("listening", "addr", ":8080") http.ListenAndServe(":8080", mux) }

To keep secrets out of logs, implement slog.LogValuer on sensitive types so they log a redacted value wherever they appear:

type APIKey string func (APIKey) LogValue() slog.Value { return slog.StringValue("[redacted]") }

Good to know

Passing a logger through context.Context is a common pattern but not part of slog itself, so teams usually write a small helper for it. Correlating logs with traces means adding the OpenTelemetry trace ID as a field in middleware.

zap

zap from Uber was built for low-allocation logging in high-throughput services. Its Logger takes typed fields such as zap.String and zap.Int to avoid reflection, and SugaredLogger offers a looser key-value API. zap also ships an slog handler in its zapslog package, so code can log through slog while zap does the encoding.

Good to know

zap's speed matters on paths that log thousands of lines per second per instance. For typical API logging the difference from slog rarely shows up in latency.

zerolog

zerolog writes JSON with zero allocations through a chained API, as in log.Info().Str("order_id", id).Msg("payment authorized"). It is a popular choice for services where log volume is high and JSON is the only output format needed.

Good to know

The chained API is easy to misuse, since forgetting the final Msg or Send call silently drops the event.

logrus

logrus was the most widely used structured logger before slog and is still common in older codebases. Its README states it is in maintenance mode, taking security and bug fixes only. For new code, slog covers the same ground without a dependency.

Practices that matter more than the library

  • Log events with stable messages and put variable data in fields, so equivalent events can be grouped and counted.
  • Use Error only for failures that need attention. Rejected logins and validation errors are expected outcomes.
  • Add an event field such as payment.failed for anything dashboards or alerts depend on.
  • Never log passwords, tokens, cookies or whole request bodies.
  • Include the trace ID so a log line leads to the full request.

Frequently asked questions

What is the best logging library for Go?

For most new code, the standard library's log/slog, added in Go 1.21. It provides structured key-value logging, levels and JSON output with no dependency. zap and zerolog are faster and allocate less, which matters on very hot paths, and both can sit behind the slog API.

How do I log in JSON with slog?

Create a logger with slog.New(slog.NewJSONHandler(os.Stdout, nil)) and make it the default with slog.SetDefault. Calls such as slog.Info("order created", "order_id", id) then write one JSON object per line with time, level, msg and your fields.

Is logrus still maintained?

logrus is in maintenance mode. Its README says it receives security and bug fixes but no new features, and recommends looking at alternatives such as zap, zerolog and slog for new projects.

How do I get structured logs with trace IDs in a Go API without setting up a logger?

With Encore, import encore.dev/rlog and call rlog.Info, rlog.Warn or rlog.Error with a message and alternating key-value fields, and use rlog.With for shared fields. Encore attaches the service, endpoint, trace ID and authenticated user ID to every line and records the log in the request's trace.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders