- Docs
- Operate
- Logging
Logging
Log structured events through one interface and swap slog, zerolog, pretty, or noop by config.
Every service logs through one interface, log.Logger. Swap the adapter and every line changes shape with no call-site changes: noop for tests, slog for the stdlib, zerolog for high-throughput JSON, pretty for local development. Resolve one logger per process and inject it.
Quick start
Section titled “Quick start”LOG_ADAPTER=slogLOG_ADAPTER=zerologThe only option is min_level (minimum severity). Options.Validate() accepts the zero value, and the adapter choice is validated at resolve time. The container wires nothing, so register the adapter first:
import ( "github.com/zenta-dev/zever/adapters/log/slog" "github.com/zenta-dev/zever/config" "github.com/zenta-dev/zever/container")
slog.Register()c := container.New(config.Default())logger, err := c.Log()if err != nil { return err}handler := middleware.RequestLogger(logger)(mux)Logger has no Close; logging probes as a no-op in c.Close(ctx). Full keys: log config reference.
Write typed fields
Section titled “Write typed fields”Event has one typed method per kind, so slog and zerolog output stays queryable instead of a flat string.
logger.Info().Str("user_id", userID).Dur("duration", elapsed).Msg("request handled")logger.Error().Err(err).Msg("failed to process order")
if logger.Enabled(log.LevelDebug) { logger.Debug().Any("payload", expensiveToCompute()).Send()}level=INFO msg="request handled" user_id=u_123 duration=12msPrefer Str/Int/Dur over Any for hot fields, because Any uses reflection. Reserve Any for dynamic payloads, and never pass request bodies, headers, or config maps through it. RequestLogger emits one line per request (method, path, status, duration_ms (fractional milliseconds), plus request_id when present); Recover logs panics with value and stack, then responds 500. Emit one line per failure, with no duplicates.
The API: Logger exposes Debug(), Info(), Warn(), Error(), Fatal(), With(), WithContext(ctx), Enabled(level), Sync(), and Name(). Event exposes Str, Int, Int64, Float64, Bool, Dur, Time, Err, AnErr(key, err), Any, Msg, Msgf(format, args...), and Send. Helpers: log.ContextWithLogger(ctx, l), log.FromContext(ctx, fallback), log.Open(adapter, opts), and middleware.RequestLogger(logger).
Carry a logger through context
Section titled “Carry a logger through context”log.ContextWithLogger(ctx, logger) carries only the logger value, no I/O state. log.FromContext(ctx, fallback) returns it, or the fallback for contexts that carry none, so request_id and other attached fields survive the call chain.
Keep secrets out of logs
Section titled “Keep secrets out of logs”Never pass raw config or headers into Any, which may print DSNs and keys. Use cfg.RedactedServices() and fixed field names. The same rule covers error text: name services and fields, never values. See Configuration.
Production checklist
Section titled “Production checklist”- One process, one adapter:
slogfor stdlib-only builds,zerologfor high-throughput JSON,prettyfor local shells,noopfor tests. - Buffered providers flush through their own
Shutdown(ctx), whichc.Close(ctx)probes. Do not exit beforeClosereturns or trailing lines drop. - Shutdown never blocks past 5s per service (or the parent deadline). Keep log I/O fast and never format unbounded payloads.
Troubleshooting
Section titled “Troubleshooting”No log output in tests
The resolved adapter is noop, correct for hermetic tests. Assert behavior, not lines, or resolve slog explicitly.
request_id is missing from lines
The context logger was never attached. Wrap the context with log.ContextWithLogger in middleware before handlers run.
Secrets in log files
A call passes raw config or headers into Any. Use cfg.RedactedServices() and fixed field names.
Expensive debug work on hot paths
Fields compute even when the level is off. Gate with logger.Enabled(log.LevelDebug).
Trailing lines lost at exit
The process exits before c.Close(ctx) finishes. Defer Close with a timeout parent and check the joined error.