Go zap structured logging
Wire uber-go/zap into a Meridian-backed Go service so every request emits structured JSON, ships to your aggregator, and stays cheap under load. Zero allocations on the hot path, type-safe fields, and a logger your future self can grep.
1. Install and bootstrap the production logger
Zap ships two presets: NewProduction (JSON, info+, sampled) and NewDevelopment (console, debug+). Pick production for anything past localhost; the sampler caps duplicate log floods at 100/sec.
go get go.uber.org/zap
// main.go
logger, err := zap.NewProduction()
if err != nil { panic(err) }
defer logger.Sync()
logger.Info("meridian.boot",
zap.String("region", "swc"),
zap.Int("port", 8080),
)2. Attach a request-scoped logger via middleware
Stuff a child logger into context.Context with the request id baked in. Every downstream call pulls the same logger, so trace id, user id, and route stay consistent across the whole chain without you re-passing fields.
func WithLogger(base *zap.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rid := r.Header.Get("X-Request-Id")
log := base.With(zap.String("rid", rid), zap.String("path", r.URL.Path))
ctx := context.WithValue(r.Context(), "log", log)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}3. Sample, redact, and ship to Meridian
Point zap at stdout, let the platform tail and forward to your aggregator. Use zap.Namespace to scope token fields and a redactor hook to scrub Authorization headers before they leave the box. The sampler keeps cost predictable when a hot path goes loud.
- Always defer
logger.Sync()inmain. - Never use
Sugar()on hot paths — typed fields are 4–10x faster. - Treat
Fatalas a real fatal: it callsos.Exit(1).