slogx

package module
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 15, 2026 License: GPL-2.0, GPL-3.0 Imports: 4 Imported by: 0

README

slogx

Go Reference Go version Test coverage OpenSSF Scorecard

Standard structured-logging setup for log/slog

A tiny, standard-library-only helper that installs the one slog handler shape a set of containerized Go apps kept hand-rolling: leveled text (logfmt) or JSON, UTC-normalized timestamps, and a *slog.LevelVar so the level can be set after config is read or flipped at runtime. Plus a LOG_LEVEL parser that adds the long-form warning alias slog lacks.

It is a thin wrapper around log/slog, not a logging framework and not a custom handler. Zero dependencies beyond the standard library.

Install

go get github.com/cplieger/slogx@latest

Usage

The common case — parse LOG_LEVEL and install the default logger. Parse first, install, then warn on a bad value (so the warning goes through the new handler):

lvl, ok := slogx.ParseLevel(os.Getenv("LOG_LEVEL"), slog.LevelInfo)
slogx.Setup(slogx.Options{Level: lvl})
if !ok {
	slog.Warn("invalid LOG_LEVEL, using default", "value", os.Getenv("LOG_LEVEL"), "default", "info")
}

An app whose structured log events are the product (shipped to Loki, rendered as dashboard columns) emits JSON to stdout:

slogx.Setup(slogx.Options{Format: slogx.JSON, Output: os.Stdout})

Install a handler before config is read (so early warnings still emit), then set the level once it is known — the returned *slog.LevelVar also flips the level at runtime for a debug toggle:

lv := slogx.Setup(slogx.Options{}) // Info default, on stderr

cfg := loadConfig() // may log warnings, which emit at Info
lvl, _ := slogx.ParseLevel(cfg.LogLevel, slog.LevelInfo)
lv.Set(lvl)

// later, from a settings toggle:
func setDebug(on bool) {
	if on {
		lv.Set(slog.LevelDebug)
	} else {
		lv.Set(slog.LevelInfo)
	}
}

Building your own handler options? UTCTime is exported as the escape hatch:

h := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ReplaceAttr: slogx.UTCTime})

API

  • Setup(Options) *slog.LevelVar — build a handler and install it as slog's default; returns the LevelVar backing its level.
  • NewHandler(Options) (slog.Handler, *slog.LevelVar) — the same without the SetDefault, for composition.
  • Options{Output, Format, Level, AddSource} — zero value is a text handler at Info on stderr.
  • FormatText (logfmt, default) or JSON.
  • ParseLevel(raw string, def slog.Level) (slog.Level, bool) — parse a level string (case-insensitive, warning alias, slog offset syntax); ok=false on a non-empty unparseable value.
  • ParseFormat(raw string, def Format) (Format, bool) — parse a format string (text/json, case-insensitive, trimmed); same contract as ParseLevel: empty returns the default with ok=true, a non-empty unrecognized value returns the default with ok=false so the caller can warn.
  • UTCTime(groups []string, a slog.Attr) slog.Attr — the ReplaceAttr that renders timestamps in UTC.
  • capture (subpackage slogx/capture) — a record-capturing slog.Handler for tests; see Testing.

Testing

The slogx/capture subpackage records log output so a test can assert on it without hand-rolling a buffer handler. For code that logs through slog.Default(), capture.Default(t) swaps in a recorder and restores the previous default on cleanup:

func TestWarnsWhenFull(t *testing.T) {
	rec := capture.Default(t)

	checkDisk() // logs through slog.Default()

	if rec.Count("disk almost full") != 1 {
		t.Errorf("want one warning, got %d", rec.Count("disk almost full"))
	}
}

For code that takes an injected *slog.Logger, capture.New() returns a logger plus its recorder and never touches the global default, so the test stays parallel-safe:

logger, rec := capture.New()
c := NewComponent(WithLogger(logger))
// ... exercise c, then assert on rec.Contains / Count / CountExact / Messages / Records

Count matches by substring; CountExact matches the whole message. Reach for CountExact when a message is pinned by an external contract (a log-based alert rule matching the exact msg), where a substring count would false-pass on a superstring message.

capture is a separate package so its testing import and record buffer never reach production consumers of slogx; import it only from _test.go files. Attributes added via Logger.With/WithGroup are not captured — assert on the message, level, and attributes passed directly to the log call, or use rec.Records() for the raw records.

Unsupported by design

These are deliberate non-goals, not a TODO list. The library is one cohesive concept — install the standard slog handler — and stays small on purpose.

Feature Rationale
A custom slog.Handler implementation slogx composes the standard-library Text/JSON handlers. If you need different formatting, write your own handler and use UTCTime in its options.
Secret redaction / attribute scrubbing Keeping secrets out of logs is per-call-site discipline (log token_set=true, not the token). A blanket redacting ReplaceAttr gives false confidence; the library will not add one.
Audit-event schemas A structured audit log (actor, action, outcome) is domain policy that belongs in the app, not in a generic logging helper.
LOG_LEVEL (or any) env-var names ParseLevel takes a string; the app owns which environment variable it comes from and its default.
Per-app attribute conventions Base attributes (slog.With("service", …)), key naming, and message wording are the app's editorial choices. Call .With on the logger Setup installs.
A logging facade / leveled wrapper types slog is the interface. slogx configures it and gets out of the way; it does not wrap slog.Logger in another type.

