| Tested on | RHEL 10.2 with Go 1.27.0 |
|---|---|
| Package | go 1.27.0go.uber.org/zap v1.28.0 |
| Applies to | Current supported Go releases |
| Privilege | Normal user |
| Scope | Install Zap, first structured log line, Logger vs SugaredLogger, levels and fields, request IDs with With, AtomicLevel, JSON vs console output, and common mistakes. Does not cover OpenTelemetry, log shipping, or full encoder internals. |
| Related guides | Getting started with Go Golang context Golang HTTP server Golang Gin Golang os package |
Zap is a structured logging library for Go. Instead of building log strings manually, you attach fields such as request IDs, usernames, status codes, and durations to each log entry.
zap.Logger uses strongly typed zap.Field values. zap.SugaredLogger adds looser helpers such as Infow and Infof for code that prefers alternating keys and values or printf-style messages.
Install Zap and create your first logger
Pin the release tested in this article:
go get go.uber.org/zap@v1.28.0go: downloading go.uber.org/zap v1.28.0
go: downloading go.uber.org/multierr v1.10.0
go: added go.uber.org/multierr v1.10.0
go: added go.uber.org/zap v1.28.0Save a minimal program that builds a production logger and emits one structured info line:
cat > main.go << 'EOF'
package main
import (
"go.uber.org/zap"
)
func main() {
logger, err := zap.NewProduction()
if err != nil {
panic(err)
}
defer logger.Sync()
logger.Info(
"request completed",
zap.String("method", "GET"),
zap.Int("status", 200),
)
}
EOFRun it from your module directory:
go run main.go{"level":"info","ts":1787469741.8156157,"caller":"main.go:14","msg":"request completed","method":"GET","status":200}Sample output; ts and the caller line number will differ on your machine.
level and msg are the severity and message. ts is a timestamp. caller points at the log statement. method and status are the structured fields you passed in. Always check the error from NewProduction. Upstream examples commonly defer logger.Sync() so the underlying sink can flush or sync during shutdown.
NewProduction vs NewDevelopment
Both helpers return a configured *zap.Logger, but they target different environments:
| Logger | Typical output |
|---|---|
zap.NewProduction() |
JSON to stderr with production-oriented defaults |
zap.NewDevelopment() |
Human-readable console output for local work |
They are starting points, not the only configuration path. When you need custom encoders, paths, or levels, build from zap.Config instead of these presets.
The development helper prints console-style lines for the same structured fields:
cat > dev_main.go << 'EOF'
package main
import (
"go.uber.org/zap"
)
func main() {
logger, err := zap.NewDevelopment()
if err != nil {
panic(err)
}
defer logger.Sync()
logger.Info(
"request completed",
zap.String("method", "GET"),
zap.Int("status", 200),
)
}
EOFRun the development logger example:
go run dev_main.go2026-08-23T12:52:22.476+0530 INFO dev_main.go:14 request completed {"method": "GET", "status": 200}Sample output; the timestamp and caller line will differ on your machine.
Development output is easier to read at a terminal; production JSON fits log collectors and structured queries.
Logger vs SugaredLogger
zap.Logger
The typed logger accepts explicit field constructors:
logger.Info(
"request completed",
zap.String("path", "/api/health"),
zap.Int("status", 200),
zap.Duration("latency", latency),
zap.Error(err),
)Typed fields keep names and values explicit at compile time and are Zap’s primary API for hot paths.
SugaredLogger
Call logger.Sugar() to get a sugared wrapper:
sugar := logger.Sugar()
sugar.Infow(
"request completed",
"status", 200,
"path", "/api",
)
sugar.Infof("retry attempt %d", attempt)Infow expects alternating string keys and values after the message. Keep each key paired with a value; invalid key/value arguments can produce logging errors, especially in development mode. Use Infof only for printf-style text, not for structured key/value pairs.
Use SugaredLogger when convenience and familiar key/value or printf-style APIs matter. Use Logger when you prefer strongly typed fields or want Zap’s lowest-overhead API. Pick one style per package and stay consistent.
Log levels and structured fields
Zap exposes Debug, Info, Warn, and Error on both Logger and SugaredLogger. Higher-severity helpers such as DPanic, Panic, and Fatal exist for exceptional paths. Avoid Fatal for ordinary error handling: it logs and then calls os.Exit(1).
Structured fields turn a plain message into searchable data. Typical HTTP service fields include:
zap.String("path", r.URL.Path)zap.Int("status", statusCode)zap.String("user_id", userID)zap.Duration("latency", elapsed)zap.Error(err)when a handler returns an error
One Info call can carry several fields; you do not need a separate program for each level. Set the configured minimum level so Debug lines appear only when you intend to collect them.
Add request IDs and reusable fields with With
When several log lines belong to the same request, attach shared fields once with With:
requestLogger := logger.With(
zap.String("request_id", requestID),
)
requestLogger.Info("incoming", zap.String("method", "GET"))
requestLogger.Info("completed", zap.Int("status", 200))Each line includes request_id without repeating the field at every call site. The same pattern works on a sugared logger with sugar.With("request_id", requestID).
Create a child logger carrying context that should appear on several related log lines. Pass that child through the request scope instead of the root logger when correlation matters.
Configure levels, output, and encoding
Runtime level with zap.AtomicLevel
Use zap.AtomicLevel when your program needs to change the minimum log level while it is running:
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
al := zap.NewAtomicLevelAt(zapcore.InfoLevel)
cfg := zap.NewProductionConfig()
cfg.Level = al
logger, err := cfg.Build()
if err != nil {
panic(err)
}
defer logger.Sync()
logger.Info("info only before debug enabled")
al.SetLevel(zapcore.DebugLevel)
logger.Debug("now debug is visible")After SetLevel, debug lines that were previously filtered become visible.
JSON vs console encoding
Encoding: "json" produces one JSON object per line, which suits centralized logging. Encoding: "console" produces human-oriented lines for terminals. NewProduction and NewDevelopment set these defaults for you; custom zap.Config values override them.
Output paths
OutputPaths and ErrorOutputPaths in zap.Config list where Zap writes normal and error output. NewProductionConfig defaults both to stderr. Add a file path when you want local files. Zap itself does not rotate logs; rotation may come from the runtime platform, container logging, system log management, or a rotating writer integrated through a custom Zap core.
Common Zap mistakes
| Problem | Fix |
|---|---|
| Infow reports invalid key/value arguments | Ensure string keys and values are paired correctly |
| Debug logs do not appear | Check the configured minimum level |
| Request ID missing from later logs | Log through the child logger returned by With |
| Expecting human-readable production logs | NewProduction defaults to JSON |
| Expecting Zap to rotate files automatically | Use platform logging or a rotating writer through a custom Zap core |
Calling Fatal during normal error handling |
Fatal logs and terminates the process |
Upstream examples commonly defer logger.Sync() so the underlying log sink has a chance to flush or sync before shutdown. Some sinks return harmless errors from Sync; handle the error when it matters to your application.
Summary
Uber Zap gives you structured logging in Go with a clear beginner path: install go.uber.org/zap, call zap.NewProduction() or zap.NewDevelopment(), and log with typed zap.Field values or a SugaredLogger when loose key/value pairs fit better.
Use With for request IDs and other fields that belong on every line in a scope. Tune levels with zap.AtomicLevel, pick JSON or console encoding through zap.Config, and treat file rotation as an external concern. Defer Sync on shutdown when your deployment cares about flushing the underlying sink.
References
- go.uber.org/zap — package documentation
- Uber Zap repository — source and examples
- go.uber.org/zap/zapcore — levels, encoders, and cores
- log/slog — Go standard library structured logging

