A trace ID is a request-scoped identifier that links log records produced while handling the same HTTP request. It turns scattered failures, paths, and status records into a correlated trail, so troubleshooting does not depend on guessing which entries belong together. section 9.5
In Go, carry the ID in context.Context. Generate it at the HTTP boundary, attach it with context.WithValue, and pass the resulting request context downstream. Context values form a parent chain: adding an ID returns a new Context while retaining values from its parent. section 9.5.1 section 9.5.2
Figure 9.5 Injecting and propagating trace IDs through Request.Context
The request flow shows middleware injecting one ID into Request.Context and making it available to every downstream handler that receives that request. section 9.5
Keep the trace-ID package small and defensive:
- Use package-local, unexported key types for Context storage. The same key must be used to retrieve the value, and an unexported type prevents collisions with keys from other packages. section 9.5.1
- Return the ID together with a success boolean after asserting the value to string, because Context.Value returns any. section 9.5.1
- Add an ID only when the incoming request lacks one, so earlier middleware can supply it without being overwritten. section 9.5.2
- Treat the value as peripheral metadata, not as a control-flow dependency; Context values are type-unsafe and misuse can create subtle bugs. section 9.5.2
Logging code should not need to extract and attach the ID at every call site. Wrap an existing slog.Handler with a handler that reads the ID from the logging Context, clones the slog.Record, adds a trace_id attribute when present, and delegates rendering to the wrapped handler. section 9.5.3 section 9.5.4
Listing 9.12 LogHandler (link/kit/traceid/slog.go)
package traceid
import (
"context"
"log/slog"
)
// LogKey is the key used to store the trace ID in the log record.
const LogKey = "trace_id"
// LogHandler adds trace IDs from [context.Context] to [slog.Record].
type LogHandler struct {
slog.Handler #1
LogKey string // LogKey for the trace ID (Default: [LogKey])
}
// NewLogHandler returns a new [LogHandler].
func NewLogHandler(next slog.Handler) *LogHandler {
return &LogHandler{
Handler: next, #2
LogKey: LogKey,
}
}
// Handle may add a [slog.Attr] to a [slog.Record].
func (h *LogHandler) Handle( #3
ctx context.Context, r slog.Record,
) error {
if id, ok := FromContext(ctx); ok { #4
r = r.Clone() #5
r.AddAttrs(slog.String(h.LogKey, id)) #6
}
return h.Handler.Handle(ctx, r) #7
}
The handler implementation demonstrates composition: embed the next slog.Handler, enrich only records whose Context contains an ID, then delegate handling. section 9.5.4
HTTP server integration
Wrap the logger's handler with LogHandler, pass that logger through application handlers, then wrap the server handler with trace-ID middleware. A request such as GET /health can then produce a log entry containing its path, method, and trace_id automatically. section 9.5.6 section 9.5.6




