OpenTelemetry Context Propagation [Swift]

This guide covers Swift-specific implementation of context propagation. For a comprehensive overview of context propagation concepts, W3C TraceContext, 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:

text
# {version}-{trace_id}-{span_id}-{trace_flags}
traceparent: 00-80e1afed08e019fc1110464cfa66635c-7a085853722dc6d2-01

Using this header, downstream services can link their spans to the same trace, creating a complete picture of request flow across service boundaries.

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 with NSURLSession instrumentation

OpenTelemetry Swift provides automatic context propagation through the NSURLSession instrumentation.

Installation

swift
// Package.swift
dependencies: [
    .package(url: "https://github.com/open-telemetry/opentelemetry-swift", from: "1.0.0"),
],
targets: [
    .executableTarget(
        name: "MyApp",
        dependencies: [
            .product(name: "OpenTelemetrySdk", package: "opentelemetry-swift"),
            .product(name: "URLSessionInstrumentation", package: "opentelemetry-swift"),
        ]
    )
]

Usage

Initialize the instrumentation to automatically inject traceparent headers:

swift
import URLSessionInstrumentation
import OpenTelemetryApi

// Initialize instrumentation (do this once at app startup)
let urlSessionInstrumentation = URLSessionInstrumentation(
    configuration: URLSessionInstrumentationConfiguration()
)

// All URLSession requests now automatically include traceparent headers
let url = URL(string: "https://api.example.com/users")!
let (data, response) = try await URLSession.shared.data(from: url)

Configuration options

Customize the instrumentation behavior:

swift
let config = URLSessionInstrumentationConfiguration(
    // Filter which requests to instrument
    shouldInstrument: { request in
        // Instrument all requests to your API
        return request.url?.host == "api.example.com"
    },

    // Filter which requests get tracing headers injected
    shouldInjectTracingHeaders: { request in
        // Don't inject headers to third-party services
        return request.url?.host?.contains("example.com") == true
    },

    // Custom span naming
    nameSpan: { request in
        guard let url = request.url else { return nil }
        return "\(request.httpMethod ?? "GET") \(url.path)"
    },

    // Add custom attributes after request is created
    createdRequest: { request, span in
        span.setAttribute(key: "http.method", value: request.httpMethod ?? "GET")
    },

    // Add custom attributes after response is received
    receivedResponse: { response, data, span in
        if let httpResponse = response as? HTTPURLResponse {
            span.setAttribute(key: "http.status_code", value: httpResponse.statusCode)
        }
    }
)

let urlSessionInstrumentation = URLSessionInstrumentation(configuration: config)

Manual propagation

When automatic instrumentation is not available, manually handle traceparent headers.

Extracting from incoming requests

Extract trace context from incoming HTTP headers:

swift
import OpenTelemetryApi

func extractTraceContext(from headers: [String: String]) -> SpanContext? {
    guard let traceparent = headers["traceparent"] else {
        return nil
    }

    // Parse traceparent: version-traceId-spanId-traceFlags
    let components = traceparent.split(separator: "-")
    guard components.count == 4,
          let traceId = TraceId(fromHexString: String(components[1])),
          let spanId = SpanId(fromHexString: String(components[2])),
          let flags = UInt8(String(components[3]), radix: 16) else {
        return nil
    }

    let traceFlags = TraceFlags(fromByte: flags)
    return SpanContext.createFromRemoteParent(
        traceId: traceId,
        spanId: spanId,
        traceFlags: traceFlags,
        traceState: TraceState()
    )
}

// Usage in a server handler
func handleRequest(headers: [String: String]) {
    let tracer = OpenTelemetry.instance.tracerProvider.get(
        instrumentationName: "MyApp",
        instrumentationVersion: "1.0.0"
    )

    let parentContext = extractTraceContext(from: headers)

    let spanBuilder = tracer.spanBuilder(spanName: "handle-request")
        .setSpanKind(spanKind: .server)

    if let parentContext = parentContext {
        spanBuilder.setParent(parentContext)
    }

    let span = spanBuilder.startSpan()
    defer { span.end() }

    // Process request...
}

Injecting into outgoing requests

Inject trace context into outgoing HTTP requests:

swift
import OpenTelemetryApi
import Foundation

func injectTraceContext(into request: inout URLRequest, span: Span) {
    let context = span.context

    // Format traceparent header
    let traceparent = String(
        format: "00-%@-%@-%02x",
        context.traceId.hexString,
        context.spanId.hexString,
        context.traceFlags.byte
    )

    request.setValue(traceparent, forHTTPHeaderField: "traceparent")
}

// Usage
func makeRequest(url: URL) async throws -> Data {
    let tracer = OpenTelemetry.instance.tracerProvider.get(
        instrumentationName: "MyApp",
        instrumentationVersion: "1.0.0"
    )

    let span = tracer.spanBuilder(spanName: "http-request")
        .setSpanKind(spanKind: .client)
        .startSpan()
    defer { span.end() }

    var request = URLRequest(url: url)
    injectTraceContext(into: &request, span: span)

    span.setAttribute(key: "http.url", value: url.absoluteString)
    span.setAttribute(key: "http.method", value: "GET")

    let (data, response) = try await URLSession.shared.data(for: request)

    if let httpResponse = response as? HTTPURLResponse {
        span.setAttribute(key: "http.status_code", value: httpResponse.statusCode)
    }

    return data
}

