Skip to content
HN On Hacker News ↗

GitHub - arhuman/dllog: Debug context when you need it, quiet logs when you don't. Error-triggered verbose log for Go.

▲ 4 points • 0 comments • by doolta • 4w ago • HN discussion ↗

Pangram verdict · v3.3

We believe this text is mainly AI, with some human-written content.

90 %

AI likelihood · overall

AI
6% human-written 94% AI-generated
SEGMENTS · HUMAN 0 of 3
SEGMENTS · AI 2 of 3
WORD COUNT 799
PEAK AI % 98% · §3
Analyzed
Sep 14
backend: pangram/v3.3
Segments scanned
3 windows
avg 266 words each
Distribution
6 / 94%
human / AI fraction
Verdict
AI
Pangram v3.3

Article text · 799 words · 3 segments analyzed

Human AI-generated
§1 AI · 85%

Every Go service faces the same choice: Debug floods production, Info hides the context that explains an error. dllog breaks that trade-off per operation. It buffers below-level records in a bounded per-operation ring, so a successful operation stays as quiet as your logger at Info while a failed one replays the Debug records that led to it, with their original timestamps. dllog is not a logging library and does not replace yours: it plugs into the one you already use. It works with log/slog and zap today, even mixed in the same service, and other libraries can be supported the same way. The same failure, three ways Three checkouts, logged three times by the same program: one succeeds, one ends in a declined payment, one succeeds again. Reproduce any column with go run ./examples/demo [info|debug|dllog]. slog at Info dllog at Info slog at Debug You know the middle one failed. You do not know why: the card token, the retry, the decline code were all below the level. As quiet as the left column on the two that succeed. On the one that fails, the buffered Debug records replay with their original timestamps, marked replay=true. The full story, but you pay for it on the successful checkouts too, which is why nobody leaves this on. Install go get github.com/arhuman/dllog Requires Go 1.24 or later. The root package has no dependencies: zap is optional and imported only by zapadapter. Usage Pick the entry point that matches the code you are instrumenting: HTTP server: wrap your handler in Middleware(). Anything else: open a scope with Scope, and call Trip when you fail. Using zap instead of slog: see the zap adapter. HTTP NewJSON builds the handler and its output for you. There is nothing else to wire: the service logs at Info, and a failed request also gets the Debug records that led to it.

§2 Mixed · 44%

package main import ( "log/slog" "net/http" "os" "github.com/arhuman/dllog" ) func main() { logger := slog.New(dllog.NewJSON(os.Stderr)) slog.SetDefault(logger) mux := http.NewServeMux() mux.HandleFunc("/order", func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Buffered: invisible on a successful request.

§3 AI · 98%

slog.DebugContext(ctx, "loading cart", "user", 42) slog.DebugContext(ctx, "applying discount", "code", "SUMMER") // Any Error record replays everything buffered above it first. slog.ErrorContext(ctx, "payment declined", "provider", "stripe") w.WriteHeader(http.StatusInternalServerError) }) // The middleware opens a scope per request, trips on 5xx and on panic. http.ListenAndServe(":8080", dllog.Middleware()(mux)) } A request that fails emits the two buffered Debug records, marked and carrying the time they were logged at, ahead of the error that released them: {"time":"2026-09-12T10:23:06.226402+02:00","level":"DEBUG","msg":"loading cart","user":42,"replay":true} {"time":"2026-09-12T10:23:06.226433+02:00","level":"DEBUG","msg":"applying discount","code":"SUMMER","replay":true} {"time":"2026-09-12T10:23:06.226434+02:00","level":"ERROR","msg":"payment declined","provider":"stripe"} A request that succeeds emits neither: the buffer is discarded when the scope ends. Outside HTTP Manage the scope yourself. Trip covers the common Go case where the error is returned rather than logged: func process(ctx context.Context, id string) error { ctx, done := dllog.Scope(ctx) defer done() slog.DebugContext(ctx, "fetching record", "id", id) if err := doWork(ctx); err != nil { dllog.Trip(ctx) // replay the buffer, then return the error as usual return err } return nil // buffer discarded, nothing emitted } Scope joins rather than nests: calling it on a context that already carries a scope returns that same scope and a done that does nothing, so only the creator releases the buffer. Cost Outside a scope, a Debug call costs 9.1 ns/op and zero allocations against the 4.2 ns/op of a plain slog logger configured at Info: the record is refused before it is built, and the difference is one context lookup. Inside a scope, buffering a record costs about 235 ns and one allocation, the price of having it available if the operation later fails. The buffer is strictly count-bounded: a fixed preallocated ring per scope, 256 records by default, evicting oldest-first with a synthetic record reporting anything dropped. What each buffered record retains is up to you, since a record keeps references to what you logged until the scope ends: see the caveats. Nothing grows with uptime. Full tables and methodology: docs/performance.md. Documentation Document Contents Configuration Every option, choosing the output encoding, wrapping a handler you already have Performance Benchmarks, the memory model, soak results Caveats Mutated values, buffer retention, WithGroup zap adapter Driving the same engine from zap, and its binding cost ADRs Architecture decision records A buffered record is written now and formatted later, which has consequences worth knowing before you rely on it: read the caveats. Status The log/slog handler, the HTTP middleware, and the zap adapter are implemented and tested. Neither adapter is built on the other: both drive internal/core directly, and either one's Scope is visible to the other. Pre-v1: released as v0.1.x, and the API may still change before v1. Releases are tagged and listed in the CHANGELOG. Security Report a vulnerability by email rather than a public issue: see SECURITY.md. License MIT, see LICENSE.