package api
import (
"context"
"crypto/rand"
"encoding/hex"
"net/http"
"time"
)
type ctxKey string
const requestIDKey ctxKey = "request_id"
func withRequestID(ctx context.Context) (context.Context, string) {
b := make([]byte, 16)
_, _ = rand.Read(b)
id := hex.EncodeToString(b)
return context.WithValue(ctx, requestIDKey, id), id
}
func Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
ctx, reqID := withRequestID(ctx)
r = r.WithContext(ctx)
w.Header().Set("x-request-id", reqID)
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MiB
next.ServeHTTP(w, r)
})
}
description: "To get useful traces, you need propagation and a real exporter. I set a global `TextMapPropagator` (`TraceContext` + `Baggage`) so inbound headers connect spans across services. Then I configure an OTLP exporter and a batch span processor so tracing overhead stays low. I also explicitly set a sampler: `ParentBased(TraceIDRatioBased(0.1))` is a common starting point that respects upstream sampling and keeps costs predictable. The other key piece is resource attributes like `service.name`, which is how traces are grouped in most backends. Once this is initialized, you can start spans in handlers with `otel.Tracer("...").Start(ctx, ...)` and get end-to-end visibility without special log parsing.",
}
]
},
{
title: 'Structured logging with zap + per-request fields',
description: "When a service grows past one instance, `fmt.Println` becomes a liability. I prefer `zap` for structured logs, and I treat the request logger as a derived value: a base logger plus fields like `request_id`, `method`, and `path`. That way every log line in the request path is automatically correlated, including errors returned from lower layers. I also explicitly avoid logging secrets by keeping payload logs opt-in. The main trick is keeping the logger in context (or attaching it to a request-scoped struct) so handlers stay clean and don’t balloon with parameters. In practice this pattern makes incident response faster because you can filter by `request_id` and see the full story.",
tags: %w[go logging observability http],
code_blocks: [
{
name: 'logger.go',
hljs_language: 'go',
code: <<~GO
package observability
import (
"context"
"net/http"
"go.uber.org/zap"
)
type ctxKey string
const loggerKey ctxKey = "logger"
func FromContext(ctx context.Context) *zap.Logger {
if v := ctx.Value(loggerKey); v != nil {
if lgr, ok := v.(*zap.Logger); ok {
return lgr
}
}
return zap.NewNop()
}
func Middleware(base *zap.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reqID := r.Header.Get("x-request-id")
lgr := base.With(
zap.String("request_id", reqID),
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
)
ctx := context.WithValue(r.Context(), loggerKey, lgr)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
package api
import (
"net/http"
"example.com/app/observability"
)
func healthz(w http.ResponseWriter, r *http.Request) {
log := observability.FromContext(r.Context())
log.Info("health.check")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}