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)
}
package httplog
import (
"log/slog"
"net/http"
"time"
"github.com/acme/app/logctx"
"github.com/google/uuid"
)
type statusRecorder struct {
http.ResponseWriter
status int
}
func (r *statusRecorder) WriteHeader(code int) {
r.status = code
r.ResponseWriter.WriteHeader(code)
}
func Middleware(base *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
reqID := r.Header.Get("X-Request-Id")
if reqID == "" {
reqID = uuid.NewString()
}
w.Header().Set("X-Request-Id", reqID)
logger := base.With(
"request_id", reqID,
"method", r.Method,
"path", r.URL.Path,
)
ctx := logctx.With(r.Context(), logger)
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
defer func() {
logger.InfoContext(ctx, "request completed",
"status", rec.status,
"latency_ms", time.Since(start).Milliseconds(),
)
}()
next.ServeHTTP(rec, r.WithContext(ctx))
})
}
}
package handlers
import (
"net/http"
"github.com/acme/app/logctx"
)
func GetOrder(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := logctx.From(ctx)
orderID := r.PathValue("id")
log.InfoContext(ctx, "fetching order", "order_id", orderID)
order, err := loadOrder(ctx, orderID)
if err != nil {
log.ErrorContext(ctx, "failed to load order", "order_id", orderID, "err", err)
http.Error(w, "not found", http.StatusNotFound)
return
}
// Enrich context so downstream logs carry the customer too.
ctx = logctx.Fields(ctx, "user_id", order.CustomerID)
logctx.From(ctx).InfoContext(ctx, "order authorized")
writeJSON(w, http.StatusOK, order)
}
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
package api
import (
"net/http"
"runtime/debug"
)
Expose build metadata for debugging deploys
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
package files
import (
"context"
"time"
Presigned S3 upload URLs (AWS SDK v2)
package deps
import (
"net"
"net/http"
"time"
HTTP client tuned for production: timeouts, transport, and connection reuse
Share this code
Here's the card — post it anywhere.