Files
logz/README.md

135 lines
3.9 KiB
Markdown
Raw Permalink Normal View History

# `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