Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.DS_Store
/Package.resolved
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
34 changes: 34 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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"]
),
]
)
46 changes: 46 additions & 0 deletions Sources/SmithyOpenTelemetry/OTelProvider.swift
Original file line number Diff line number Diff line change
@@ -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
140 changes: 140 additions & 0 deletions Sources/SmithyOpenTelemetry/OTelTracing.swift
Original file line number Diff line number Diff line change
@@ -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<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 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
55 changes: 55 additions & 0 deletions Sources/SmithyOpenTelemetry/OTelUtils.swift
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
6 changes: 6 additions & 0 deletions Tests/SmithyOpenTelemetryTests/SmithyOpenTelemetryTests.swift
Original file line number Diff line number Diff line change
@@ -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.
}