Add Writer io.Writer field to Options; when nil, defaults to os.Stdout (no breaking change). Enables log capture in tests via the public API and supports writing to files, multi-writers, or any io.Writer implementation in production. All remaining roadmap items validated in production (xerrors enrichment concurrency, WithFields merge semantics, Logger interface finality). API committed as stable.
logz
Structured logging backed by
log/slogwith automatic error enrichment.
Module: code.nochebuena.dev/go/logz
Tier: 1 — stdlib only (log/slog, context, errors, os)
Go: 1.25+
Dependencies: none
Overview
logz wraps log/slog behind a simple Logger interface. It adds two things on top of plain slog:
- Automatic error enrichment —
Errorinspects the error forErrorCode()andErrorContext()methods and appends the code and context fields to the log record automatically. This pairs withxerrors.Errwithout importingxerrors. - Context propagation helpers —
WithRequestID,WithField,WithFieldsstore values incontext.Context;WithContextcreates a child logger pre-loaded with those values.
Installation
go get code.nochebuena.dev/go/logz
Quick start
import (
"log/slog"
"code.nochebuena.dev/go/logz"
)
logger := logz.New(logz.Options{
Level: slog.LevelInfo,
JSON: true,
StaticArgs: []any{"service", "api"},
})
logger.Info("server started", "port", 8080)
logger.Error("request failed", err)
Usage
Creating a logger
// Zero value: INFO level, text output, no static args.
logger := logz.New(logz.Options{})
// Production: JSON, custom level, static service tag.
logger := logz.New(logz.Options{
Level: slog.LevelInfo,
JSON: true,
StaticArgs: []any{"service", "payments", "env", "prod"},
})
The library does not read environment variables. Reading LOG_LEVEL or LOG_JSON_OUTPUT is the application's responsibility — pass the parsed values into Options.
Logging
logger.Debug("cache miss", "key", cacheKey)
logger.Info("user created", "user_id", id)
logger.Warn("slow query", "duration_ms", 520)
logger.Error("save failed", err, "table", "orders")
Error automatically enriches the log record when err satisfies the duck-type interfaces:
| Method | What it adds |
|---|---|
ErrorCode() string |
error_code attribute |
ErrorContext() map[string]any |
all key-value pairs in the map |
Child loggers
// Attach fixed attrs to every record from this logger.
reqLogger := logger.With("request_id", id, "user_id", uid)
// Attach attrs stored in context.
reqLogger := logger.WithContext(ctx)
Context helpers
// Store values.
ctx = logz.WithRequestID(ctx, requestID)
ctx = logz.WithField(ctx, "user_id", userID)
ctx = logz.WithFields(ctx, map[string]any{"tenant": "acme", "region": "us-east"})
// Retrieve.
id := logz.GetRequestID(ctx)
// Build a child logger with all context values pre-attached.
reqLogger := logger.WithContext(ctx)
WithFields merges with any existing fields in the context — it does not overwrite them.
Design decisions
No singleton — logz.New(opts) returns a plain value. Each component that needs logging receives a logz.Logger via constructor injection. Tests can create isolated loggers without global state.
Error replaces LogError — enrichment is automatic and zero-overhead when the error is a plain error. Callers need only one method instead of two.
Fatal removed — calling os.Exit(1) inside a library is untestable and bypasses deferred cleanup. Callers log the error then decide how to exit:
logger.Error("fatal startup failure", err)
os.Exit(1)
No env-var reading — libraries should not read environment variables. The application reads LOG_LEVEL/LOG_JSON_OUTPUT and passes parsed values into Options.
Duck-typing bridge — logz defines private errorWithCode and errorWithContext interfaces. xerrors.Err satisfies both structurally — no import of xerrors is needed.
Ecosystem
Tier 0: xerrors
↑ (duck-types — no direct import)
Tier 1: logz ← you are here
↑
Tier 2: httpclient, httputil
↑
Tier 4: httpmw, httpauth, httpserver
License
MIT