diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53fc04c --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/Package.resolved +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..df8e8c4 --- /dev/null +++ b/Package.swift @@ -0,0 +1,34 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "smithy-swift-opentelemetry", + platforms: [ + .macOS(.v12), + .iOS(.v16), + .tvOS(.v16), + .watchOS(.v9), + ], + products: [ + .library(name: "SmithyOpenTelemetry", targets: ["SmithyOpenTelemetry"]), + ], + dependencies: [ + .package(url: "https://github.com/smithy-lang/smithy-swift", from: "0.153.0"), + .package(url: "https://github.com/open-telemetry/opentelemetry-swift", from: "1.13.0"), + ], + targets: [ + .target( + name: "SmithyOpenTelemetry", + dependencies: [ + .product(name: "Smithy", package: "smithy-swift"), + .product(name: "ClientRuntime", package: "smithy-swift"), + .product(name: "OpenTelemetrySdk", package: "opentelemetry-swift"), + ] + ), + .testTarget( + name: "SmithyOpenTelemetryTests", + dependencies: ["SmithyOpenTelemetry"] + ), + ] +) diff --git a/Sources/SmithyOpenTelemetry/OTelProvider.swift b/Sources/SmithyOpenTelemetry/OTelProvider.swift new file mode 100644 index 0000000..88e778d --- /dev/null +++ b/Sources/SmithyOpenTelemetry/OTelProvider.swift @@ -0,0 +1,46 @@ +// +// 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 +import protocol ClientRuntime.TelemetryProvider +import protocol ClientRuntime.TelemetryContextManager +import protocol ClientRuntime.LoggerProvider +import protocol ClientRuntime.MeterProvider +import protocol ClientRuntime.TracerProvider +import enum ClientRuntime.DefaultTelemetry + +// 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 diff --git a/Sources/SmithyOpenTelemetry/OTelTracing.swift b/Sources/SmithyOpenTelemetry/OTelTracing.swift new file mode 100644 index 0000000..5663c65 --- /dev/null +++ b/Sources/SmithyOpenTelemetry/OTelTracing.swift @@ -0,0 +1,140 @@ +// +// 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 +import enum ClientRuntime.SpanKind +import protocol ClientRuntime.TelemetryContext +import protocol ClientRuntime.TracerProvider +import protocol ClientRuntime.TraceSpan +import protocol ClientRuntime.Tracer +import enum ClientRuntime.TraceSpanStatus + +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: ClientRuntime.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 ClientRuntime.Tracer { + let tracer = self.sdkTracerProvider.get(instrumentationName: scope) + return OTelTracerImpl(otelTracer: tracer) + } +} + +public final class OTelTracerImpl: ClientRuntime.Tracer { + private let otelTracer: OpenTelemetryTracer + + public init(otelTracer: OpenTelemetryTracer) { + self.otelTracer = otelTracer + } + + public func createSpan( + name: String, + initialAttributes: Attributes?, spanKind: ClientRuntime.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 ClientRuntime.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/SmithyOpenTelemetry/OTelUtils.swift b/Sources/SmithyOpenTelemetry/OTelUtils.swift new file mode 100644 index 0000000..1246d24 --- /dev/null +++ b/Sources/SmithyOpenTelemetry/OTelUtils.swift @@ -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(name: key)) { + otelKeys[key] = AttributeValue.string(stringValue) + } else if let intValue = self.get(key: AttributeKey(name: key)) { + otelKeys[key] = AttributeValue.int(intValue) + } else if let doubleValue = self.get(key: AttributeKey(name: key)) { + otelKeys[key] = AttributeValue.double(doubleValue) + } else if let boolValue = self.get(key: AttributeKey(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 diff --git a/Tests/SmithyOpenTelemetryTests/SmithyOpenTelemetryTests.swift b/Tests/SmithyOpenTelemetryTests/SmithyOpenTelemetryTests.swift new file mode 100644 index 0000000..b1bdeaf --- /dev/null +++ b/Tests/SmithyOpenTelemetryTests/SmithyOpenTelemetryTests.swift @@ -0,0 +1,6 @@ +import Testing +@testable import SmithyOpenTelemetry + +@Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. +}