diff --git a/Examples/Logging Tracer/main.swift b/Examples/Logging Tracer/main.swift index 9b05d8def..0e377cbe6 100644 --- a/Examples/Logging Tracer/main.swift +++ b/Examples/Logging Tracer/main.swift @@ -10,12 +10,12 @@ Logger.printHeader() OpenTelemetry.registerTracerProvider(tracerProvider: LoggingTracerProvider()) -var tracer = OpenTelemetry.instance.tracerProvider.get(instrumentationName: "ConsoleApp", instrumentationVersion: "semver:1.0.0") - +let tracer = OpenTelemetry.instance.tracerProvider.get(instrumentationName: "ConsoleApp", instrumentationVersion: "semver:1.0.0") let span1 = tracer.spanBuilder(spanName: "Main (span1)").startSpan() OpenTelemetry.instance.contextProvider.setActiveSpan(span1) let semaphore = DispatchSemaphore(value: 0) DispatchQueue.global().async { + let tracer = OpenTelemetry.instance.tracerProvider.get(instrumentationName: "ConsoleApp", instrumentationVersion: "semver:1.0.0") let span2 = tracer.spanBuilder(spanName: "Main (span2)").startSpan() OpenTelemetry.instance.contextProvider.setActiveSpan(span2) OpenTelemetry.instance.contextProvider.activeSpan?.setAttribute(key: "myAttribute", value: "myValue") @@ -26,4 +26,4 @@ DispatchQueue.global().async { span1.end() -semaphore.wait() +semaphore.wait() \ No newline at end of file diff --git a/Examples/Network Sample/main.swift b/Examples/Network Sample/main.swift index 4a8e8136b..c8cf25ac7 100644 --- a/Examples/Network Sample/main.swift +++ b/Examples/Network Sample/main.swift @@ -26,9 +26,14 @@ func simpleNetworkCall() { semaphore.wait() } -class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionTaskDelegate { +final class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionTaskDelegate, @unchecked Sendable { let semaphore = DispatchSemaphore(value: 0) - var callCount = 0 + private let queue = DispatchQueue(label: "callCount") + private var _callCount = 0 + var callCount: Int { + get { queue.sync { _callCount } } + set { queue.sync { _callCount = newValue } } + } func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { semaphore.signal() diff --git a/Examples/OTLP Exporter/main.swift b/Examples/OTLP Exporter/main.swift index c6a9f4d59..0ee140ab1 100644 --- a/Examples/OTLP Exporter/main.swift +++ b/Examples/OTLP Exporter/main.swift @@ -50,7 +50,7 @@ tracerProviderSDK?.addSpanProcessor(SignPostIntegration()) } - func createSpans() { + @MainActor func createSpans() { let parentSpan1 = tracer.spanBuilder(spanName: "Main").setSpanKind(spanKind: .client).startSpan() parentSpan1.setAttribute(key: sampleKey, value: sampleValue) OpenTelemetry.instance.contextProvider.setActiveSpan(parentSpan1) @@ -71,7 +71,7 @@ parentSpan1.end() } - func doWork() { + @MainActor func doWork() { let childSpan = tracer.spanBuilder(spanName: "doWork").setSpanKind(spanKind: .client).startSpan() childSpan.setAttribute(key: sampleKey, value: sampleValue) Thread.sleep(forTimeInterval: Double.random(in: 0 ..< 10) / 100) diff --git a/Examples/OTLP HTTP Exporter/main.swift b/Examples/OTLP HTTP Exporter/main.swift index a71963c1a..f4051fa04 100644 --- a/Examples/OTLP HTTP Exporter/main.swift +++ b/Examples/OTLP HTTP Exporter/main.swift @@ -43,7 +43,7 @@ tracerProviderSDK?.addSpanProcessor(SignPostIntegration()) } - func createSpans() { + @MainActor func createSpans() { let parentSpan1 = tracer.spanBuilder(spanName: "Main").setSpanKind(spanKind: .client).startSpan() parentSpan1.setAttribute(key: sampleKey, value: sampleValue) OpenTelemetry.instance.contextProvider.setActiveSpan(parentSpan1) @@ -64,7 +64,7 @@ parentSpan1.end() } - func doWork() { + @MainActor func doWork() { let childSpan = tracer.spanBuilder(spanName: "doWork").setSpanKind(spanKind: .client).startSpan() childSpan.setAttribute(key: sampleKey, value: sampleValue) Thread.sleep(forTimeInterval: Double.random(in: 0 ..< 10) / 100) diff --git a/Examples/Prometheus Sample/main.swift b/Examples/Prometheus Sample/main.swift index 9517919b4..714db55fa 100644 --- a/Examples/Prometheus Sample/main.swift +++ b/Examples/Prometheus Sample/main.swift @@ -17,9 +17,9 @@ let localAddress = "192.168.1.28" let promOptions = PrometheusExporterOptions(url: "http://\(localAddress):9184/metrics") let promExporter = PrometheusExporter(options: promOptions) -let metricsHttpServer = PrometheusExporterHttpServer(exporter: promExporter) -DispatchQueue.global(qos: .default).async { +Task { @MainActor in + let metricsHttpServer = PrometheusExporterHttpServer(exporter: promExporter) do { try metricsHttpServer.start() } catch { @@ -84,7 +84,7 @@ while counter < 3000 { sleep(1) } -metricsHttpServer.stop() +// Server will be stopped when the task completes print("Metrics server shutdown.") print("Press Enter key to exit.") diff --git a/Examples/Simple Exporter/main.swift b/Examples/Simple Exporter/main.swift index 3dc22791c..3d91b1ff3 100644 --- a/Examples/Simple Exporter/main.swift +++ b/Examples/Simple Exporter/main.swift @@ -49,14 +49,14 @@ tracerProviderSDK?.addSpanProcessor(SignPostIntegration()) } - func simpleSpan() { + @MainActor func simpleSpan() { let span = tracer.spanBuilder(spanName: "SimpleSpan").setSpanKind(spanKind: .client).startSpan() span.setAttribute(key: sampleKey, value: sampleValue) Thread.sleep(forTimeInterval: 0.5) span.end() } - func childSpan() { + @MainActor func childSpan() { let span = tracer.spanBuilder(spanName: "parentSpan").setSpanKind(spanKind: .client).setActive(true).startSpan() span.setAttribute(key: sampleKey, value: sampleValue) Thread.sleep(forTimeInterval: 0.2) @@ -67,9 +67,11 @@ span.end() } - simpleSpan() - sleep(1) - childSpan() - sleep(1) + Task { @MainActor in + simpleSpan() + sleep(1) + childSpan() + sleep(1) + } #endif diff --git a/Package.swift b/Package.swift index 4f1923fb4..b6ed4dfdf 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import Foundation @@ -40,7 +40,8 @@ let package = Package( .target( name: "SharedTestUtils", dependencies: [], - path: "Tests/Shared/TestUtils" + path: "Tests/Shared/TestUtils", + swiftSettings: [.unsafeFlags(["-Xfrontend", "-disable-availability-checking", "-strict-concurrency=minimal"])] ), .target( name: "OTelSwiftLog", @@ -132,7 +133,8 @@ let package = Package( .testTarget( name: "OTelSwiftLogTests", dependencies: ["OTelSwiftLog"], - path: "Tests/BridgesTests/OTelSwiftLog" + path: "Tests/BridgesTests/OTelSwiftLog", + swiftSettings: [.unsafeFlags(["-Xfrontend", "-disable-availability-checking", "-strict-concurrency=minimal"])] ), .testTarget( name: "SwiftMetricsShimTests", @@ -140,12 +142,14 @@ let package = Package( "SwiftMetricsShim", .product(name: "OpenTelemetrySdk", package: "opentelemetry-swift-core") ], - path: "Tests/ImportersTests/SwiftMetricsShim" + path: "Tests/ImportersTests/SwiftMetricsShim", + swiftSettings: [.unsafeFlags(["-Xfrontend", "-disable-availability-checking", "-strict-concurrency=minimal"])] ), .testTarget( name: "PrometheusExporterTests", dependencies: ["PrometheusExporter"], - path: "Tests/ExportersTests/Prometheus" + path: "Tests/ExportersTests/Prometheus", + swiftSettings: [.unsafeFlags(["-Xfrontend", "-disable-availability-checking", "-strict-concurrency=minimal"])] ), .testTarget( name: "OpenTelemetryProtocolExporterTests", @@ -154,32 +158,38 @@ let package = Package( "OpenTelemetryProtocolExporterHttp", "SharedTestUtils", ], - path: "Tests/ExportersTests/OpenTelemetryProtocol" + path: "Tests/ExportersTests/OpenTelemetryProtocol", + swiftSettings: [.unsafeFlags(["-Xfrontend", "-disable-availability-checking", "-strict-concurrency=minimal"])] ), .testTarget( name: "InMemoryExporterTests", dependencies: ["InMemoryExporter"], - path: "Tests/ExportersTests/InMemory" + path: "Tests/ExportersTests/InMemory", + swiftSettings: [.unsafeFlags(["-Xfrontend", "-disable-availability-checking", "-strict-concurrency=minimal"])] ), .testTarget( name: "PersistenceExporterTests", dependencies: ["PersistenceExporter"], - path: "Tests/ExportersTests/PersistenceExporter" + path: "Tests/ExportersTests/PersistenceExporter", + swiftSettings: [.unsafeFlags(["-Xfrontend", "-disable-availability-checking", "-strict-concurrency=minimal"])] ), .testTarget( name: "ContribTests", dependencies: [ "BaggagePropagationProcessor", "InMemoryExporter" - ] + ], + path: "Tests/ContribTests", + swiftSettings: [.unsafeFlags(["-Xfrontend", "-disable-availability-checking", "-strict-concurrency=minimal"])] ), - .testTarget( + .testTarget( name: "SessionTests", dependencies: [ "Sessions", .product(name: "OpenTelemetrySdk", package: "opentelemetry-swift-core") ], - path: "Tests/InstrumentationTests/SessionTests" + path: "Tests/InstrumentationTests/SessionTests", + swiftSettings: [.unsafeFlags(["-Xfrontend", "-disable-availability-checking", "-strict-concurrency=minimal"])] ), .executableTarget( name: "LoggingTracer", @@ -235,7 +245,8 @@ extension Package { "OpenTracingShim", .product(name: "OpenTelemetrySdk", package: "opentelemetry-swift-core") ], - path: "Tests/ImportersTests/OpenTracingShim" + path: "Tests/ImportersTests/OpenTracingShim", + swiftSettings: [.unsafeFlags(["-Xfrontend", "-disable-availability-checking", "-strict-concurrency=minimal"])] ) ]) #endif @@ -266,12 +277,14 @@ extension Package { condition: .when(platforms: [.iOS, .macOS, .tvOS, .macCatalyst, .linux]) ) ], - path: "Sources/Exporters/Jaeger" + path: "Sources/Exporters/Jaeger", + swiftSettings: [.unsafeFlags(["-Xfrontend", "-disable-availability-checking", "-strict-concurrency=minimal"])] ), .testTarget( name: "JaegerExporterTests", dependencies: ["JaegerExporter"], - path: "Tests/ExportersTests/Jaeger" + path: "Tests/ExportersTests/Jaeger", + swiftSettings: [.unsafeFlags(["-Xfrontend", "-disable-availability-checking", "-strict-concurrency=minimal"])] ), .executableTarget( name: "SimpleExporter", @@ -298,7 +311,8 @@ extension Package { dependencies: [ "NetworkStatus" ], - path: "Tests/InstrumentationTests/NetworkStatusTests" + path: "Tests/InstrumentationTests/NetworkStatusTests", + swiftSettings: [.unsafeFlags(["-Xfrontend", "-disable-availability-checking", "-strict-concurrency=minimal"])] ), .target( name: "URLSessionInstrumentation", @@ -314,7 +328,8 @@ extension Package { "URLSessionInstrumentation", "SharedTestUtils", ], - path: "Tests/InstrumentationTests/URLSessionTests" + path: "Tests/InstrumentationTests/URLSessionTests", + swiftSettings: [.unsafeFlags(["-Xfrontend", "-disable-availability-checking", "-strict-concurrency=minimal"])] ), .executableTarget( name: "NetworkSample", @@ -335,7 +350,8 @@ extension Package { .testTarget( name: "ZipkinExporterTests", dependencies: ["ZipkinExporter"], - path: "Tests/ExportersTests/Zipkin" + path: "Tests/ExportersTests/Zipkin", + swiftSettings: [.unsafeFlags(["-Xfrontend", "-disable-availability-checking", "-strict-concurrency=minimal"])] ), .executableTarget( name: "OTLPExporter", @@ -380,7 +396,8 @@ extension Package { "ResourceExtension", .product(name: "OpenTelemetrySdk", package: "opentelemetry-swift-core") ], - path: "Tests/InstrumentationTests/SDKResourceExtensionTests" + path: "Tests/InstrumentationTests/SDKResourceExtensionTests", + swiftSettings: [.unsafeFlags(["-Xfrontend", "-disable-availability-checking", "-strict-concurrency=minimal"])] ), .target( name: "MetricKitInstrumentation", @@ -388,7 +405,7 @@ extension Package { .product(name: "OpenTelemetrySdk", package: "opentelemetry-swift-core") ], path: "Sources/Instrumentation/MetricKit", - exclude: ["README.md"] + exclude: ["README.md", "StackTraceFormat.md"] ), .testTarget( name: "MetricKitInstrumentationTests", @@ -397,7 +414,8 @@ extension Package { "InMemoryExporter", .product(name: "OpenTelemetrySdk", package: "opentelemetry-swift-core") ], - path: "Tests/InstrumentationTests/MetricKitTests" + path: "Tests/InstrumentationTests/MetricKitTests", + swiftSettings: [.unsafeFlags(["-Xfrontend", "-disable-availability-checking", "-strict-concurrency=minimal"])] ), .executableTarget( name: "PrometheusSample", @@ -424,4 +442,4 @@ if ProcessInfo.processInfo.environment["OTEL_ENABLE_SWIFTLINT"] != nil { .plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins") ] } -} +} \ No newline at end of file diff --git a/Sources/Bridges/OTelSwiftLog/LogHandler.swift b/Sources/Bridges/OTelSwiftLog/LogHandler.swift index 28575a0e4..979cbea9f 100644 --- a/Sources/Bridges/OTelSwiftLog/LogHandler.swift +++ b/Sources/Bridges/OTelSwiftLog/LogHandler.swift @@ -7,7 +7,7 @@ let bridgeName: String = "OTelSwiftLog" let version: String = "1.0.0" /// A custom log handler to translate swift logs into otel logs -public struct OTelLogHandler: LogHandler { +public struct OTelLogHandler: LogHandler, @unchecked Sendable { /// Get or set the configured log level. /// /// - note: `LogHandler`s must treat the log level as a value type. This means that the change in metadata must @@ -41,7 +41,6 @@ public struct OTelLogHandler: LogHandler { self.loggerProvider = loggerProvider logger = self.loggerProvider.loggerBuilder(instrumentationScopeName: bridgeName) .setInstrumentationVersion(version) - .setEventDomain("device") .setIncludeTraceContext(true) .setAttributes(attributes) .setIncludeTraceContext(includeTraceContext) @@ -138,4 +137,4 @@ func convertSeverity(level: Logging.Logger.Level) -> OpenTelemetryApi.Severity { case .critical: return OpenTelemetryApi.Severity.error2 // should this be fatal instead? } -} +} \ No newline at end of file diff --git a/Sources/Exporters/InMemory/InMemoryExporter.swift b/Sources/Exporters/InMemory/InMemoryExporter.swift index 4bc1ac915..7d45f00e0 100644 --- a/Sources/Exporters/InMemory/InMemoryExporter.swift +++ b/Sources/Exporters/InMemory/InMemoryExporter.swift @@ -6,7 +6,7 @@ import Foundation import OpenTelemetrySdk -public class InMemoryExporter: SpanExporter { +public final class InMemoryExporter: SpanExporter, @unchecked Sendable { private var finishedSpanItems: [SpanData] = [] private var isRunning: Bool = true @@ -41,4 +41,4 @@ public class InMemoryExporter: SpanExporter { finishedSpanItems.removeAll() isRunning = false } -} +} \ No newline at end of file diff --git a/Sources/Exporters/Jaeger/Jaeger Thrift/agent+Exts.swift b/Sources/Exporters/Jaeger/Jaeger Thrift/agent+Exts.swift index 550df1e9a..35461f9c6 100644 --- a/Sources/Exporters/Jaeger/Jaeger Thrift/agent+Exts.swift +++ b/Sources/Exporters/Jaeger/Jaeger Thrift/agent+Exts.swift @@ -194,7 +194,7 @@ } extension AgentProcessor: TProcessor { - static let processorHandlers: ProcessorHandlerDictionary = { + nonisolated(unsafe) static let processorHandlers: ProcessorHandlerDictionary = { var processorHandlers = ProcessorHandlerDictionary() processorHandlers["emitZipkinBatch"] = { _, inProtocol, _, _ in diff --git a/Sources/Exporters/Jaeger/Jaeger Thrift/jaeger+Exts.swift b/Sources/Exporters/Jaeger/Jaeger Thrift/jaeger+Exts.swift index b2ea1ad73..55d451732 100644 --- a/Sources/Exporters/Jaeger/Jaeger Thrift/jaeger+Exts.swift +++ b/Sources/Exporters/Jaeger/Jaeger Thrift/jaeger+Exts.swift @@ -661,7 +661,7 @@ } extension CollectorProcessor: TProcessor { - static let processorHandlers: ProcessorHandlerDictionary = { + nonisolated(unsafe) static let processorHandlers: ProcessorHandlerDictionary = { var processorHandlers = ProcessorHandlerDictionary() processorHandlers["submitBatches"] = { sequenceID, inProtocol, outProtocol, handler in diff --git a/Sources/Exporters/Jaeger/JaegerSpanExporter.swift b/Sources/Exporters/Jaeger/JaegerSpanExporter.swift index 6c6a6699b..9e4e85129 100644 --- a/Sources/Exporters/Jaeger/JaegerSpanExporter.swift +++ b/Sources/Exporters/Jaeger/JaegerSpanExporter.swift @@ -9,7 +9,7 @@ import OpenTelemetrySdk import Thrift - public class JaegerSpanExporter: SpanExporter { + public final class JaegerSpanExporter: SpanExporter, @unchecked Sendable { let collectorAddress: String let process: Process @@ -34,4 +34,4 @@ public func shutdown(explicitTimeout: TimeInterval? = nil) {} } -#endif +#endif \ No newline at end of file diff --git a/Sources/Exporters/Jaeger/Sender.swift b/Sources/Exporters/Jaeger/Sender.swift index c430b5188..0ea9a1bbc 100644 --- a/Sources/Exporters/Jaeger/Sender.swift +++ b/Sources/Exporters/Jaeger/Sender.swift @@ -9,7 +9,7 @@ import Network import Thrift - public class Sender { + public final class Sender: @unchecked Sendable { private let host: String private let port = 6832 @@ -58,4 +58,4 @@ } } -#endif +#endif \ No newline at end of file diff --git a/Sources/Exporters/Persistence/PersistencePerformancePreset.swift b/Sources/Exporters/Persistence/PersistencePerformancePreset.swift index 6b8197ea1..6b189ed90 100644 --- a/Sources/Exporters/Persistence/PersistencePerformancePreset.swift +++ b/Sources/Exporters/Persistence/PersistencePerformancePreset.swift @@ -49,7 +49,7 @@ protocol ExportPerformancePreset { var exportDelayChangeRate: Double { get } } -public struct PersistencePerformancePreset: Equatable, StoragePerformancePreset, ExportPerformancePreset { +public struct PersistencePerformancePreset: Equatable, StoragePerformancePreset, ExportPerformancePreset, Sendable { // MARK: - StoragePerformancePreset let maxFileSize: UInt64 diff --git a/Sources/Exporters/Persistence/Storage/FilesOrchestrator.swift b/Sources/Exporters/Persistence/Storage/FilesOrchestrator.swift index 61c34e10a..403fc308a 100644 --- a/Sources/Exporters/Persistence/Storage/FilesOrchestrator.swift +++ b/Sources/Exporters/Persistence/Storage/FilesOrchestrator.swift @@ -169,7 +169,7 @@ func fileCreationDateFrom(fileName: String) -> Date { return Date(timeIntervalSinceReferenceDate: TimeInterval(millisecondsSinceReferenceDate)) } -private enum FixedWidthIntegerError: Error { +private enum FixedWidthIntegerError: Error, @unchecked Sendable { case overflow(overflowingValue: T) } diff --git a/Sources/Exporters/Prometheus/PrometheusExporter.swift b/Sources/Exporters/Prometheus/PrometheusExporter.swift index 36fc3f231..45dfce0c1 100644 --- a/Sources/Exporters/Prometheus/PrometheusExporter.swift +++ b/Sources/Exporters/Prometheus/PrometheusExporter.swift @@ -7,7 +7,7 @@ import Foundation import NIOConcurrencyHelpers import OpenTelemetrySdk -public class PrometheusExporter: MetricExporter { +public final class PrometheusExporter: MetricExporter, @unchecked Sendable { var aggregationTemporalitySelector: AggregationTemporalitySelector public func getAggregationTemporality(for instrument: OpenTelemetrySdk.InstrumentType) -> OpenTelemetrySdk.AggregationTemporality { @@ -55,4 +55,4 @@ public struct PrometheusExporterOptions { public init(url: String) { self.url = url } -} +} \ No newline at end of file diff --git a/Sources/Exporters/Zipkin/Implementation/ZipkinConversionExtension.swift b/Sources/Exporters/Zipkin/Implementation/ZipkinConversionExtension.swift index 1b19b4d64..18c6875eb 100644 --- a/Sources/Exporters/Zipkin/Implementation/ZipkinConversionExtension.swift +++ b/Sources/Exporters/Zipkin/Implementation/ZipkinConversionExtension.swift @@ -18,8 +18,10 @@ enum ZipkinConversionExtension { "http.host": 3, "db.instance": 4] - static var localEndpointCache = [String: ZipkinEndpoint]() - static var remoteEndpointCache = [String: ZipkinEndpoint]() + static let localEndpointCacheLock = NSLock() + nonisolated(unsafe) static var localEndpointCache = [String: ZipkinEndpoint]() + static let remoteEndpointCacheLock = NSLock() + nonisolated(unsafe) static var remoteEndpointCache = [String: ZipkinEndpoint]() static let defaultServiceName = "unknown_service:" + ProcessInfo.processInfo.processName @@ -47,10 +49,12 @@ enum ZipkinConversionExtension { var localEndpoint = defaultLocalEndpoint if let serviceName = attributeEnumerationState.serviceName, !serviceName.isEmpty, defaultServiceName != serviceName { - if localEndpointCache[serviceName] == nil { - localEndpointCache[serviceName] = defaultLocalEndpoint.clone(serviceName: serviceName) + localEndpointCacheLock.withLock { + if localEndpointCache[serviceName] == nil { + localEndpointCache[serviceName] = defaultLocalEndpoint.clone(serviceName: serviceName) + } + localEndpoint = localEndpointCache[serviceName] ?? localEndpoint } - localEndpoint = localEndpointCache[serviceName] ?? localEndpoint } if let serviceNamespace = attributeEnumerationState.serviceNamespace, !serviceNamespace.isEmpty { @@ -59,10 +63,12 @@ enum ZipkinConversionExtension { var remoteEndpoint: ZipkinEndpoint? if otelSpan.kind == .client || otelSpan.kind == .producer, attributeEnumerationState.RemoteEndpointServiceName != nil { - remoteEndpoint = remoteEndpointCache[attributeEnumerationState.RemoteEndpointServiceName!] - if remoteEndpoint == nil { - remoteEndpoint = ZipkinEndpoint(serviceName: attributeEnumerationState.RemoteEndpointServiceName!) - remoteEndpointCache[attributeEnumerationState.RemoteEndpointServiceName!] = remoteEndpoint! + remoteEndpointCacheLock.withLock { + remoteEndpoint = remoteEndpointCache[attributeEnumerationState.RemoteEndpointServiceName!] + if remoteEndpoint == nil { + remoteEndpoint = ZipkinEndpoint(serviceName: attributeEnumerationState.RemoteEndpointServiceName!) + remoteEndpointCache[attributeEnumerationState.RemoteEndpointServiceName!] = remoteEndpoint! + } } } diff --git a/Sources/Importers/OpenTracingShim/TraceShim.swift b/Sources/Importers/OpenTracingShim/TraceShim.swift index 6855dffca..9acb67aa8 100644 --- a/Sources/Importers/OpenTracingShim/TraceShim.swift +++ b/Sources/Importers/OpenTracingShim/TraceShim.swift @@ -8,8 +8,8 @@ import OpenTelemetryApi import OpenTelemetrySdk import Opentracing -public class TraceShim { - public static var instance = TraceShim() +public final class TraceShim: @unchecked Sendable { + public static let instance = TraceShim() public private(set) var otTracer: OTTracer diff --git a/Sources/Importers/SwiftMetricsShim/MetricHandlers.swift b/Sources/Importers/SwiftMetricsShim/MetricHandlers.swift index fa2997d65..48eed12c7 100644 --- a/Sources/Importers/SwiftMetricsShim/MetricHandlers.swift +++ b/Sources/Importers/SwiftMetricsShim/MetricHandlers.swift @@ -6,10 +6,10 @@ import CoreMetrics import OpenTelemetryApi -class SwiftCounterMetric: CounterHandler, SwiftMetric { +final class SwiftCounterMetric: CounterHandler, SwiftMetric, @unchecked Sendable { let metricName: String let metricType: MetricType = .counter - let counter: Locked + var counter: Locked let labels: [String: AttributeValue] required init(name: String, @@ -29,7 +29,7 @@ class SwiftCounterMetric: CounterHandler, SwiftMetric { func reset() {} } -class SwiftGaugeMetric: RecorderHandler, SwiftMetric { +final class SwiftGaugeMetric: RecorderHandler, SwiftMetric, @unchecked Sendable { let metricName: String let metricType: MetricType = .gauge var counter: DoubleGauge @@ -54,7 +54,7 @@ class SwiftGaugeMetric: RecorderHandler, SwiftMetric { } } -class SwiftHistogramMetric: RecorderHandler, SwiftMetric { +final class SwiftHistogramMetric: RecorderHandler, SwiftMetric, @unchecked Sendable { let metricName: String let metricType: MetricType = .histogram var measure: DoubleHistogram @@ -77,7 +77,7 @@ class SwiftHistogramMetric: RecorderHandler, SwiftMetric { } } -class SwiftSummaryMetric: TimerHandler, SwiftMetric { +final class SwiftSummaryMetric: TimerHandler, SwiftMetric, @unchecked Sendable { let metricName: String let metricType: MetricType = .summary var measure: DoubleCounter diff --git a/Sources/Importers/SwiftMetricsShim/SwiftMetricsShim.swift b/Sources/Importers/SwiftMetricsShim/SwiftMetricsShim.swift index 4702e8c6d..027ea5542 100644 --- a/Sources/Importers/SwiftMetricsShim/SwiftMetricsShim.swift +++ b/Sources/Importers/SwiftMetricsShim/SwiftMetricsShim.swift @@ -6,7 +6,7 @@ import CoreMetrics import OpenTelemetryApi -public class OpenTelemetrySwiftMetrics: MetricsFactory { +public final class OpenTelemetrySwiftMetrics: MetricsFactory, @unchecked Sendable { let meter: any OpenTelemetryApi.Meter var metrics = [MetricKey: SwiftMetric]() let lock = Lock() diff --git a/Sources/Instrumentation/NetworkStatus/NetworkMonitor.swift b/Sources/Instrumentation/NetworkStatus/NetworkMonitor.swift index 1f19cf4d3..02489755b 100644 --- a/Sources/Instrumentation/NetworkStatus/NetworkMonitor.swift +++ b/Sources/Instrumentation/NetworkStatus/NetworkMonitor.swift @@ -9,7 +9,7 @@ import Network - public class NetworkMonitor: NetworkMonitorProtocol { + public final class NetworkMonitor: NetworkMonitorProtocol, @unchecked Sendable { let monitor = NWPathMonitor() var connection: Connection = .unavailable let monitorQueue = DispatchQueue(label: "OTel-Network-Monitor") @@ -20,7 +20,7 @@ } public init() throws { - let pathHandler = { (path: NWPath) in + let pathHandler: @Sendable (NWPath) -> Void = { (path: NWPath) in let availableInterfaces = path.availableInterfaces let wifiInterface = self.getWifiInterface(interfaces: availableInterfaces) let cellInterface = self.getCellInterface(interfaces: availableInterfaces) diff --git a/Sources/Instrumentation/NetworkStatus/NetworkStatusInjector.swift b/Sources/Instrumentation/NetworkStatus/NetworkStatusInjector.swift index 23549ca11..46298d176 100644 --- a/Sources/Instrumentation/NetworkStatus/NetworkStatusInjector.swift +++ b/Sources/Instrumentation/NetworkStatus/NetworkStatusInjector.swift @@ -19,27 +19,27 @@ public func inject(span: Span) { let (type, subtype, carrier) = netstat.status() - span.setAttribute(key: SemanticAttributes.networkConnectionType.rawValue, value: AttributeValue.string(type)) + span.setAttribute(key: SemanticConventions.Network.connectionType.rawValue, value: AttributeValue.string(type)) if let subtype: String = subtype { - span.setAttribute(key: SemanticAttributes.networkConnectionSubtype.rawValue, value: AttributeValue.string(subtype)) + span.setAttribute(key: SemanticConventions.Network.connectionSubtype.rawValue, value: AttributeValue.string(subtype)) } if let carrierInfo: CTCarrier = carrier { if let carrierName = carrierInfo.carrierName { - span.setAttribute(key: SemanticAttributes.networkCarrierName.rawValue, value: AttributeValue.string(carrierName)) + span.setAttribute(key: SemanticConventions.Network.carrierName.rawValue, value: AttributeValue.string(carrierName)) } if let isoCountryCode = carrierInfo.isoCountryCode { - span.setAttribute(key: SemanticAttributes.networkCarrierIcc.rawValue, value: AttributeValue.string(isoCountryCode)) + span.setAttribute(key: SemanticConventions.Network.carrierIcc.rawValue, value: AttributeValue.string(isoCountryCode)) } if let mobileCountryCode = carrierInfo.mobileCountryCode { - span.setAttribute(key: SemanticAttributes.networkCarrierMcc.rawValue, value: AttributeValue.string(mobileCountryCode)) + span.setAttribute(key: SemanticConventions.Network.carrierMcc.rawValue, value: AttributeValue.string(mobileCountryCode)) } if let mobileNetworkCode = carrierInfo.mobileNetworkCode { - span.setAttribute(key: SemanticAttributes.networkCarrierMnc.rawValue, value: AttributeValue.string(mobileNetworkCode)) + span.setAttribute(key: SemanticConventions.Network.carrierMnc.rawValue, value: AttributeValue.string(mobileNetworkCode)) } } } diff --git a/Sources/Instrumentation/Sessions/Session.swift b/Sources/Instrumentation/Sessions/Session.swift index acd9e4d70..70130b570 100644 --- a/Sources/Instrumentation/Sessions/Session.swift +++ b/Sources/Instrumentation/Sessions/Session.swift @@ -23,7 +23,7 @@ import Foundation /// print("Duration: \(session.duration!) seconds") /// } /// ``` -public struct Session: Equatable { +public struct Session: Equatable, Sendable { /// Unique identifier for the session public let id: String /// Expiration time for the session diff --git a/Sources/Instrumentation/Sessions/SessionConfig.swift b/Sources/Instrumentation/Sessions/SessionConfig.swift index fa8a91be1..11129dd3b 100644 --- a/Sources/Instrumentation/Sessions/SessionConfig.swift +++ b/Sources/Instrumentation/Sessions/SessionConfig.swift @@ -22,7 +22,7 @@ import Foundation /// /// let manager = SessionManager(configuration: config) /// ``` -public struct SessionConfig { +public struct SessionConfig: Sendable { /// Duration in seconds after which a session expires if left inactive public let sessionTimeout: TimeInterval diff --git a/Sources/Instrumentation/Sessions/SessionEventInstrumentation.swift b/Sources/Instrumentation/Sessions/SessionEventInstrumentation.swift index 9e4ae1def..fcd0d6f9d 100644 --- a/Sources/Instrumentation/Sessions/SessionEventInstrumentation.swift +++ b/Sources/Instrumentation/Sessions/SessionEventInstrumentation.swift @@ -7,13 +7,13 @@ import Foundation import OpenTelemetryApi /// Enum to specify the type of session event -public enum SessionEventType { +public enum SessionEventType: Sendable { case start case end } /// Represents a session event with its associated session and event type -public struct SessionEvent { +public struct SessionEvent: Sendable { let session: Session let eventType: SessionEventType } @@ -29,14 +29,14 @@ public struct SessionEvent { /// - Sessions created after instrumentation is applied trigger notifications /// - All session events are converted to OpenTelemetry log records with appropriate attributes /// - Session end events include duration and end time attributes -public class SessionEventInstrumentation { +public final class SessionEventInstrumentation: @unchecked Sendable { private let logger: Logger /// Queue for storing session events that were created before instrumentation was initialized. /// This allows capturing session events that occur during application startup before /// the OpenTelemetry SDK is fully initialized. /// Limited to 20 items to prevent memory issues. - static var queue: [SessionEvent] = [] + nonisolated(unsafe) static var queue: [SessionEvent] = [] /// Maximum number of sessions that can be queued before instrumentation is applied static let maxQueueSize: UInt8 = 32 @@ -49,26 +49,29 @@ public class SessionEventInstrumentation { /// Flag to track if the instrumentation has been applied. /// Controls whether new sessions are queued or immediately processed via notifications. - static var isApplied = false + nonisolated(unsafe) static var isApplied = false public init() { logger = OpenTelemetry.instance.loggerProvider.get(instrumentationScopeName: SessionEventInstrumentation.instrumentationKey) - guard !SessionEventInstrumentation.isApplied else { - return - } + + Task { + guard !SessionEventInstrumentation.isApplied else { + return + } - SessionEventInstrumentation.isApplied = true - // Process any queued sessions - processQueuedSessions() + SessionEventInstrumentation.isApplied = true + // Process any queued sessions + await processQueuedSessions() + } // Start observing for new session notifications NotificationCenter.default.addObserver( forName: SessionEventInstrumentation.sessionEventNotification, object: nil, queue: nil - ) { notification in + ) { [weak self] notification in if let sessionEvent = notification.object as? SessionEvent { - self.createSessionEvent(session: sessionEvent.session, eventType: sessionEvent.eventType) + self?.createSessionEvent(session: sessionEvent.session, eventType: sessionEvent.eventType) } } } @@ -78,7 +81,7 @@ public class SessionEventInstrumentation { /// This method is called during the `apply()` process to handle any sessions that /// were created before the instrumentation was initialized. It creates log records /// for all queued sessions and then clears the queue. - private func processQueuedSessions() { + private func processQueuedSessions() async { let sessionEvents = SessionEventInstrumentation.queue if sessionEvents.isEmpty { diff --git a/Sources/Instrumentation/Sessions/SessionManager.swift b/Sources/Instrumentation/Sessions/SessionManager.swift index bc2bc5ad8..3ba7fa883 100644 --- a/Sources/Instrumentation/Sessions/SessionManager.swift +++ b/Sources/Instrumentation/Sessions/SessionManager.swift @@ -8,7 +8,7 @@ import Foundation /// Manages OpenTelemetry sessions with automatic expiration and persistence. /// Provides thread-safe access to session information and handles session lifecycle. /// Sessions are automatically extended on access and persisted to UserDefaults. -public class SessionManager { +public class SessionManager: @unchecked Sendable { private var configuration: SessionConfig private var session: Session? private var lock = NSLock() @@ -24,7 +24,7 @@ public class SessionManager { /// This method is thread-safe and will extend the session expireTime time /// - Returns: The current active session @discardableResult - public func getSession() -> Session { + open func getSession() -> Session { // We only lock once when fetching the current session to expire with thread safety return lock.withLock { refreshSession() diff --git a/Sources/Instrumentation/Sessions/SessionManagerProvider.swift b/Sources/Instrumentation/Sessions/SessionManagerProvider.swift index 6529faf59..ef6c69e30 100644 --- a/Sources/Instrumentation/Sessions/SessionManagerProvider.swift +++ b/Sources/Instrumentation/Sessions/SessionManagerProvider.swift @@ -20,7 +20,7 @@ import Foundation /// let session = SessionManagerProvider.getInstance().getSession() /// ``` public class SessionManagerProvider { - private static var _instance: SessionManager? + nonisolated(unsafe) private static var _instance: SessionManager? private static let lock = NSLock() /// Registers a SessionManager instance as the singleton. @@ -28,9 +28,9 @@ public class SessionManagerProvider { /// Call this early in your app lifecycle to ensure consistent session management. /// - Parameter sessionManager: The SessionManager instance to register public static func register(sessionManager: SessionManager) { - lock.withLock { - _instance = sessionManager - } + lock.lock() + defer { lock.unlock() } + _instance = sessionManager } /// Returns the registered SessionManager instance or creates a default one. @@ -39,11 +39,11 @@ public class SessionManagerProvider { /// If no instance has been registered, creates one with default configuration. /// - Returns: The singleton SessionManager instance public static func getInstance() -> SessionManager { - return lock.withLock { - if _instance == nil { - _instance = SessionManager() - } - return _instance! + lock.lock() + defer { lock.unlock() } + if _instance == nil { + _instance = SessionManager() } + return _instance! } } \ No newline at end of file diff --git a/Sources/Instrumentation/Sessions/SessionSpanProcessor.swift b/Sources/Instrumentation/Sessions/SessionSpanProcessor.swift index 2aa9a7928..eafdf2b77 100644 --- a/Sources/Instrumentation/Sessions/SessionSpanProcessor.swift +++ b/Sources/Instrumentation/Sessions/SessionSpanProcessor.swift @@ -20,7 +20,7 @@ public class SessionSpanProcessor: SpanProcessor { /// Initializes the span processor with a session manager /// - Parameter sessionManager: The session manager to use for retrieving session IDs (defaults to singleton) public init(sessionManager: SessionManager? = nil) { - self.sessionManager = sessionManager ?? SessionManagerProvider.getInstance() + self.sessionManager = sessionManager ?? SessionManager() } /// Called when a span starts - adds the current session ID as an attribute diff --git a/Sources/Instrumentation/Sessions/SessionStore.swift b/Sources/Instrumentation/Sessions/SessionStore.swift index c81a10276..37e82998d 100644 --- a/Sources/Instrumentation/Sessions/SessionStore.swift +++ b/Sources/Instrumentation/Sessions/SessionStore.swift @@ -7,7 +7,7 @@ import Foundation /// Handles persistence of OpenTelemetry sessions to UserDefaults /// Provides static methods for saving and loading session data -internal class SessionStore { +internal final class SessionStore: @unchecked Sendable { /// UserDefaults key for storing session ID static let idKey = "otel-session-id" /// UserDefaults key for storing previous session ID @@ -23,28 +23,35 @@ internal class SessionStore { /// in memory and saves to disk on an interval (every 30 seconds). /// The most recent session to be saved to disk + @MainActor private static var pendingSession: Session? /// The previous session + @MainActor private static var prevSession: Session? /// The interval period after which the current session is saved to disk private static let saveInterval: TimeInterval = 30 // in seconds /// The timer responsible for saving the current session to disk + @MainActor private static var saveTimer: Timer? /// Schedules a session to be saved to UserDefaults on the next timer interval /// - Parameter session: The session to save static func scheduleSave(session: Session) { - pendingSession = session + Task { @MainActor in + pendingSession = session - if saveTimer == nil { - // save initial session - saveImmediately(session: session) + if saveTimer == nil { + // save initial session + saveImmediately(session: session) - // save future sessions on a interval - saveTimer = Timer.scheduledTimer(withTimeInterval: saveInterval, repeats: true) { _ in - // only write to disk if it is a new sesssion - if let pending = pendingSession, prevSession != pending { - saveImmediately(session: pending) + // save future sessions on a interval + saveTimer = Timer.scheduledTimer(withTimeInterval: saveInterval, repeats: true) { _ in + Task { @MainActor in + // only write to disk if it is a new sesssion + if let pending = pendingSession, prevSession != pending { + saveImmediately(session: pending) + } + } } } } @@ -60,10 +67,12 @@ internal class SessionStore { UserDefaults.standard.set(session.previousId, forKey: previousIdKey) UserDefaults.standard.set(session.sessionTimeout, forKey: sessionTimeoutKey) - // update prev session - prevSession = session - // clear pending session, since it is now outdated - pendingSession = nil + Task { @MainActor in + // update prev session + prevSession = session + // clear pending session, since it is now outdated + pendingSession = nil + } } /// Loads a previously saved session from UserDefaults @@ -79,24 +88,31 @@ internal class SessionStore { let previousId = UserDefaults.standard.string(forKey: previousIdKey) - // reset sessions so it does not get overridden in the next scheduled save - pendingSession = nil - prevSession = Session( + let session = Session( id: id, expireTime: expireTime, previousId: previousId, startTime: startTime, sessionTimeout: sessionTimeout ) - return prevSession + + Task { @MainActor in + // reset sessions so it does not get overridden in the next scheduled save + pendingSession = nil + prevSession = session + } + + return session } /// Cleans up timer and UserDefaults static func teardown() { - saveTimer?.invalidate() - saveTimer = nil - pendingSession = nil - prevSession = nil + Task { @MainActor in + saveTimer?.invalidate() + saveTimer = nil + pendingSession = nil + prevSession = nil + } UserDefaults.standard.removeObject(forKey: idKey) UserDefaults.standard.removeObject(forKey: startTimeKey) UserDefaults.standard.removeObject(forKey: expireTimeKey) diff --git a/Sources/Instrumentation/URLSession/URLSessionInstrumentation.swift b/Sources/Instrumentation/URLSession/URLSessionInstrumentation.swift index ef72475b2..f0eceb54f 100644 --- a/Sources/Instrumentation/URLSession/URLSessionInstrumentation.swift +++ b/Sources/Instrumentation/URLSession/URLSessionInstrumentation.swift @@ -24,9 +24,9 @@ struct NetworkRequestState { } } -private var idKey: Void? +nonisolated(unsafe) private var idKey: Void? -public class URLSessionInstrumentation { +public final class URLSessionInstrumentation: @unchecked Sendable { private var requestMap = [String: NetworkRequestState]() private var _configuration: URLSessionInstrumentationConfiguration @@ -44,7 +44,7 @@ public class URLSessionInstrumentation { private let configurationQueue = DispatchQueue( label: "io.opentelemetry.configuration") - static var instrumentedKey = "io.opentelemetry.instrumentedCall" + static let instrumentedKey = "io.opentelemetry.instrumentedCall" static let excludeList: [String] = [ "__NSCFURLProxySessionConnection" @@ -843,14 +843,14 @@ public class URLSessionInstrumentation { } } -class FakeDelegate: NSObject, URLSessionTaskDelegate { +final class FakeDelegate: NSObject, URLSessionTaskDelegate, @unchecked Sendable { func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {} } @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) -class AsyncTaskDelegate: NSObject, URLSessionTaskDelegate { - private weak var instrumentation: URLSessionInstrumentation? +final class AsyncTaskDelegate: NSObject, URLSessionTaskDelegate, @unchecked Sendable { + private let instrumentation: URLSessionInstrumentation? private let sessionTaskId: String init(instrumentation: URLSessionInstrumentation, sessionTaskId: String) { diff --git a/Sources/Instrumentation/URLSession/URLSessionLogger.swift b/Sources/Instrumentation/URLSession/URLSessionLogger.swift index e5f87dfa4..b95550026 100644 --- a/Sources/Instrumentation/URLSession/URLSessionLogger.swift +++ b/Sources/Instrumentation/URLSession/URLSessionLogger.swift @@ -12,8 +12,8 @@ import os.log #endif // os(iOS) && !targetEnvironment(macCatalyst) class URLSessionLogger { - static var runningSpans = [String: Span]() - static var runningSpansQueue = DispatchQueue(label: "io.opentelemetry.URLSessionLogger") + nonisolated(unsafe) static var runningSpans = [String: Span]() + static let runningSpansQueue = DispatchQueue(label: "io.opentelemetry.URLSessionLogger") #if os(iOS) && !targetEnvironment(macCatalyst) static var netstatInjector: NetworkStatusInjector? = { () -> NetworkStatusInjector? in @@ -40,30 +40,30 @@ class URLSessionLogger { var attributes = [String: AttributeValue]() - attributes[SemanticAttributes.httpMethod.rawValue] = AttributeValue.string(request.httpMethod ?? "unknown_method") + attributes[SemanticConventions.Http.requestMethod.rawValue] = AttributeValue.string(request.httpMethod ?? "unknown_method") if let requestURL = request.url { - attributes[SemanticAttributes.httpUrl.rawValue] = AttributeValue.string(requestURL.absoluteString) + attributes[SemanticConventions.Url.full.rawValue] = AttributeValue.string(requestURL.absoluteString) } if let requestURLPath = request.url?.path { - attributes[SemanticAttributes.httpTarget.rawValue] = AttributeValue.string(requestURLPath) + attributes[SemanticConventions.Url.path.rawValue] = AttributeValue.string(requestURLPath) } if let host = request.url?.host { - attributes[SemanticAttributes.netPeerName.rawValue] = AttributeValue.string(host) + attributes[SemanticConventions.Network.peerAddress.rawValue] = AttributeValue.string(host) } if let requestScheme = request.url?.scheme { - attributes[SemanticAttributes.httpScheme.rawValue] = AttributeValue.string(requestScheme) + attributes[SemanticConventions.Url.scheme.rawValue] = AttributeValue.string(requestScheme) } if let port = request.url?.port { - attributes[SemanticAttributes.netPeerPort.rawValue] = AttributeValue.int(port) + attributes[SemanticConventions.Network.peerPort.rawValue] = AttributeValue.int(port) } if let bodySize = request.httpBody?.count { - attributes[SemanticAttributes.httpRequestBodySize.rawValue] = AttributeValue.int(bodySize) + attributes[SemanticConventions.Http.requestBodySize.rawValue] = AttributeValue.int(bodySize) } var spanName = "HTTP " + (request.httpMethod ?? "") @@ -111,13 +111,13 @@ class URLSessionLogger { } let statusCode = httpResponse.statusCode - span.setAttribute(key: SemanticAttributes.httpStatusCode.rawValue, + span.setAttribute(key: SemanticConventions.Http.responseStatusCode.rawValue, value: AttributeValue.int(statusCode)) span.status = statusForStatusCode(code: statusCode) if let contentLengthHeader = httpResponse.allHeaderFields["Content-Length"] as? String, let contentLength = Int(contentLengthHeader) { - span.setAttribute(key: SemanticAttributes.httpResponseBodySize.rawValue, + span.setAttribute(key: SemanticConventions.Http.responseBodySize.rawValue, value: AttributeValue.int(contentLength)) } @@ -134,7 +134,7 @@ class URLSessionLogger { guard span != nil else { return } - span.setAttribute(key: SemanticAttributes.httpStatusCode.rawValue, value: AttributeValue.int(statusCode)) + span.setAttribute(key: SemanticConventions.Http.responseStatusCode.rawValue, value: AttributeValue.int(statusCode)) span.status = URLSessionLogger.statusForStatusCode(code: statusCode) instrumentation.configuration.receivedError?(error, dataOrFile, statusCode, span) diff --git a/Tests/ExportersTests/PersistenceExporter/Export/DataExportWorkerTests.swift b/Tests/ExportersTests/PersistenceExporter/Export/DataExportWorkerTests.swift index 00b2e6061..b3355a5fd 100644 --- a/Tests/ExportersTests/PersistenceExporter/Export/DataExportWorkerTests.swift +++ b/Tests/ExportersTests/PersistenceExporter/Export/DataExportWorkerTests.swift @@ -19,7 +19,7 @@ class DataExportWorkerTests: XCTestCase { // MARK: - Data Exports - func testItExportsAllData() { + @MainActor func testItExportsAllData() { let v1ExportExpectation = expectation(description: "V1 exported") let v2ExportExpectation = expectation(description: "V2 exported") let v3ExportExpectation = expectation(description: "V3 exported") @@ -104,7 +104,7 @@ class DataExportWorkerTests: XCTestCase { // MARK: - Export Interval Changes - func testWhenThereIsNoBatch_thenIntervalIncreases() { + @MainActor func testWhenThereIsNoBatch_thenIntervalIncreases() { let delayChangeExpectation = expectation(description: "Export delay is increased") let mockDelay = MockDelay { command in if case .increase = command { @@ -128,7 +128,7 @@ class DataExportWorkerTests: XCTestCase { worker.cancelSynchronously() } - func testWhenBatchFails_thenIntervalIncreases() { + @MainActor func testWhenBatchFails_thenIntervalIncreases() { let delayChangeExpectation = expectation(description: "Export delay is increased") let mockDelay = MockDelay { command in if case .increase = command { @@ -164,7 +164,7 @@ class DataExportWorkerTests: XCTestCase { worker.cancelSynchronously() } - func testWhenBatchSucceeds_thenIntervalDecreases() { + @MainActor func testWhenBatchSucceeds_thenIntervalDecreases() { let delayChangeExpectation = expectation(description: "Export delay is decreased") let mockDelay = MockDelay { command in if case .decrease = command { @@ -217,7 +217,7 @@ class DataExportWorkerTests: XCTestCase { worker.queue.sync(flags: .barrier) {} } - func testItFlushesAllData() { + @MainActor func testItFlushesAllData() { let v1ExportExpectation = expectation(description: "V1 exported") let v2ExportExpectation = expectation(description: "V2 exported") let v3ExportExpectation = expectation(description: "V3 exported") diff --git a/Tests/ExportersTests/PersistenceExporter/PersistenceExporterDecoratorTests.swift b/Tests/ExportersTests/PersistenceExporter/PersistenceExporterDecoratorTests.swift index c5649b934..495e35588 100644 --- a/Tests/ExportersTests/PersistenceExporter/PersistenceExporterDecoratorTests.swift +++ b/Tests/ExportersTests/PersistenceExporter/PersistenceExporterDecoratorTests.swift @@ -99,7 +99,7 @@ class PersistenceExporterDecoratorTests: XCTestCase { XCTAssertFalse(result!.needsRetry) } - func testWhenItIsFlushed_thenItFlushesTheWriterAndWorker() { + @MainActor func testWhenItIsFlushed_thenItFlushesTheWriterAndWorker() { let writerIsFlushedExpectation = expectation(description: "FileWriter was flushed") let workerIsFlushedExpectation = expectation(description: "DataExportWorker was flushed") @@ -123,7 +123,7 @@ class PersistenceExporterDecoratorTests: XCTestCase { waitForExpectations(timeout: 1, handler: nil) } - func testWhenObjectsDataIsExportedSeparately_thenObjectsAreExported() throws { + @MainActor func testWhenObjectsDataIsExportedSeparately_thenObjectsAreExported() throws { let v1ExportExpectation = expectation(description: "V1 exported") let v2ExportExpectation = expectation(description: "V2 exported") let v3ExportExpectation = expectation(description: "V3 exported") @@ -148,9 +148,11 @@ class PersistenceExporterDecoratorTests: XCTestCase { worker: &worker, decoratedExporter: decoratedExporter) - fileWriter.onWrite = { _, data in - if let dataExporter = worker.dataExporter { - XCTAssertFalse(dataExporter.export(data: data).needsRetry) + fileWriter.onWrite = { [weak worker] _, data in + Task { @MainActor in + if let dataExporter = worker?.dataExporter { + XCTAssertFalse(dataExporter.export(data: data).needsRetry) + } } } @@ -161,7 +163,7 @@ class PersistenceExporterDecoratorTests: XCTestCase { waitForExpectations(timeout: 1, handler: nil) } - func testWhenObjectsDataIsExportedConcatenated_thenObjectsAreExported() throws { + @MainActor func testWhenObjectsDataIsExportedConcatenated_thenObjectsAreExported() throws { let v1ExportExpectation = expectation(description: "V1 exported") let v2ExportExpectation = expectation(description: "V2 exported") let v3ExportExpectation = expectation(description: "V3 exported") diff --git a/Tests/ExportersTests/PersistenceExporter/PersistenceMetricExporterDecoratorTests.swift b/Tests/ExportersTests/PersistenceExporter/PersistenceMetricExporterDecoratorTests.swift index 90e138c56..9e46b28a9 100644 --- a/Tests/ExportersTests/PersistenceExporter/PersistenceMetricExporterDecoratorTests.swift +++ b/Tests/ExportersTests/PersistenceExporter/PersistenceMetricExporterDecoratorTests.swift @@ -44,7 +44,7 @@ class PersistenceMetricExporterDecoratorTests: XCTestCase { super.tearDown() } - func testWhenExportMetricIsCalled_thenMetricsAreExported() throws { + @MainActor func testWhenExportMetricIsCalled_thenMetricsAreExported() throws { let metricsExportExpectation = expectation(description: "metrics exported") let mockMetricExporter = MetricExporterMock(onExport: { metrics in metrics.forEach { metric in diff --git a/Tests/ExportersTests/PersistenceExporter/PersistenceSpanExporterDecoratorTests.swift b/Tests/ExportersTests/PersistenceExporter/PersistenceSpanExporterDecoratorTests.swift index cce3acb48..a9f23810d 100644 --- a/Tests/ExportersTests/PersistenceExporter/PersistenceSpanExporterDecoratorTests.swift +++ b/Tests/ExportersTests/PersistenceExporter/PersistenceSpanExporterDecoratorTests.swift @@ -47,7 +47,7 @@ class PersistenceSpanExporterDecoratorTests: XCTestCase { super.tearDown() } - func testWhenExportMetricIsCalled_thenSpansAreExported() throws { + @MainActor func testWhenExportMetricIsCalled_thenSpansAreExported() throws { let spansExportExpectation = expectation(description: "spans exported") let exporterShutdownExpectation = expectation(description: "exporter shut down") diff --git a/Tests/ExportersTests/PersistenceExporter/Storage/OrchestratedFileWriterTests.swift b/Tests/ExportersTests/PersistenceExporter/Storage/OrchestratedFileWriterTests.swift index 304f7c357..daca8d13c 100644 --- a/Tests/ExportersTests/PersistenceExporter/Storage/OrchestratedFileWriterTests.swift +++ b/Tests/ExportersTests/PersistenceExporter/Storage/OrchestratedFileWriterTests.swift @@ -19,7 +19,7 @@ class OrchestratedFileWriterTests: XCTestCase { super.tearDown() } - func testItWritesDataToSingleFile() throws { + @MainActor func testItWritesDataToSingleFile() throws { let expectation = expectation(description: "write completed") let writer = OrchestratedFileWriter( orchestrator: FilesOrchestrator(directory: temporaryDirectory, @@ -47,7 +47,7 @@ class OrchestratedFileWriterTests: XCTestCase { XCTAssertEqual(try temporaryDirectory.files()[0].read(), data) } - func testGivenErrorVerbosity_whenIndividualDataExceedsMaxWriteSize_itDropsDataAndPrintsError() throws { + @MainActor func testGivenErrorVerbosity_whenIndividualDataExceedsMaxWriteSize_itDropsDataAndPrintsError() throws { let expectation1 = expectation(description: "write completed") let expectation2 = expectation(description: "second write completed") diff --git a/Tests/ExportersTests/Prometheus/PrometheusExporterTests.swift b/Tests/ExportersTests/Prometheus/PrometheusExporterTests.swift index e4d0be49f..763e604e1 100644 --- a/Tests/ExportersTests/Prometheus/PrometheusExporterTests.swift +++ b/Tests/ExportersTests/Prometheus/PrometheusExporterTests.swift @@ -16,7 +16,7 @@ class PrometheusExporterTests: XCTestCase { let metricPushIntervalSec = 0.05 let waitDuration = 0.1 + 0.1 - func testMetricsHttpServerAsync() { + @MainActor func testMetricsHttpServerAsync() { let promOptions = PrometheusExporterOptions(url: "http://localhost:9184/metrics/") let promExporter = PrometheusExporter(options: promOptions) let metricsHttpServer = PrometheusExporterHttpServer(exporter: promExporter) diff --git a/Tests/ImportersTests/SwiftMetricsShim/SwiftMetricsShimTests.swift b/Tests/ImportersTests/SwiftMetricsShim/SwiftMetricsShimTests.swift index 73ea567c6..d9e92d8c3 100644 --- a/Tests/ImportersTests/SwiftMetricsShim/SwiftMetricsShimTests.swift +++ b/Tests/ImportersTests/SwiftMetricsShim/SwiftMetricsShimTests.swift @@ -72,7 +72,7 @@ class SwiftMetricsShimTests: XCTestCase { // MARK: - Test Lifecycle - func testDestroy() { + @MainActor func testDestroy() { let handler = metrics.makeCounter(label: "my_label", dimensions: []) XCTAssertEqual(metrics.metrics.count, 1) handler.increment(by: 1) @@ -83,7 +83,7 @@ class SwiftMetricsShimTests: XCTestCase { // MARK: - Test Metric: Counter - func testCounter() throws { + @MainActor func testCounter() throws { let counter = Counter(label: "my_counter") counter.increment() @@ -97,7 +97,7 @@ class SwiftMetricsShimTests: XCTestCase { XCTAssertNil(data.attributes["label_one"]) } - func testCounter_withLabels() throws { + @MainActor func testCounter_withLabels() throws { let counter = Counter(label: "my_counter", dimensions: [("label_one", "value")]) counter.increment(by: 5) @@ -113,7 +113,7 @@ class SwiftMetricsShimTests: XCTestCase { // MARK: - Test Metric: Gauge - func testGauge() throws { + @MainActor func testGauge() throws { let gauge = Gauge(label: "my_gauge") gauge.record(100) @@ -129,7 +129,7 @@ class SwiftMetricsShimTests: XCTestCase { // MARK: - Test Metric: Histogram - func testHistogram() throws { + @MainActor func testHistogram() throws { let histogram = Gauge(label: "my_histogram", dimensions: [], aggregate: true) histogram.record(100) @@ -145,7 +145,7 @@ class SwiftMetricsShimTests: XCTestCase { // MARK: - Test Metric: Summary - func testSummary() throws { + @MainActor func testSummary() throws { let timer = CoreMetrics.Timer(label: "my_timer") timer.recordSeconds(1) @@ -161,7 +161,7 @@ class SwiftMetricsShimTests: XCTestCase { // MARK: - Test Concurrency - func testConcurrency() throws { + @MainActor func testConcurrency() throws { DispatchQueue.concurrentPerform(iterations: 5) { _ in let counter = Counter(label: "my_counter") counter.increment() diff --git a/Tests/InstrumentationTests/URLSessionTests/URLSessionInstrumentationTests.swift b/Tests/InstrumentationTests/URLSessionTests/URLSessionInstrumentationTests.swift index 6bb07a4ed..41b008eae 100644 --- a/Tests/InstrumentationTests/URLSessionTests/URLSessionInstrumentationTests.swift +++ b/Tests/InstrumentationTests/URLSessionTests/URLSessionInstrumentationTests.swift @@ -21,8 +21,8 @@ class URLSessionInstrumentationTests: XCTestCase { public var receivedErrorCalled: Bool = false } - class SessionDelegate: NSObject, URLSessionDelegate, URLSessionDataDelegate { - var semaphore: DispatchSemaphore + final class SessionDelegate: NSObject, URLSessionDelegate, URLSessionDataDelegate, @unchecked Sendable { + let semaphore: DispatchSemaphore init(semaphore: DispatchSemaphore) { self.semaphore = semaphore @@ -37,29 +37,42 @@ class URLSessionInstrumentationTests: XCTestCase { } } - class CountingSessionDelegate: NSObject, URLSessionDelegate, URLSessionDataDelegate { - var callCount: Int = 0 + final class CountingSessionDelegate: NSObject, URLSessionDelegate, URLSessionDataDelegate, @unchecked Sendable { + private let lock = NSLock() + private var _callCount: Int = 0 + + var callCount: Int { + lock.lock() + defer { lock.unlock() } + return _callCount + } + + private func incrementCallCount() { + lock.lock() + defer { lock.unlock() } + _callCount += 1 + } func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { - callCount += 1 + incrementCallCount() } func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - callCount += 1 + incrementCallCount() } func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { - callCount += 1 + incrementCallCount() } } - static var requestCopy: URLRequest! - static var responseCopy: HTTPURLResponse! + nonisolated(unsafe) static var requestCopy: URLRequest! + nonisolated(unsafe) static var responseCopy: HTTPURLResponse! - static var activeBaggage: Baggage! - static var customBaggage: Baggage! + nonisolated(unsafe) static var activeBaggage: Baggage! + nonisolated(unsafe) static var customBaggage: Baggage! - static var config = URLSessionInstrumentationConfiguration(shouldRecordPayload: nil, + nonisolated(unsafe) static var config = URLSessionInstrumentationConfiguration(shouldRecordPayload: nil, shouldInstrument: { req in checker.shouldInstrumentCalled = true if req.url?.path == "/dontinstrument" || req.url?.host == "dontinstrument.com" { @@ -98,13 +111,13 @@ class URLSessionInstrumentationTests: XCTestCase { URLSessionInstrumentationTests.checker.receivedErrorCalled = true }, baggageProvider: { _, _ in - customBaggage + return customBaggage }) - static var checker = Check() - static var semaphore: DispatchSemaphore! + nonisolated(unsafe) static var checker = Check() + nonisolated(unsafe) static var semaphore: DispatchSemaphore! var sessionDelegate: SessionDelegate! - static var instrumentation: URLSessionInstrumentation! + nonisolated(unsafe) static var instrumentation: URLSessionInstrumentation! static let server = HttpTestServer(url: URL(string: "http://localhost:33333"), config: nil) @@ -759,7 +772,7 @@ class URLSessionInstrumentationTests: XCTestCase { XCTAssertNotNil(URLSessionInstrumentationTests.requestCopy?.allHTTPHeaderFields?[W3CTraceContextPropagator.traceparent]) } - public func testNonInstrumentedRequestCompletes() { + @MainActor public func testNonInstrumentedRequestCompletes() { let request = URLRequest(url: URL(string: "http://localhost:33333/dontinstrument")!) let expectation = expectation(description: "Non-instrumented request completes") diff --git a/Tests/Shared/TestUtils/HttpTestServer.swift b/Tests/Shared/TestUtils/HttpTestServer.swift index 60f712774..0c99bcdb5 100644 --- a/Tests/Shared/TestUtils/HttpTestServer.swift +++ b/Tests/Shared/TestUtils/HttpTestServer.swift @@ -19,7 +19,7 @@ import Musl /// A unified HTTP test server using POSIX sockets /// Combines functionality from both OTLP exporter tests and URLSession instrumentation tests -public class HttpTestServer { +public class HttpTestServer: @unchecked Sendable { private var serverSocket: Int32 = -1 public private(set) var serverPort: Int = 0 private var isRunning = false @@ -518,7 +518,7 @@ internal struct HTTPRequestData { } /// HTTPMethod for compatibility -public struct HTTPMethod: Equatable, RawRepresentable { +public struct HTTPMethod: Equatable, RawRepresentable, Sendable { public let rawValue: String public init(rawValue: String) { @@ -573,7 +573,7 @@ public struct HTTPHeader { } /// HTTPResponseStatus for compatibility -public struct HTTPResponseStatus { +public struct HTTPResponseStatus: Sendable { public let code: UInt public let reasonPhrase: String