Go Structured Logging with Zap and Centralized Collection
Master Go logging with uber-go/zap. Learn high-performance structured logging, context propagation, and centralized log shipping for Go applications.
Go's standard library logger is intentionally minimal. For production applications, you need something more powerful. Uber's zap is the go-to choice for high-performance structured logging in Go. This guide covers everything from basic setup to production-ready centralized logging.
Why Zap?
Zap is blazing fast and designed for performance-critical applications:
- Zero allocation in hot paths
- Structured logging with type safety
- Leveled logging with sampling support
- Multiple output formats (JSON, console)
Getting Started
go get go.uber.org/zap
Basic Usage
package main
import "go.uber.org/zap"
func main() {
// Production logger (JSON output)
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("Server starting",
zap.String("host", "localhost"),
zap.Int("port", 8080),
)
// Sugar logger (more convenient, slightly slower)
sugar := logger.Sugar()
sugar.Infow("Request processed",
"method", "GET",
"path", "/users",
"duration_ms", 45,
)
}
Custom Logger Configuration
func NewLogger(env string) (*zap.Logger, error) {
var config zap.Config
if env == "production" {
config = zap.NewProductionConfig()
config.EncoderConfig.TimeKey = "timestamp"
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
} else {
config = zap.NewDevelopmentConfig()
config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
}
config.OutputPaths = []string{"stdout"}
config.ErrorOutputPaths = []string{"stderr"}
return config.Build()
}
Adding Context with Fields
// Create child loggers with context
func handleRequest(logger *zap.Logger, req *http.Request) {
reqLogger := logger.With(
zap.String("request_id", req.Header.Get("X-Request-ID")),
zap.String("method", req.Method),
zap.String("path", req.URL.Path),
)
reqLogger.Info("Request started")
// All subsequent logs include the context
reqLogger.Debug("Processing request")
reqLogger.Info("Request completed", zap.Int("status", 200))
}
HTTP Middleware
func LoggingMiddleware(logger *zap.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Create request-scoped logger
reqID := r.Header.Get("X-Request-ID")
if reqID == "" {
reqID = uuid.New().String()
}
reqLogger := logger.With(
zap.String("request_id", reqID),
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Add logger to context
ctx := context.WithValue(r.Context(), loggerKey, reqLogger)
r = r.WithContext(ctx)
// Wrap response writer to capture status
ww := &responseWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(ww, r)
reqLogger.Info("Request completed",
zap.Int("status", ww.status),
zap.Duration("duration", time.Since(start)),
)
})
}
}
// Get logger from context
func LoggerFromContext(ctx context.Context) *zap.Logger {
if logger, ok := ctx.Value(loggerKey).(*zap.Logger); ok {
return logger
}
return zap.NewNop()
}
Error Logging
func processOrder(logger *zap.Logger, orderID string) error {
logger.Info("Processing order", zap.String("order_id", orderID))
if err := validateOrder(orderID); err != nil {
logger.Error("Order validation failed",
zap.String("order_id", orderID),
zap.Error(err),
)
return err
}
// Log with stack trace for unexpected errors
defer func() {
if r := recover(); r != nil {
logger.Error("Panic in order processing",
zap.Any("panic", r),
zap.Stack("stack"),
)
}
}()
return nil
}
Shipping Logs to Remote Services
Custom Writer for HTTP
type HTTPWriter struct {
url string
apiKey string
client *http.Client
buffer chan []byte
}
func NewHTTPWriter(url, apiKey string) *HTTPWriter {
w := &HTTPWriter{
url: url,
apiKey: apiKey,
client: &http.Client{Timeout: 5 * time.Second},
buffer: make(chan []byte, 1000),
}
go w.flush()
return w
}
func (w *HTTPWriter) Write(p []byte) (int, error) {
select {
case w.buffer <- append([]byte{}, p...):
return len(p), nil
default:
return 0, errors.New("buffer full")
}
}
func (w *HTTPWriter) flush() {
for data := range w.buffer {
req, _ := http.NewRequest("POST", w.url, bytes.NewReader(data))
req.Header.Set("Authorization", "Bearer "+w.apiKey)
req.Header.Set("Content-Type", "application/json")
w.client.Do(req)
}
}
// Use with zap
func NewRemoteLogger(url, apiKey string) *zap.Logger {
writer := NewHTTPWriter(url, apiKey)
core := zapcore.NewCore(
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
zapcore.AddSync(writer),
zap.InfoLevel,
)
return zap.New(core)
}
Using Syslog
import "log/syslog"
func NewSyslogLogger() (*zap.Logger, error) {
writer, err := syslog.Dial("tcp", "logs.example.com:514",
syslog.LOG_INFO|syslog.LOG_LOCAL0, "myapp")
if err != nil {
return nil, err
}
core := zapcore.NewCore(
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
zapcore.AddSync(writer),
zap.InfoLevel,
)
return zap.New(core), nil
}
Sampling for High-Volume Logs
config := zap.NewProductionConfig()
config.Sampling = &zap.SamplingConfig{
Initial: 100, // Log first 100 entries per second
Thereafter: 10, // Then log every 10th entry
}
logger, _ := config.Build()
Testing with Zap
import "go.uber.org/zap/zaptest"
func TestOrderProcessing(t *testing.T) {
logger := zaptest.NewLogger(t)
err := processOrder(logger, "order-123")
assert.NoError(t, err)
}
// Or capture logs for assertions
func TestLogging(t *testing.T) {
core, logs := observer.New(zap.InfoLevel)
logger := zap.New(core)
processOrder(logger, "order-123")
assert.Equal(t, 1, logs.Len())
assert.Equal(t, "Processing order", logs.All()[0].Message)
}
Production Best Practices
- Use structured fields - Never string concatenate log messages
- Add context early - Create child loggers with request context
- Sample high-volume logs - Protect against log storms
- Buffer remote writes - Don't block on network I/O
- Include stack traces for errors - Use zap.Stack() for panics
Conclusion
Zap provides the performance and features Go applications need for production logging. Combined with a centralized log management service like 401 Clicks, you'll have full visibility into your Go services with minimal overhead.
Start with the production config, add request context middleware, and ship your logs to a central service. Your debugging sessions will thank you.
Admin
Published on January 21, 2026