log

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jun 15, 2026 License: MIT Imports: 7 Imported by: 2

README

log

Quality Go Reference GitHub Tag License

Simple slog wrapper

github.com/toaweme/log is a thin layer over the standard library's log/slog. Everything is an ordinary slog.Handler, so it composes with the stdlib and any other handler you already use. It has zero dependencies. It adds:

  • log.New(...) assembles outputs, a level, and filters without hand-wiring handlers.
  • Filtering - drop noisy records or shorten fat attribute values, by level, message, or attribute match (with * prefix wildcards).
  • Fan-out - send one record to several outputs at once (console + file + ...).
  • Custom levels - TRACE below DEBUG and FATAL above ERROR, rendered with their names instead of slog's numeric fallback.
  • log.Discard() - a silent log.Logger for tests and libraries that should produce no output.
go get github.com/toaweme/log

Quick start

For app code that just wants to log, use the package-level helpers. They write text to stdout at DEBUG out of the box, no setup:

log.Info("server", "port", 8080)
log.Error("request", "err", err)
log.Trace("entered", "i", i)

log.SetLevel(slog.LevelInfo) // raise the threshold

When you're ready to inject a logger instead of reaching for the global, build one with log.New.

Build a logger: log.New

log.New takes a handful of options and assembles the handlers for you. With no options it writes text to stdout at DEBUG.

logger := log.New(
    log.WithText(os.Stdout),         // text output
    log.WithLevel(slog.LevelInfo),
)

logger.Info("ready")
logger = logger.With("svc", "api") // every record now carries svc=api

log.Logger is the interface you pass around. It is itself a slog.Handler, so it drops into anything that expects one.

Option What it adds
log.WithText(w) a text handler writing to w
log.WithJSON(w) a JSON handler writing to w
log.WithOutput(h) any slog.Handler you already have (memory sink, exporter, ...)
log.WithLevel(l) minimum level for the Text/JSON outputs (default DEBUG)
log.WithFilters(f...) wraps every output in a FilterHandler

Pass as many outputs as you like; they fan out automatically.

Recipes

Console + rotating file

This package never imports a rotation library, so it stays dependency-free. log.WithJSON takes an io.Writer, so pass your own rotating writer (here lumberjack) to it:

logger := log.New(
    log.WithText(os.Stdout),
    log.WithJSON(&lumberjack.Logger{
        Filename:   "/var/log/app.log",
        MaxSize:    20, // MB
        MaxBackups: 5,
        Compress:   true,
    }),
)

Human-readable text on the console, structured JSON in a rotated file, from one logger.

Make it the global, for the package helpers

Build the logger you want once at startup and install it, so log.Info and friends route through it:

func setupLogging(path string) {
    logger := log.New(
        log.WithText(os.Stdout),
        log.WithJSON(&lumberjack.Logger{Filename: path, MaxSize: 20, MaxBackups: 5, Compress: true}),
        log.WithFilters(
            log.Deny().Attr("component", "cache*"), // hush a chatty subsystem
        ),
    )
    log.SetDefault(logger)
}

After SetDefault, log.Info(...) writes to both outputs and obeys the filters.

Console + an in-memory sink (e.g. a live log view)

Use log.WithOutput to add any handler you have, like one that pushes records to subscribers for a UI:

mem := NewMemoryHandler(subscribers...) // your own slog.Handler

logger := log.New(
    log.WithText(os.Stdout),
    log.WithOutput(mem),
).With("pid", os.Getpid())

Building a raw handler yourself? Pass log.HandlerOptions(level) as its *slog.HandlerOptions so it renders the custom TRACE/FATAL level names the same way Text/JSON do.

Inject the logger into your types

Depend on the log.Logger interface, not a global. It keeps types testable and mockable:

type Server struct {
    log log.Logger
}

func NewServer(l log.Logger) *Server {
    return &Server{log: l.With("component", "server")}
}

func (s *Server) handle() {
    s.log.Debug("handling request")
}

Pass log.New(...) in production and log.Default() (or a buffer-backed log.New(log.WithText(&buf))) in tests.

A silent logger: log.Discard()

When a test or a library just needs a log.Logger that produces no output, use log.Discard(). It drops every record.

srv := NewServer(log.Discard()) // logs nothing

func TestThing(t *testing.T) {
    thing := New(log.Discard()) // keep test output clean
    // ...
}

It is the idiomatic null logger for this package, the equivalent of wiring up slog.New(slog.DiscardHandler) yourself.

Filtering

log.WithFilters wraps your outputs in a FilterHandler that runs an ordered list of filters over each record. Filters are built fluently:

log.New(
    log.WithText(os.Stdout),
    log.WithFilters(
        // drop everything below Info (a level floor)
        log.Deny().Below(slog.LevelInfo),
        // drop a chatty subsystem; * is a prefix match
        log.Deny().Attr("component", "cache-*"),
        // truncate the fat "body" attr to 200 chars wherever it appears
        log.Shorten("body").Limit(200),
    ),
)

