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.
Each level also has a *c variant (Debugc/Infoc/Warnc/Errorc) that takes a frame-skip count for correct source attribution when the logger is reached through a helper of your own; see the Logger methods. BuildInfo logs the binary's embedded VCS/build metadata at startup.
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})
Side-effect hooks (Events) ¶
Events attaches a per-level callback that fires IN ADDITION to normal handling — the record is still written to every sink. Use it for side effects like alerting or Sentry capture on ERROR, without disturbing stdout logging:
log := logger.New(os.Stdout, logger.Config{
Service: "myapp",
Level: logger.LevelInfo,
TraceIDFn: otel.GetTraceID,
Events: logger.Events{
Error: func(ctx context.Context, r slog.Record) {
alert.Capture(ctx, r) // r carries service, trace_id, and any With/Named attrs
},
},
})
The callback receives the full slog.Record with the accumulated With/Named/service attributes and the trace_id replayed in, so it sees the same context the sink writes. It runs synchronously before the write, so keep it fast (offload slow work to a goroutine) and do not log at the same level from within it (that re-enters the hook).
Events is a hook, not a router: it is additive to whatever sink you built. To send full records to multiple destinations by level, compose a FanoutHandler (above); the two combine — a fan-out sink plus an Error hook.
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.
- Events: optional per-level side-effect hook (see above); zero = none.
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 EventFn
- type Events
- type FanoutHandler
- type Level
- type Logger
- func (l *Logger) BuildInfo(ctx context.Context)
- func (l *Logger) Debug(ctx context.Context, msg string, args ...any)
- func (l *Logger) Debugc(ctx context.Context, skip int, msg string, args ...any)
- func (l *Logger) Error(ctx context.Context, msg string, args ...any)
- func (l *Logger) Errorc(ctx context.Context, skip int, msg string, args ...any)
- func (l *Logger) Handler() slog.Handler
- func (l *Logger) Info(ctx context.Context, msg string, args ...any)
- func (l *Logger) Infoc(ctx context.Context, skip int, 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) Warnc(ctx context.Context, skip int, 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
// Events assigns an optional side-effect callback per level (e.g. fire an
// alert on Error). It is additive: the record is still written to the sink
// (including any FanoutHandler). Zero value installs no hook. See Events.
Events Events
}
Config configures a Logger.
type EventFn ¶ added in v0.6.0
EventFn is a side-effect callback invoked for a record at its level, IN ADDITION to normal handling — the record is still written to the underlying sink (including any FanoutHandler). Typical use: capture ERROR records to Sentry or fire an alert, while stdout logging is untouched.
Unlike a lossy decoded snapshot, the callback receives the full slog.Record, reconstructed to include the attributes accumulated via With/Named and the service tag (which live on the handler, not the record) plus the trace_id, so the hook sees the same context the record is written with.
The callback runs synchronously on the logging goroutine, before the write. Keep it fast: offload slow work (network calls) to a goroutine or buffered channel. Do NOT log at the hooked level from inside the callback — that re-enters the hook and recurses.
type Events ¶ added in v0.6.0
Events assigns an EventFn per level. The zero value (all nil) installs no hook; pass it in Config.Events. Hooks are additive to the sink, never a replacement for it — for multi-sink routing use a FanoutHandler.
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, TraceIDFn and Events 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).
The handler stack, innermost (closest to the sink) first, is:
h (the given sink, e.g. a FanoutHandler)
└─ eventHandler (if cfg.Events is set: side-effect hooks, additive)
└─ WithAttrs(service)
└─ traceHandler (if cfg.TraceIDFn is set)
The ordering is deliberate: the event hook sits below service/trace so its callback sees the service attribute (accumulated via WithAttrs) and the trace_id (added to the record by traceHandler), i.e. the full context the record is written with. See Events.
func (*Logger) BuildInfo ¶ added in v0.6.0
BuildInfo logs the build metadata embedded in the Go binary (VCS revision, dirty flag, build settings, Go and module versions) at info level. Call it once at startup so every process records exactly what is running. When the info is unavailable (e.g. `go run` without VCS stamping) it logs a warning instead.
func (*Logger) Debugc ¶ added in v0.6.0
Debugc logs at debug level, skipping skip extra source frames.
func (*Logger) Errorc ¶ added in v0.6.0
Errorc logs at error level, skipping skip extra source frames.
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.