// Package logger provides structured logging using stdlib log/slog. // // JSON output always, one format for every environment, TTY or not. A // log line is a record to be queried, not prose to be read. package logger import ( "fmt" "io" "log/slog" "os" "path/filepath" "go.uber.org/fx" "sneak.berlin/go/simplexcalc/internal/globals" ) // Params defines dependencies for Logger. type Params struct { fx.In Globals *globals.Globals // Output is the log destination. Optional in the fx graph: when // absent (production) it defaults to os.Stdout; tests inject a // buffer here to assert on the output contract. Output io.Writer `optional:"true"` } // Logger wraps slog with application-specific functionality. type Logger struct { log *slog.Logger level *slog.LevelVar globals *globals.Globals } // New creates a new Logger instance. func New(_ fx.Lifecycle, params Params) (*Logger, error) { l := &Logger{ level: new(slog.LevelVar), globals: params.Globals, } l.level.Set(slog.LevelInfo) out := params.Output if out == nil { out = os.Stdout } // replaceAttr simplifies the source attribute to "file.go:line". replaceAttr := func(_ []string, a slog.Attr) slog.Attr { if a.Key == slog.SourceKey { if src, ok := a.Value.Any().(*slog.Source); ok { a.Value = slog.StringValue( fmt.Sprintf("%s:%d", filepath.Base(src.File), src.Line), ) } } return a } handler := slog.NewJSONHandler(out, &slog.HandlerOptions{ Level: l.level, AddSource: true, ReplaceAttr: replaceAttr, }) l.log = slog.New(handler) return l, nil } // EnableDebugLogging sets the log level to debug. func (l *Logger) EnableDebugLogging() { l.level.Set(slog.LevelDebug) l.log.Debug("debug logging enabled", "debug", true) } // Get returns the underlying slog.Logger. func (l *Logger) Get() *slog.Logger { return l.log } // Identify logs application startup information. func (l *Logger) Identify() { l.log.Info("starting", "appname", l.globals.Appname, "version", l.globals.Version, "arch", l.globals.Buildarch, ) }