Builders:

  • log.Deny() drops matching records.
  • log.Allow() passes matching records through unchanged.
  • log.Shorten(keys...) truncates the given attribute values (default limit 100, change with .Limit(n)).

Match criteria (chain as many as you need; all must match):

  • .Message("...") - exact message match.
  • .Attr(key, val) - attribute equals val; a val ending in * is a prefix match. The record's message is available under the synthetic "msg" key.
  • .Below(level) - matches records strictly below level. Paired with Deny it acts as a floor.

Filters can be changed at runtime on a *FilterHandler via AddFilter and SetFilters; both are safe to call while logging.

Fan-out and custom levels directly

The primitives log.New builds on are exported for hand-assembly:

// fan one record out to several handlers
multi := log.NewMultiHandler(
    slog.NewTextHandler(os.Stdout, log.HandlerOptions(slog.LevelDebug)),
    slog.NewJSONHandler(file, log.HandlerOptions(slog.LevelDebug)),
)

// wrap any handler in filters
filtered := log.NewFilterHandler(multi, log.Deny().Below(slog.LevelInfo))

logger := log.Wrap(slog.New(filtered)) // adopt an existing *slog.Logger

A MultiHandler drops a record only when every child would discard it, and one failing output does not stop the others (errors are joined). log.Wrap adopts any *slog.Logger as a log.Logger; logger.Slog() gets the *slog.Logger back.

The custom levels are log.LevelTrace (below DEBUG) and log.LevelFatal (above ERROR). Every log.Logger has Trace/Fatal helpers, and WithLevel returns a logger at a new threshold while keeping the same outputs:

quiet := logger.WithLevel(slog.LevelError) // same outputs, higher threshold

Opinions

  • Fatal does not exit. It logs a FATAL record and returns. slog itself ships no Fatal, and os.Exit inside a logging call skips deferred cleanup and unflushed writers, including the FATAL record itself. If you want to exit, call os.Exit(1) yourself, after the record is flushed or shipped.
  • Filter.Below is a floor, not a ceiling. It matches records below the given level. See Filtering.
  • There is a global logger. Created in init, writing text to stdout. It is there for convenience; prefer injecting log.Logger in code you care about and treat the global as a quick-start. log.SetLevel only moves the built-in default; once you SetDefault your own logger, set its level when you build it.

Documentation

Overview

Package log is a small, opinionated slog extension. Composable handlers (filtering, fan-out, per-logger level), two custom levels (TRACE, FATAL).

Note: Fatal logs a FATAL record and returns. It does NOT call os.Exit; the decision to exit (and to flush or ship logs first) stays with the caller.

Index

Constants

View Source
const (
	// LevelTrace sits below slog.LevelDebug for the noisiest diagnostics.
	LevelTrace = slog.Level(-8)
	// LevelFatal sits above slog.LevelError. Logging at this level does not
	// exit the process; it only emits a FATAL record.
	LevelFatal = slog.Level(12)
)

Variables

This section is empty.

Functions

func Debug

func Debug(msg string, args ...any)

Debug logs at slog.LevelDebug on the default logger.

func Error

func Error(msg string, args ...any)

Error logs at slog.LevelError on the default logger.

func Fatal

func Fatal(msg string, args ...any)

Fatal logs at LevelFatal on the default logger; it does not exit the process.

func HandlerOptions added in v0.2.0

func HandlerOptions(level slog.Leveler) *slog.HandlerOptions

HandlerOptions returns slog.HandlerOptions wired to level with the custom-level name rendering, for building raw handlers passed to WithOutput.

func Info

func Info(msg string, args ...any)

Info logs at slog.LevelInfo on the default logger.

func Level

func Level() slog.Level

Level returns the default logger's minimum level.

func SetDefault

func SetDefault(l Logger)

SetDefault replaces the process-wide Logger.

func SetLevel

func SetLevel(level slog.Level)

SetLevel sets the minimum level of the default logger built in init. It has no effect after SetDefault replaces the default with a logger of your own.

func Trace

func Trace(msg string, args ...any)

Trace logs at LevelTrace on the default logger.

func Warn

func Warn(msg string, args ...any)

Warn logs at slog.LevelWarn on the default logger.

Types

type Filter

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

Filter selects log records and decides what happens to them, built fluently:

log.Deny().Below(slog.LevelInfo)
log.Deny().Attr("path", "/healthz*")
log.Shorten("body").Limit(200).Message("http response")

A record must match every set criterion (level, message, attributes) for the filter's action to apply; criteria left unset are ignored.

func Allow

func Allow() Filter

Allow starts a filter that passes matching records through unchanged.

func Deny

func Deny() Filter

Deny starts a filter that drops matching records.

func Shorten

func Shorten(keys ...string) Filter

Shorten starts a filter that truncates the given attribute keys on matching records. The default length limit is 100; change it with Limit.

func (Filter) Attr

func (f Filter) Attr(key, val string) Filter

Attr matches when the record's attribute key equals val. A val ending in "*" matches by prefix. The record message is available under the "msg" key.

func (Filter) Below

func (f Filter) Below(level slog.Level) Filter

