logger

package
v0.8.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 20, 2026 License: MIT Imports: 9 Imported by: 0

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

View Source
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

type EventFn func(ctx context.Context, r slog.Record)

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

type Events struct {
	Debug EventFn
	Info  EventFn
	Warn  EventFn
	Error EventFn
}

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

func (f *FanoutHandler) Enabled(ctx context.Context, level slog.Level) bool

Enabled reports whether any wrapped handler is enabled for the level.

func (*FanoutHandler) Handle

func (f *FanoutHandler) Handle(ctx context.Context, r slog.Record) error

Handle forwards the record to every wrapped handler that is enabled for it.

func (*FanoutHandler) WithAttrs

func (f *FanoutHandler) WithAttrs(attrs []slog.Attr) slog.Handler

WithAttrs implements slog.Handler, fanning attrs out to each child handler.

func (*FanoutHandler) WithGroup

func (f *FanoutHandler) WithGroup(name string) slog.Handler

WithGroup implements slog.Handler, fanning the group out to each child handler.

type Level

type Level = slog.Level

Level mirrors slog.Level for convenience.

type Logger

type Logger struct {
	// contains filtered or unexported fields
}

Logger is the application logger.

func New

func New(w io.Writer, cfg Config) *Logger

New builds a Logger writing JSON to w. For multi-sink behavior, build the Logger with NewWithHandler and a FanoutHandler instead.

func NewWithHandler

func NewWithHandler(h slog.Handler, cfg Config) *Logger

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

func (l *Logger) BuildInfo(ctx context.Context)

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) Debug

func (l *Logger) Debug(ctx context.Context, msg string, args ...any)

Debug logs at debug level.

func (*Logger) Debugc added in v0.6.0

func (l *Logger) Debugc(ctx context.Context, skip int, msg string, args ...any)

Debugc logs at debug level, skipping skip extra source frames.

func (*Logger) Error

func (l *Logger) Error(ctx context.Context, msg string, args ...any)

func (*Logger) Errorc added in v0.6.0

func (l *Logger) Errorc(ctx context.Context, skip int, msg string, args ...any)

Errorc logs at error level, skipping skip extra source frames.

func (*Logger) Handler

func (l *Logger) Handler() slog.Handler

Handler returns the underlying slog.Handler, so callers can build a stdlib *slog.Logger that shares the same sink and formatting.

func (*Logger) Info

func (l *Logger) Info(ctx context.Context, msg string, args ...any)

Info logs at info level.

func (*Logger) Infoc added in v0.6.0

func (l *Logger) Infoc(ctx context.Context, skip int, msg string, args ...any)

Infoc logs at info level, skipping skip extra source frames.

func (*Logger) Named

func (l *Logger) Named(name string) *Logger

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.

func (*Logger) Slog

func (l *Logger) Slog() *slog.Logger

Slog returns a stdlib *slog.Logger backed by the same handler.

func (*Logger) Warn

func (l *Logger) Warn(ctx context.Context, msg string, args ...any)

Warn logs at warn level.

func (*Logger) Warnc added in v0.6.0

func (l *Logger) Warnc(ctx context.Context, skip int, msg string, args ...any)

Warnc logs at warn level, skipping skip extra source frames.

func (*Logger) With

func (l *Logger) With(args ...any) *Logger

With returns a child Logger whose records always carry the given attributes. The child shares the parent's handler.

type TraceIDFn

type TraceIDFn func(ctx context.Context) string

TraceIDFn extracts a trace id from the context. Return "" when none exists.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL