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/
135 lines
3.9 KiB
Markdown
135 lines
3.9 KiB
Markdown
# `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`](https://pkg.go.dev/log/slog) behind a simple `Logger` interface. It adds two things on top of plain slog:
|
|
|
|
1. **Automatic error enrichment** — `Error` 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 helpers** — `WithRequestID`, `WithField`, `WithFields` store values in `context.Context`; `WithContext` creates a child logger pre-loaded with those values.
|
|
|
|
## Installation
|
|
|
|
```sh
|
|
go get code.nochebuena.dev/go/logz
|
|
```
|
|
|
|
## Quick start
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// 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:
|
|
```go
|
|
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
|