[SDTEST-2694] Switched to the new OpenTelemetry Swift Core SDK#191
Conversation
There was a problem hiding this comment.
Pull request overview
This PR migrates the project’s OpenTelemetry dependency from a fork to the official Swift Core SDK and vendors URLSession instrumentation sources into DatadogSDKTesting to preserve network tracing behavior.
Changes:
- Switched Xcode project SwiftPM dependency to
open-telemetry/opentelemetry-swift-coreand removed products from the old package. - Added vendored URLSession instrumentation implementation under
Sources/DatadogSDKTesting/NetworkInstrumentation. - Replaced the
InMemoryExporterfallback with a localNoopSpanExporterand adjusted timestamp/duration nanosecond conversions.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| Sources/EventsExporter/Utils/DateFormatting.swift | Imports OTel API and adds toNanosecondsUInt helper for UInt64 nanosecond conversions. |
| Sources/EventsExporter/Spans/SpanEncoder.swift | Switches span start/duration fields to use toNanosecondsUInt. |
| Sources/DatadogSDKTesting/Utils/NoopSpanExporter.swift | Adds a NOOP exporter used when tracing is disabled / exporter creation fails. |
| Sources/DatadogSDKTesting/NetworkInstrumentation/URLSessionLogger.swift | Adds vendored request/response span creation and header propagation logic. |
| Sources/DatadogSDKTesting/NetworkInstrumentation/URLSessionInstrumentationConfiguration.swift | Adds configuration surface for URLSession instrumentation + semantic convention selection. |
| Sources/DatadogSDKTesting/NetworkInstrumentation/URLSessionInstrumentation.swift | Adds vendored swizzling-based URLSession instrumentation implementation. |
| Sources/DatadogSDKTesting/NetworkInstrumentation/InstrumentationUtils.swift | Adds runtime helper utilities used by the swizzling implementation. |
| Sources/DatadogSDKTesting/NetworkInstrumentation/DDNetworkInstrumentation.swift | Drops dependency import for external instrumentation product and uses vendored implementation. |
| Sources/DatadogSDKTesting/DDTracer.swift | Replaces InMemoryExporter usage with NoopSpanExporter. |
| Sources/DatadogSDKTesting/DDTestSuite.swift | Switches suite duration to toNanosecondsUInt. |
| Sources/DatadogSDKTesting/DDTestSession.swift | Switches session duration to toNanosecondsUInt. |
| Sources/DatadogSDKTesting/DDTestModule.swift | Switches module duration to toNanosecondsUInt. |
| Sources/DatadogSDKTesting/DDTest.swift | Switches test duration to toNanosecondsUInt. |
| LICENSE-3rdparty.csv | Updates OpenTelemetry Authors attribution string. |
| DatadogSDKTesting.xcodeproj/project.pbxproj | Updates SwiftPM package reference/products and adds new Swift sources to build. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| }() | ||
|
|
||
| public extension TimeInterval { | ||
| var toNanosecondsUInt: UInt64 { UInt64(toNanoseconds) } |
There was a problem hiding this comment.
toNanosecondsUInt uses UInt64(toNanoseconds), which will trap at runtime if toNanoseconds is negative or not representable in UInt64 (e.g., if endTime < startTime or due to clock adjustments). Consider using a clamping conversion (e.g. max with 0) or a safe initializer like UInt64(clamping:) to avoid crashing.
| var toNanosecondsUInt: UInt64 { UInt64(toNanoseconds) } | |
| var toNanosecondsUInt: UInt64 { UInt64(clamping: toNanoseconds) } |
| var instrumentedRequest = request | ||
| objc_setAssociatedObject(instrumentedRequest, URLSessionInstrumentation.instrumentedKey, true, .OBJC_ASSOCIATION_COPY_NONATOMIC) | ||
| let propagators = OpenTelemetry.instance.propagators | ||
|
|
||
| var traceHeaders = tracePropagationHTTPHeaders(span: span, | ||
| customBaggage: customBaggage, | ||
| textMapPropagator: propagators.textMapPropagator, | ||
| textMapBaggagePropagator: propagators.textMapBaggagePropagator) | ||
|
|
||
| if let originalHeaders = request.allHTTPHeaderFields { | ||
| traceHeaders.merge(originalHeaders) { _, new in new } | ||
| } | ||
| instrumentedRequest.allHTTPHeaderFields = traceHeaders | ||
| return instrumentedRequest |
There was a problem hiding this comment.
objc_setAssociatedObject requires an UnsafeRawPointer key, but this call passes URLSessionInstrumentation.instrumentedKey which is a String. This won’t compile, and even if it did, using a non-stable key would make associated objects unreliable. Use a static/global UInt8 (or similar) and pass its address (&keyVar) as the association key.
| } | ||
| let imp = imp_implementationWithBlock( | ||
| unsafeBitCast(block, to: AnyObject.self)) | ||
| class_addMethod(cls, selector, imp, "@@@") |
There was a problem hiding this comment.
class_addMethod is called with the type encoding "@@@", which doesn’t match the actual selector signature (it should be a void return with several object arguments). An incorrect encoding can cause undefined behavior at runtime. Provide the correct Objective-C type encoding for urlSession(_:task:didFinishCollecting:) when adding the method.
| class_addMethod(cls, selector, imp, "@@@") | |
| class_addMethod(cls, selector, imp, "v@:@@@") |
| private func internalEnd(endTime: Date? = nil) { | ||
| duration = (endTime ?? DDTestMonitor.clock.now).timeIntervalSince(startTime).toNanoseconds | ||
| duration = (endTime ?? DDTestMonitor.clock.now).timeIntervalSince(startTime).toNanosecondsUInt |
There was a problem hiding this comment.
toNanosecondsUInt is defined in EventsExporter (in DateFormatting.swift), but this file only imports OpenTelemetryApi. As-is, this will fail to compile with “TimeInterval has no member toNanosecondsUInt”. Add an import EventsExporter (or move the TimeInterval extension into a module that these sources already import).
| private func internalEnd(endTime: Date? = nil) { | ||
| duration = (endTime ?? DDTestMonitor.clock.now).timeIntervalSince(startTime).toNanoseconds | ||
| duration = (endTime ?? DDTestMonitor.clock.now).timeIntervalSince(startTime).toNanosecondsUInt |
There was a problem hiding this comment.
This file uses toNanosecondsUInt, but it’s defined in the EventsExporter target. Since this file doesn’t import EventsExporter, this will fail to compile. Add the appropriate import or move the TimeInterval extension into a module that DatadogSDKTesting already imports.
| // For iOS 15+/macOS 12+, handle async/await methods differently | ||
| if #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) { | ||
| // Check if we can determine if this is an async/await call | ||
| // For iOS 15/macOS 12, we can't use Task.basePriority, so we check other indicators | ||
| var isAsyncContext = false | ||
|
|
||
| if #available(OSX 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { | ||
| isAsyncContext = Task.basePriority != nil | ||
| } else { | ||
| // For iOS 15/macOS 12, check if the task has no delegate and no session delegate | ||
| // This is a heuristic that works for async/await methods | ||
| isAsyncContext = task.delegate == nil && | ||
| (task.value(forKey: "session") as? URLSession)?.delegate == nil && | ||
| task.state == .suspended | ||
| } | ||
|
|
||
| if isAsyncContext { | ||
| // This is likely an async/await call | ||
| let instrumentedRequest = URLSessionLogger.processAndLogRequest(request, | ||
| sessionTaskId: taskId, | ||
| instrumentation: self, | ||
| shouldInjectHeaders: true) | ||
| if let instrumentedRequest { | ||
| task.setValue(instrumentedRequest, forKey: "currentRequest") | ||
| } | ||
| self.setIdKey(value: taskId, for: task) | ||
|
|
||
| // For async/await methods, we need to ensure the delegate is set | ||
| // to capture the completion, but only if there's no existing delegate | ||
| // AND no session delegate (session delegates are called for async/await too) | ||
| if task.delegate == nil, | ||
| task.state == .suspended, | ||
| (task.value(forKey: "session") as? URLSession)?.delegate == nil { | ||
| task.delegate = AsyncTaskDelegate(instrumentation: self, sessionTaskId: taskId) | ||
| } | ||
| return | ||
| } | ||
| } | ||
|
|
||
| if #available(OSX 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { | ||
| guard Task.basePriority != nil else { | ||
| // If not inside a Task basePriority is nil | ||
| return | ||
| } | ||
|
|
||
| let instrumentedRequest = URLSessionLogger.processAndLogRequest(request, | ||
| sessionTaskId: taskId, | ||
| instrumentation: self, | ||
| shouldInjectHeaders: true) | ||
| if let instrumentedRequest { | ||
| task.setValue(instrumentedRequest, forKey: "currentRequest") | ||
| } | ||
| self.setIdKey(value: taskId, for: task) | ||
|
|
||
| if task.delegate == nil, task.state == .suspended, (task.value(forKey: "session") as? URLSession)?.delegate == nil { | ||
| task.delegate = FakeDelegate() | ||
| } | ||
| } |
There was a problem hiding this comment.
The async/await instrumentation path relies on heuristics (e.g., Task.basePriority checks, delegate/session delegate checks, KVC for "session" and "currentRequest") and introduces AsyncTaskDelegate/FakeDelegate. There are no tests validating that async/await URLSession APIs are correctly instrumented (span created, headers injected, span ended on completion) across iOS 15/16+ branches. Add tests covering these code paths to prevent regressions.
| private func internalEnd(endTime: Date? = nil) { | ||
| duration = (endTime ?? DDTestMonitor.clock.now).timeIntervalSince(startTime).toNanoseconds | ||
| duration = (endTime ?? DDTestMonitor.clock.now).timeIntervalSince(startTime).toNanosecondsUInt |
There was a problem hiding this comment.
toNanosecondsUInt isn’t available in this module unless EventsExporter is imported (the extension is currently defined in Sources/EventsExporter/Utils/DateFormatting.swift). With only OpenTelemetryApi imported here, this will not compile. Import EventsExporter here or relocate the extension into DatadogSDKTesting.
| public init() {} | ||
|
|
||
| public func export(spans: [SpanData], explicitTimeout: TimeInterval? = nil) -> SpanExporterResultCode { | ||
| .success | ||
| } | ||
|
|
||
| public func flush(explicitTimeout: TimeInterval? = nil) -> SpanExporterResultCode { | ||
| .success | ||
| } | ||
|
|
||
| public func shutdown(explicitTimeout: TimeInterval? = nil) {} |
There was a problem hiding this comment.
NoopSpanExporter is declared with the default (internal) access level, but its initializer and methods are marked public. Swift doesn’t allow members to be more visible than their enclosing type, so this won’t compile. Either make the type public or drop the public modifiers on its members.
| public init() {} | |
| public func export(spans: [SpanData], explicitTimeout: TimeInterval? = nil) -> SpanExporterResultCode { | |
| .success | |
| } | |
| public func flush(explicitTimeout: TimeInterval? = nil) -> SpanExporterResultCode { | |
| .success | |
| } | |
| public func shutdown(explicitTimeout: TimeInterval? = nil) {} | |
| init() {} | |
| func export(spans: [SpanData], explicitTimeout: TimeInterval? = nil) -> SpanExporterResultCode { | |
| .success | |
| } | |
| func flush(explicitTimeout: TimeInterval? = nil) -> SpanExporterResultCode { | |
| .success | |
| } | |
| func shutdown(explicitTimeout: TimeInterval? = nil) {} |
| mutating func setData(_ data: URLRequest) { | ||
| request = data |
There was a problem hiding this comment.
NetworkRequestState.setData(_:) takes a URLRequest parameter named data and assigns it back to request, which looks like a copy/paste mistake (and the type doesn’t match the name). If the intent is to store received payload bytes, this should probably accept Data and update dataProcessed instead.
| mutating func setData(_ data: URLRequest) { | |
| request = data | |
| mutating func setData(_ data: Data) { | |
| self.dataProcessed = data |
| let testEndTime = endTime ?? DDTestMonitor.clock.now | ||
| duration = testEndTime.timeIntervalSince(startTime).toNanoseconds | ||
| duration = testEndTime.timeIntervalSince(startTime).toNanosecondsUInt |
There was a problem hiding this comment.
toNanosecondsUInt is not defined in OpenTelemetryApi; it’s currently added in the EventsExporter target. Since this file doesn’t import EventsExporter, this line will not compile. Add an import EventsExporter here (if appropriate) or move the TimeInterval extension into DatadogSDKTesting.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 23 out of 23 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private let configurationQueue = DispatchQueue( | ||
| label: "io.opentelemetry.configuration") | ||
|
|
||
| static var instrumentedKey = "io.opentelemetry.instrumentedCall" |
There was a problem hiding this comment.
instrumentedKey is a String, but objc_setAssociatedObject keys must be an UnsafeRawPointer (typically &someStaticVar). As-is, any use of URLSessionInstrumentation.instrumentedKey as an association key will not compile and/or won’t work correctly. Define a static key storage (e.g. static var instrumentedKey: UInt8 = 0) and always pass &instrumentedKey.
| static var instrumentedKey = "io.opentelemetry.instrumentedCall" | |
| private static var instrumentedKey: UInt8 = 0 |
| import os.log | ||
| #if os(iOS) && !targetEnvironment(macCatalyst) | ||
| import NetworkStatus | ||
| #endif // os(iOS) && !targetEnvironment(macCatalyst) |
There was a problem hiding this comment.
NetworkStatus / NetworkStatusInjector are imported/used here, but there’s no corresponding dependency in the project. This will fail to compile on iOS builds where the #if os(iOS) condition is true. Consider guarding with #if canImport(NetworkStatus) (and/or providing a fallback path that skips network-status injection).
| class URLSessionInstrumentation { | ||
| private var requestMap = [String: NetworkRequestState]() | ||
|
|
||
| private var _configuration: URLSessionInstrumentationConfiguration | ||
| public var configuration: URLSessionInstrumentationConfiguration { | ||
| get { | ||
| configurationQueue.sync { _configuration } | ||
| } | ||
| set { | ||
| configurationQueue.sync { _configuration = newValue } | ||
| } | ||
| } | ||
|
|
||
| private let queue = DispatchQueue( | ||
| label: "io.opentelemetry.ddnetworkinstrumentation") | ||
| private let configurationQueue = DispatchQueue( | ||
| label: "io.opentelemetry.configuration") | ||
|
|
||
| static var instrumentedKey = "io.opentelemetry.instrumentedCall" | ||
|
|
||
| static let excludeList: [String] = [ | ||
| "__NSCFURLProxySessionConnection" | ||
| ] | ||
|
|
||
| static let AVTaskClassList: [AnyClass] = [ | ||
| "__NSCFBackgroundAVAggregateAssetDownloadTask", | ||
| "__NSCFBackgroundAVAssetDownloadTask", | ||
| "__NSCFBackgroundAVAggregateAssetDownloadTaskNoChildTask" | ||
| ] | ||
| .compactMap { NSClassFromString($0) } | ||
|
|
||
| public var startedRequestSpans: [Span] { | ||
| var spans = [Span]() | ||
| URLSessionLogger.runningSpansQueue.sync { | ||
| spans = Array(URLSessionLogger.runningSpans.values) | ||
| } | ||
| return spans | ||
| } | ||
|
|
||
| public init(configuration: URLSessionInstrumentationConfiguration) { | ||
| self._configuration = configuration | ||
| injectInNSURLClasses() | ||
| } |
There was a problem hiding this comment.
URLSessionInstrumentation is an internal class, but it exposes public API (public var configuration, public init, public var startedRequestSpans). Swift doesn’t allow public members on an internal type, so this won’t compile. Either make the class public or remove the public modifiers (likely preferred since it’s only used internally by DDNetworkInstrumentation).
| let key = String(selector.hashValue) | ||
| objc_setAssociatedObject(session, key, true, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) | ||
| let castedIMP = unsafeBitCast(originalIMP, |
There was a problem hiding this comment.
These objc_setAssociatedObject calls pass a String (key = String(selector.hashValue)) as the association key. The Objective-C runtime requires an UnsafeRawPointer key, so this won’t compile (and even if bridged somehow, it wouldn’t be stable/unique). Use dedicated static key variables and pass their addresses (e.g. &someKey) instead.
| var instrumentedRequest = request | ||
| objc_setAssociatedObject(instrumentedRequest, URLSessionInstrumentation.instrumentedKey, true, .OBJC_ASSOCIATION_COPY_NONATOMIC) | ||
| let propagators = OpenTelemetry.instance.propagators | ||
|
|
There was a problem hiding this comment.
objc_setAssociatedObject is being called with instrumentedRequest (a URLRequest value type) and URLSessionInstrumentation.instrumentedKey (currently a String). Associated objects only work with Objective-C reference types and require an UnsafeRawPointer key. This code won’t compile/work as intended; consider associating on the underlying NSURLRequest object or tracking “instrumented” state via another mechanism (e.g. a dedicated header or mapping by task identifier).
|
|
||
| func end(time: Date?) { | ||
| duration = (time ?? Date()).timeIntervalSince(startTime).toNanoseconds | ||
| duration = (time ?? Date()).timeIntervalSince(startTime).toNanosecondsUInt |
There was a problem hiding this comment.
toNanosecondsUInt is defined as a public extension in the EventsExporter module, but this test file only does import protocol EventsExporter.Logger. Scoped import protocol won’t make the TimeInterval extension visible, so this is likely to fail to compile. Import EventsExporter (or move toNanosecondsUInt into a module that this test already imports).
What and why?
Switched OpenTelemetry from my fork to the official version
How?
Review checklist