// Package logger provides a structured logger built on zap with OpenTelemetry // trace correlation. // // In production (the default) logs are emitted as JSON at Info level. When the // ENV environment variable is set to "development" the logger switches to a // console encoder at Debug level for human-readable output. package logger import ( "context" "os" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // ctxKey is an unexported type so context values cannot collide with callers. type ctxKey struct{} // fallback is returned by FromContext when no logger has been injected into // the context; it discards all output so callers never receive a nil logger. var fallback = zap.NewNop() // New creates a structured logger tagged with serviceName. The encoder and // level are selected from the ENV environment variable as described in the // package documentation. func New(serviceName string) *zap.Logger { var cfg zap.Config if os.Getenv("ENV") == "development" { cfg = zap.NewDevelopmentConfig() cfg.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder } else { cfg = zap.NewProductionConfig() cfg.Level = zap.NewAtomicLevelAt(zapcore.InfoLevel) } l, err := cfg.Build(zap.Fields(zap.String("service", serviceName))) if err != nil { // A logger that cannot be constructed is a fatal misconfiguration; // there is no safe way to continue. panic(err) } return l } // WithContext returns a copy of ctx that carries l so it can later be // retrieved via FromContext. func WithContext(ctx context.Context, l *zap.Logger) context.Context { return context.WithValue(ctx, ctxKey{}, l) } // FromContext returns the logger stored in ctx. When an active OpenTelemetry // span is present, the returned logger is decorated with a trace_id field so // log lines can be correlated to traces. If no logger was stored in ctx a // no-op logger is returned. func FromContext(ctx context.Context) *zap.Logger { if v, ok := ctx.Value(ctxKey{}).(*zap.Logger); ok && v != nil { if sc := trace.SpanContextFromContext(ctx); sc.HasTraceID() { return v.With(zap.String("trace_id", sc.TraceID().String())) } return v } return fallback }