···55 "context"
66 "flag"
77 "fmt"
88+ "log/slog"
89 "net/http"
910 "os"
1011 "os/signal"
···6263 Out: os.Stdout,
6364 TimeFormat: time.RFC3339,
6465 })
6666+ }
6767+6868+ // Configure slog default handler to match zerolog format.
6969+ // Third-party libraries (e.g. indigo OAuth) use slog, so without this
7070+ // their output bypasses zerolog and breaks structured log parsing.
7171+ if os.Getenv("LOG_FORMAT") == "json" {
7272+ slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
7373+ } else {
7474+ slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, nil)))
6575 }
66766777 log.Info().Msg("Starting Arabica Coffee Tracker")
+10-4
internal/middleware/logging.go
···5858 // Calculate duration
5959 duration := time.Since(start)
60606161+ // Use the context logger (enriched with trace_id by RequestIDMiddleware)
6262+ ctxLogger := zerolog.Ctx(r.Context())
6363+ if ctxLogger.GetLevel() == zerolog.Disabled {
6464+ ctxLogger = &logger
6565+ }
6666+6167 // Select log level based on status code
6268 var logEvent *zerolog.Event
6369 if rw.statusCode >= 500 {
6464- logEvent = logger.Error()
7070+ logEvent = ctxLogger.Error()
6571 } else if rw.statusCode >= 400 {
6666- logEvent = logger.Warn()
7272+ logEvent = ctxLogger.Warn()
6773 } else {
6868- logEvent = logger.Info()
7474+ logEvent = ctxLogger.Info()
6975 }
70767177 // Add core fields
···96102 logEvent.Str("user_did", did)
97103 }
981049999- if logger.GetLevel() == zerolog.DebugLevel {
105105+ if ctxLogger.GetLevel() == zerolog.DebugLevel {
100106 // Log all request headers for debugging malicious traffic (debug mode only)
101107 headers := make(map[string]string)
102108 for name, values := range r.Header {
+51
internal/middleware/request_id.go
···11+package middleware
22+33+import (
44+ "crypto/rand"
55+ "encoding/hex"
66+ "net/http"
77+88+ "github.com/rs/zerolog"
99+ "go.opentelemetry.io/otel/trace"
1010+)
1111+1212+// RequestIDMiddleware extracts the OTel trace ID from the current span (set by
1313+// otelhttp) and injects it into the zerolog logger on the request context. If
1414+// no active trace exists, it generates a random 8-byte hex ID as a fallback.
1515+//
1616+// Every downstream handler that uses zerolog.Ctx(r.Context()) will
1717+// automatically include the trace_id field in its log output, making it easy
1818+// to correlate all log lines from a single request.
1919+//
2020+// The trace ID is also set as the X-Trace-ID response header so that it can be
2121+// correlated with client-side error reports.
2222+func RequestIDMiddleware(logger zerolog.Logger) func(http.Handler) http.Handler {
2323+ return func(next http.Handler) http.Handler {
2424+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2525+ traceID := extractTraceID(r)
2626+2727+ // Create a sub-logger with trace_id and inject into context
2828+ subLogger := logger.With().Str("trace_id", traceID).Logger()
2929+ ctx := subLogger.WithContext(r.Context())
3030+3131+ // Set response header for client-side correlation
3232+ w.Header().Set("X-Trace-ID", traceID)
3333+3434+ next.ServeHTTP(w, r.WithContext(ctx))
3535+ })
3636+ }
3737+}
3838+3939+// extractTraceID returns the OTel trace ID if an active span exists,
4040+// otherwise generates a random fallback ID.
4141+func extractTraceID(r *http.Request) string {
4242+ sc := trace.SpanFromContext(r.Context()).SpanContext()
4343+ if sc.HasTraceID() {
4444+ return sc.TraceID().String()
4545+ }
4646+4747+ // Fallback: generate a random ID (e.g. for requests filtered out of tracing)
4848+ var buf [8]byte
4949+ _, _ = rand.Read(buf[:])
5050+ return hex.EncodeToString(buf[:])
5151+}