Skip to content

feat: Add OTel-Swift supported Tracing #910

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Jun 25, 2025
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9f8f532
feat: Add OTel-Swift supported Tracing
dayaffe Feb 20, 2025
e34b38e
raise minimum macOS to v12
dayaffe Feb 20, 2025
0652e6c
Merge branch 'main' into day/implement-tracing-otel
dayaffe Feb 20, 2025
c738d11
add import for OpenTelemetryConcurrency for linux support
dayaffe Feb 20, 2025
fdf23b7
add linux and visionOS #if
dayaffe Feb 21, 2025
cbca5f8
only add opentelemetry on non-linux and non-vision os
dayaffe Feb 21, 2025
1266afa
Merge branch 'main' into day/implement-tracing-otel
dayaffe May 21, 2025
d315e22
Merge branch 'main' into day/implement-tracing-otel
dayaffe Jun 18, 2025
8e1d756
Merge branch 'main' into day/implement-tracing-otel
dayaffe Jun 19, 2025
c0d6252
only add telemetry dependency if correct os
dayaffe Jun 20, 2025
27982f8
fix client runtime dependencies
dayaffe Jun 20, 2025
39969cf
try again
dayaffe Jun 20, 2025
07a1320
revert previous bad change
dayaffe Jun 20, 2025
05e39a9
add preconcurrency flag
dayaffe Jun 20, 2025
014ed05
Merge branch 'main' into day/implement-tracing-otel
dayaffe Jun 20, 2025
3311c02
try making swift 6 compatible
dayaffe Jun 20, 2025
914fa3b
reduce version to previous one we were using
dayaffe Jun 20, 2025
e72332f
add back preconcurrency
dayaffe Jun 20, 2025
45f6a89
another try at concurrency
dayaffe Jun 20, 2025
2be51b5
address comments
dayaffe Jun 20, 2025
e7a12d0
fix failures
dayaffe Jun 20, 2025
6d3cdea
remove extra comma
dayaffe Jun 20, 2025
95cb3b2
more fixes
dayaffe Jun 20, 2025
093a18f
Merge branch 'main' into day/implement-tracing-otel
dayaffe Jun 20, 2025
891c9ee
Merge branch 'main' into day/implement-tracing-otel
dayaffe Jun 24, 2025
0167e76
Merge branch 'main' into day/implement-tracing-otel
dayaffe Jun 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -58,7 +58,9 @@ let package = Package(
var dependencies: [Package.Dependency] = [
.package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.52.1"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
.package(url: "https://github.com/open-telemetry/opentelemetry-swift", from: "1.13.0"),
]

let isDocCEnabled = ProcessInfo.processInfo.environment["AWS_SWIFT_SDK_ENABLE_DOCC"] != nil
if isDocCEnabled {
dependencies.append(.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"))
@@ -95,6 +97,27 @@ let package = Package(
"SmithyChecksums",
"SmithyCBOR",
.product(name: "AwsCommonRuntimeKit", package: "aws-crt-swift"),
// Only include these on macOS, iOS, tvOS, and watchOS (visionOS and Linux are excluded)
.product(
name: "InMemoryExporter",
package: "opentelemetry-swift",
condition: .when(platforms: [.macOS, .iOS, .tvOS, .watchOS])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is because OTel-Swift is currently not working on non-Apple platforms?

),
.product(
name: "OpenTelemetryApi",
package: "opentelemetry-swift",
condition: .when(platforms: [.macOS, .iOS, .tvOS, .watchOS])
),
.product(
name: "OpenTelemetrySdk",
package: "opentelemetry-swift",
condition: .when(platforms: [.macOS, .iOS, .tvOS, .watchOS])
),
.product(
name: "OpenTelemetryProtocolExporterHTTP",
package: "opentelemetry-swift",
condition: .when(platforms: [.macOS, .iOS, .tvOS, .watchOS])
),
],
resources: [
.copy("PrivacyInfo.xcprivacy")
15 changes: 8 additions & 7 deletions Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift
Original file line number Diff line number Diff line change
@@ -188,8 +188,7 @@ public class CRTClientEngine: HTTPClient {
public func send(request: HTTPRequest) async throws -> HTTPResponse {
let telemetryContext = telemetry.contextManager.current()
let tracer = telemetry.tracerProvider.getTracer(
scope: telemetry.tracerScope,
attributes: telemetry.tracerAttributes
scope: telemetry.tracerScope
)
let queuedStart = Date().timeIntervalSinceReferenceDate
let span = tracer.createSpan(
@@ -243,16 +242,17 @@ public class CRTClientEngine: HTTPClient {
func executeHTTP2Request(request: HTTPRequest) async throws -> HTTPResponse {
let telemetryContext = telemetry.contextManager.current()
let tracer = telemetry.tracerProvider.getTracer(
scope: telemetry.tracerScope,
attributes: telemetry.tracerAttributes)
scope: telemetry.tracerScope
)
do {
// START - smithy.client.http.requests.queued_duration
let queuedStart = Date().timeIntervalSinceReferenceDate
let span = tracer.createSpan(
name: telemetry.spanName,
initialAttributes: telemetry.spanAttributes,
spanKind: SpanKind.internal,
parentContext: telemetryContext)
spanKind: .internal,
parentContext: telemetryContext
)
defer {
span.end()
}
@@ -323,7 +323,8 @@ public class CRTClientEngine: HTTPClient {
telemetry.connectionsUptime.record(
value: Date().timeIntervalSinceReferenceDate - connectionUptimeStart,
attributes: Attributes(),
context: telemetryContext)
context: telemetryContext
)
}
// TICK - smithy.client.http.bytes_sent
try await stream.write(
Original file line number Diff line number Diff line change
@@ -463,8 +463,8 @@ public final class URLSessionHTTPClient: HTTPClient {
public func send(request: HTTPRequest) async throws -> HTTPResponse {
let telemetryContext = telemetry.contextManager.current()
let tracer = telemetry.tracerProvider.getTracer(
scope: telemetry.tracerScope,
attributes: telemetry.tracerAttributes)
scope: telemetry.tracerScope
)
do {
// START - smithy.client.http.requests.queued_duration
let queuedStart = Date().timeIntervalSinceReferenceDate
23 changes: 14 additions & 9 deletions Sources/ClientRuntime/Orchestrator/Orchestrator.swift
Original file line number Diff line number Diff line change
@@ -175,8 +175,8 @@ public struct Orchestrator<
public func execute(input: InputType) async throws -> OutputType {
let telemetryContext = telemetry.contextManager.current()
let tracer = telemetry.tracerProvider.getTracer(
scope: telemetry.tracerScope,
attributes: telemetry.tracerAttributes)
scope: telemetry.tracerScope
)

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

// TICK - smithy.client.call.attempts
telemetry.rpcAttempts.add(
value: 1,
attributes: telemetry.metricsAttributes,
context: telemetryContext)
context: telemetryContext
)

// DURATION - smithy.client.call.attempt_duration
do {
@@ -349,7 +350,8 @@ public struct Orchestrator<
telemetry.resolveIdentityDuration.record(
value: Date().timeIntervalSinceReferenceDate - identityStart,
attributes: authSchemeAttributes,
context: telemetryContext)
context: telemetryContext
)
// END - smithy.client.call.auth.resolve_identity_duration

// START - smithy.client.call.resolve_endpoint_duration
@@ -362,7 +364,8 @@ public struct Orchestrator<
telemetry.resolveEndpointDuration.record(
value: Date().timeIntervalSinceReferenceDate - endpointStart,
attributes: telemetry.metricsAttributes,
context: telemetryContext)
context: telemetryContext
)
// END - smithy.client.call.resolve_endpoint_duration

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

context.updateRequest(updated: signed)
@@ -405,7 +409,8 @@ public struct Orchestrator<
telemetry.deserializationDuration.record(
value: Date().timeIntervalSinceReferenceDate - deserializeStart,
attributes: telemetry.metricsAttributes,
context: telemetryContext)
context: telemetryContext
)
// END - smithy.client.call.deserialization_duration
context.updateOutput(updated: output)

2 changes: 1 addition & 1 deletion Sources/ClientRuntime/Telemetry/DefaultTelemetry.swift
Original file line number Diff line number Diff line change
@@ -146,7 +146,7 @@ extension DefaultTelemetry {
fileprivate static let defaultTraceSpan: TraceSpan = NoOpTraceSpan()

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

fileprivate final class NoOpTracer: Tracer {
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
import Foundation

// OpenTelemetrySdk specific imports
@preconcurrency import protocol OpenTelemetrySdk.SpanExporter

/// Namespace for the SDK Telemetry implementation.
public enum OpenTelemetrySwift {
/// The SDK TelemetryProviderOTel Implementation.
///
/// - contextManager: no-op
/// - loggerProvider: provides SwiftLoggers
/// - meterProvider: no-op
/// - tracerProvider: provides OTelTracerProvider with InMemoryExporter
public static func provider(spanExporter: any SpanExporter) -> TelemetryProvider {
return OpenTelemetrySwiftProvider(spanExporter: spanExporter)
}

public final class OpenTelemetrySwiftProvider: TelemetryProvider {
public let contextManager: TelemetryContextManager
public let loggerProvider: LoggerProvider
public let meterProvider: MeterProvider
public let tracerProvider: TracerProvider

public init(spanExporter: SpanExporter) {
self.contextManager = DefaultTelemetry.defaultContextManager
self.loggerProvider = DefaultTelemetry.defaultLoggerProvider
self.meterProvider = DefaultTelemetry.defaultMeterProvider
self.tracerProvider = OTelTracerProvider(spanExporter: spanExporter)
}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
// OpenTelemetryApi specific imports
@preconcurrency import protocol OpenTelemetryApi.Tracer
@preconcurrency import protocol OpenTelemetryApi.Span
@preconcurrency import enum OpenTelemetryApi.SpanKind
@preconcurrency import enum OpenTelemetryApi.Status
@preconcurrency import enum OpenTelemetryApi.AttributeValue

// OpenTelemetrySdk specific imports
@preconcurrency import class OpenTelemetrySdk.TracerProviderSdk
@preconcurrency import class OpenTelemetrySdk.TracerProviderBuilder
@preconcurrency import struct OpenTelemetrySdk.SimpleSpanProcessor
@preconcurrency import protocol OpenTelemetrySdk.SpanExporter
@preconcurrency import struct OpenTelemetrySdk.Resource

// Smithy specific imports
import struct Smithy.AttributeKey
import struct Smithy.Attributes

public typealias OpenTelemetryTracer = OpenTelemetryApi.Tracer
public typealias OpenTelemetrySpanKind = OpenTelemetryApi.SpanKind
public typealias OpenTelemetrySpan = OpenTelemetryApi.Span
public typealias OpenTelemetryStatus = OpenTelemetryApi.Status

// Trace
public final class OTelTracerProvider: TracerProvider {
private let sdkTracerProvider: TracerProviderSdk

public init(spanExporter: SpanExporter) {
self.sdkTracerProvider = TracerProviderBuilder()
.add(spanProcessor: SimpleSpanProcessor(spanExporter: spanExporter))
.with(resource: Resource())
.build()
}

public func getTracer(scope: String) -> any Tracer {
let tracer = self.sdkTracerProvider.get(instrumentationName: scope)
return OTelTracerImpl(otelTracer: tracer)
}
}

public final class OTelTracerImpl: Tracer {
private let otelTracer: OpenTelemetryTracer

public init(otelTracer: OpenTelemetryTracer) {
self.otelTracer = otelTracer
}

public func createSpan(
name: String,
initialAttributes: Attributes?, spanKind: SpanKind, parentContext: (any TelemetryContext)?
) -> any TraceSpan {
let spanBuilder = self.otelTracer
.spanBuilder(spanName: name)
.setSpanKind(spanKind: spanKind.toOTelSpanKind())

initialAttributes?.getKeys().forEach { key in
spanBuilder.setAttribute(
key: key,
value: (initialAttributes?.get(key: AttributeKey<String>(name: key)))!
)
}

return OTelTraceSpanImpl(name: name, otelSpan: spanBuilder.startSpan())
}
}

private final class OTelTraceSpanImpl: TraceSpan {
let name: String
private let otelSpan: OpenTelemetrySpan

public init(name: String, otelSpan: OpenTelemetrySpan) {
self.name = name
self.otelSpan = otelSpan
}

func emitEvent(name: String, attributes: Attributes?) {
if let attributes = attributes, !(attributes.size == 0) {
self.otelSpan.addEvent(name: name, attributes: attributes.toOtelAttributes())
} else {
self.otelSpan.addEvent(name: name)
}
}

func setAttribute<T>(key: AttributeKey<T>, value: T) {
self.otelSpan.setAttribute(key: key.getName(), value: AttributeValue.init(value))
}

func setStatus(status: TraceSpanStatus) {
self.otelSpan.status = status.toOTelStatus()
}

func end() {
self.otelSpan.end()
}
}

extension SpanKind {
func toOTelSpanKind() -> OpenTelemetrySpanKind {
switch self {
case .client:
return .client
case .consumer:
return .consumer
case .internal:
return .internal
case .producer:
return .producer
case .server:
return .server
}
}
}

extension TraceSpanStatus {
func toOTelStatus() -> OpenTelemetryStatus {
switch self {
case .error:
return .error(description: "An error occured!") // status doesn't have error description
case .ok:
return .ok
case .unset:
return .unset
}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
// OpenTelemetryApi specific imports
@preconcurrency import enum OpenTelemetryApi.AttributeValue
@preconcurrency import class OpenTelemetryApi.AttributeArray

// Smithy imports
import struct Smithy.Attributes
import struct Smithy.AttributeKey

extension Attributes {
public func toOtelAttributes() -> [String: AttributeValue] {
let keys: [String] = self.getKeys()
var otelKeys: [String: AttributeValue] = [:]

guard !keys.isEmpty else {
return [:]
}

keys.forEach { key in
// Try to get the value as different types
if let stringValue = self.get(key: AttributeKey<String>(name: key)) {
otelKeys[key] = AttributeValue.string(stringValue)
} else if let intValue = self.get(key: AttributeKey<Int>(name: key)) {
otelKeys[key] = AttributeValue.int(intValue)
} else if let doubleValue = self.get(key: AttributeKey<Double>(name: key)) {
otelKeys[key] = AttributeValue.double(doubleValue)
} else if let boolValue = self.get(key: AttributeKey<Bool>(name: key)) {
otelKeys[key] = AttributeValue.bool(boolValue)
} else if let arrayValue = self.get(key: AttributeKey<[String]>(name: key)) {
let attributeArray = arrayValue.map { AttributeValue.string($0) }
otelKeys[key] = AttributeValue.array(AttributeArray(values: attributeArray))
} else if let arrayValue = self.get(key: AttributeKey<[Int]>(name: key)) {
let attributeArray = arrayValue.map { AttributeValue.int($0) }
otelKeys[key] = AttributeValue.array(AttributeArray(values: attributeArray))
} else if let arrayValue = self.get(key: AttributeKey<[Double]>(name: key)) {
let attributeArray = arrayValue.map { AttributeValue.double($0) }
otelKeys[key] = AttributeValue.array(AttributeArray(values: attributeArray))
} else if let arrayValue = self.get(key: AttributeKey<[Bool]>(name: key)) {
let attributeArray = arrayValue.map { AttributeValue.bool($0) }
otelKeys[key] = AttributeValue.array(AttributeArray(values: attributeArray))
}
// If none of the above types match, the value is skipped
}

return otelKeys
}
}
#endif
3 changes: 1 addition & 2 deletions Sources/ClientRuntime/Telemetry/Tracing/TracerProvider.swift
Original file line number Diff line number Diff line change
@@ -13,7 +13,6 @@ public protocol TracerProvider: Sendable {
/// Gets a scoped Tracer.
///
/// - Parameter scope: the unique scope of the Tracer
/// - Parameter attributes: instrumentation scope attributes
/// - Returns: a Tracer
func getTracer(scope: String, attributes: Attributes?) -> Tracer
func getTracer(scope: String) -> Tracer
}
8 changes: 8 additions & 0 deletions Sources/Smithy/Attribute.swift
Original file line number Diff line number Diff line change
@@ -13,6 +13,10 @@ public struct AttributeKey<ValueType>: Sendable {
self.name = name
}

public func getName() -> String {
return self.name
}

func toString() -> String {
return "AttributeKey: \(name)"
}
@@ -33,6 +37,10 @@ public struct Attributes: Sendable {
get(key: key) != nil
}

public func getKeys() -> [String] {
return Array(self.attributes.keys)
}

public mutating func set<T: Sendable>(key: AttributeKey<T>, value: T?) {
attributes[key.name] = value
}