Text map propagator

Use the standard text map propagator for consistent propagation:

swift
import OpenTelemetryApi

// Create a propagator
let propagator = W3CTraceContextPropagator()

// Carrier for headers
class HTTPHeadersCarrier: TextMapSetter, TextMapGetter {
    var headers: [String: String] = [:]

    func set(key: String, value: String) {
        headers[key] = value
    }

    func get(key: String) -> String? {
        return headers[key]
    }

    func getAll(key: String) -> [String]? {
        guard let value = headers[key] else { return nil }
        return [value]
    }

    var keys: [String] {
        return Array(headers.keys)
    }
}

// Inject context into headers
func injectContext(span: Span) -> [String: String] {
    let carrier = HTTPHeadersCarrier()
    propagator.inject(spanContext: span.context, carrier: carrier)
    return carrier.headers
}

// Extract context from headers
func extractContext(from headers: [String: String]) -> SpanContext? {
    let carrier = HTTPHeadersCarrier()
    carrier.headers = headers
    return propagator.extract(carrier: carrier)
}

Server-side Swift (Vapor)

For Vapor applications, create middleware for automatic context propagation:

swift
import Vapor
import OpenTelemetryApi

struct TracingMiddleware: AsyncMiddleware {
    let tracer: Tracer

    init() {
        self.tracer = OpenTelemetry.instance.tracerProvider.get(
            instrumentationName: "VaporApp",
            instrumentationVersion: "1.0.0"
        )
    }

    func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
        // Extract parent context from request headers
        let parentContext = extractTraceContext(from: request.headers)

        // Create span for this request
        let spanBuilder = tracer.spanBuilder(spanName: "\(request.method) \(request.url.path)")
            .setSpanKind(spanKind: .server)

        if let parentContext = parentContext {
            spanBuilder.setParent(parentContext)
        }

        let span = spanBuilder.setActive(true).startSpan()
        defer { span.end() }

        // Add request attributes
        span.setAttribute(key: "http.method", value: request.method.rawValue)
        span.setAttribute(key: "http.url", value: request.url.string)
        span.setAttribute(key: "http.target", value: request.url.path)

        do {
            let response = try await next.respond(to: request)

            span.setAttribute(key: "http.status_code", value: Int(response.status.code))

            // Inject traceparent into response headers
            var modifiedResponse = response
            modifiedResponse.headers.add(
                name: "traceparent",
                value: formatTraceparent(span.context)
            )

            return modifiedResponse
        } catch {
            span.status = .error(description: error.localizedDescription)
            throw error
        }
    }

    private func extractTraceContext(from headers: HTTPHeaders) -> SpanContext? {
        guard let traceparent = headers.first(name: "traceparent") else {
            return nil
        }

        let components = traceparent.split(separator: "-")
        guard components.count == 4,
              let traceId = TraceId(fromHexString: String(components[1])),
              let spanId = SpanId(fromHexString: String(components[2])),
              let flags = UInt8(String(components[3]), radix: 16) else {
            return nil
        }

        return SpanContext.createFromRemoteParent(
            traceId: traceId,
            spanId: spanId,
            traceFlags: TraceFlags(fromByte: flags),
            traceState: TraceState()
        )
    }

    private func formatTraceparent(_ context: SpanContext) -> String {
        return String(
            format: "00-%@-%@-%02x",
            context.traceId.hexString,
            context.spanId.hexString,
            context.traceFlags.byte
        )
    }
}

// Usage in configure.swift
app.middleware.use(TracingMiddleware())

iOS client to backend correlation

For iOS apps communicating with a backend, ensure the trace flows through:

swift
class APIClient {
    private let tracer: Tracer
    private let baseURL: URL

    init(baseURL: URL) {
        self.baseURL = baseURL
        self.tracer = OpenTelemetry.instance.tracerProvider.get(
            instrumentationName: "APIClient",
            instrumentationVersion: "1.0.0"
        )
    }

    func request<T: Decodable>(
        endpoint: String,
        method: String = "GET",
        body: Data? = nil
    ) async throws -> T {
        let span = tracer.spanBuilder(spanName: "\(method) \(endpoint)")
            .setSpanKind(spanKind: .client)
            .startSpan()
        defer { span.end() }

        let url = baseURL.appendingPathComponent(endpoint)
        var request = URLRequest(url: url)
        request.httpMethod = method
        request.httpBody = body

        // Inject trace context
        let traceparent = String(
            format: "00-%@-%@-%02x",
            span.context.traceId.hexString,
            span.context.spanId.hexString,
            span.context.traceFlags.byte
        )
        request.setValue(traceparent, forHTTPHeaderField: "traceparent")

        // Add span attributes
        span.setAttribute(key: "http.method", value: method)
        span.setAttribute(key: "http.url", value: url.absoluteString)

        do {
            let (data, response) = try await URLSession.shared.data(for: request)

            if let httpResponse = response as? HTTPURLResponse {
                span.setAttribute(key: "http.status_code", value: httpResponse.statusCode)
            }

            return try JSONDecoder().decode(T.self, from: data)
        } catch {
            span.status = .error(description: error.localizedDescription)
            throw error
        }
    }
}

What's next?