diff --git a/Package.swift b/Package.swift index 1a3f5b592..802fa949c 100644 --- a/Package.swift +++ b/Package.swift @@ -23,7 +23,7 @@ let libXML2TargetOrNil: Target? = nil let package = Package( name: "smithy-swift", platforms: [ - .macOS(.v10_15), + .macOS(.v12), .iOS(.v13), .tvOS(.v13), .watchOS(.v6) @@ -58,6 +58,7 @@ let package = Package( var dependencies: [Package.Dependency] = [ .package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.45.0"), .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 { @@ -95,6 +96,23 @@ let package = Package( "SmithyChecksums", "SmithyCBOR", .product(name: "AwsCommonRuntimeKit", package: "aws-crt-swift"), + .product(name: "InMemoryExporter", package: "opentelemetry-swift"), + // Only include these on macOS, iOS, tvOS, and watchOS (visionOS and Linux are excluded) + .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") diff --git a/Sources/ClientRuntime/Telemetry/Providers/OpenTelemetry/OTelProvider.swift b/Sources/ClientRuntime/Telemetry/Providers/OpenTelemetry/OTelProvider.swift new file mode 100644 index 000000000..14dbe701f --- /dev/null +++ b/Sources/ClientRuntime/Telemetry/Providers/OpenTelemetry/OTelProvider.swift @@ -0,0 +1,41 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +#if !(os(Linux) || os(visionOS)) +import OpenTelemetryApi +import OpenTelemetrySdk +import Smithy +import OpenTelemetryProtocolExporterHttp +import Foundation + +/// 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 diff --git a/Sources/ClientRuntime/Telemetry/Providers/OpenTelemetry/OTelTracing.swift b/Sources/ClientRuntime/Telemetry/Providers/OpenTelemetry/OTelTracing.swift new file mode 100644 index 000000000..58aede021 --- /dev/null +++ b/Sources/ClientRuntime/Telemetry/Providers/OpenTelemetry/OTelTracing.swift @@ -0,0 +1,120 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +#if !(os(Linux) || os(visionOS)) +@preconcurrency import OpenTelemetryApi +@preconcurrency import OpenTelemetrySdk +import Smithy + +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, attributes: Attributes?) -> 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(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(key: AttributeKey, 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 diff --git a/Sources/ClientRuntime/Telemetry/Providers/OpenTelemetry/OTelUtils.swift b/Sources/ClientRuntime/Telemetry/Providers/OpenTelemetry/OTelUtils.swift new file mode 100644 index 000000000..5acc802b4 --- /dev/null +++ b/Sources/ClientRuntime/Telemetry/Providers/OpenTelemetry/OTelUtils.swift @@ -0,0 +1,29 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +#if !(os(Linux) || os(visionOS)) +import OpenTelemetryApi + +import Smithy + +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 + otelKeys[key] = AttributeValue(self.get(key: AttributeKey(name: key))!) + } + + return otelKeys + } +} +#endif diff --git a/Sources/Smithy/Attribute.swift b/Sources/Smithy/Attribute.swift index 42cb8ea59..d6a11b0c5 100644 --- a/Sources/Smithy/Attribute.swift +++ b/Sources/Smithy/Attribute.swift @@ -13,6 +13,10 @@ public struct AttributeKey: Sendable { self.name = name } + public func getName() -> String { + return self.name + } + func toString() -> String { return "AttributeKey: \(name)" } @@ -33,6 +37,10 @@ public struct Attributes { get(key: key) != nil } + public func getKeys() -> [String] { + return Array(self.attributes.keys) + } + public mutating func set(key: AttributeKey, value: T?) { attributes[key.name] = value } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/PackageManifestGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/PackageManifestGenerator.kt index 714824402..221aee6f4 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/PackageManifestGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/PackageManifestGenerator.kt @@ -24,7 +24,7 @@ class PackageManifestGenerator( writer.write("name: \$S,", ctx.settings.moduleName) writer.openBlock("platforms: [", "],") { - writer.write(".macOS(.v10_15), .iOS(.v13)") + writer.write(".macOS(.v12), .iOS(.v13)") } writer.openBlock("products: [", "],") { diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/manifestanddocs/PackageManifestGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/manifestanddocs/PackageManifestGeneratorTests.kt index 5f786be36..72b115673 100644 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/manifestanddocs/PackageManifestGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/manifestanddocs/PackageManifestGeneratorTests.kt @@ -30,7 +30,7 @@ class PackageManifestGeneratorTests { assertNotNull(packageManifest) val expected = """ platforms: [ - .macOS(.v10_15), .iOS(.v13) + .macOS(.v12), .iOS(.v13) ], """ packageManifest.shouldContain(expected)