Golang Zap Logger: Structured Logging with Logger and SugaredLogger

Deepak Prasad
Tested on RHEL 10.2 with Go 1.27.0
Package go 1.27.0
go.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:

bash
go get go.uber.org/zap@v1.28.0
output
go: 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.0

Save a minimal program that builds a production logger and emits one structured info line:

bash
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),
	)
}
EOF

Run it from your module directory:

bash
go run main.go
output
{"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:

bash
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),
	)
}
EOF

Run the development logger example:

bash
go run dev_main.go
output
2026-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:

go
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:

go
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:

go
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:

go
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


Frequently Asked Questions

1. What is the difference between zap.Logger and zap.SugaredLogger?

Logger logs with strongly typed zap.Field values such as zap.String and zap.Int. SugaredLogger adds printf-style helpers and Infow with alternating keys and values. Logger is the typed API; SugaredLogger trades some strictness for convenience.

2. Should I use zap.NewProduction() or zap.NewDevelopment()?

NewProduction defaults to JSON encoding suited for production aggregation. NewDevelopment defaults to human-readable console output for local debugging. Both are starting points; customize zap.Config when you need different paths or encoders.

3. What is the difference between Zap and Go log/slog?

log/slog is in the Go standard library and fits projects that want structured logging without a third-party dependency. Zap remains a mature third-party logger with a long ecosystem. Choose based on your dependency policy, existing tooling, and feature needs rather than a single performance number.

4. How do I add a request ID to every Zap log?

Create a child logger with logger.With(zap.String("request_id", id)) or sugar.With("request_id", id), then log through that child for the rest of the request scope.

5. Can Zap change log levels at runtime?

Yes. Wire zap.NewAtomicLevel or zap.AtomicLevel into zap.Config.Level, then call SetLevel on the atomic level while the application runs.

6. Does Zap rotate log files automatically?

No. Zap writes to the outputs you configure. File rotation comes from the platform, container logging, system log management, or a rotating writer integrated through a custom Zap core.

7. Do I need to call logger.Sync()?

Upstream examples often defer logger.Sync so the underlying log sink can flush or sync before shutdown. Some sinks return harmless errors from Sync; handle the error when it matters to your application.
Antony Shikubu

Systems Integration Engineer

Highly skilled software developer with expertise in Python, Golang, and AWS cloud services.

  • Go (programming language)
  • Python (programming language)
  • Amazon Web Services