Skip to content

Commit 12a2733

Browse files
authored
feat: Add OTel-Swift supported Tracing (#910)
1 parent ad28dd5 commit 12a2733

File tree

10 files changed

+286
-21
lines changed

10 files changed

+286
-21
lines changed

Package.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ let package = Package(
5858
var dependencies: [Package.Dependency] = [
5959
.package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.52.1"),
6060
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
61+
.package(url: "https://github.com/open-telemetry/opentelemetry-swift", from: "1.13.0"),
6162
]
63+
6264
let isDocCEnabled = ProcessInfo.processInfo.environment["AWS_SWIFT_SDK_ENABLE_DOCC"] != nil
6365
if isDocCEnabled {
6466
dependencies.append(.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"))
@@ -95,6 +97,27 @@ let package = Package(
9597
"SmithyChecksums",
9698
"SmithyCBOR",
9799
.product(name: "AwsCommonRuntimeKit", package: "aws-crt-swift"),
100+
// Only include these on macOS, iOS, tvOS, and watchOS (visionOS and Linux are excluded)
101+
.product(
102+
name: "InMemoryExporter",
103+
package: "opentelemetry-swift",
104+
condition: .when(platforms: [.macOS, .iOS, .tvOS, .watchOS])
105+
),
106+
.product(
107+
name: "OpenTelemetryApi",
108+
package: "opentelemetry-swift",
109+
condition: .when(platforms: [.macOS, .iOS, .tvOS, .watchOS])
110+
),
111+
.product(
112+
name: "OpenTelemetrySdk",
113+
package: "opentelemetry-swift",
114+
condition: .when(platforms: [.macOS, .iOS, .tvOS, .watchOS])
115+
),
116+
.product(
117+
name: "OpenTelemetryProtocolExporterHTTP",
118+
package: "opentelemetry-swift",
119+
condition: .when(platforms: [.macOS, .iOS, .tvOS, .watchOS])
120+
),
98121
],
99122
resources: [
100123
.copy("PrivacyInfo.xcprivacy")

Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,7 @@ public class CRTClientEngine: HTTPClient {
188188
public func send(request: HTTPRequest) async throws -> HTTPResponse {
189189
let telemetryContext = telemetry.contextManager.current()
190190
let tracer = telemetry.tracerProvider.getTracer(
191-
scope: telemetry.tracerScope,
192-
attributes: telemetry.tracerAttributes
191+
scope: telemetry.tracerScope
193192
)
194193
let queuedStart = Date().timeIntervalSinceReferenceDate
195194
let span = tracer.createSpan(
@@ -243,16 +242,17 @@ public class CRTClientEngine: HTTPClient {
243242
func executeHTTP2Request(request: HTTPRequest) async throws -> HTTPResponse {
244243
let telemetryContext = telemetry.contextManager.current()
245244
let tracer = telemetry.tracerProvider.getTracer(
246-
scope: telemetry.tracerScope,
247-
attributes: telemetry.tracerAttributes)
245+
scope: telemetry.tracerScope
246+
)
248247
do {
249248
// START - smithy.client.http.requests.queued_duration
250249
let queuedStart = Date().timeIntervalSinceReferenceDate
251250
let span = tracer.createSpan(
252251
name: telemetry.spanName,
253252
initialAttributes: telemetry.spanAttributes,
254-
spanKind: SpanKind.internal,
255-
parentContext: telemetryContext)
253+
spanKind: .internal,
254+
parentContext: telemetryContext
255+
)
256256
defer {
257257
span.end()
258258
}
@@ -323,7 +323,8 @@ public class CRTClientEngine: HTTPClient {
323323
telemetry.connectionsUptime.record(
324324
value: Date().timeIntervalSinceReferenceDate - connectionUptimeStart,
325325
attributes: Attributes(),
326-
context: telemetryContext)
326+
context: telemetryContext
327+
)
327328
}
328329
// TICK - smithy.client.http.bytes_sent
329330
try await stream.write(

Sources/ClientRuntime/Networking/Http/URLSession/URLSessionHTTPClient.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -463,8 +463,8 @@ public final class URLSessionHTTPClient: HTTPClient {
463463
public func send(request: HTTPRequest) async throws -> HTTPResponse {
464464
let telemetryContext = telemetry.contextManager.current()
465465
let tracer = telemetry.tracerProvider.getTracer(
466-
scope: telemetry.tracerScope,
467-
attributes: telemetry.tracerAttributes)
466+
scope: telemetry.tracerScope
467+
)
468468
do {
469469
// START - smithy.client.http.requests.queued_duration
470470
let queuedStart = Date().timeIntervalSinceReferenceDate

Sources/ClientRuntime/Orchestrator/Orchestrator.swift

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,8 @@ public struct Orchestrator<
175175
public func execute(input: InputType) async throws -> OutputType {
176176
let telemetryContext = telemetry.contextManager.current()
177177
let tracer = telemetry.tracerProvider.getTracer(
178-
scope: telemetry.tracerScope,
179-
attributes: telemetry.tracerAttributes)
178+
scope: telemetry.tracerScope
179+
)
180180

181181
// DURATION - smithy.client.call.duration
182182
do {
@@ -309,14 +309,15 @@ public struct Orchestrator<
309309
// with the thrown error in context.result
310310
let telemetryContext = telemetry.contextManager.current()
311311
let tracer = telemetry.tracerProvider.getTracer(
312-
scope: telemetry.tracerScope,
313-
attributes: telemetry.tracerAttributes)
312+
scope: telemetry.tracerScope
313+
)
314314

315315
// TICK - smithy.client.call.attempts
316316
telemetry.rpcAttempts.add(
317317
value: 1,
318318
attributes: telemetry.metricsAttributes,
319-
context: telemetryContext)
319+
context: telemetryContext
320+
)
320321

321322
// DURATION - smithy.client.call.attempt_duration
322323
do {
@@ -349,7 +350,8 @@ public struct Orchestrator<
349350
telemetry.resolveIdentityDuration.record(
350351
value: Date().timeIntervalSinceReferenceDate - identityStart,
351352
attributes: authSchemeAttributes,
352-
context: telemetryContext)
353+
context: telemetryContext
354+
)
353355
// END - smithy.client.call.auth.resolve_identity_duration
354356

355357
// START - smithy.client.call.resolve_endpoint_duration
@@ -362,7 +364,8 @@ public struct Orchestrator<
362364
telemetry.resolveEndpointDuration.record(
363365
value: Date().timeIntervalSinceReferenceDate - endpointStart,
364366
attributes: telemetry.metricsAttributes,
365-
context: telemetryContext)
367+
context: telemetryContext
368+
)
366369
// END - smithy.client.call.resolve_endpoint_duration
367370

368371
context.updateRequest(updated: withEndpoint)
@@ -380,7 +383,8 @@ public struct Orchestrator<
380383
telemetry.signingDuration.record(
381384
value: Date().timeIntervalSinceReferenceDate - signingStart,
382385
attributes: authSchemeAttributes,
383-
context: telemetryContext)
386+
context: telemetryContext
387+
)
384388
// END - smithy.client.call.auth.signing_duration
385389

386390
context.updateRequest(updated: signed)
@@ -405,7 +409,8 @@ public struct Orchestrator<
405409
telemetry.deserializationDuration.record(
406410
value: Date().timeIntervalSinceReferenceDate - deserializeStart,
407411
attributes: telemetry.metricsAttributes,
408-
context: telemetryContext)
412+
context: telemetryContext
413+
)
409414
// END - smithy.client.call.deserialization_duration
410415
context.updateOutput(updated: output)
411416

Sources/ClientRuntime/Telemetry/DefaultTelemetry.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ extension DefaultTelemetry {
146146
fileprivate static let defaultTraceSpan: TraceSpan = NoOpTraceSpan()
147147

148148
fileprivate final class NoOpTracerProvider: TracerProvider {
149-
func getTracer(scope: String, attributes: Attributes?) -> Tracer { defaultTracer }
149+
func getTracer(scope: String) -> Tracer { defaultTracer }
150150
}
151151

152152
fileprivate final class NoOpTracer: Tracer {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
9+
import Foundation
10+
11+
// OpenTelemetrySdk specific imports
12+
@preconcurrency import protocol OpenTelemetrySdk.SpanExporter
13+
14+
/// Namespace for the SDK Telemetry implementation.
15+
public enum OpenTelemetrySwift {
16+
/// The SDK TelemetryProviderOTel Implementation.
17+
///
18+
/// - contextManager: no-op
19+
/// - loggerProvider: provides SwiftLoggers
20+
/// - meterProvider: no-op
21+
/// - tracerProvider: provides OTelTracerProvider with InMemoryExporter
22+
public static func provider(spanExporter: any SpanExporter) -> TelemetryProvider {
23+
return OpenTelemetrySwiftProvider(spanExporter: spanExporter)
24+
}
25+
26+
public final class OpenTelemetrySwiftProvider: TelemetryProvider {
27+
public let contextManager: TelemetryContextManager
28+
public let loggerProvider: LoggerProvider
29+
public let meterProvider: MeterProvider
30+
public let tracerProvider: TracerProvider
31+
32+
public init(spanExporter: SpanExporter) {
33+
self.contextManager = DefaultTelemetry.defaultContextManager
34+
self.loggerProvider = DefaultTelemetry.defaultLoggerProvider
35+
self.meterProvider = DefaultTelemetry.defaultMeterProvider
36+
self.tracerProvider = OTelTracerProvider(spanExporter: spanExporter)
37+
}
38+
}
39+
}
40+
#endif
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
9+
// OpenTelemetryApi specific imports
10+
@preconcurrency import protocol OpenTelemetryApi.Tracer
11+
@preconcurrency import protocol OpenTelemetryApi.Span
12+
@preconcurrency import enum OpenTelemetryApi.SpanKind
13+
@preconcurrency import enum OpenTelemetryApi.Status
14+
@preconcurrency import enum OpenTelemetryApi.AttributeValue
15+
16+
// OpenTelemetrySdk specific imports
17+
@preconcurrency import class OpenTelemetrySdk.TracerProviderSdk
18+
@preconcurrency import class OpenTelemetrySdk.TracerProviderBuilder
19+
@preconcurrency import struct OpenTelemetrySdk.SimpleSpanProcessor
20+
@preconcurrency import protocol OpenTelemetrySdk.SpanExporter
21+
@preconcurrency import struct OpenTelemetrySdk.Resource
22+
23+
// Smithy specific imports
24+
import struct Smithy.AttributeKey
25+
import struct Smithy.Attributes
26+
27+
public typealias OpenTelemetryTracer = OpenTelemetryApi.Tracer
28+
public typealias OpenTelemetrySpanKind = OpenTelemetryApi.SpanKind
29+
public typealias OpenTelemetrySpan = OpenTelemetryApi.Span
30+
public typealias OpenTelemetryStatus = OpenTelemetryApi.Status
31+
32+
// Trace
33+
public final class OTelTracerProvider: TracerProvider {
34+
private let sdkTracerProvider: TracerProviderSdk
35+
36+
public init(spanExporter: SpanExporter) {
37+
self.sdkTracerProvider = TracerProviderBuilder()
38+
.add(spanProcessor: SimpleSpanProcessor(spanExporter: spanExporter))
39+
.with(resource: Resource())
40+
.build()
41+
}
42+
43+
public func getTracer(scope: String) -> any Tracer {
44+
let tracer = self.sdkTracerProvider.get(instrumentationName: scope)
45+
return OTelTracerImpl(otelTracer: tracer)
46+
}
47+
}
48+
49+
public final class OTelTracerImpl: Tracer {
50+
private let otelTracer: OpenTelemetryTracer
51+
52+
public init(otelTracer: OpenTelemetryTracer) {
53+
self.otelTracer = otelTracer
54+
}
55+
56+
public func createSpan(
57+
name: String,
58+
initialAttributes: Attributes?, spanKind: SpanKind, parentContext: (any TelemetryContext)?
59+
) -> any TraceSpan {
60+
let spanBuilder = self.otelTracer
61+
.spanBuilder(spanName: name)
62+
.setSpanKind(spanKind: spanKind.toOTelSpanKind())
63+
64+
initialAttributes?.getKeys().forEach { key in
65+
spanBuilder.setAttribute(
66+
key: key,
67+
value: (initialAttributes?.get(key: AttributeKey<String>(name: key)))!
68+
)
69+
}
70+
71+
return OTelTraceSpanImpl(name: name, otelSpan: spanBuilder.startSpan())
72+
}
73+
}
74+
75+
private final class OTelTraceSpanImpl: TraceSpan {
76+
let name: String
77+
private let otelSpan: OpenTelemetrySpan
78+
79+
public init(name: String, otelSpan: OpenTelemetrySpan) {
80+
self.name = name
81+
self.otelSpan = otelSpan
82+
}
83+
84+
func emitEvent(name: String, attributes: Attributes?) {
85+
if let attributes = attributes, !(attributes.size == 0) {
86+
self.otelSpan.addEvent(name: name, attributes: attributes.toOtelAttributes())
87+
} else {
88+
self.otelSpan.addEvent(name: name)
89+
}
90+
}
91+
92+
func setAttribute<T>(key: AttributeKey<T>, value: T) {
93+
self.otelSpan.setAttribute(key: key.getName(), value: AttributeValue.init(value))
94+
}
95+
96+
func setStatus(status: TraceSpanStatus) {
97+
self.otelSpan.status = status.toOTelStatus()
98+
}
99+
100+
func end() {
101+
self.otelSpan.end()
102+
}
103+
}
104+
105+
extension SpanKind {
106+
func toOTelSpanKind() -> OpenTelemetrySpanKind {
107+
switch self {
108+
case .client:
109+
return .client
110+
case .consumer:
111+
return .consumer
112+
case .internal:
113+
return .internal
114+
case .producer:
115+
return .producer
116+
case .server:
117+
return .server
118+
}
119+
}
120+
}
121+
122+
extension TraceSpanStatus {
123+
func toOTelStatus() -> OpenTelemetryStatus {
124+
switch self {
125+
case .error:
126+
return .error(description: "An error occured!") // status doesn't have error description
127+
case .ok:
128+
return .ok
129+
case .unset:
130+
return .unset
131+
}
132+
}
133+
}
134+
#endif

0 commit comments

Comments
 (0)