Files
logz/README.md
Rene Nochebuena 3667b92fab feat(logz): initial stable release v0.9.0
Structured logger backed by log/slog with request-context enrichment, extra-field context helpers, and duck-typed automatic error enrichment.

What's included:
- `Logger` interface with Debug / Info / Warn / Error / With / WithContext; `New(Options)` constructor writing to os.Stdout
- `WithRequestID` / `GetRequestID` and `WithField` / `WithFields` context helpers — package owns both context keys
- Automatic error_code and context-field enrichment in Logger.Error via duck-typed errorWithCode / errorWithContext interfaces (no xerrors import)

Tested-via: todo-api POC integration
Reviewed-against: docs/adr/
2026-03-18 13:31:39 -06:00

3.9 KiB

logz

Structured logging backed by log/slog with 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:

  1. Automatic error enrichmentError inspects the error for ErrorCode() and ErrorContext() methods and appends the code and context fields to the log record automatically. This pairs with xerrors.Err without importing xerrors.
  2. Context propagation helpersWithRequestID, WithField, WithFields store values in context.Context; WithContext creates 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 singletonlogz.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 bridgelogz 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