Below matches records strictly below level (e.g. Below(Info) matches Debug and Trace). Paired with Deny it acts as a level floor.

func (Filter) Limit

func (f Filter) Limit(n int) Filter

Limit sets the Shorten length limit.

func (Filter) Message

func (f Filter) Message(msg string) Filter

Message matches records whose message equals msg exactly.

type FilterHandler added in v0.2.0

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

FilterHandler is a slog.Handler that applies an ordered list of filters to each record before passing it to a wrapped handler. Filters run in order; the first Deny match drops the record, and Shorten matches rewrite attributes.

func NewFilterHandler added in v0.2.0

func NewFilterHandler(handler slog.Handler, filters ...Filter) *FilterHandler

NewFilterHandler wraps handler with the given filters.

func (*FilterHandler) AddFilter added in v0.2.0

func (f *FilterHandler) AddFilter(filter Filter)

AddFilter appends a filter; safe to call concurrently with logging.

func (*FilterHandler) Enabled added in v0.2.0

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

Enabled reports whether the wrapped handler emits records at level. Filters are evaluated in Handle, not here, since they can match on message or attrs.

func (*FilterHandler) Handle added in v0.2.0

func (f *FilterHandler) Handle(ctx context.Context, record slog.Record) error

Handle applies each matching filter to the record and forwards the result to the wrapped handler. A matching Deny returns early and drops the record.

func (*FilterHandler) SetFilters added in v0.2.0

func (f *FilterHandler) SetFilters(filters []Filter)

SetFilters replaces the filter list; safe to call concurrently with logging.

func (*FilterHandler) WithAttrs added in v0.2.0

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

WithAttrs returns a new FilterHandler sharing the same filters, with attrs applied to the wrapped handler.

func (*FilterHandler) WithGroup added in v0.2.0

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

WithGroup returns a new FilterHandler sharing the same filters, with the named group applied to the wrapped handler.

type Logger

type Logger interface {
	slog.Handler
	With(args ...any) Logger
	// WithLevel returns a logger with a new minimum level, preserving the
	// underlying outputs, format, and attributes.
	WithLevel(level slog.Level) Logger
	Trace(msg string, args ...any)
	Debug(msg string, args ...any)
	Info(msg string, args ...any)
	Warn(msg string, args ...any)
	Error(msg string, args ...any)
	// Fatal logs at LevelFatal and returns; it does not exit the process.
	Fatal(msg string, args ...any)
	// Slog returns the wrapped *slog.Logger as an escape hatch.
	Slog() *slog.Logger
}

Logger is the extended slog contract: a slog.Handler plus level-aware helpers and the custom Trace/Fatal levels.

func Default

func Default() Logger

Default returns the process-wide Logger backing the package-level helpers.

func Discard added in v0.2.0

func Discard() Logger

Discard returns a Logger that drops every record. Useful as a default in tests or libraries that take a Logger but should stay silent.

func New

func New(opts ...Option) Logger

New assembles a Logger from the given outputs, level, and filters. With no outputs it writes text to stdout at Debug.

func Wrap

func Wrap(l *slog.Logger) Logger

Wrap adopts an existing *slog.Logger as a Logger.

type MultiHandler

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

MultiHandler fans a record out to several handlers, so one logger can write to multiple outputs (e.g. a text console and a JSON file) at once.

func NewMultiHandler

func NewMultiHandler(handlers ...slog.Handler) *MultiHandler

NewMultiHandler returns a handler that dispatches to each of handlers in order.

func (*MultiHandler) Enabled

func (m *MultiHandler) Enabled(ctx context.Context, level slog.Level) bool

Enabled reports whether any child handler is enabled for the level, so a record is dropped only when every output would discard it.

func (*MultiHandler) Handle

func (m *MultiHandler) Handle(ctx context.Context, r slog.Record) error

Handle dispatches the record to every enabled child handler and joins any errors, so one failing output does not stop the others.

func (*MultiHandler) WithAttrs

func (m *MultiHandler) WithAttrs(attrs []slog.Attr) slog.Handler

WithAttrs returns a new MultiHandler with attrs applied to every child handler.

func (*MultiHandler) WithGroup

func (m *MultiHandler) WithGroup(name string) slog.Handler

WithGroup returns a new MultiHandler with the group applied to every child.

type Option

type Option func(*builder)

Option configures a Logger built by New.

func WithFilters

func WithFilters(filters ...Filter) Option

WithFilters wraps the assembled outputs in a FilterHandler.

func WithJSON

func WithJSON(w io.Writer) Option

WithJSON adds a JSON handler writing to w. Pass a rotating writer (e.g. a lumberjack.Logger) here to keep that dependency out of this module.

func WithLevel

func WithLevel(level slog.Level) Option

WithLevel sets the minimum level for the Text and JSON outputs (default Debug).

func WithOutput

func WithOutput(h slog.Handler) Option

WithOutput adds an arbitrary slog.Handler (a memory sink, an exporter, ...). The handler controls its own level; WithLevel does not affect it.

func WithText

func WithText(w io.Writer) Option

WithText adds a text handler writing to w.

Jump to

Keyboard shortcuts

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