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/
This commit is contained in:
134
README.md
Normal file
134
README.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# `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
|
||||
Reference in New Issue
Block a user