Tracing HTTP Requests with Go's net/http/httptrace
net/http/httptrace has been in the standard library since Go 1.7 and most Go developers I talk to have never used it. It exposes hooks for the points in an outgoing HTTP request that you usually cannot see from outside the transport: DNS resolution, connection acquisition, TLS handshake, the moment bytes go on the wire, the moment the first response byte comes back.
The interesting part is how it plugs in. There is no Tracer interface on http.Client , no middleware to register. You attach a ClientTrace to a context.Context and the transport pulls it back out via httptrace.ContextClientTrace at the points where it matters. I want to walk through that design choice first because it explains how the package composes with the rest of the stdlib, then build two things with it: a curl --trace -style CLI and a reusable http.RoundTripper that logs timings for every request.
Why Context, Not an Interface
The obvious design for request tracing would be to define a Tracer interface, add a Tracer field to http.Client or http.Transport , and call methods on it from inside the transport. That is roughly how most languages handle this.
Go's standard library does not work that way. Instead, httptrace.WithClientTrace returns a new context carrying a *ClientTrace , you attach that context to your request with req.WithContext(ctx) , and the transport pulls the trace back out via httptrace.ContextClientTrace at the points where it matters.
trace := & httptrace . ClientTrace { DNSStart: func ( info httptrace . DNSStartInfo ) { fmt. Printf ( "DNS start: %s
" , info.Host) }, DNSDone: func ( info httptrace . DNSDoneInfo ) { fmt. Printf ( "DNS done: %v
" , info.Addrs) }, } ctx := httptrace. WithClientTrace (context. Background (), trace) req, _ := http. NewRequestWithContext (ctx, http.MethodGet, "https://example.com" , nil ) http.DefaultClient. Do (req)
This is unusual but it pays off. The trace travels with the request, so any middleware that forwards the context propagates tracing for free. Nothing on the client is shared mutable state, so concurrent requests from the same http.Client can carry different traces. And the transport ignores the trace entirely if no one attached one, so the cost when unused is a nil check.
... continue reading