diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 6845215121..99d61151a3 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -36,12 +36,30 @@ 3C41693C29FBF4D50042B9D2 /* DatadogWebViewTracking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; }; 3C6C7FDB2B45738C006F5CBC /* OpenTelemetryApi in Frameworks */ = {isa = PBXBuildFile; productRef = 3C6C7FDA2B45738C006F5CBC /* OpenTelemetryApi */; }; 3C6C7FDD2B457392006F5CBC /* OpenTelemetryApi in Frameworks */ = {isa = PBXBuildFile; productRef = 3C6C7FDC2B457392006F5CBC /* OpenTelemetryApi */; }; + 3C6C7FE72B459AAA006F5CBC /* OTelSpan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE02B459AAA006F5CBC /* OTelSpan.swift */; }; + 3C6C7FE82B459AAA006F5CBC /* OTelSpan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE02B459AAA006F5CBC /* OTelSpan.swift */; }; + 3C6C7FE92B459AAA006F5CBC /* OTelSpanBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE12B459AAA006F5CBC /* OTelSpanBuilder.swift */; }; + 3C6C7FEA2B459AAA006F5CBC /* OTelSpanBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE12B459AAA006F5CBC /* OTelSpanBuilder.swift */; }; + 3C6C7FEB2B459AAA006F5CBC /* OTelTraceId+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE22B459AAA006F5CBC /* OTelTraceId+Datadog.swift */; }; + 3C6C7FEC2B459AAA006F5CBC /* OTelTraceId+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE22B459AAA006F5CBC /* OTelTraceId+Datadog.swift */; }; + 3C6C7FEF2B459AAA006F5CBC /* OTelSpanId+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE42B459AAA006F5CBC /* OTelSpanId+Datadog.swift */; }; + 3C6C7FF02B459AAA006F5CBC /* OTelSpanId+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE42B459AAA006F5CBC /* OTelSpanId+Datadog.swift */; }; + 3C6C7FFB2B459AF6006F5CBC /* OTelSpanId+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FF22B459AB3006F5CBC /* OTelSpanId+DatadogTests.swift */; }; + 3C6C7FFC2B459AF6006F5CBC /* OTelTraceId+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FF32B459AB3006F5CBC /* OTelTraceId+DatadogTests.swift */; }; + 3C6C7FFD2B459AF6006F5CBC /* OTelSpanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FF42B459AB3006F5CBC /* OTelSpanTests.swift */; }; + 3C6C7FFE2B459AF6006F5CBC /* OTelSpanId+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FF22B459AB3006F5CBC /* OTelSpanId+DatadogTests.swift */; }; + 3C6C7FFF2B459AF6006F5CBC /* OTelTraceId+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FF32B459AB3006F5CBC /* OTelTraceId+DatadogTests.swift */; }; + 3C6C80002B459AF6006F5CBC /* OTelSpanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FF42B459AB3006F5CBC /* OTelSpanTests.swift */; }; 3C74305C29FBC0480053B80F /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2DA2385298D57AA00C6C7E6 /* DatadogInternal.framework */; }; 3C85D42129F7C5C900AFF894 /* WebViewTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C85D41429F7C59C00AFF894 /* WebViewTracking.swift */; }; 3C85D42A29F7C70300AFF894 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257953E298ABA65008A1BE5 /* TestUtilities.framework */; }; 3C85D42C29F7C87D00AFF894 /* HostsSanitizerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C85D42B29F7C87D00AFF894 /* HostsSanitizerMock.swift */; }; 3C85D42D29F7C87D00AFF894 /* HostsSanitizerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C85D42B29F7C87D00AFF894 /* HostsSanitizerMock.swift */; }; 3C9C6BB429F7C0C000581C43 /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23039A5298D513C001A1FA3 /* DatadogInternal.framework */; }; + 3CB012DD2B482E0400557951 /* NOPOTelSpan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB012DB2B482E0400557951 /* NOPOTelSpan.swift */; }; + 3CB012DE2B482E0400557951 /* NOPOTelSpan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB012DB2B482E0400557951 /* NOPOTelSpan.swift */; }; + 3CB012DF2B482E0400557951 /* NOPOTelSpanBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB012DC2B482E0400557951 /* NOPOTelSpanBuilder.swift */; }; + 3CB012E02B482E0400557951 /* NOPOTelSpanBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB012DC2B482E0400557951 /* NOPOTelSpanBuilder.swift */; }; 3CB32AD42ACB733000D602ED /* URLSessionSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB32AD32ACB733000D602ED /* URLSessionSwizzler.swift */; }; 3CB32AD52ACB733000D602ED /* URLSessionSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB32AD32ACB733000D602ED /* URLSessionSwizzler.swift */; }; 3CB32AD72ACB735600D602ED /* URLSessionSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB32AD62ACB735600D602ED /* URLSessionSwizzlerTests.swift */; }; @@ -1882,8 +1900,17 @@ 3C2206F22AB9CE9300DE780C /* MetaTypeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaTypeExtensions.swift; sourceTree = ""; }; 3C394EF62AA5F49F008F48BA /* URLSessionDataDelegateSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataDelegateSwizzler.swift; sourceTree = ""; }; 3C394EF92AA5F4C8008F48BA /* URLSessionDataDelegateSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataDelegateSwizzlerTests.swift; sourceTree = ""; }; + 3C6C7FE02B459AAA006F5CBC /* OTelSpan.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpan.swift; sourceTree = ""; }; + 3C6C7FE12B459AAA006F5CBC /* OTelSpanBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpanBuilder.swift; sourceTree = ""; }; + 3C6C7FE22B459AAA006F5CBC /* OTelTraceId+Datadog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelTraceId+Datadog.swift"; sourceTree = ""; }; + 3C6C7FE42B459AAA006F5CBC /* OTelSpanId+Datadog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelSpanId+Datadog.swift"; sourceTree = ""; }; + 3C6C7FF22B459AB3006F5CBC /* OTelSpanId+DatadogTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelSpanId+DatadogTests.swift"; sourceTree = ""; }; + 3C6C7FF32B459AB3006F5CBC /* OTelTraceId+DatadogTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelTraceId+DatadogTests.swift"; sourceTree = ""; }; + 3C6C7FF42B459AB3006F5CBC /* OTelSpanTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpanTests.swift; sourceTree = ""; }; 3C85D41429F7C59C00AFF894 /* WebViewTracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewTracking.swift; sourceTree = ""; }; 3C85D42B29F7C87D00AFF894 /* HostsSanitizerMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HostsSanitizerMock.swift; sourceTree = ""; }; + 3CB012DB2B482E0400557951 /* NOPOTelSpan.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NOPOTelSpan.swift; sourceTree = ""; }; + 3CB012DC2B482E0400557951 /* NOPOTelSpanBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NOPOTelSpanBuilder.swift; sourceTree = ""; }; 3CB32AD32ACB733000D602ED /* URLSessionSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionSwizzler.swift; sourceTree = ""; }; 3CB32AD62ACB735600D602ED /* URLSessionSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionSwizzlerTests.swift; sourceTree = ""; }; 3CBDE66D2AA08BF600F6A7B6 /* URLSessionTaskDelegateSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTaskDelegateSwizzler.swift; sourceTree = ""; }; @@ -3084,6 +3111,29 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 3C6C7FDE2B459AAA006F5CBC /* OpenTelemetry */ = { + isa = PBXGroup; + children = ( + 3CB012DB2B482E0400557951 /* NOPOTelSpan.swift */, + 3CB012DC2B482E0400557951 /* NOPOTelSpanBuilder.swift */, + 3C6C7FE02B459AAA006F5CBC /* OTelSpan.swift */, + 3C6C7FE12B459AAA006F5CBC /* OTelSpanBuilder.swift */, + 3C6C7FE22B459AAA006F5CBC /* OTelTraceId+Datadog.swift */, + 3C6C7FE42B459AAA006F5CBC /* OTelSpanId+Datadog.swift */, + ); + path = OpenTelemetry; + sourceTree = ""; + }; + 3C6C7FF12B459AB3006F5CBC /* OpenTelemetry */ = { + isa = PBXGroup; + children = ( + 3C6C7FF22B459AB3006F5CBC /* OTelSpanId+DatadogTests.swift */, + 3C6C7FF32B459AB3006F5CBC /* OTelTraceId+DatadogTests.swift */, + 3C6C7FF42B459AB3006F5CBC /* OTelSpanTests.swift */, + ); + path = OpenTelemetry; + sourceTree = ""; + }; 3CE11A3B29F7BEE700202522 /* DatadogWebViewTracking */ = { isa = PBXGroup; children = ( @@ -5486,6 +5536,7 @@ D25EE93529C4C3C300CE3839 /* DatadogTrace */ = { isa = PBXGroup; children = ( + 3C6C7FDE2B459AAA006F5CBC /* OpenTelemetry */, 61A2CC382A44B0EA0000FF25 /* Trace.swift */, 61A2CC352A44B0A20000FF25 /* TraceConfiguration.swift */, 61A2CC3B2A44BED30000FF25 /* Tracer.swift */, @@ -5508,6 +5559,7 @@ D25EE93F29C4C3C400CE3839 /* DatadogTraceTests */ = { isa = PBXGroup; children = ( + 3C6C7FF12B459AB3006F5CBC /* OpenTelemetry */, 619CE75D2A458CE1005588CB /* TraceConfigurationTests.swift */, 61AD4E172451C7FF006E34EA /* TracingFeatureMocks.swift */, 61C5A89824509C1100DA608C /* DDSpanTests.swift */, @@ -8275,15 +8327,21 @@ D2C1A50C29C4C4CB00946C31 /* DDNoOps.swift in Sources */, D2C1A4FC29C4C4CB00946C31 /* RequestBuilder.swift in Sources */, D2C1A50D29C4C4CB00946C31 /* SpanTagsReducer.swift in Sources */, + 3CB012DD2B482E0400557951 /* NOPOTelSpan.swift in Sources */, D2C1A51A29C4C5DD00946C31 /* JSONEncoder.swift in Sources */, D2C1A51829C4C53F00946C31 /* OTSpan.swift in Sources */, D2C1A51429C4C53F00946C31 /* OTSpanContext.swift in Sources */, + 3C6C7FEB2B459AAA006F5CBC /* OTelTraceId+Datadog.swift in Sources */, + 3C6C7FE92B459AAA006F5CBC /* OTelSpanBuilder.swift in Sources */, D2C1A51329C4C53F00946C31 /* OTReference.swift in Sources */, + 3C6C7FEF2B459AAA006F5CBC /* OTelSpanId+Datadog.swift in Sources */, D2C1A4FB29C4C4CB00946C31 /* MessageReceivers.swift in Sources */, 61A2CC362A44B0A20000FF25 /* TraceConfiguration.swift in Sources */, 61A2CC392A44B0EA0000FF25 /* Trace.swift in Sources */, D2C1A50029C4C4CB00946C31 /* ActiveSpansPool.swift in Sources */, + 3CB012DF2B482E0400557951 /* NOPOTelSpanBuilder.swift in Sources */, D2C1A50929C4C4CB00946C31 /* SpanEventEncoder.swift in Sources */, + 3C6C7FE72B459AAA006F5CBC /* OTelSpan.swift in Sources */, D2C1A4FE29C4C4CB00946C31 /* SpanEventMapper.swift in Sources */, D2C1A50329C4C4CB00946C31 /* DDFormat.swift in Sources */, D2C1A51629C4C53F00946C31 /* OTConstants.swift in Sources */, @@ -8314,10 +8372,13 @@ D2C1A51D29C4C75700946C31 /* SpanEventBuilderTests.swift in Sources */, D2C1A52229C4C75700946C31 /* DDNoopTracerTests.swift in Sources */, D2C1A51C29C4C75700946C31 /* ContextMessageReceiverTests.swift in Sources */, + 3C6C7FFD2B459AF6006F5CBC /* OTelSpanTests.swift in Sources */, 619CE7612A458D66005588CB /* TraceTests.swift in Sources */, D2C1A52029C4C75700946C31 /* DDSpanTests.swift in Sources */, + 3C6C7FFC2B459AF6006F5CBC /* OTelTraceId+DatadogTests.swift in Sources */, D2C1A51B29C4C75700946C31 /* DDSpanContextTests.swift in Sources */, D2C1A52729C4C7D000946C31 /* TracingFeatureMocks.swift in Sources */, + 3C6C7FFB2B459AF6006F5CBC /* OTelSpanId+DatadogTests.swift in Sources */, D2C1A51F29C4C75700946C31 /* ActiveSpansPoolTests.swift in Sources */, D2C1A52529C4C75700946C31 /* SpanSanitizerTests.swift in Sources */, ); @@ -8464,15 +8525,21 @@ D2C1A53929C4F2DF00946C31 /* DDNoOps.swift in Sources */, D2C1A53A29C4F2DF00946C31 /* RequestBuilder.swift in Sources */, D2C1A53B29C4F2DF00946C31 /* SpanTagsReducer.swift in Sources */, + 3CB012DE2B482E0400557951 /* NOPOTelSpan.swift in Sources */, D2C1A53C29C4F2DF00946C31 /* JSONEncoder.swift in Sources */, D2C1A53D29C4F2DF00946C31 /* OTSpan.swift in Sources */, D2C1A53E29C4F2DF00946C31 /* OTSpanContext.swift in Sources */, + 3C6C7FEC2B459AAA006F5CBC /* OTelTraceId+Datadog.swift in Sources */, + 3C6C7FEA2B459AAA006F5CBC /* OTelSpanBuilder.swift in Sources */, D2C1A53F29C4F2DF00946C31 /* OTReference.swift in Sources */, + 3C6C7FF02B459AAA006F5CBC /* OTelSpanId+Datadog.swift in Sources */, D2C1A54129C4F2DF00946C31 /* MessageReceivers.swift in Sources */, 61A2CC372A44B0A20000FF25 /* TraceConfiguration.swift in Sources */, 61A2CC3A2A44B0EA0000FF25 /* Trace.swift in Sources */, D2C1A54229C4F2DF00946C31 /* ActiveSpansPool.swift in Sources */, + 3CB012E02B482E0400557951 /* NOPOTelSpanBuilder.swift in Sources */, D2C1A54329C4F2DF00946C31 /* SpanEventEncoder.swift in Sources */, + 3C6C7FE82B459AAA006F5CBC /* OTelSpan.swift in Sources */, D2C1A54429C4F2DF00946C31 /* SpanEventMapper.swift in Sources */, D2C1A54529C4F2DF00946C31 /* DDFormat.swift in Sources */, D2C1A54629C4F2DF00946C31 /* OTConstants.swift in Sources */, @@ -8503,10 +8570,13 @@ D2C1A56229C4F2E800946C31 /* SpanEventBuilderTests.swift in Sources */, D2C1A56329C4F2E800946C31 /* DDNoopTracerTests.swift in Sources */, D2C1A56529C4F2E800946C31 /* ContextMessageReceiverTests.swift in Sources */, + 3C6C80002B459AF6006F5CBC /* OTelSpanTests.swift in Sources */, 619CE7622A458D66005588CB /* TraceTests.swift in Sources */, D2C1A56629C4F2E800946C31 /* DDSpanTests.swift in Sources */, + 3C6C7FFF2B459AF6006F5CBC /* OTelTraceId+DatadogTests.swift in Sources */, D2C1A56729C4F2E800946C31 /* DDSpanContextTests.swift in Sources */, D2C1A56829C4F2E800946C31 /* TracingFeatureMocks.swift in Sources */, + 3C6C7FFE2B459AF6006F5CBC /* OTelSpanId+DatadogTests.swift in Sources */, D2C1A56929C4F2E800946C31 /* ActiveSpansPoolTests.swift in Sources */, D2C1A56A29C4F2E800946C31 /* SpanSanitizerTests.swift in Sources */, ); diff --git a/DatadogTrace/Sources/DDNoOps.swift b/DatadogTrace/Sources/DDNoOps.swift index 1f013b0161..85730d6e51 100644 --- a/DatadogTrace/Sources/DDNoOps.swift +++ b/DatadogTrace/Sources/DDNoOps.swift @@ -6,6 +6,7 @@ import Foundation import DatadogInternal +import OpenTelemetryApi internal struct DDNoopGlobals { static let tracer = DDNoopTracer() @@ -13,7 +14,7 @@ internal struct DDNoopGlobals { static let context = DDNoopSpanContext() } -internal struct DDNoopTracer: OTTracer { +internal class DDNoopTracer: OTTracer, OpenTelemetryApi.Tracer { var activeSpan: OTSpan? = nil private func warn() { @@ -44,6 +45,13 @@ internal struct DDNoopTracer: OTTracer { warn() return DDNoopGlobals.span } + + // MARK: - Open Telemetry + + func spanBuilder(spanName: String) -> OpenTelemetryApi.SpanBuilder { + warn() + return NOPOTelSpanBuilder() + } } internal struct DDNoopSpan: OTSpan { diff --git a/DatadogTrace/Sources/DatadogTracer.swift b/DatadogTrace/Sources/DatadogTracer.swift index 6d737634c7..ceb9c5b3b9 100644 --- a/DatadogTrace/Sources/DatadogTracer.swift +++ b/DatadogTrace/Sources/DatadogTracer.swift @@ -6,8 +6,9 @@ import Foundation import DatadogInternal +import OpenTelemetryApi -internal class DatadogTracer: OTTracer { +internal final class DatadogTracer: OTTracer, OpenTelemetryApi.Tracer { internal weak var core: DatadogCoreProtocol? /// Global tags configured for Trace feature. @@ -156,4 +157,18 @@ internal class DatadogTracer: OTTracer { forKey: SpanCoreContext.key ) } + + // MARK: - OpenTelemetry + + func spanBuilder(spanName: String) -> OpenTelemetryApi.SpanBuilder { + OTelSpanBuilder( + active: false, + attributes: [:], + parent: .currentSpan, + spanKind: .client, + spanName: spanName, + startTime: nil, + tracer: self + ) + } } diff --git a/DatadogTrace/Sources/OpenTelemetry/NOPOTelSpan.swift b/DatadogTrace/Sources/OpenTelemetry/NOPOTelSpan.swift new file mode 100644 index 0000000000..76e2705483 --- /dev/null +++ b/DatadogTrace/Sources/OpenTelemetry/NOPOTelSpan.swift @@ -0,0 +1,47 @@ +/* +* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +* This product includes software developed at Datadog (https://www.datadoghq.com/). +* Copyright 2019-Present Datadog, Inc. +*/ + +import Foundation +import OpenTelemetryApi + +internal class NOPOTelSpan: Span { + var kind: OpenTelemetryApi.SpanKind = .internal + + var name: String = "" + + var context = SpanContext.create( + traceId: TraceId.invalid, + spanId: SpanId.invalid, + traceFlags: TraceFlags(), + traceState: TraceState() + ) + + var isRecording = false + + var status = Status.unset + + var description: String = "OTelNoOpSpan" + + func updateName(name: String) {} + + func setAttribute(key: String, value: OpenTelemetryApi.AttributeValue?) {} + + func addEvent(name: String) {} + + func addEvent(name: String, timestamp: Date) {} + + func addEvent(name: String, attributes: [String: OpenTelemetryApi.AttributeValue]) {} + + func addEvent(name: String, attributes: [String: OpenTelemetryApi.AttributeValue], timestamp: Date) {} + + func end() { + OpenTelemetry.instance.contextProvider.removeContextForSpan(self) + } + + func end(time: Date) { + end() + } +} diff --git a/DatadogTrace/Sources/OpenTelemetry/NOPOTelSpanBuilder.swift b/DatadogTrace/Sources/OpenTelemetry/NOPOTelSpanBuilder.swift new file mode 100644 index 0000000000..d20796a70a --- /dev/null +++ b/DatadogTrace/Sources/OpenTelemetry/NOPOTelSpanBuilder.swift @@ -0,0 +1,58 @@ +/* +* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +* This product includes software developed at Datadog (https://www.datadoghq.com/). +* Copyright 2019-Present Datadog, Inc. +*/ + +import Foundation +import OpenTelemetryApi + +internal class NOPOTelSpanBuilder: SpanBuilder { + @discardableResult + func startSpan() -> Span { + return NOPOTelSpan() + } + + @discardableResult + func setParent(_ parent: Span) -> Self { + return self + } + + @discardableResult + func setParent(_ parent: SpanContext) -> Self { + return self + } + + @discardableResult + func setNoParent() -> Self { + return self + } + + @discardableResult + func addLink(spanContext: SpanContext) -> Self { + return self + } + + @discardableResult + func addLink(spanContext: SpanContext, attributes: [String: OpenTelemetryApi.AttributeValue]) -> Self { + return self + } + + @discardableResult + func setSpanKind(spanKind: SpanKind) -> Self { + return self + } + + @discardableResult + func setStartTime(time: Date) -> Self { + return self + } + + func setAttribute(key: String, value: OpenTelemetryApi.AttributeValue) -> Self { + return self + } + + func setActive(_ active: Bool) -> Self { + return self + } +} diff --git a/DatadogTrace/Sources/OpenTelemetry/OTelSpan.swift b/DatadogTrace/Sources/OpenTelemetry/OTelSpan.swift new file mode 100644 index 0000000000..1ffcc037f0 --- /dev/null +++ b/DatadogTrace/Sources/OpenTelemetry/OTelSpan.swift @@ -0,0 +1,181 @@ +/* +* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +* This product includes software developed at Datadog (https://www.datadoghq.com/). +* Copyright 2019-Present Datadog, Inc. +*/ + +import Foundation +import OpenTelemetryApi + +internal enum DatadogTagKeys: String { + case spanKind = "span.kind" + case errorMessage = "error.Message" +} + +internal class OTelSpan: OpenTelemetryApi.Span { + private var _status: OpenTelemetryApi.Status + private var _name: String + + var attributes: [String: OpenTelemetryApi.AttributeValue] + let context: OpenTelemetryApi.SpanContext + var kind: OpenTelemetryApi.SpanKind + let ddSpan: DDSpan + let tracer: DatadogTracer + let queue: DispatchQueue + + /// `isRecording` indicates whether the span is recording or not + /// and events can be added to it. + var isRecording: Bool + + var status: OpenTelemetryApi.Status { + get { + fatalError("Not implemented yet") + } + set { + fatalError("Not implemented yet") + } + } + + /// `name` of the span is akin to operation name in Datadog + var name: String { + get { + queue.sync { + _name + } + } + set { + queue.sync { + guard isRecording else { + return + } + _name = newValue + } + ddSpan.setOperationName(name) + } + } + + init( + attributes: [String: OpenTelemetryApi.AttributeValue], + kind: OpenTelemetryApi.SpanKind, + name: String, + parentSpanID: OpenTelemetryApi.SpanId?, + spanContext: OpenTelemetryApi.SpanContext, + spanKind: OpenTelemetryApi.SpanKind, + startTime: Date, + tracer: DatadogTracer + ) { + self._name = name + self._status = .unset + self.attributes = attributes + self.context = spanContext + self.kind = kind + self.isRecording = true + self.queue = tracer.queue + self.tracer = tracer + self.ddSpan = .init( + tracer: tracer, + context: .init( + traceID: context.traceId.toDatadog(), + spanID: context.spanId.toDatadog(), + parentSpanID: parentSpanID?.toDatadog(), + baggageItems: .init() + ), + operationName: name, + startTime: startTime, + tags: [:] + ) + } + + // swiftlint:disable unavailable_function + func addEvent(name: String) { + fatalError("Not implemented yet") + } + + func addEvent(name: String, timestamp: Date) { + fatalError("Not implemented yet") + } + + func addEvent(name: String, attributes: [String: OpenTelemetryApi.AttributeValue]) { + fatalError("Not implemented yet") + } + + func addEvent(name: String, attributes: [String: OpenTelemetryApi.AttributeValue], timestamp: Date) { + fatalError("Not implemented yet") + } + // swiftlint:enable unavailable_function + + func end() { + end(time: Date()) + } + + func end(time: Date) { + var ended = false + var tags: [String: String] = [:] + + queue.sync { + guard isRecording else { + ended = true + return + } + isRecording = false + tags = makeTags() + } + + // if the span was already ended before, we don't want to end it again + guard !ended else { + return + } + + // There is no need to lock here, because `DDSpan` is thread-safe + for (key, value) in tags { + ddSpan.setTag(key: key, value: value) + } + + // SpanKind maps to the `span.kind` tag in Datadog + ddSpan.setTag(key: DatadogTagKeys.spanKind.rawValue, value: kind.rawValue) + ddSpan.finish(at: time) + } + + private func makeTags() -> [String: String] { + var tags = [String: String]() + for (key, value) in attributes { + switch value { + case .string(let value): + tags[key] = value + case .bool(let value): + tags[key] = value.description + case .int(let value): + tags[key] = value.description + case .double(let value): + tags[key] = value.description + // swiftlint:disable unavailable_function + case .stringArray: + fatalError("Not implemented yet") + case .boolArray: + fatalError("Not implemented yet") + case .intArray: + fatalError("Not implemented yet") + case .doubleArray: + fatalError("Not implemented yet") + case .set: + fatalError("Not implemented yet") + // swiftlint:enable unavailable_function + } + } + return tags + } + + var description: String { + return "OTelSpan" + } + + func setAttribute(key: String, value: OpenTelemetryApi.AttributeValue?) { + queue.sync { + guard isRecording else { + return + } + + attributes[key] = value + } + } +} diff --git a/DatadogTrace/Sources/OpenTelemetry/OTelSpanBuilder.swift b/DatadogTrace/Sources/OpenTelemetry/OTelSpanBuilder.swift new file mode 100644 index 0000000000..93a4deb244 --- /dev/null +++ b/DatadogTrace/Sources/OpenTelemetry/OTelSpanBuilder.swift @@ -0,0 +1,140 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation +import OpenTelemetryApi + +internal class OTelSpanBuilder: OpenTelemetryApi.SpanBuilder { + var tracer: DatadogTracer + var spanName: String + var spanKind = SpanKind.client + var attributes: [String: OpenTelemetryApi.AttributeValue] + var startTime: Date? + var active: Bool + var parent: Parent + + enum Parent { + case currentSpan + case span(OpenTelemetryApi.Span) + case spanContext(OpenTelemetryApi.SpanContext) + case noParent + + func context() -> OpenTelemetryApi.SpanContext? { + switch self { + case .currentSpan: + return OpenTelemetry.instance.contextProvider.activeSpan?.context + case .span(let span): + return span.context + case .spanContext(let context): + return context + case .noParent: + return nil + } + } + } + + init( + active: Bool, + attributes: [String: OpenTelemetryApi.AttributeValue], + parent: Parent, + spanKind: SpanKind, + spanName: String, + startTime: Date?, + tracer: DatadogTracer + ) { + self.tracer = tracer + self.spanName = spanName + self.spanKind = spanKind + self.attributes = attributes + self.startTime = startTime + self.active = active + self.parent = parent + } + + func setParent(_ parent: OpenTelemetryApi.Span) -> Self { + self.parent = .span(parent) + return self + } + + func setParent(_ parent: OpenTelemetryApi.SpanContext) -> Self { + self.parent = .spanContext(parent) + return self + } + + func setNoParent() -> Self { + self.parent = .noParent + return self + } + + // swiftlint:disable unavailable_function + func addLink(spanContext: OpenTelemetryApi.SpanContext) -> Self { + fatalError("Not implemented yet") + } + + func addLink(spanContext: OpenTelemetryApi.SpanContext, attributes: [String: OpenTelemetryApi.AttributeValue]) -> Self { + fatalError("Not implemented yet") + } + // swiftlint:enable unavailable_function + + func setSpanKind(spanKind: OpenTelemetryApi.SpanKind) -> Self { + self.spanKind = spanKind + return self + } + + func setStartTime(time: Date) -> Self { + self.startTime = time + return self + } + + func setActive(_ active: Bool) -> Self { + self.active = active + return self + } + + func startSpan() -> OpenTelemetryApi.Span { + let parentContext = parent.context() + let traceId: TraceId + let spanId = SpanId.random() + let traceState: TraceState + + if let parentContext = parentContext, parentContext.isValid { + traceId = parentContext.traceId + traceState = parentContext.traceState + } else { + traceId = TraceId.random() + traceState = .init() + } + + let spanContext = SpanContext.create( + traceId: traceId, + spanId: spanId, + traceFlags: TraceFlags(), + traceState: traceState + ) + + let createdSpan = OTelSpan( + attributes: attributes, + kind: spanKind, + name: spanName, + parentSpanID: parentContext?.spanId, + spanContext: spanContext, + spanKind: spanKind, + startTime: startTime ?? Date(), + tracer: tracer + ) + + if active { + OpenTelemetry.instance.contextProvider.setActiveSpan(createdSpan) + } + + return createdSpan + } + + func setAttribute(key: String, value: OpenTelemetryApi.AttributeValue) -> Self { + attributes[key] = value + return self + } +} diff --git a/DatadogTrace/Sources/OpenTelemetry/OTelSpanId+Datadog.swift b/DatadogTrace/Sources/OpenTelemetry/OTelSpanId+Datadog.swift new file mode 100644 index 0000000000..6c8ff2c0b8 --- /dev/null +++ b/DatadogTrace/Sources/OpenTelemetry/OTelSpanId+Datadog.swift @@ -0,0 +1,20 @@ +/* +* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +* This product includes software developed at Datadog (https://www.datadoghq.com/). +* Copyright 2019-Present Datadog, Inc. +*/ + +import Foundation +import OpenTelemetryApi +import DatadogInternal + +extension OpenTelemetryApi.SpanId { + /// Converts OpenTelemetry `SpanId` to Datadog `SpanID`. + /// - Returns: Datadog `SpanID`. + func toDatadog() -> SpanID { + var data = Data(count: 8) + self.copyBytesTo(dest: &data, destOffset: 0) + let integerLiteral = UInt64(bigEndian: data.withUnsafeBytes { $0.load(as: UInt64.self) }) + return .init(integerLiteral: integerLiteral) + } +} diff --git a/DatadogTrace/Sources/OpenTelemetry/OTelTraceId+Datadog.swift b/DatadogTrace/Sources/OpenTelemetry/OTelTraceId+Datadog.swift new file mode 100644 index 0000000000..9f34b8cf8d --- /dev/null +++ b/DatadogTrace/Sources/OpenTelemetry/OTelTraceId+Datadog.swift @@ -0,0 +1,20 @@ +/* +* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +* This product includes software developed at Datadog (https://www.datadoghq.com/). +* Copyright 2019-Present Datadog, Inc. +*/ + +import Foundation +import OpenTelemetryApi +import DatadogInternal + +extension OpenTelemetryApi.TraceId { + /// Converts OpenTelemetry `TraceId` to Datadog `TraceID`. + /// - Returns: Datadog `TraceID` with only higher order bits considered. + func toDatadog() -> TraceID { + var data = Data(count: 16) + self.copyBytesTo(dest: &data, destOffset: 0) + let integerLiteral = UInt64(bigEndian: data.withUnsafeBytes { $0.load(as: UInt64.self) }) + return .init(integerLiteral: integerLiteral) + } +} diff --git a/DatadogTrace/Sources/Tracer.swift b/DatadogTrace/Sources/Tracer.swift index fd3431f569..1f8cb36a13 100644 --- a/DatadogTrace/Sources/Tracer.swift +++ b/DatadogTrace/Sources/Tracer.swift @@ -6,6 +6,7 @@ import Foundation import DatadogInternal +import OpenTelemetryApi /// Datadog - specific span tags to be used with `Tracer.shared().startSpan(operationName:references:tags:startTime:)` /// and `span.setTag(key:value:)`. @@ -49,7 +50,7 @@ public class Tracer { /// It requires `Trace.enable(with:in:)` to be called first - otherwise it will return no-op implementation. /// - Parameter core: the instance of Datadog SDK the Trace feature was enabled in (global instance by default) /// - Returns: the Tracer that conforms to Open Tracing API (`OTTracer`) - public static func shared(in core: DatadogCoreProtocol = CoreRegistry.default) -> OTTracer { + public static func shared(in core: DatadogCoreProtocol = CoreRegistry.default) -> OTTracer & OpenTelemetryApi.Tracer { do { guard !(core is NOPDatadogCore) else { throw ProgrammerError( diff --git a/DatadogTrace/Tests/OpenTelemetry/OTelSpanId+DatadogTests.swift b/DatadogTrace/Tests/OpenTelemetry/OTelSpanId+DatadogTests.swift new file mode 100644 index 0000000000..8085592062 --- /dev/null +++ b/DatadogTrace/Tests/OpenTelemetry/OTelSpanId+DatadogTests.swift @@ -0,0 +1,20 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +import TestUtilities +import DatadogInternal +import OpenTelemetryApi + +@testable import DatadogTrace + +class OTelSpanIdDatadogTests: XCTestCase { + func testToDatadog() { + let otelId = SpanId.random() + let ddId = otelId.toDatadog() + XCTAssertEqual(otelId.rawValue, ddId.rawValue) + } +} diff --git a/DatadogTrace/Tests/OpenTelemetry/OTelSpanTests.swift b/DatadogTrace/Tests/OpenTelemetry/OTelSpanTests.swift new file mode 100644 index 0000000000..e5d43d89fd --- /dev/null +++ b/DatadogTrace/Tests/OpenTelemetry/OTelSpanTests.swift @@ -0,0 +1,209 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +import TestUtilities +import DatadogInternal + +@testable import DatadogTrace + +final class OTelSpanTests: XCTestCase { + func testSpanResourceNameDefault() { + let writeSpanExpectation = expectation(description: "write span event") + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let span = tracer.spanBuilder(spanName: "OperationName").startSpan() + + // When + span.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 1) + let recordedSpan = recordedSpans.first! + XCTAssertEqual(recordedSpan.resource, "OperationName") + XCTAssertEqual(recordedSpan.operationName, "OperationName") + } + + func testSpanSetName() { + let writeSpanExpectation = expectation(description: "write span event") + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let span = tracer.spanBuilder(spanName: "OperationName").startSpan() + + // When + span.name = "NewOperationName" + span.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 1) + let recordedSpan = recordedSpans.first! + XCTAssertEqual(recordedSpan.resource, "NewOperationName") + XCTAssertEqual(recordedSpan.operationName, "NewOperationName") + } + + func testSpanEnd() { + // Given + let (name, ignoredName) = ("trueName", "invalidName") + let (attributes, ignoredAttributes) = (["key": "value"], ["ignoredKey": "ignoredValue"]) + + let writeSpanExpectation = expectation(description: "write span event") + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + let tracer: DatadogTracer = .mockWith(core: core) + let span = tracer.spanBuilder(spanName: name).startSpan() + for (key, value) in attributes { + span.setAttribute(key: key, value: value) + } + XCTAssertTrue(span.isRecording) + + // When + span.end() + XCTAssertFalse(span.isRecording) + + // Then ignores + span.name = ignoredName + for (key, value) in ignoredAttributes { + span.setAttribute(key: key, value: value) + } + + span.end() + + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 1) + let recordedSpan = recordedSpans.first! + + XCTAssertEqual(recordedSpan.resource, name) + XCTAssertEqual(recordedSpan.operationName, name) + let expectedTags = [ + "key": "value", + "span.kind": "client", + ] + DDAssertDictionariesEqual(recordedSpan.tags, expectedTags) + } + + func testSetParentSpan() { + let writeSpanExpectation = expectation(description: "write span event") + writeSpanExpectation.expectedFulfillmentCount = 2 + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let parentSpan = tracer.spanBuilder(spanName: "Parent").startSpan() + _ = tracer.spanBuilder(spanName: "Noise").startSpan() + let childSpan = tracer.spanBuilder(spanName: "Child").setParent(parentSpan).startSpan() + + // When + childSpan.end() + parentSpan.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 2) + let child = recordedSpans.first! + let parent = recordedSpans.last! + XCTAssertEqual(parent.parentID, nil) + XCTAssertEqual(child.parentID, parent.spanID) + } + + func testSetParentContext() { + let writeSpanExpectation = expectation(description: "write span event") + writeSpanExpectation.expectedFulfillmentCount = 2 + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let parentSpan = tracer.spanBuilder(spanName: "Parent").startSpan() + _ = tracer.spanBuilder(spanName: "Noise").startSpan() + let childSpan = tracer.spanBuilder(spanName: "Child").setParent(parentSpan.context).startSpan() + + // When + childSpan.end() + parentSpan.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 2) + let child = recordedSpans.first! + let parent = recordedSpans.last! + XCTAssertEqual(parent.parentID, nil) + XCTAssertEqual(child.parentID, parent.spanID) + } + + func testSetNoParent() { + let writeSpanExpectation = expectation(description: "write span event") + writeSpanExpectation.expectedFulfillmentCount = 2 + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let parentSpan = tracer.spanBuilder(spanName: "Parent").startSpan() + _ = tracer.spanBuilder(spanName: "Noise").startSpan() + let childSpan = tracer.spanBuilder(spanName: "Child").setNoParent().startSpan() + + // When + childSpan.end() + parentSpan.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 2) + let child = recordedSpans.first! + let parent = recordedSpans.last! + XCTAssertEqual(parent.parentID, nil) + XCTAssertEqual(child.parentID, nil) + } + + func testSetAttribute() { + let writeSpanExpectation = expectation(description: "write span event") + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let span = tracer.spanBuilder(spanName: "Span").startSpan() + + // When + span.setAttribute(key: "key", value: .bool(true)) + span.setAttribute(key: "key2", value: .string("value2")) + span.setAttribute(key: "key3", value: .int(3)) + span.setAttribute(key: "key4", value: .double(4.0)) + + span.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let recordedSpans = core.spans() + XCTAssertEqual(recordedSpans.count, 1) + let recordedSpan = recordedSpans.first! + let expectedTags = + [ + "key": "true", + "key2": "value2", + "key3": "3", + "key4": "4.0", + "span.kind": "client", + ] + DDAssertDictionariesEqual(recordedSpan.tags, expectedTags) + } +} + +extension PassthroughCoreMock { + func spans() -> [SpanEvent] { + let events: [SpanEventsEnvelope] = self.events() + return events.flatMap { $0.spans } + } +} diff --git a/DatadogTrace/Tests/OpenTelemetry/OTelTraceId+DatadogTests.swift b/DatadogTrace/Tests/OpenTelemetry/OTelTraceId+DatadogTests.swift new file mode 100644 index 0000000000..f5bcc823bc --- /dev/null +++ b/DatadogTrace/Tests/OpenTelemetry/OTelTraceId+DatadogTests.swift @@ -0,0 +1,20 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +import TestUtilities +import DatadogInternal +import OpenTelemetryApi + +@testable import DatadogTrace + +class OTelTraceIdDatadogTests: XCTestCase { + func testToDatadog_onlyHigherOrderBitsAreConsidered() { + let otelId = TraceId.random() + let ddId = otelId.toDatadog() + XCTAssertEqual(otelId.rawHigherLong, ddId.rawValue) + } +}