How do you add distributed tracing with OpenTelemetry in Go? How does the trace context flow through context.Context and HTTP/gRPC?
Question 567HardGo 1.22 to 1.25
Setup takes three steps:
- Create a
TracerProviderwith an exporter (usually OTLP), a resource (service name) and a sampler. - Register it globally along with a propagator.
- Wrap your transports.
Within a process, the active span lives in context.Context. tracer.Start(ctx, ...) returns a child context that holds the new span. Across processes, the propagator writes the W3C traceparent/tracestate headers into HTTP headers or gRPC metadata, and the receiving side extracts them.
func initTracing(ctx context.Context) (func(context.Context) error, error) {
exp, err := otlptracegrpc.New(ctx) // honours OTEL_EXPORTER_OTLP_ENDPOINT
if err != nil {
return nil, err
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exp),
sdktrace.WithResource(resource.NewSchemaless(semconv.ServiceName("orders"))),
sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1))),
)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{}, propagation.Baggage{}))
return tp.Shutdown, nil // call on exit to flush spans
}
var tracer = otel.Tracer("orders/service")
func (s *Service) Place(ctx context.Context, o Order) error {
ctx, span := tracer.Start(ctx, "Place", trace.WithAttributes(attribute.Int("items", len(o.Items))))
defer span.End()
if err := s.repo.Save(ctx, o); err != nil { // pass ctx down or the trace breaks
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
return nil
}
// HTTP server: otelhttp.NewHandler(mux, "server")
// HTTP client: &http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}
// gRPC server: grpc.NewServer(grpc.StatsHandler(otelgrpc.NewServerHandler()))
// gRPC client: grpc.NewClient(addr, grpc.WithStatsHandler(otelgrpc.NewClientHandler()), ...)
Gotchas:
- Using
context.Background()in the middle of a call chain, or starting a goroutine without passingctx, silently breaks the trace. - For background work that must outlive the request but stay linked to it, use
context.WithoutCancel(ctx)(Go 1.21). - Forgetting
span.End()leaks the span. - Not calling
Shutdownon exit loses the last batch of spans. - Keep attributes low-cardinality where your backend indexes them.
- Use
ParentBasedsampling so every service keeps or drops the same trace. - The OTel gRPC interceptors are deprecated. Use the stats handlers shown above.
More on Observability, Debugging & Production Operations
- Q565What does GOTRACEBACK control, and how do you get a core dump from a crashing Go program and analyze it?
- Q566How do you instrument a Go service with Prometheus metrics (counters, gauges, histograms)? What are the label cardinality pitfalls?
- Q568Explain gRPC in Go: unary vs streaming RPCs, interceptors, deadlines, status codes and error details.
- Q569How do you keep protobuf messages backward and forward compatible? Why must you never reuse field numbers?
- Q570How do you implement health checks (liveness vs readiness) for a Go service running in Kubernetes, and how do they interact with graceful shutdown?
- Q571How do you build a minimal, secure Docker image for a Go service (multi-stage build, scratch/distroless, CGO_ENABLED=0, CA certs, time zone data)?