package boot import ( "context" "io" "log/slog" "atlas9.dev/c/demo/lib" ) // LogHandler builds the process-wide slog handler. The app always logs JSON, // in dev and production alike; pretty output and trace grouping are the // consumer's job (cmd/dev, cmd/traces). Records logged with a context // (slog.InfoContext etc.) carry a "request" attribute from the request ID // stored in the context, which is what groups lines into traces. func LogHandler(w io.Writer) slog.Handler { return requestIDHandler{slog.NewJSONHandler(w, &slog.HandlerOptions{Level: slog.LevelDebug})} } // requestIDHandler adds the request ID from the context, if any, to each // log record. DEBUG records are request-scoped detail (db statements, access // checks, request shapes); outside a request they are dropped, which keeps // background polling (task workers) from flooding the logs. type requestIDHandler struct { slog.Handler } func (h requestIDHandler) Enabled(ctx context.Context, level slog.Level) bool { if level == slog.LevelDebug && lib.GetRequestID(ctx) == "" { return false } return h.Handler.Enabled(ctx, level) } func (h requestIDHandler) Handle(ctx context.Context, r slog.Record) error { if id := lib.GetRequestID(ctx); id != "" { r.AddAttrs(slog.String("request", id)) } return h.Handler.Handle(ctx, r) } func (h requestIDHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return requestIDHandler{h.Handler.WithAttrs(attrs)} } func (h requestIDHandler) WithGroup(name string) slog.Handler { return requestIDHandler{h.Handler.WithGroup(name)} }