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:

text
# {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.

Traceparent header

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:

  • version is always 00
  • trace_id is a hex-encoded trace ID (16 bytes, 32 hex characters)
  • span_id is a hex-encoded span ID (8 bytes, 16 hex characters)
  • trace_flags is 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

go
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

go
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

go
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

go
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:

go
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:

go
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:

go
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:

go
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:

go
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:

go
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:

go
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:

go
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:

go
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:

go
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:

  1. Navigate to Uptrace demo and open Chrome DevTools
  2. In the "Network" tab, click on an HTTP request and open the "Headers" tab
  3. Locate the Traceparent header and extract the trace ID to find the trace

Traceparent header

What's next?