Documentation
¶
Overview ¶
Package logger is a thin wrapper around log/slog tailored for services.
It injects a trace_id into every record (via a pluggable TraceIDFn), tags records with the service name, exposes context-aware Debug/Info/Warn/Error methods, and can fan records out to multiple sinks by level (see FanoutHandler) — e.g. everything to stdout and ERROR+ to Sentry. The same *Logger is meant to be used everywhere: HTTP/gRPC handlers, business code, and background workers.
Usage ¶
Build one Logger at startup and inject it. Pair TraceIDFn with the otel package so every line carries the active trace id:
log := logger.New(os.Stdout, logger.Config{
Service: "myapp",
Level: logger.LevelInfo,
AddSource: true,
TraceIDFn: otel.GetTraceID,
})
log.Info(ctx, "service started", "addr", ":8080")
access := log.Named("access") // child tagged logger=access
access.Info(ctx, "request", "method", "GET", "path", "/")
Multiple sinks ¶
To split output by level, build a FanoutHandler and pass it to NewWithHandler. Each wrapped handler's own Enabled check gates which records it receives, so per-sink level filtering lives on the handlers:
stdout := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: logger.LevelInfo})
h := logger.NewFanout(stdout, sentryHandler) // sentryHandler.Enabled gates ERROR+
log := logger.NewWithHandler(h, logger.Config{Service: "myapp", TraceIDFn: otel.GetTraceID})
Interop with slog ¶
Handler returns the underlying slog.Handler and Slog returns a stdlib *slog.Logger backed by it, so third-party middleware (e.g. httplog) can log through the same sink, formatting, and trace-id injection. Trace-id injection is installed as a handler wrapper rather than in the *Logger write path, so it applies uniformly to *Logger, Slog(), and anything sharing the handler.
Config ¶
Config fields:
- Service: tags every record with attribute "service".
- Level: minimum level handled (LevelDebug/LevelInfo/LevelWarn/LevelError).
- AddSource: attach a "source" attribute shortened to file:line.
- TraceIDFn: injects "trace_id"; may be nil. Typically otel.GetTraceID.
Deriving child loggers:
- Named(name): child tagged logger=name (access logger, worker logger, ...).
- With(args...): child whose records always carry the given attributes.
Children share the parent's handler, so output, level, and any fan-out stay unified — the intended way to have several logger instances without proliferating handlers or config.
Index ¶
- Constants
- type Config
- type FanoutHandler
- type Level
- type Logger
- func (l *Logger) Debug(ctx context.Context, msg string, args ...any)
- func (l *Logger) Error(ctx context.Context, msg string, args ...any)
- func (l *Logger) Handler() slog.Handler
- func (l *Logger) Info(ctx context.Context, msg string, args ...any)
- func (l *Logger) Named(name string) *Logger
- func (l *Logger) Slog() *slog.Logger
- func (l *Logger) Warn(ctx context.Context, msg string, args ...any)
- func (l *Logger) With(args ...any) *Logger
- type TraceIDFn
Constants ¶
const ( LevelDebug = slog.LevelDebug LevelInfo = slog.LevelInfo LevelWarn = slog.LevelWarn LevelError = slog.LevelError )
Log levels, re-exported from slog for convenience.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// Service tags every record with this name (attribute "service").
Service string
// Level is the minimum level handled.
Level Level
// AddSource attaches a "source" attribute (file:line) to records.
AddSource bool
// TraceIDFn injects "trace_id"; may be nil.
TraceIDFn TraceIDFn
}
Config configures a Logger.
type FanoutHandler ¶
type FanoutHandler struct {
// contains filtered or unexported fields
}
FanoutHandler dispatches each record to every wrapped handler whose own Enabled check passes. This lets you, for example, send all records to a stdout JSON handler while also forwarding ERROR+ records to a Sentry handler.
stdout := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: LevelInfo})
h := logger.NewFanout(stdout, sentryHandler) // sentryHandler.Enabled gates ERROR+
log := logger.NewWithHandler(h, logger.Config{Service: "svc", TraceIDFn: otel.GetTraceID})
func NewFanout ¶
func NewFanout(handlers ...slog.Handler) *FanoutHandler
NewFanout builds a FanoutHandler over the given handlers (nil handlers are skipped). Per-sink level filtering is the responsibility of each handler.
func (*FanoutHandler) Enabled ¶
Enabled reports whether any wrapped handler is enabled for the level.
func (*FanoutHandler) Handle ¶
Handle forwards the record to every wrapped handler that is enabled for it.
type Logger ¶
type Logger struct {
// contains filtered or unexported fields
}
Logger is the application logger.
func New ¶
New builds a Logger writing JSON to w. For multi-sink behavior, build the Logger with NewWithHandler and a FanoutHandler instead.
func NewWithHandler ¶
NewWithHandler builds a Logger from a custom slog.Handler (e.g. a FanoutHandler). The service attribute and TraceIDFn from cfg are applied.
Trace-id injection is installed as a handler wrapper (not in the Logger's write path), so it also applies to any *slog.Logger derived via Slog() and to third-party middleware that logs through the same handler (e.g. httplog).
func (*Logger) Handler ¶
Handler returns the underlying slog.Handler, so callers can build a stdlib *slog.Logger that shares the same sink and formatting.
func (*Logger) Named ¶
Named returns a child Logger tagged with logger=name. Use it to derive purpose-specific instances (e.g. an access logger, a worker logger) that share the same underlying handler — so output, level, and any multi-sink fan-out (stdout + Sentry, ...) stay unified — while remaining distinguishable in the logs. This is the intended way to have several logger instances without proliferating handlers/config.