Skip to content

Commit 7001892

Browse files
authored
Add support for Default Baggage Injection in URLSessionInstrumentation (#639)
* add default baggage provider to urlsessioninstrumentation * add missing `DataCompression` dep to `OpenTelemetryProtocolExporterHttp` * add missing deps to all package files
1 parent 76b833c commit 7001892

File tree

6 files changed

+105
-13
lines changed

6 files changed

+105
-13
lines changed

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ let package = Package(
104104
path: "Sources/Exporters/OpenTelemetryProtocolCommon"),
105105
.target(name: "OpenTelemetryProtocolExporterHttp",
106106
dependencies: ["OpenTelemetrySdk",
107-
"OpenTelemetryProtocolExporterCommon"],
107+
"OpenTelemetryProtocolExporterCommon",
108+
"DataCompression"],
108109
path: "Sources/Exporters/OpenTelemetryProtocolHttp"),
109110
.target(name: "OpenTelemetryProtocolExporterGrpc",
110111
dependencies: ["OpenTelemetrySdk",

[email protected]

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ let package = Package(
109109
path: "Sources/Exporters/OpenTelemetryProtocolCommon"),
110110
.target(name: "OpenTelemetryProtocolExporterHttp",
111111
dependencies: ["OpenTelemetrySdk",
112-
"OpenTelemetryProtocolExporterCommon"],
112+
"OpenTelemetryProtocolExporterCommon",
113+
"DataCompression"],
113114
path: "Sources/Exporters/OpenTelemetryProtocolHttp"),
114115
.target(name: "OpenTelemetryProtocolExporterGrpc",
115116
dependencies: ["OpenTelemetrySdk",

[email protected]

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ let package = Package(
6464
path: "Sources/Exporters/OpenTelemetryProtocolCommon"),
6565
.target(name: "OpenTelemetryProtocolExporterHttp",
6666
dependencies: ["OpenTelemetrySdk",
67-
"OpenTelemetryProtocolExporterCommon"],
67+
"OpenTelemetryProtocolExporterCommon",
68+
"DataCompression"],
6869
path: "Sources/Exporters/OpenTelemetryProtocolHttp"),
6970
.target(name: "OpenTelemetryProtocolExporterGrpc",
7071
dependencies: ["OpenTelemetrySdk",

Sources/Instrumentation/URLSession/URLSessionInstrumentationConfiguration.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ public struct URLSessionInstrumentationConfiguration {
2424
createdRequest: ((URLRequest, Span) -> Void)? = nil,
2525
receivedResponse: ((URLResponse, DataOrFile?, Span) -> Void)? = nil,
2626
receivedError: ((Error, DataOrFile?, HTTPStatus, Span) -> Void)? = nil,
27-
delegateClassesToInstrument: [AnyClass]? = nil)
27+
delegateClassesToInstrument: [AnyClass]? = nil,
28+
defaultBaggageProvider: (() -> (Baggage)?)? = nil)
2829
{
2930
self.shouldRecordPayload = shouldRecordPayload
3031
self.shouldInstrument = shouldInstrument
@@ -36,6 +37,7 @@ public struct URLSessionInstrumentationConfiguration {
3637
self.receivedResponse = receivedResponse
3738
self.receivedError = receivedError
3839
self.delegateClassesToInstrument = delegateClassesToInstrument
40+
self.defaultBaggageProvider = defaultBaggageProvider
3941
}
4042

4143
// Instrumentation Callbacks
@@ -73,4 +75,14 @@ public struct URLSessionInstrumentationConfiguration {
7375

7476
/// The array of URLSession delegate classes that will be instrumented by the library, will autodetect if nil is passed.
7577
public var delegateClassesToInstrument: [AnyClass]?
78+
79+
/// Implement this callback to provide a default baggage instance for all instrumented requests.
80+
/// The provided baggage is merged with active baggage (if any) to create a combined baggage.
81+
/// The combined baggage is then injected into request headers using the configured `TextMapBaggagePropagator`.
82+
/// This allows consistent propagation across requests, regardless of the active context.
83+
///
84+
/// Note: The injected baggage depends on the propagator in use (e.g., W3C or custom).
85+
/// Returns: A `Baggage` instance or `nil` if no default baggage is needed.
86+
public let defaultBaggageProvider: (() -> (Baggage)?)?
87+
7688
}

Sources/Instrumentation/URLSession/URLSessionLogger.swift

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,22 @@ class URLSessionLogger {
160160
var instrumentedRequest = request
161161
objc_setAssociatedObject(instrumentedRequest, URLSessionInstrumentation.instrumentedKey, true, .OBJC_ASSOCIATION_COPY_NONATOMIC)
162162
let propagators = OpenTelemetry.instance.propagators
163-
var traceHeaders = tracePropagationHTTPHeaders(span: span, textMapPropagator: propagators.textMapPropagator, textMapBaggagePropagator: propagators.textMapBaggagePropagator)
163+
164+
let defaultBaggage = instrumentation.configuration.defaultBaggageProvider?()
165+
166+
var traceHeaders = tracePropagationHTTPHeaders(span: span,
167+
defaultBaggage: defaultBaggage,
168+
textMapPropagator: propagators.textMapPropagator,
169+
textMapBaggagePropagator: propagators.textMapBaggagePropagator)
170+
164171
if let originalHeaders = request.allHTTPHeaderFields {
165172
traceHeaders.merge(originalHeaders) { _, new in new }
166173
}
167174
instrumentedRequest.allHTTPHeaderFields = traceHeaders
168175
return instrumentedRequest
169176
}
170177

171-
private static func tracePropagationHTTPHeaders(span: Span?, textMapPropagator: TextMapPropagator, textMapBaggagePropagator: TextMapBaggagePropagator) -> [String: String] {
178+
private static func tracePropagationHTTPHeaders(span: Span?, defaultBaggage: Baggage?, textMapPropagator: TextMapPropagator, textMapBaggagePropagator: TextMapBaggagePropagator) -> [String: String] {
172179
var headers = [String: String]()
173180

174181
struct HeaderSetter: Setter {
@@ -182,9 +189,19 @@ class URLSessionLogger {
182189
}
183190
textMapPropagator.inject(spanContext: currentSpan.context, carrier: &headers, setter: HeaderSetter())
184191

185-
if let baggage = OpenTelemetry.instance.contextProvider.activeBaggage {
186-
textMapBaggagePropagator.inject(baggage: baggage, carrier: &headers, setter: HeaderSetter())
192+
let baggageBuilder = OpenTelemetry.instance.baggageManager.baggageBuilder()
193+
194+
if let activeBaggage = OpenTelemetry.instance.contextProvider.activeBaggage {
195+
activeBaggage.getEntries().forEach { baggageBuilder.put(key: $0.key, value: $0.value, metadata: $0.metadata) }
187196
}
197+
198+
if let defaultBaggage {
199+
defaultBaggage.getEntries().forEach { baggageBuilder.put(key: $0.key, value: $0.value, metadata: $0.metadata) }
200+
}
201+
202+
let combinedBaggage = baggageBuilder.build()
203+
textMapBaggagePropagator.inject(baggage: combinedBaggage, carrier: &headers, setter: HeaderSetter())
204+
188205
return headers
189206
}
190207
}

Tests/InstrumentationTests/URLSessionTests/URLSessionInstrumentationTests.swift

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ class URLSessionInstrumentationTests: XCTestCase {
3939
static var requestCopy: URLRequest!
4040
static var responseCopy: HTTPURLResponse!
4141

42+
static var activeBaggage: Baggage!
43+
static var defaultBaggage: Baggage!
44+
4245
static var config = URLSessionInstrumentationConfiguration(shouldRecordPayload: nil,
4346
shouldInstrument: { req in
4447
checker.shouldInstrumentCalled = true
@@ -76,25 +79,31 @@ class URLSessionInstrumentationTests: XCTestCase {
7679
},
7780
receivedError: { _, _, _, _ in
7881
URLSessionInstrumentationTests.checker.receivedErrorCalled = true
82+
},
83+
defaultBaggageProvider: {
84+
defaultBaggage
7985
})
8086

8187
static var checker = Check()
8288
static var semaphore: DispatchSemaphore!
8389
var sessionDelegate: SessionDelegate!
8490
static var instrumentation: URLSessionInstrumentation!
85-
static var baggage: Baggage!
8691

8792
static let server = HttpTestServer(url: URL(string: "http://localhost:33333"), config: nil)
8893

8994
override class func setUp() {
9095
OpenTelemetry.registerPropagators(textPropagators: [W3CTraceContextPropagator()], baggagePropagator: W3CBaggagePropagator())
9196
OpenTelemetry.registerTracerProvider(tracerProvider: TracerProviderSdk())
9297

93-
baggage = DefaultBaggageManager.instance.baggageBuilder()
98+
defaultBaggage = DefaultBaggageManager.instance.baggageBuilder()
99+
.put(key: EntryKey(name: "bar")!, value: EntryValue(string: "baz")!, metadata: nil)
100+
.build()
101+
102+
activeBaggage = DefaultBaggageManager.instance.baggageBuilder()
94103
.put(key: EntryKey(name: "foo")!, value: EntryValue(string: "bar")!, metadata: nil)
95104
.build()
96105

97-
OpenTelemetry.instance.contextProvider.setActiveBaggage(baggage)
106+
OpenTelemetry.instance.contextProvider.setActiveBaggage(activeBaggage)
98107

99108
let sem = DispatchSemaphore(value: 0)
100109
DispatchQueue.global(qos: .default).async {
@@ -111,7 +120,8 @@ class URLSessionInstrumentationTests: XCTestCase {
111120

112121
override class func tearDown() {
113122
server.stop()
114-
OpenTelemetry.instance.contextProvider.removeContextForBaggage(baggage)
123+
defaultBaggage = nil
124+
OpenTelemetry.instance.contextProvider.removeContextForBaggage(activeBaggage)
115125
}
116126

117127
override func setUp() {
@@ -262,13 +272,63 @@ class URLSessionInstrumentationTests: XCTestCase {
262272

263273
XCTAssertTrue(URLSessionInstrumentationTests.checker.shouldInstrumentCalled)
264274

275+
XCTAssertEqual(1, URLSessionLogger.runningSpans.count)
276+
277+
let span = try XCTUnwrap(URLSessionLogger.runningSpans["111"])
278+
XCTAssertEqual("HTTP GET", span.name)
279+
}
280+
281+
public func testShouldInstrumentRequest_PropagateCombinedActiveAndDefaultBaggages() throws {
282+
let request1 = URLRequest(url: URL(string: "http://defaultName.com")!)
283+
let request2 = URLRequest(url: URL(string: "http://dontinstrument.com")!)
284+
285+
let processedRequest1 = try XCTUnwrap(URLSessionLogger.processAndLogRequest(request1, sessionTaskId: "111", instrumentation: URLSessionInstrumentationTests.instrumentation, shouldInjectHeaders: true))
286+
let processedRequest2 = URLSessionLogger.processAndLogRequest(request2, sessionTaskId: "222", instrumentation: URLSessionInstrumentationTests.instrumentation, shouldInjectHeaders: true)
287+
288+
// `processedRequest2` is expected to be nil, because its URL was marked as not to be instrumented.
289+
XCTAssertNil(processedRequest2)
290+
291+
XCTAssertTrue(URLSessionInstrumentationTests.checker.shouldInstrumentCalled)
292+
293+
let processedHeaders1 = try XCTUnwrap(processedRequest1.allHTTPHeaderFields)
294+
295+
// headers injected from `TextMapPropagator` implementation
296+
XCTAssertTrue(processedHeaders1.contains(where: { $0.key == W3CTraceContextPropagator.traceparent }))
297+
298+
// headers injected from `TextMapBaggagePropagator` implementation
299+
let baggageHeaderValue = try XCTUnwrap(processedHeaders1[W3CBaggagePropagator.headerBaggage])
300+
301+
// foo=bar propagated through active baggage defined in `setUp`
302+
XCTAssertTrue(baggageHeaderValue.contains("foo=bar"))
303+
304+
// bar=baz propagated through default baggage provided in `URLSessionInstrumentationConfiguration`
305+
XCTAssertTrue(baggageHeaderValue.contains("bar=baz"))
306+
307+
XCTAssertEqual(1, URLSessionLogger.runningSpans.count)
308+
309+
let span = try XCTUnwrap(URLSessionLogger.runningSpans["111"])
310+
XCTAssertEqual("HTTP GET", span.name)
311+
}
312+
313+
public func testShouldInstrumentRequest_PropagateOnlyActiveBaggage() throws {
314+
Self.defaultBaggage = nil
315+
316+
let request1 = URLRequest(url: URL(string: "http://defaultName.com")!)
317+
318+
let processedRequest1 = try XCTUnwrap(URLSessionLogger.processAndLogRequest(request1, sessionTaskId: "111", instrumentation: URLSessionInstrumentationTests.instrumentation, shouldInjectHeaders: true))
319+
320+
XCTAssertTrue(URLSessionInstrumentationTests.checker.shouldInstrumentCalled)
321+
265322
let processedHeaders1 = try XCTUnwrap(processedRequest1.allHTTPHeaderFields)
266323

267324
// headers injected from `TextMapPropagator` implementation
268325
XCTAssertTrue(processedHeaders1.contains(where: { $0.key == W3CTraceContextPropagator.traceparent }))
269326

270327
// headers injected from `TextMapBaggagePropagator` implementation
271-
XCTAssertTrue(processedHeaders1.contains(where: { $0.key == W3CBaggagePropagator.headerBaggage && $0.value == "foo=bar" }))
328+
let baggageHeaderValue = try XCTUnwrap(processedHeaders1[W3CBaggagePropagator.headerBaggage])
329+
330+
// bar=baz propagated through default baggage provided in `URLSessionInstrumentationConfiguration`
331+
XCTAssertEqual(baggageHeaderValue, "foo=bar")
272332

273333
XCTAssertEqual(1, URLSessionLogger.runningSpans.count)
274334

0 commit comments

Comments
 (0)