go 103 lines · 3 tabs

Request-Scoped Structured Logging with slog and Context in Go

Shared by codesnips Jul 2026
3 tabs
package logctx

import (
	"context"
	"log/slog"
)

type ctxKey struct{}

func With(ctx context.Context, logger *slog.Logger) context.Context {
	return context.WithValue(ctx, ctxKey{}, logger)
}

func From(ctx context.Context) *slog.Logger {
	if logger, ok := ctx.Value(ctxKey{}).(*slog.Logger); ok && logger != nil {
		return logger
	}
	return slog.Default()
}

func Fields(ctx context.Context, args ...any) context.Context {
	enriched := From(ctx).With(args...)
	return With(ctx, enriched)
}
3 files · go Explain with highlit

This snippet shows how to attach a request-scoped structured logger to Go's context.Context so that every log line emitted during a request automatically carries shared fields like a request ID, method, and path. It uses the standard library's log/slog, so there are no third-party dependencies, and it demonstrates the common pattern of storing a logger in context rather than threading it through every function signature.

The logctx package defines the plumbing. A private, unexported ctxKey type is used as the context key so no other package can collide with it — this is the idiomatic way to avoid key collisions in context. With returns a child context carrying the given *slog.Logger, and From retrieves it, falling back to slog.Default() when none is present so callers never get a nil logger. The Fields helper wraps logger.With(...) so that additional structured attributes can be layered onto the request logger and re-stored in context, which keeps slog's attribute-inheritance behaviour intact.

The Middleware file wires this into an HTTP stack. For each request it generates or reuses an X-Request-Id, builds a base logger seeded with request_id, method, and path, and injects it via logctx.With. Because the logger lives in the request context, any downstream handler or service reached through r.Context() inherits those fields for free. A statusRecorder wraps http.ResponseWriter to capture the status code, and a defer logs a single completion line with latency in milliseconds and the response status — a compact access log built from structured fields rather than string formatting.

The handler usage file shows the payoff: the handler calls logctx.From(ctx) and never sees the request ID directly, yet every line it writes is correlated. Adding logctx.Fields(ctx, "user_id", id) enriches the context so subsequent logs in the same request include the user.

The trade-off is that context becomes an implicit dependency, and forgetting the middleware silently degrades to the default logger. The benefit is clean call sites, consistent correlation IDs, and JSON output that aggregators can index. This pattern is worth reaching for in any service where per-request tracing matters.


Related snips

Share this code

Here's the card — post it anywhere.

Request-Scoped Structured Logging with slog and Context in Go — share card
Link copied