Disclaimer

This project is built with care and follows security best practices, but it is intended for personal / self-hosted use. No guarantees of fitness for production environments. Use at your own risk.

This project was built with AI-assisted tooling using Claude Opus and Kiro. The human maintainer defines architecture, supervises implementation, and makes all final decisions.

License

GPL-3.0 — see LICENSE.

Documentation

Overview

Package slogx is a standard structured-logging setup, extracted from the Go apps that all installed the same log/slog handler by hand.

It is a thin, cohesive helper around log/slog — not a logging framework and not a replacement handler:

  • Setup installs slog's default logger the standard way: a leveled text (logfmt) or JSON handler with UTC-normalized timestamps, returning the *slog.LevelVar so the level can be set after config is read or flipped at runtime. NewHandler is the same without the SetDefault, for composition.
  • ParseLevel maps a LOG_LEVEL string to an slog.Level, adding the long-form "warning" alias slog lacks and reporting whether the value was recognized.
  • UTCTime is the exported ReplaceAttr that renders timestamps in UTC, for consumers that build their own slog.HandlerOptions.

It deliberately does not own log-level environment-variable names, secret redaction, audit-event schemas, or per-app attribute conventions — those stay in the consuming app. It carries no dependencies beyond the standard library.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewHandler

func NewHandler(opts Options) (slog.Handler, *slog.LevelVar)

NewHandler builds a leveled slog.Handler with UTCTime applied and returns it alongside the *slog.LevelVar backing its level. Hold the LevelVar to change the level after install: install a handler before config is read (so early warnings emit at the default level), then Set the parsed level; or flip it at runtime for a debug toggle. Setup wraps this and installs the result as the default logger.

func ParseLevel

func ParseLevel(raw string, def slog.Level) (level slog.Level, ok bool)

ParseLevel maps a log-level string to an slog.Level. It is case-insensitive, trims surrounding space, and maps the long-form "warning" to "warn" (which slog.Level.UnmarshalText does not accept); otherwise it delegates to UnmarshalText, so offset syntax such as "warn+1" or "debug-2" also works.

An empty string returns def with ok=true — an unset level is not an error. A non-empty unparseable value returns def with ok=false, so the caller can warn. Parse the level BEFORE installing the handler (via Setup), then warn on ok==false afterward, so the warning is emitted through the configured handler.

Example
package main

import (
	"fmt"
	"log/slog"

	"github.com/cplieger/slogx"
)

func main() {
	lvl, ok := slogx.ParseLevel("warning", slog.LevelInfo)
	fmt.Println(lvl, ok)

	_, ok = slogx.ParseLevel("banana", slog.LevelInfo)
	fmt.Println(ok)
}
Output:
WARN true
false

func Setup

func Setup(opts Options) *slog.LevelVar

Setup builds a handler per opts and installs it as slog's default logger, returning the *slog.LevelVar backing its level for later changes (install early, then Set the level once config is read; or flip it at runtime). It is the one-call standard logger setup most apps make once at startup.

func UTCTime

func UTCTime(groups []string, a slog.Attr) slog.Attr

UTCTime is a slog ReplaceAttr that renders a record's built-in time key in UTC, so log-line timestamps are zone-stable regardless of the container's TZ. It rewrites only the top-level time attribute; a user attribute that happens to share the "time" key inside a group is left untouched. Setup and NewHandler apply it automatically; it is exported for consumers that build their own slog.HandlerOptions.

Types

type Format

type Format int

Format selects the slog handler's output encoding.

const (
	// Text emits canonical logfmt (time=… level=… msg=… k=v). It is the
	// default for a container's own lifecycle and diagnostic logs.
	Text Format = iota
	// JSON emits one JSON object per line, for apps whose structured log events
	// are the product (shipped to Loki and rendered as dashboard columns).
	JSON
)

func ParseFormat added in v1.2.0

func ParseFormat(raw string, def Format) (format Format, ok bool)

ParseFormat maps a log-format string to a Format. It is case-insensitive, trims surrounding space, and accepts "text" and "json" — the same contract ParseLevel gives a log-level string.

An empty string returns def with ok=true — an unset format is not an error. A non-empty unrecognized value returns def with ok=false, so the caller can warn (a config may hold an expanded secret in the wrong field, so warn field-name-only when that is a possibility). Parse the format BEFORE installing the handler (via Setup), then warn on ok==false afterward, so the warning is emitted through the configured handler.

type Options

type Options struct {
	// Output is the destination writer; nil means os.Stderr. Apps whose JSON log
	// events are the product typically set os.Stdout.
	Output io.Writer
	// Format is Text (default) or JSON.
	Format Format
	// Level is the initial level; the zero value is slog.LevelInfo. It is held in
	// the *slog.LevelVar that NewHandler and Setup return, so it can be changed
	// after the handler is installed.
	Level slog.Level
	// AddSource records the source file:line of each call site. Off by default —
	// useful when debugging, noisy for production.
	AddSource bool
}

Options configures a handler built by NewHandler or Setup. The zero value is valid and yields a text handler at Info level on stderr.

Directories

Path Synopsis
Package capture provides a slog.Handler that records log records for assertion in tests, plus helpers to install it as the default logger.
Package capture provides a slog.Handler that records log records for assertion in tests, plus helpers to install it as the default logger.

Jump to

Keyboard shortcuts

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