// Package logger provides the project-wide structured logging facade built on // log/slog. All modules must use this package instead of creating their own // logger instances. Development builds render human-friendly TextHandler output; // production builds render JSON for machine parsing. package logger import ( "io" "log/slog" "os" "strings" ) // Format selects the structured log output encoding. type Format string const ( // FormatText is the human-readable development format. FormatText Format = "text" // FormatJSON is the machine-parseable production format. FormatJSON Format = "json" ) // Options configures a Logger. type Options struct { // Level is the minimum level to record, one of debug, info, warn, error. // Unknown values fall back to info. Level string // Format is the output encoding, one of text, json. Unknown values fall // back to text. Format string // Out is the destination writer. Defaults to os.Stderr when nil. Out io.Writer // Ring, when non-nil, additionally captures every emitted record into a // bounded ring buffer served by the admin observability log tail. The // console/file output through Out is unchanged. Ring *RingBuffer } // New builds a slog.Logger from opts. func New(opts Options) *slog.Logger { level := ParseLevel(opts.Level) out := opts.Out if out == nil { out = os.Stderr } handlerOpts := &slog.HandlerOptions{Level: level} var handler slog.Handler if Format(opts.Format) == FormatJSON { handler = slog.NewJSONHandler(out, handlerOpts) } else { handler = slog.NewTextHandler(out, handlerOpts) } return slog.New(newRingHandler(handler, opts.Ring)) } // ParseLevel maps a string level name to a slog.Level. Unknown or empty values // produce slog.LevelInfo. func ParseLevel(s string) slog.Level { switch strings.ToLower(strings.TrimSpace(s)) { case "debug": return slog.LevelDebug case "info": return slog.LevelInfo case "warn", "warning": return slog.LevelWarn case "error": return slog.LevelError default: return slog.LevelInfo } } // WithSource returns a child logger that tags every emitted record with a // source so issues can be traced back to the owning module/sub-feature. source // is conventionally "module/subfeature", e.g. "runtime/loop" or // "channel/adapter". Every module that logs should call this once on its base // logger so all records carry provenance. func WithSource(l *slog.Logger, source string) *slog.Logger { if l == nil { return nil } return l.With(slog.String("source", source)) } // Scoped returns a child logger tagged with module and subfeature, for finer // provenance when a module contains several subsystems. func Scoped(l *slog.Logger, module, subfeature string) *slog.Logger { if l == nil { return nil } return l.With(slog.String("module", module), slog.String("sub", subfeature)) }