OpenTelemetry Traceparent HTTP Header [Go]
This guide covers Go-specific implementation of the traceparent header. For a comprehensive overview of context propagation concepts, W3C TraceContext, propagators, baggage, and troubleshooting, see the OpenTelemetry Context Propagation guide.
What is traceparent header?
The traceparent HTTP header contains information about the incoming request in a distributed tracing system, for example:
# {version}-{trace_id}-{span_id}-{trace_flags}
traceparent: 00-80e1afed08e019fc1110464cfa66635c-7a085853722dc6d2-01
You can find the traceparent header in HTTP responses using browser developer tools to extract trace IDs and locate specific traces in distributed tracing tools.

Using the header, you can extract a trace ID to find the trace in a distributed tracing tool. For example, from the header above, the trace ID is 80e1afed08e019fc1110464cfa66635c.
Traceparent header format
The traceparent header uses the version-trace_id-parent_id-trace_flags format where:
versionis always00trace_idis a hex-encoded trace ID (16 bytes, 32 hex characters)span_idis a hex-encoded span ID (8 bytes, 16 hex characters)trace_flagsis a hex-encoded 8-bit field containing tracing flags such as sampling
Automatic propagation
OpenTelemetry Go handles traceparent headers automatically when using instrumentation libraries. The otelhttp package automatically injects traceparent headers into outgoing HTTP requests and extracts them from incoming requests.
HTTP server with automatic extraction
import (
"net/http"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func main() {
// Create your HTTP handler
mux := http.NewServeMux()
mux.HandleFunc("/api/users", handleUsers)
// Wrap with otelhttp for automatic propagation
handler := otelhttp.NewHandler(mux, "my-service")
http.ListenAndServe(":8080", handler)
}
func handleUsers(w http.ResponseWriter, r *http.Request) {
// Traceparent header is automatically extracted
// Current span context is available via r.Context()
ctx := r.Context()
// Use context for downstream operations
processRequest(ctx)
w.WriteHeader(http.StatusOK)
}
HTTP client with automatic injection
import (
"net/http"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
// Create instrumented HTTP client
client := &http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
}
// All requests automatically include traceparent headers
req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/data", nil)
resp, err := client.Do(req)
Manual propagation
When automatic instrumentation is not available, you can manually handle traceparent headers using OpenTelemetry's Propagators API.
Extracting from incoming requests
import (
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
)
func handleRequest(w http.ResponseWriter, r *http.Request) {
// Extract trace context from incoming request headers
ctx := otel.GetTextMapPropagator().Extract(r.Context(),
propagation.HeaderCarrier(r.Header))
// Create span with extracted context
ctx, span := tracer.Start(ctx, "handle_request",
trace.WithSpanKind(trace.SpanKindServer),
)
defer span.End()
// Process request with traced context
result := processRequest(ctx)
w.WriteHeader(http.StatusOK)
w.Write([]byte(result))
}
Injecting into outgoing requests
import (
"context"
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
)
func makeRequest(ctx context.Context, url string) error {
// Create a span for this operation
ctx, span := tracer.Start(ctx, "http_request",
trace.WithSpanKind(trace.SpanKindClient),
)
defer span.End()
// Create HTTP request
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return err
}
// Inject trace context into request headers
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))
// Make the request
resp, err := http.DefaultClient.Do(req)
if err != nil {
span.RecordError(err)
return err
}
defer resp.Body.Close()
return nil
}
Injecting into response headers
You can inject the traceparent header into HTTP responses with the following middleware:
import (
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
)
type TraceparentHandler struct {
next http.Handler
props propagation.TextMapPropagator
}
func NewTraceparentHandler(next http.Handler) *TraceparentHandler {
return &TraceparentHandler{
next: next,
props: otel.GetTextMapPropagator(),
}
}
func (h *TraceparentHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Inject traceparent into response headers
h.props.Inject(req.Context(), propagation.HeaderCarrier(w.Header()))
h.next.ServeHTTP(w, req)
}
Make sure to run the middleware after the first span is created:
import (
"net/http"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func setupServer() {
var handler http.Handler
handler = router
// First, use otelhttp to start a trace
handler = otelhttp.NewHandler(handler, "")
// Then, use the middleware to inject traceparent into responses
handler = NewTraceparentHandler(handler)
// Finally, serve requests
http.ListenAndServe(":3000", handler)
}
Debugging propagation
Logging trace context
Log incoming traceparent headers and current span context for debugging:
import (
"fmt"
"net/http"
"go.opentelemetry.io/otel/trace"
)
func debugMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Log incoming traceparent header
traceparent := r.Header.Get("traceparent")
if traceparent != "" {
fmt.Printf("Incoming traceparent: %s\n", traceparent)
} else {
fmt.Println("No traceparent header found")
}
// Log current span context
span := trace.SpanFromContext(r.Context())
if span.SpanContext().IsValid() {
ctx := span.SpanContext()
fmt.Printf("Current trace context - TraceID: %s, SpanID: %s, Sampled: %v\n",
ctx.TraceID().String(),
ctx.SpanID().String(),
ctx.IsSampled())
} else {
fmt.Println("No valid span context found")
}
next.ServeHTTP(w, r)
})
}
Inspecting headers in requests
Debug outgoing requests to verify traceparent injection:
import (
"fmt"
"net/http"
"net/http/httputil"
)
func debugRequest(req *http.Request) {
// Dump request headers for inspection
dump, err := httputil.DumpRequestOut(req, false)
if err != nil {
fmt.Printf("Error dumping request: %v\n", err)
return
}
fmt.Printf("Outgoing request:\n%s\n", string(dump))
// Specifically check for traceparent
if traceparent := req.Header.Get("traceparent"); traceparent != "" {
fmt.Printf("✓ Traceparent header present: %s\n", traceparent)
} else {
fmt.Println("✗ Traceparent header missing")
}
}
Getting trace information
Access current trace context information and format it as a traceparent header:
import (
"context"
"errors"
"fmt"
"go.opentelemetry.io/otel/trace"
)
type TraceInfo struct {
TraceID string
SpanID string
IsSampled bool
IsRemote bool
TraceState string
Traceparent string
}
func GetTraceInfo(ctx context.Context) (*TraceInfo, error) {
span := trace.SpanFromContext(ctx)
spanCtx := span.SpanContext()
if !spanCtx.IsValid() {
return nil, errors.New("no valid span context available")
}
// Format trace flags as hex
flags := "00"
if spanCtx.IsSampled() {
flags = "01"
}
return &TraceInfo{
TraceID: spanCtx.TraceID().String(),
SpanID: spanCtx.SpanID().String(),
IsSampled: spanCtx.IsSampled(),
IsRemote: spanCtx.IsRemote(),
TraceState: spanCtx.TraceState().String(),
Traceparent: fmt.Sprintf("00-%s-%s-%s",
spanCtx.TraceID().String(),
spanCtx.SpanID().String(),
flags),
}, nil
}
// Usage in HTTP handler
func handleTraceInfo(w http.ResponseWriter, r *http.Request) {
info, err := GetTraceInfo(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "Current trace info:\n")
fmt.Fprintf(w, " TraceID: %s\n", info.TraceID)
fmt.Fprintf(w, " SpanID: %s\n", info.SpanID)
fmt.Fprintf(w, " Sampled: %v\n", info.IsSampled)
fmt.Fprintf(w, " Traceparent: %s\n", info.Traceparent)
}
Custom propagators
For specialized use cases, you can implement custom propagators:
import (
"context"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
)
type CustomPropagator struct{}
func (p CustomPropagator) Inject(ctx context.Context, carrier propagation.TextMapCarrier) {
span := trace.SpanFromContext(ctx)
if !span.SpanContext().IsValid() {
return
}
sc := span.SpanContext()
// Inject custom headers alongside standard traceparent
carrier.Set("x-trace-id", sc.TraceID().String())
carrier.Set("x-span-id", sc.SpanID().String())
if sc.IsSampled() {
carrier.Set("x-sampled", "true")
}
}
func (p CustomPropagator) Extract(ctx context.Context, carrier propagation.TextMapCarrier) context.Context {
traceID := carrier.Get("x-trace-id")
spanID := carrier.Get("x-span-id")
if traceID == "" || spanID == "" {
return ctx
}
// Parse custom headers and create span context
tid, err := trace.TraceIDFromHex(traceID)
if err != nil {
return ctx
}
sid, err := trace.SpanIDFromHex(spanID)
if err != nil {
return ctx
}
flags := trace.FlagsSampled
if carrier.Get("x-sampled") != "true" {
flags = 0
}
spanContext := trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
SpanID: sid,
TraceFlags: flags,
Remote: true,
})
return trace.ContextWithSpanContext(ctx, spanContext)
}
func (p CustomPropagator) Fields() []string {
return []string{"x-trace-id", "x-span-id", "x-sampled"}
}
Configuration
Configure propagation format using environment variables or programmatically:
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/contrib/propagators/b3"
)
// W3C TraceContext (recommended, default)
otel.SetTextMapPropagator(propagation.TraceContext{})
// W3C TraceContext + Baggage
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
// Multiple formats for compatibility
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{}, // W3C standard
b3.New(), // Zipkin B3 format
propagation.Baggage{}, // Baggage support
))
Working with multiple header values
Starting from OpenTelemetry Go SDK v1.35.0, the ValuesGetter interface allows extracting multiple values for a single header key. This is useful when dealing with multiple propagation formats or debugging scenarios where multiple traceparent headers might be present.
ValuesGetter interface
The ValuesGetter interface extends TextMapCarrier to support multiple values:
import (
"go.opentelemetry.io/otel/propagation"
)
// ValuesGetter retrieves multiple values for a single key
type ValuesGetter interface {
propagation.TextMapCarrier
Values(key string) []string
}
propagation.HeaderCarrier implements ValuesGetter automatically, so HTTP headers support multiple values out of the box.
Extracting multiple traceparent headers
In edge cases where multiple tracing systems inject their own traceparent headers, you can extract and inspect all values:
import (
"fmt"
"net/http"
"go.opentelemetry.io/otel/propagation"
)
func handleMultipleTraceparents(w http.ResponseWriter, r *http.Request) {
carrier := propagation.HeaderCarrier(r.Header)
// Extract all traceparent values if multiple exist
traceparents := carrier.Values("traceparent")
if len(traceparents) > 1 {
fmt.Printf("Warning: Multiple traceparent headers found: %d\n", len(traceparents))
for i, tp := range traceparents {
fmt.Printf(" [%d] %s\n", i, tp)
}
}
// Standard extraction still works - uses the first value
ctx := propagation.TraceContext{}.Extract(r.Context(), carrier)
// Process with extracted context
processRequest(w, r.WithContext(ctx))
}
Custom propagator with multiple values
Implement a custom propagator that handles multiple header values:
import (
"context"
"fmt"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
)
type MultiValuePropagator struct{}
func (p MultiValuePropagator) Inject(ctx context.Context, carrier propagation.TextMapCarrier) {
span := trace.SpanFromContext(ctx)
if !span.SpanContext().IsValid() {
return
}
sc := span.SpanContext()
flags := "00"
if sc.IsSampled() {
flags = "01"
}
traceparent := fmt.Sprintf("00-%s-%s-%s",
sc.TraceID().String(),
sc.SpanID().String(),
flags)
carrier.Set("traceparent", traceparent)
}
func (p MultiValuePropagator) Extract(ctx context.Context, carrier propagation.TextMapCarrier) context.Context {
// Check if carrier supports multiple values
if valuesGetter, ok := carrier.(propagation.ValuesGetter); ok {
values := valuesGetter.Values("traceparent")
if len(values) > 1 {
// Log warning about multiple values
fmt.Printf("Multiple traceparent headers detected, using first: %s\n", values[0])
}
if len(values) > 0 {
return p.extractFromValue(ctx, values[0])
}
}
// Fallback to single value
traceparent := carrier.Get("traceparent")
if traceparent == "" {
return ctx
}
return p.extractFromValue(ctx, traceparent)
}
func (p MultiValuePropagator) extractFromValue(ctx context.Context, traceparent string) context.Context {
// Parse and extract trace context (implementation omitted for brevity)
// This would parse the traceparent string and create a SpanContext
return ctx
}
func (p MultiValuePropagator) Fields() []string {
return []string{"traceparent"}
}
Example
Uptrace is an OpenTelemetry APM that uses the TraceparentHandler middleware to add the traceparent header to all HTTP responses.
To see it in action:
- Navigate to Uptrace demo and open Chrome DevTools
- In the "Network" tab, click on an HTTP request and open the "Headers" tab
- Locate the
Traceparentheader and extract the trace ID to find the trace
