check / check (push) Waiting to run
fx printed its dependency graph and its start and stop hooks through its console logger on standard error. fx.WithLogger now hands those events to the service's logger: building the graph at DEBUG, the hooks, the start and the stopping signal at INFO, failures at ERROR. The fx logger's constructor takes the configuration, so the level DEBUG=true sets is in force before fx replays the events it held back; a failure before that, such as an invalid configuration value, still goes to fx's console logger. FxLogger in internal/logger holds one SlogLogger per level. fx moves from v1.20.1, which has no SlogLogger, to v1.24.0. The README names the Go runtime and that early failure as writers outside internal/logger. Model: opus-5-5
147 lines
3.6 KiB
Go
147 lines
3.6 KiB
Go
// Package logger provides structured logging with dynamic level
|
|
// control.
|
|
package logger
|
|
|
|
import (
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"time"
|
|
|
|
"go.uber.org/fx"
|
|
"go.uber.org/fx/fxevent"
|
|
"sneak.berlin/go/webhooker/internal/globals"
|
|
)
|
|
|
|
//nolint:revive // LoggerParams is a standard fx naming convention.
|
|
type LoggerParams struct {
|
|
fx.In
|
|
|
|
Globals *globals.Globals
|
|
}
|
|
|
|
// Logger wraps slog with dynamic level control and structured
|
|
// output.
|
|
type Logger struct {
|
|
logger *slog.Logger
|
|
levelVar *slog.LevelVar
|
|
params LoggerParams
|
|
}
|
|
|
|
// New creates a Logger that outputs text (TTY) or JSON (non-TTY)
|
|
// to stdout.
|
|
//
|
|
//nolint:revive // lc parameter is required by fx even if unused.
|
|
func New(lc fx.Lifecycle, params LoggerParams) (*Logger, error) {
|
|
l := new(Logger)
|
|
l.params = params
|
|
|
|
// Use slog.LevelVar for dynamic log level changes
|
|
l.levelVar = new(slog.LevelVar)
|
|
l.levelVar.Set(slog.LevelInfo)
|
|
|
|
// Determine if we're running in a terminal
|
|
tty := false
|
|
if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 {
|
|
tty = true
|
|
}
|
|
|
|
//nolint:revive // groups param unused but required by slog ReplaceAttr signature.
|
|
replaceAttr := func(_ []string, a slog.Attr) slog.Attr {
|
|
// Always use UTC for timestamps
|
|
if a.Key == slog.TimeKey {
|
|
if t, ok := a.Value.Any().(time.Time); ok {
|
|
return slog.Time(slog.TimeKey, t.UTC())
|
|
}
|
|
|
|
return a
|
|
}
|
|
|
|
return a
|
|
}
|
|
|
|
var handler slog.Handler
|
|
|
|
opts := &slog.HandlerOptions{
|
|
Level: l.levelVar,
|
|
ReplaceAttr: replaceAttr,
|
|
}
|
|
|
|
if tty {
|
|
// Use text handler for terminal output (human-readable)
|
|
handler = slog.NewTextHandler(os.Stdout, opts)
|
|
} else {
|
|
// Use JSON handler for production (machine-readable)
|
|
handler = slog.NewJSONHandler(os.Stdout, opts)
|
|
}
|
|
|
|
l.logger = slog.New(handler)
|
|
|
|
// Set as default logger
|
|
slog.SetDefault(l.logger)
|
|
|
|
return l, nil
|
|
}
|
|
|
|
// EnableDebugLogging switches the log level to debug.
|
|
func (l *Logger) EnableDebugLogging() {
|
|
l.levelVar.Set(slog.LevelDebug)
|
|
l.logger.Debug("debug logging enabled", "debug", true)
|
|
}
|
|
|
|
// Get returns the underlying slog.Logger.
|
|
func (l *Logger) Get() *slog.Logger {
|
|
return l.logger
|
|
}
|
|
|
|
// Identify logs the application name and version at startup.
|
|
func (l *Logger) Identify() {
|
|
l.logger.Info("starting",
|
|
"appname", l.params.Globals.Appname,
|
|
"version", l.params.Globals.Version,
|
|
)
|
|
}
|
|
|
|
// Writer returns an io.Writer suitable for standard library
|
|
// loggers.
|
|
func (l *Logger) Writer() io.Writer {
|
|
return os.Stdout
|
|
}
|
|
|
|
// FxLogger writes fx's own events through a slog logger: how the
|
|
// dependency graph was built at DEBUG, since it repeats on every
|
|
// start; the start and stop hooks, the start itself and the signal
|
|
// that stops the service at INFO; every failure at ERROR.
|
|
//
|
|
// The formatting is fx's own fxevent.SlogLogger. That logger takes a
|
|
// single level for every event that is not a failure, so FxLogger
|
|
// holds one at each level and picks between them.
|
|
type FxLogger struct {
|
|
graph *fxevent.SlogLogger
|
|
lifecycle *fxevent.SlogLogger
|
|
}
|
|
|
|
// NewFxLogger returns an FxLogger that writes through log.
|
|
func NewFxLogger(log *slog.Logger) *FxLogger {
|
|
graph := &fxevent.SlogLogger{Logger: log}
|
|
graph.UseLogLevel(slog.LevelDebug)
|
|
|
|
lifecycle := &fxevent.SlogLogger{Logger: log}
|
|
lifecycle.UseLogLevel(slog.LevelInfo)
|
|
|
|
return &FxLogger{graph: graph, lifecycle: lifecycle}
|
|
}
|
|
|
|
// LogEvent implements fxevent.Logger.
|
|
func (f *FxLogger) LogEvent(event fxevent.Event) {
|
|
switch event.(type) {
|
|
case *fxevent.Supplied, *fxevent.Provided, *fxevent.Replaced,
|
|
*fxevent.Decorated, *fxevent.BeforeRun, *fxevent.Run,
|
|
*fxevent.Invoking, *fxevent.Invoked,
|
|
*fxevent.LoggerInitialized:
|
|
f.graph.LogEvent(event)
|
|
default:
|
|
f.lifecycle.LogEvent(event)
|
|
}
|
|
}
|