// Package observability provides slog-based structured logging for push-gateway. // // The logger emits JSON in production and a human-readable console format in // development (DEV_MODE=true). Trace correlation is handled by attaching a // trace_id attribute from the OpenTelemetry span context when present. package observability import ( "context" "log/slog" "os" "time" "go.opentelemetry.io/otel/trace" ) // ctxKey is an unexported type so context values cannot collide with callers. type ctxKey struct{} // logger is the process-wide *slog.Logger initialized by InitLogger. var logger *slog.Logger // InitLogger configures the global slog logger. In DevMode a console handler // with colored levels is used; otherwise a JSON handler at Info level is used. // The logger is tagged with service=push-gateway on every emitted record. func InitLogger(devMode bool) *slog.Logger { var handler slog.Handler opts := &slog.HandlerOptions{ Level: slog.LevelInfo, } if devMode { opts.Level = slog.LevelDebug handler = slog.NewTextHandler(os.Stdout, opts) } else { handler = slog.NewJSONHandler(os.Stdout, opts) } logger = slog.New(handler).With("service", "push-gateway") slog.SetDefault(logger) return logger } // Logger returns the process-wide logger initialized by InitLogger. When Init // has not been called a fallback JSON logger is returned so callers never // receive a nil logger. func Logger() *slog.Logger { if logger == nil { logger = slog.New(slog.NewJSONHandler(os.Stdout, nil)).With("service", "push-gateway") } return logger } // WithContext returns a copy of ctx carrying l so it can later be retrieved // via FromContext. func WithContext(ctx context.Context, l *slog.Logger) context.Context { return context.WithValue(ctx, ctxKey{}, l) } // FromContext returns the logger stored in ctx. When an OpenTelemetry span is // active 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 the // process-wide logger is returned (never nil). func FromContext(ctx context.Context) *slog.Logger { l, ok := ctx.Value(ctxKey{}).(*slog.Logger) if !ok || l == nil { l = Logger() } if sc := trace.SpanContextFromContext(ctx); sc.HasTraceID() { return l.With("trace_id", sc.TraceID().String()) } return l } // Now returns the current UTC time in RFC3339 format. Convenience helper for // log field construction. func Now() string { return time.Now().UTC().Format(time.RFC3339) }