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:
# {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:
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 with NSURLSession instrumentation
OpenTelemetry Swift provides automatic context propagation through the NSURLSession instrumentation.
Installation
// 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:
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:
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:
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:
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:
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:
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:
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
}
}
}