// Package observability is the shared logging + metrics + OTel stub. // // M0: slog (stdlib structured logging, JSON output) and Prometheus // metrics via promhttp. OTel is a stub — we wire it in M9. // // All services should call Init() in main and then use the returned // logger and the package-level metrics. package observability import ( "context" "log/slog" "net/http" "os" "strings" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) // Init returns a slog.Logger configured for the env. JSON in non-dev // for log shippers, text in dev for human eyes. func Init(env, level, serviceName string) *slog.Logger { lvl := parseLevel(level) opts := &slog.HandlerOptions{Level: lvl} var h slog.Handler if env == "dev" { h = slog.NewTextHandler(os.Stdout, opts) } else { h = slog.NewJSONHandler(os.Stdout, opts) } logger := slog.New(h).With( slog.String("service", serviceName), slog.String("env", env), ) slog.SetDefault(logger) return logger } func parseLevel(s string) slog.Level { switch strings.ToLower(s) { case "debug": return slog.LevelDebug case "warn": return slog.LevelWarn case "error": return slog.LevelError default: return slog.LevelInfo } } // MetricsHandler returns an http.Handler that serves Prometheus // metrics from the given registry. func MetricsHandler(reg *prometheus.Registry) http.Handler { return promhttp.HandlerFor(reg, promhttp.HandlerOpts{Registry: reg}) } // HealthHandler is the standard /health endpoint. Liveness only — // readiness (can serve traffic) is a separate concern, added per // service as it comes online. func HealthHandler(serviceName string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"status":"ok","service":"` + serviceName + `"}`)) }) } // CtxWithTrace is a stub for M9 when OTel is wired in. For now // it just attaches the request id (or generates one) so logs can // be correlated. func CtxWithTrace(ctx context.Context, traceID string) context.Context { if traceID == "" { // tiny random — not crypto-strong, but unique enough traceID = newTraceID() } return context.WithValue(ctx, traceKey{}, traceID) } // TraceFromCtx returns the trace id, or "" if none. func TraceFromCtx(ctx context.Context) string { v, _ := ctx.Value(traceKey{}).(string) return v } type traceKey struct{} func newTraceID() string { b := make([]byte, 8) if _, err := readRandom(b); err != nil { return "0" } return hexEncode(b) }