diff --git a/Package.swift b/Package.swift index 2366ce3c..435c511e 100644 --- a/Package.swift +++ b/Package.swift @@ -3,6 +3,12 @@ import PackageDescription let package = Package( name: "swift-distributed-tracing", + platforms: [ + .macOS(.v10_15), + .iOS(.v13), + .tvOS(.v13), + .watchOS(.v6), + ], products: [ .library(name: "Instrumentation", targets: ["Instrumentation"]), .library(name: "Tracing", targets: ["Tracing"]), diff --git a/Package@swift-5.2.swift b/Package@swift-5.2.swift deleted file mode 100644 index 111d4fff..00000000 --- a/Package@swift-5.2.swift +++ /dev/null @@ -1,62 +0,0 @@ -// swift-tools-version:5.2 -import PackageDescription - -let package = Package( - name: "swift-distributed-tracing", - products: [ - .library(name: "Instrumentation", targets: ["Instrumentation"]), - .library(name: "Tracing", targets: ["Tracing"]), - ], - dependencies: [ - .package(url: "https://github.com/apple/swift-distributed-tracing-baggage.git", .upToNextMinor(from: "0.4.1")), - ], - targets: [ - // ==== -------------------------------------------------------------------------------------------------------- - // MARK: Instrumentation - - .target( - name: "Instrumentation", - dependencies: [ - .product(name: "InstrumentationBaggage", package: "swift-distributed-tracing-baggage"), - ] - ), - .testTarget( - name: "InstrumentationTests", - dependencies: [ - .target(name: "Instrumentation"), - ] - ), - - // ==== -------------------------------------------------------------------------------------------------------- - // MARK: Tracing - - .target( - name: "Tracing", - dependencies: [ - .target(name: "Instrumentation"), - ] - ), - .testTarget( - name: "TracingTests", - dependencies: [ - .target(name: "Tracing"), - ] - ), - - // ==== -------------------------------------------------------------------------------------------------------- - // MARK: Performance / Benchmarks - - .target( - name: "_TracingBenchmarks", - dependencies: [ - .product(name: "InstrumentationBaggage", package: "swift-distributed-tracing-baggage"), - .target(name: "Tracing"), - .target(name: "_TracingBenchmarkTools"), - ] - ), - .target( - name: "_TracingBenchmarkTools", - dependencies: [] - ), - ] -) diff --git a/Package@swift-5.3.swift b/Package@swift-5.3.swift deleted file mode 100644 index 111d4fff..00000000 --- a/Package@swift-5.3.swift +++ /dev/null @@ -1,62 +0,0 @@ -// swift-tools-version:5.2 -import PackageDescription - -let package = Package( - name: "swift-distributed-tracing", - products: [ - .library(name: "Instrumentation", targets: ["Instrumentation"]), - .library(name: "Tracing", targets: ["Tracing"]), - ], - dependencies: [ - .package(url: "https://github.com/apple/swift-distributed-tracing-baggage.git", .upToNextMinor(from: "0.4.1")), - ], - targets: [ - // ==== -------------------------------------------------------------------------------------------------------- - // MARK: Instrumentation - - .target( - name: "Instrumentation", - dependencies: [ - .product(name: "InstrumentationBaggage", package: "swift-distributed-tracing-baggage"), - ] - ), - .testTarget( - name: "InstrumentationTests", - dependencies: [ - .target(name: "Instrumentation"), - ] - ), - - // ==== -------------------------------------------------------------------------------------------------------- - // MARK: Tracing - - .target( - name: "Tracing", - dependencies: [ - .target(name: "Instrumentation"), - ] - ), - .testTarget( - name: "TracingTests", - dependencies: [ - .target(name: "Tracing"), - ] - ), - - // ==== -------------------------------------------------------------------------------------------------------- - // MARK: Performance / Benchmarks - - .target( - name: "_TracingBenchmarks", - dependencies: [ - .product(name: "InstrumentationBaggage", package: "swift-distributed-tracing-baggage"), - .target(name: "Tracing"), - .target(name: "_TracingBenchmarkTools"), - ] - ), - .target( - name: "_TracingBenchmarkTools", - dependencies: [] - ), - ] -) diff --git a/Package@swift-5.4.swift b/Package@swift-5.4.swift deleted file mode 100644 index 111d4fff..00000000 --- a/Package@swift-5.4.swift +++ /dev/null @@ -1,62 +0,0 @@ -// swift-tools-version:5.2 -import PackageDescription - -let package = Package( - name: "swift-distributed-tracing", - products: [ - .library(name: "Instrumentation", targets: ["Instrumentation"]), - .library(name: "Tracing", targets: ["Tracing"]), - ], - dependencies: [ - .package(url: "https://github.com/apple/swift-distributed-tracing-baggage.git", .upToNextMinor(from: "0.4.1")), - ], - targets: [ - // ==== -------------------------------------------------------------------------------------------------------- - // MARK: Instrumentation - - .target( - name: "Instrumentation", - dependencies: [ - .product(name: "InstrumentationBaggage", package: "swift-distributed-tracing-baggage"), - ] - ), - .testTarget( - name: "InstrumentationTests", - dependencies: [ - .target(name: "Instrumentation"), - ] - ), - - // ==== -------------------------------------------------------------------------------------------------------- - // MARK: Tracing - - .target( - name: "Tracing", - dependencies: [ - .target(name: "Instrumentation"), - ] - ), - .testTarget( - name: "TracingTests", - dependencies: [ - .target(name: "Tracing"), - ] - ), - - // ==== -------------------------------------------------------------------------------------------------------- - // MARK: Performance / Benchmarks - - .target( - name: "_TracingBenchmarks", - dependencies: [ - .product(name: "InstrumentationBaggage", package: "swift-distributed-tracing-baggage"), - .target(name: "Tracing"), - .target(name: "_TracingBenchmarkTools"), - ] - ), - .target( - name: "_TracingBenchmarkTools", - dependencies: [] - ), - ] -) diff --git a/Package@swift-5.5.swift b/Package@swift-5.5.swift deleted file mode 100644 index 111d4fff..00000000 --- a/Package@swift-5.5.swift +++ /dev/null @@ -1,62 +0,0 @@ -// swift-tools-version:5.2 -import PackageDescription - -let package = Package( - name: "swift-distributed-tracing", - products: [ - .library(name: "Instrumentation", targets: ["Instrumentation"]), - .library(name: "Tracing", targets: ["Tracing"]), - ], - dependencies: [ - .package(url: "https://github.com/apple/swift-distributed-tracing-baggage.git", .upToNextMinor(from: "0.4.1")), - ], - targets: [ - // ==== -------------------------------------------------------------------------------------------------------- - // MARK: Instrumentation - - .target( - name: "Instrumentation", - dependencies: [ - .product(name: "InstrumentationBaggage", package: "swift-distributed-tracing-baggage"), - ] - ), - .testTarget( - name: "InstrumentationTests", - dependencies: [ - .target(name: "Instrumentation"), - ] - ), - - // ==== -------------------------------------------------------------------------------------------------------- - // MARK: Tracing - - .target( - name: "Tracing", - dependencies: [ - .target(name: "Instrumentation"), - ] - ), - .testTarget( - name: "TracingTests", - dependencies: [ - .target(name: "Tracing"), - ] - ), - - // ==== -------------------------------------------------------------------------------------------------------- - // MARK: Performance / Benchmarks - - .target( - name: "_TracingBenchmarks", - dependencies: [ - .product(name: "InstrumentationBaggage", package: "swift-distributed-tracing-baggage"), - .target(name: "Tracing"), - .target(name: "_TracingBenchmarkTools"), - ] - ), - .target( - name: "_TracingBenchmarkTools", - dependencies: [] - ), - ] -) diff --git a/README.md b/README.md index 8e67175b..7588e9fa 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ This project uses the context progagation type defined independently in: + [Extracting & injecting Baggage](#extracting--injecting-baggage) + [Tracing your library](#tracing-your-library) * In-Depth Guide for: **Instrument developers** - + [Creating an `Instrument`](#instrument-developers--creating-an-instrument) + + [Creating an `InstrumentProtocol`](#instrument-developers--creating-an-instrument) + [Creating a `Tracer`](#creating-a--tracer-) * [Contributing](#contributing) @@ -119,7 +119,7 @@ To your main target, add a dependency on `Tracing` library and the instrument yo ), ``` -Then (in an application, libraries should _never_ invoke `bootstrap`), you will want to bootstrap the specific tracer you want to use in your application. A `Tracer` is a type of `Instrument` and can be offered used to globally bootstrap the tracing system, like this: +Then (in an application, libraries should _never_ invoke `bootstrap`), you will want to bootstrap the specific tracer you want to use in your application. A `Tracer` is a type of `InstrumentProtocol` and can be offered used to globally bootstrap the tracing system, like this: ```swift @@ -295,7 +295,7 @@ To your main target, add a dependency on the `Instrumentation library` and the i Instead of providing each instrumented library with a specific instrument explicitly, you *bootstrap* the `InstrumentationSystem` which acts as a singleton that libraries/frameworks access when calling out to the configured -`Instrument`: +`InstrumentProtocol`: ```swift InstrumentationSystem.bootstrap(FancyInstrument()) @@ -316,7 +316,7 @@ This is because tracing systems may attempt to emit metrics about their status e #### Bootstrapping multiple instruments using MultiplexInstrument -It is important to note that `InstrumentationSystem.bootstrap(_: Instrument)` must only be called once. In case you +It is important to note that `InstrumentationSystem.bootstrap(_: InstrumentProtocol)` must only be called once. In case you want to bootstrap the system to use multiple instruments, you group them in a `MultiplexInstrument` first, which you then pass along to the `bootstrap` method like this: @@ -444,7 +444,7 @@ Spans form hierarchies with their parent spans, and end up being visualized usin The above trace is achieved by starting and ending spans in all the mentioned functions, for example, like this: ```swift -let tracer: Tracer +let tracer: any TracerProtocol func makeDinner(context: LoggingContext) async throws -> Meal { tracer.withSpan(operationName: "makeDinner", context) { @@ -481,7 +481,7 @@ func get(url: String, context: LoggingContext) { } ``` -On the receiving side, an HTTP server should use the following `Instrument` API to extract the HTTP headers of the given +On the receiving side, an HTTP server should use the following `InstrumentProtocol` API to extract the HTTP headers of the given `HTTPRequest` into: ```swift @@ -538,10 +538,10 @@ func get(url: String, context: LoggingContext) { ## Instrument developers: Creating an instrument -Creating an instrument means adopting the `Instrument` protocol (or `Tracer` in case you develop a tracer). -`Instrument` is part of the `Instrumentation` library & `Tracing` contains the `Tracer` protocol. +Creating an instrument means adopting the `InstrumentProtocol` protocol (or `TracerProtocol` in case you develop a tracer). +`InstrumentProtocol` is part of the `Instrumentation` library & `Tracing` contains the `TracerProtocol` protocol. -`Instrument` has two requirements: +`InstrumentProtocol` has two requirements: 1. A method to inject values inside a `LoggingContext` into a generic carrier (e.g. HTTP headers) 2. A method to extract values from a generic carrier (e.g. HTTP headers) and store them in a `LoggingContext` @@ -552,11 +552,11 @@ act on the provided information or to add additional information to be carried a > Check out the [`Baggage` documentation](https://github.com/apple/swift-distributed-tracing-baggage) for more information on how to retrieve values from the `LoggingContext` and how to set values on it. -### Creating a `Tracer` +### Creating a Tracer When creating a tracer you need to create two types: -1. Your tracer conforming to `Tracer` +1. Your tracer conforming to `TracerProtocol` 2. A span class conforming to `Span` > The `Span` conforms to the standard rules defined in [OpenTelemetry](https://github.com/open-telemetry/opentelemetry-specification/blob/v0.7.0/specification/trace/api.md#span), so if unsure about usage patterns, you can refer to this specification and examples referring to it. diff --git a/Sources/Instrumentation/Instrument.swift b/Sources/Instrumentation/Instrument.swift index 7224b7e1..59ff81c8 100644 --- a/Sources/Instrumentation/Instrument.swift +++ b/Sources/Instrumentation/Instrument.swift @@ -50,9 +50,9 @@ public protocol Injector: _SwiftInstrumentationSendable { /// Conforming types are usually cross-cutting tools like tracers. They are agnostic of what specific `Carrier` is used /// to propagate metadata across boundaries, but instead just specify what values to use for which keys. -public protocol Instrument: _SwiftInstrumentationSendable { +public protocol InstrumentProtocol: _SwiftInstrumentationSendable { /// Extract values from a `Carrier` by using the given extractor and inject them into the given `Baggage`. - /// It's quite common for `Instrument`s to come up with new values if they weren't passed along in the given `Carrier`. + /// It's quite common for `InstrumentProtocol`s to come up with new values if they weren't passed along in the given `Carrier`. /// /// - Parameters: /// - carrier: The `Carrier` that was used to propagate values across boundaries. diff --git a/Sources/Instrumentation/InstrumentationSystem.swift b/Sources/Instrumentation/InstrumentationSystem.swift index 9c512a87..ac72be2f 100644 --- a/Sources/Instrumentation/InstrumentationSystem.swift +++ b/Sources/Instrumentation/InstrumentationSystem.swift @@ -15,23 +15,24 @@ import InstrumentationBaggage /// `InstrumentationSystem` is a global facility where the default cross-cutting tool can be configured. -/// It is set up just once in a given program to select the desired ``Instrument`` implementation. +/// It is set up just once in a given program to select the desired ``InstrumentProtocol`` implementation. /// /// # Bootstrap multiple Instruments /// If you need to use more that one cross-cutting tool you can do so by using ``MultiplexInstrument``. /// -/// # Access the Instrument -/// ``instrument``: Returns whatever you passed to ``bootstrap(_:)`` as an ``Instrument``. +/// # Access the InstrumentProtocol +/// ``instrument``: Returns whatever you passed to ``bootstrap(_:)`` as an ``InstrumentProtocol``. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage public enum InstrumentationSystem { private static let lock = ReadWriteLock() - private static var _instrument: Instrument = NoOpInstrument() + private static var _instrument: InstrumentProtocol = NoOpInstrument() private static var isInitialized = false - /// Globally select the desired ``Instrument`` implementation. + /// Globally select the desired ``InstrumentProtocol`` implementation. /// - /// - Parameter instrument: The ``Instrument`` you want to share globally within your system. + /// - Parameter instrument: The ``InstrumentProtocol`` you want to share globally within your system. /// - Warning: Do not call this method more than once. This will lead to a crash. - public static func bootstrap(_ instrument: Instrument) { + public static func bootstrap(_ instrument: InstrumentProtocol) { self.lock.withWriterLock { precondition( !self.isInitialized, """ @@ -47,23 +48,24 @@ public enum InstrumentationSystem { /// For testing scenarios one may want to set instruments multiple times, rather than the set-once semantics enforced by ``bootstrap(_:)``. /// /// - Parameter instrument: the instrument to boostrap the system with, if `nil` the ``NoOpInstrument`` is bootstrapped. - internal static func bootstrapInternal(_ instrument: Instrument?) { + internal static func bootstrapInternal(_ instrument: InstrumentProtocol?) { self.lock.withWriterLock { self._instrument = instrument ?? NoOpInstrument() } } - /// Returns the globally configured ``Instrument``. + /// Returns the globally configured ``InstrumentProtocol``. /// - /// Defaults to a no-op ``Instrument`` if ``bootstrap(_:)`` wasn't called before. - public static var instrument: Instrument { + /// Defaults to a no-op ``InstrumentProtocol`` if ``bootstrap(_:)`` wasn't called before. + public static var instrument: InstrumentProtocol { self.lock.withReaderLock { self._instrument } } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage extension InstrumentationSystem { /// :nodoc: INTERNAL API: Do Not Use - public static func _findInstrument(where predicate: (Instrument) -> Bool) -> Instrument? { + public static func _findInstrument(where predicate: (InstrumentProtocol) -> Bool) -> InstrumentProtocol? { self.lock.withReaderLock { if let multiplex = self._instrument as? MultiplexInstrument { return multiplex.firstInstrument(where: predicate) diff --git a/Sources/Instrumentation/MultiplexInstrument.swift b/Sources/Instrumentation/MultiplexInstrument.swift index 66793578..fd96c196 100644 --- a/Sources/Instrumentation/MultiplexInstrument.swift +++ b/Sources/Instrumentation/MultiplexInstrument.swift @@ -14,27 +14,27 @@ import InstrumentationBaggage -/// A pseudo-``Instrument`` that may be used to instrument using multiple other ``Instrument``s across a +/// A pseudo-``InstrumentProtocol`` that may be used to instrument using multiple other ``InstrumentProtocol``s across a /// common `Baggage`. public struct MultiplexInstrument { - private var instruments: [Instrument] + private var instruments: [InstrumentProtocol] /// Create a ``MultiplexInstrument``. /// - /// - Parameter instruments: An array of ``Instrument``s, each of which will be used to ``Instrument/inject(_:into:using:)`` or ``Instrument/extract(_:into:using:)`` + /// - Parameter instruments: An array of ``InstrumentProtocol``s, each of which will be used to ``InstrumentProtocol/inject(_:into:using:)`` or ``InstrumentProtocol/extract(_:into:using:)`` /// through the same `Baggage`. - public init(_ instruments: [Instrument]) { + public init(_ instruments: [InstrumentProtocol]) { self.instruments = instruments } } extension MultiplexInstrument { - func firstInstrument(where predicate: (Instrument) -> Bool) -> Instrument? { + func firstInstrument(where predicate: (InstrumentProtocol) -> Bool) -> InstrumentProtocol? { self.instruments.first(where: predicate) } } -extension MultiplexInstrument: Instrument { +extension MultiplexInstrument: InstrumentProtocol { public func inject(_ baggage: Baggage, into carrier: inout Carrier, using injector: Inject) where Inject: Injector, Carrier == Inject.Carrier { diff --git a/Sources/Instrumentation/NoOpInstrument.swift b/Sources/Instrumentation/NoOpInstrument.swift index 437b31b7..040da89b 100644 --- a/Sources/Instrumentation/NoOpInstrument.swift +++ b/Sources/Instrumentation/NoOpInstrument.swift @@ -14,8 +14,8 @@ import InstrumentationBaggage -/// A "no op" implementation of an ``Instrument``. -public struct NoOpInstrument: Instrument { +/// A "no op" implementation of an ``InstrumentProtocol``. +public struct NoOpInstrument: InstrumentProtocol { public init() {} public func inject(_ baggage: Baggage, into carrier: inout Carrier, using injector: Inject) diff --git a/Sources/Tracing/Docs.docc/InDepthGuide.md b/Sources/Tracing/Docs.docc/InDepthGuide.md index 052f9f27..9dd1eeb3 100644 --- a/Sources/Tracing/Docs.docc/InDepthGuide.md +++ b/Sources/Tracing/Docs.docc/InDepthGuide.md @@ -8,7 +8,7 @@ When instrumenting server applications there are typically three parties involve 1. **Application developers** create server-side applications 2. **Library/Framework developers** provide building blocks to create these applications -3. **Instrument developers** provide tools to collect distributed metadata about your application +3. **InstrumentProtocol developers** provide tools to collect distributed metadata about your application For applications to be instrumented correctly these three parts have to play along nicely. @@ -42,7 +42,7 @@ To your main target, add a dependency on the `Instrumentation library` and the i Instead of providing each instrumented library with a specific instrument explicitly, you *bootstrap* the `InstrumentationSystem` which acts as a singleton that libraries/frameworks access when calling out to the configured -`Instrument`: +`InstrumentProtocol`: ```swift InstrumentationSystem.bootstrap(FancyInstrument()) @@ -63,7 +63,7 @@ This is because tracing systems may attempt to emit metrics about their status e #### Bootstrapping multiple instruments using MultiplexInstrument -It is important to note that `InstrumentationSystem.bootstrap(_: Instrument)` must only be called once. In case you +It is important to note that `InstrumentationSystem.bootstrap(_: InstrumentProtocol)` must only be called once. In case you want to bootstrap the system to use multiple instruments, you group them in a `MultiplexInstrument` first, which you then pass along to the `bootstrap` method like this: @@ -188,7 +188,7 @@ Spans form hierarchies with their parent spans, and end up being visualized usin The above trace is achieved by starting and ending spans in all the mentioned functions, for example, like this: ```swift -let tracer: Tracer +let tracer: any TracerProtocol func makeDinner(context: LoggingContext) async throws -> Meal { tracer.withSpan(operationName: "makeDinner", context) { @@ -225,7 +225,7 @@ func get(url: String, context: LoggingContext) { } ``` -On the receiving side, an HTTP server should use the following `Instrument` API to extract the HTTP headers of the given +On the receiving side, an HTTP server should use the following `InstrumentProtocol` API to extract the HTTP headers of the given `HTTPRequest` into: ```swift @@ -280,12 +280,12 @@ func get(url: String, context: LoggingContext) { > In the above example we used the semantic `http.method` attribute that gets exposed via the `TracingOpenTelemetrySupport` library. -## Instrument developers: Creating an instrument +## InstrumentProtocol developers: Creating an instrument -Creating an instrument means adopting the `Instrument` protocol (or ``Tracer`` in case you develop a tracer). -`Instrument` is part of the `Instrumentation` library & `Tracing` contains the ``Tracer`` protocol. +Creating an instrument means adopting the `InstrumentProtocol` protocol (or ``Tracer`` in case you develop a tracer). +`InstrumentProtocol` is part of the `Instrumentation` library & `Tracing` contains the ``Tracer`` protocol. -`Instrument` has two requirements: +`InstrumentProtocol` has two requirements: 1. A method to inject values inside a `LoggingContext` into a generic carrier (e.g. HTTP headers) 2. A method to extract values from a generic carrier (e.g. HTTP headers) and store them in a `LoggingContext` diff --git a/Sources/Tracing/Docs.docc/index.md b/Sources/Tracing/Docs.docc/index.md index f541c062..db770d34 100644 --- a/Sources/Tracing/Docs.docc/index.md +++ b/Sources/Tracing/Docs.docc/index.md @@ -62,7 +62,7 @@ To your main target, add a dependency on the `Tracing` library and the instrumen ), ``` -Then (in an application, libraries should _never_ invoke `bootstrap`), you will want to bootstrap the specific tracer you want to use in your application. A ``Tracer`` is a type of `Instrument` and can be offered used to globally bootstrap the tracing system, like this: +Then (in an application, libraries should _never_ invoke `bootstrap`), you will want to bootstrap the specific tracer you want to use in your application. A ``Tracer`` is a type of `InstrumentProtocol` and can be offered used to globally bootstrap the tracing system, like this: ```swift diff --git a/Sources/Tracing/InstrumentationSystem+Tracing.swift b/Sources/Tracing/InstrumentationSystem+Tracing.swift index 1be4949c..a67710e7 100644 --- a/Sources/Tracing/InstrumentationSystem+Tracing.swift +++ b/Sources/Tracing/InstrumentationSystem+Tracing.swift @@ -15,13 +15,29 @@ @_exported import Instrumentation extension InstrumentationSystem { + #if swift(>=5.7.0) /// Returns the ``Tracer`` bootstrapped as part of the `InstrumentationSystem`. /// /// If the system was bootstrapped with a `MultiplexInstrument` this function attempts to locate the _first_ /// tracing instrument as passed to the multiplex instrument. If none is found, a ``NoOpTracer`` is returned. /// /// - Returns: A ``Tracer`` if the system was bootstrapped with one, and ``NoOpTracer`` otherwise. - public static var tracer: Tracer { - (self._findInstrument(where: { $0 is Tracer }) as? Tracer) ?? NoOpTracer() + public static var tracer: any TracerProtocol { + let found: (any TracerProtocol)? = + (self._findInstrument(where: { $0 is (any TracerProtocol) }) as? (any TracerProtocol)) + return found ?? NoOpTracer() + } + #endif + + /// Returns the ``Tracer`` bootstrapped as part of the `InstrumentationSystem`. + /// + /// If the system was bootstrapped with a `MultiplexInstrument` this function attempts to locate the _first_ + /// tracing instrument as passed to the multiplex instrument. If none is found, a ``NoOpTracer`` is returned. + /// + /// - Returns: A ``Tracer`` if the system was bootstrapped with one, and ``NoOpTracer`` otherwise. + public static var legacyTracer: any LegacyTracerProtocol { + let found: (any LegacyTracerProtocol)? = + (self._findInstrument(where: { $0 is (any LegacyTracerProtocol) }) as? (any LegacyTracerProtocol)) + return found ?? NoOpTracer() } } diff --git a/Sources/Tracing/NoOpTracer.swift b/Sources/Tracing/NoOpTracer.swift index 8d18a874..8bfcc03c 100644 --- a/Sources/Tracing/NoOpTracer.swift +++ b/Sources/Tracing/NoOpTracer.swift @@ -16,20 +16,21 @@ import Dispatch @_exported import Instrumentation @_exported import InstrumentationBaggage -/// No operation ``Tracer``, used when no tracing is required. -public struct NoOpTracer: Tracer { +/// Tracer that ignores all operations, used when no tracing is required. +public struct NoOpTracer: LegacyTracerProtocol { + public typealias Span = NoOpSpan + public init() {} - public func startSpan( - _ operationName: String, - baggage: Baggage, - ofKind kind: SpanKind, - at time: DispatchWallTime, - function: String, - file fileID: String, - line: UInt - ) -> Span { - NoOpSpan(operationName: operationName, baggage: baggage) + public func startAnySpan(_ operationName: String, + baggage: @autoclosure () -> Baggage, + ofKind kind: SpanKind, + at time: DispatchWallTime, + function: String, + file fileID: String, + line: UInt) -> any SpanProtocol + { + NoOpSpan(baggage: baggage()) } public func forceFlush() {} @@ -46,22 +47,22 @@ public struct NoOpTracer: Tracer { // no-op } - public final class NoOpSpan: Span { + public final class NoOpSpan: SpanProtocol { public let baggage: Baggage - public let isRecording = false + public var isRecording: Bool { + false + } - private let _operationName: String public var operationName: String { get { - self._operationName + "noop" } set { // ignore } } - public init(operationName: String, baggage: Baggage) { - self._operationName = operationName + public init(baggage: Baggage) { self.baggage = baggage } @@ -87,3 +88,19 @@ public struct NoOpTracer: Tracer { } } } + +#if swift(>=5.7.0) +extension NoOpTracer: TracerProtocol { + public func startSpan( + _ operationName: String, + baggage: @autoclosure () -> Baggage, + ofKind kind: SpanKind, + at time: DispatchWallTime, + function: String, + file fileID: String, + line: UInt + ) -> NoOpSpan { + NoOpSpan(baggage: baggage()) + } +} +#endif diff --git a/Sources/Tracing/Span.swift b/Sources/Tracing/SpanProtocol.swift similarity index 99% rename from Sources/Tracing/Span.swift rename to Sources/Tracing/SpanProtocol.swift index ff271a78..2c44def6 100644 --- a/Sources/Tracing/Span.swift +++ b/Sources/Tracing/SpanProtocol.swift @@ -26,7 +26,7 @@ import struct Dispatch.DispatchWallTime /// Creating a `Span` is delegated to a ``Tracer`` and end users should never create them directly. /// /// - SeeAlso: For more details refer to the [OpenTelemetry Specification: Span](https://github.com/open-telemetry/opentelemetry-specification/blob/v0.7.0/specification/trace/api.md#span) which this type is compatible with. -public protocol Span: AnyObject, _SwiftTracingSendableSpan { +public protocol SpanProtocol: AnyObject, _SwiftTracingSendableSpan { /// The read-only `Baggage` of this `Span`, set when starting this `Span`. var baggage: Baggage { get } @@ -48,10 +48,12 @@ public protocol Span: AnyObject, _SwiftTracingSendableSpan { var operationName: String { get set } /// Set the status. + /// /// - Parameter status: The status of this `Span`. func setStatus(_ status: SpanStatus) /// Add a ``SpanEvent`` in place. + /// /// - Parameter event: The ``SpanEvent`` to add to this `Span`. func addEvent(_ event: SpanEvent) @@ -69,6 +71,7 @@ public protocol Span: AnyObject, _SwiftTracingSendableSpan { var isRecording: Bool { get } /// Add a ``SpanLink`` in place. + /// /// - Parameter link: The `SpanLink` to add to this `Span`. func addLink(_ link: SpanLink) @@ -88,7 +91,7 @@ public protocol Span: AnyObject, _SwiftTracingSendableSpan { func end(at time: DispatchWallTime) } -extension Span { +extension SpanProtocol { /// End this `Span` at the current time. /// /// ### Rules about ending Spans @@ -108,14 +111,15 @@ extension Span { } /// Adds a ``SpanLink`` between this `Span` and the given `Span`. + /// /// - Parameter other: The `Span` to link to. /// - Parameter attributes: The ``SpanAttributes`` describing this link. Defaults to no attributes. - public func addLink(_ other: Span, attributes: SpanAttributes = [:]) { + public func addLink(_ other: SpanProtocol, attributes: SpanAttributes = [:]) { self.addLink(SpanLink(baggage: other.baggage, attributes: attributes)) } } -extension Span { +extension SpanProtocol { /// Record a failure described by the given error. /// /// - Parameters: diff --git a/Sources/Tracing/Tracer.swift b/Sources/Tracing/Tracer.swift index e0b7eb2f..b675e677 100644 --- a/Sources/Tracing/Tracer.swift +++ b/Sources/Tracing/Tracer.swift @@ -16,14 +16,31 @@ import Dispatch @_exported import Instrumentation @_exported import InstrumentationBaggage -/// An `Instrument` with added functionality for distributed tracing. It uses the span-based tracing model and is -/// based on the OpenTracing/OpenTelemetry spec. -public protocol Tracer: Instrument { - /// Start a new ``Span`` with the given `Baggage` at a given time. +/// Convenience access to static `startSpan` and `withSpan` APIs invoked on the globally bootstrapped tracer. +/// +/// If no tracer was bootstrapped using ``InstrumentationSystem/bootstrap(_:)`` these operations are no-ops. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage +public enum Tracer { + // namespace for short-hand operations on global bootstrapped tracer +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage +extension Tracer { + /// Start a new ``Span`` using the global bootstrapped tracer reimplementation. /// - /// - Note: Prefer to use `withSpan` to start a span as it automatically takes care of ending the span, - /// and recording errors when thrown. Use `startSpan` iff you need to pass the span manually to a different - /// location in your source code to end it. + /// The current task-local `Baggage` is picked up and provided to the underlying tracer. + /// It is also possible to pass a specific `baggage` explicitly, in which case attempting + /// to pick up the task-local baggage is prevented. This can be useful when we know that + /// we're about to start a top-level span, or if a span should be started from a different, + /// stored away previously, + /// + /// - Note: Prefer ``withSpan(_:baggage:ofKind:at:function:file:line:operation:)`` to start + /// a span as it automatically takes care of ending the span, and recording errors when thrown. + /// Use `startSpan` iff you need to pass the span manually to a different + /// location in your source code to end it. + /// + /// - Warning: You must `end()` the span when it the measured operation has completed explicitly, + /// otherwise the span object will potentially never be released nor reported. /// /// - Parameters: /// - operationName: The name of the operation being traced. This may be a handler function, database call, ... @@ -33,292 +50,163 @@ public protocol Tracer: Instrument { /// - function: The function name in which the span was started /// - fileID: The `fileID` where the span was started. /// - line: The file line where the span was started. - func startSpan( + static func startSpan( _ operationName: String, - baggage: Baggage, - ofKind kind: SpanKind, - at time: DispatchWallTime, - function: String, - file fileID: String, - line: UInt - ) -> Span - - /// Export all ended spans to the configured backend that have not yet been exported. - /// - /// This function should only be called in cases where it is absolutely necessary, - /// such as when using some FaaS providers that may suspend the process after an invocation, but before the backend exports the completed spans. - /// - /// This function should not block indefinitely, implementations should offer a configurable timeout for flush operations. - func forceFlush() -} - -extension Tracer { - #if swift(>=5.3.0) - /// Start a new ``Span`` with the given `Baggage` starting "now". - /// - /// - Parameters: - /// - operationName: The name of the operation being traced. This may be a handler function, database call, ... - /// - baggage: Baggage potentially containing trace identifiers of a parent ``Span``. - /// - kind: The ``SpanKind`` of the ``Span`` to be created. Defaults to ``SpanKind/internal``. - /// - function: The function name in which the span was started. - /// - fileID: The `fileID` where the span was started. - /// - line: The file line where the span was started. - public func startSpan( - _ operationName: String, - baggage: Baggage, + baggage: @autoclosure () -> Baggage = .current ?? .topLevel, ofKind kind: SpanKind = .internal, + at time: DispatchWallTime = .now(), function: String = #function, file fileID: String = #fileID, line: UInt = #line - ) -> Span { - self.startSpan( + ) -> any SpanProtocol { + // Effectively these end up calling the same method, however + // we try to not use the deprecated methods ourselves anyway + #if swift(>=5.7.0) + InstrumentationSystem.tracer.startSpan( operationName, - baggage: baggage, - ofKind: kind, - at: .now(), function: function, file: fileID, line: line ) - } - #else - /// Start a new ``Span`` with the given `Baggage` starting "now". - /// - /// - Parameters: - /// - operationName: The name of the operation being traced. This may be a handler function, database call, ... - /// - baggage: Baggage potentially containing trace identifiers of a parent ``Span``. - /// - kind: The ``SpanKind`` of the ``Span`` to be created. Defaults to ``SpanKind/internal``. - /// - function: The function name in which the span was started. - /// - file: The `file` where the span was started. - /// - line: The file line where the span was started. - public func startSpan( - _ operationName: String, - baggage: Baggage, - ofKind kind: SpanKind = .internal, - function: String = #function, - file: String = #file, - line: UInt = #line - ) -> Span { - self.startSpan( + #else + InstrumentationSystem.legacyTracer.startAnySpan( operationName, - baggage: baggage, + baggage: baggage(), ofKind: kind, - at: .now(), + at: time, function: function, - file: file, + file: fileID, line: line ) + #endif } - #endif -} - -// ==== ---------------------------------------------------------------------------------------------------------------- -// MARK: Starting spans: `withSpan` -extension Tracer { - #if swift(>=5.3.0) - /// Execute a specific task within a newly created ``Span``. + /// Start a new ``Span`` and automatically end when the `operation` completes, + /// including recording the `error` in case the operation throws. /// - /// DO NOT `end()` the passed in span manually. It will be ended automatically when the `operation` returns. + /// The current task-local `Baggage` is picked up and provided to the underlying tracer. + /// It is also possible to pass a specific `baggage` explicitly, in which case attempting + /// to pick up the task-local baggage is prevented. This can be useful when we know that + /// we're about to start a top-level span, or if a span should be started from a different, + /// stored away previously, + /// + /// - Warning: You MUST NOT ``SpanProtocol/end()`` the span explicitly, because at the end of the `withSpan` + /// operation closure returning the span will be closed automatically. /// /// - Parameters: /// - operationName: The name of the operation being traced. This may be a handler function, database call, ... - /// - baggage: Baggage potentially containing trace identifiers of a parent ``Span``. - /// - kind: The ``SpanKind`` of the ``Span`` to be created. Defaults to ``SpanKind/internal``. - /// - operation: operation to wrap in a span start/end and execute immediately - /// - function: The function name in which the span was started. + /// - baggage: The `Baggage` providing information on where to start the new ``Span``. + /// - kind: The ``SpanKind`` of the new ``Span``. + /// - time: The time at which to start the new ``Span``. + /// - function: The function name in which the span was started /// - fileID: The `fileID` where the span was started. /// - line: The file line where the span was started. + /// - operation: The operation that this span should be measuring /// - Returns: the value returned by `operation` /// - Throws: the error the `operation` has thrown (if any) - public func withSpan( + public static func withSpan( _ operationName: String, - baggage: Baggage, + baggage: @autoclosure () -> Baggage = .current ?? .topLevel, ofKind kind: SpanKind = .internal, function: String = #function, file fileID: String = #fileID, line: UInt = #line, - _ operation: (Span) throws -> T + _ operation: (any SpanProtocol) throws -> T ) rethrows -> T { - let span = self.startSpan( + #if swift(>=5.7.0) + try InstrumentationSystem.legacyTracer.withAnySpan( operationName, - baggage: baggage, ofKind: kind, - at: .now(), function: function, file: fileID, line: line - ) - defer { span.end() } - do { - return try operation(span) - } catch { - span.recordError(error) - throw error // rethrow + ) { anySpan in + try operation(anySpan) } - } - #else - /// Execute a specific task within a newly created ``Span``. - /// - /// DO NOT `end()` the passed in span manually. It will be ended automatically when the `operation` returns. - /// - /// - Parameters: - /// - operationName: The name of the operation being traced. This may be a handler function, database call, ... - /// - baggage: Baggage potentially containing trace identifiers of a parent ``Span``. - /// - kind: The ``SpanKind`` of the ``Span`` to be created. Defaults to ``SpanKind/internal``. - /// - operation: operation to wrap in a span start/end and execute immediately - /// - function: The function name in which the span was started. - /// - file: The `#file` where the span was started. - /// - line: The file line where the span was started. - /// - Returns: the value returned by `operation` - /// - Throws: the error the `operation` has thrown (if any) - public func withSpan( - _ operationName: String, - baggage: Baggage, - ofKind kind: SpanKind = .internal, - function: String = #function, - file: String = #file, - line: UInt = #line, - _ operation: (Span) throws -> T - ) rethrows -> T { - let span = self.startSpan( + #else + try InstrumentationSystem.legacyTracer.withAnySpan( operationName, - baggage: baggage, ofKind: kind, - at: .now(), function: function, - file: file, + file: fileID, line: line - ) - defer { span.end() } - do { - return try operation(span) - } catch { - span.recordError(error) - throw error // rethrow + ) { anySpan in + try operation(anySpan) } + #endif } - #endif -} -// ==== ---------------------------------------------------------------------------------------------------------------- -// MARK: Starting spans: Task-local Baggage propagation - -#if swift(>=5.5) && canImport(_Concurrency) -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension Tracer { - /// Execute the given operation within a newly created ``Span``, - /// started as a child of the currently stored task local `Baggage.current` or as a root span if `nil`. + /// Start a new ``Span`` and automatically end when the `operation` completes, + /// including recording the `error` in case the operation throws. + /// + /// The current task-local `Baggage` is picked up and provided to the underlying tracer. + /// It is also possible to pass a specific `baggage` explicitly, in which case attempting + /// to pick up the task-local baggage is prevented. This can be useful when we know that + /// we're about to start a top-level span, or if a span should be started from a different, + /// stored away previously, /// - /// DO NOT `end()` the passed in span manually. It will be ended automatically when the `operation` returns. + /// - Warning: You MUST NOT ``SpanProtocol/end()`` the span explicitly, because at the end of the `withSpan` + /// operation closure returning the span will be closed automatically. /// /// - Parameters: /// - operationName: The name of the operation being traced. This may be a handler function, database call, ... - /// - kind: The ``SpanKind`` of the ``Span`` to be created. Defaults to ``SpanKind/internal``. - /// - operation: operation to wrap in a span start/end and execute immediately - /// - function: The function name in which the span was started. + /// - baggage: The `Baggage` providing information on where to start the new ``Span``. + /// - kind: The ``SpanKind`` of the new ``Span``. + /// - time: The time at which to start the new ``Span``. + /// - function: The function name in which the span was started /// - fileID: The `fileID` where the span was started. /// - line: The file line where the span was started. + /// - operation: The operation that this span should be measuring /// - Returns: the value returned by `operation` /// - Throws: the error the `operation` has thrown (if any) - public func withSpan( + #if swift(>=5.7.0) + @_unsafeInheritExecutor + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public static func withSpan( _ operationName: String, + baggage: @autoclosure () -> Baggage = .current ?? .topLevel, ofKind kind: SpanKind = .internal, + at time: DispatchWallTime = .now(), function: String = #function, file fileID: String = #fileID, line: UInt = #line, - _ operation: (Span) throws -> T - ) rethrows -> T { - try self.withSpan( + _ operation: (any SpanProtocol) async throws -> T + ) async rethrows -> T { + try await InstrumentationSystem.legacyTracer.withAnySpan( operationName, - baggage: .current ?? .topLevel, + baggage: baggage(), ofKind: kind, + at: time, function: function, file: fileID, line: line - ) { span in - try Baggage.$current.withValue(span.baggage) { - try operation(span) - } + ) { anySpan in + try await operation(anySpan) } } - - /// Execute the given async operation within a newly created ``Span``, - /// started as a child of the currently stored task local `Baggage.current` or as a root span if `nil`. - /// - /// DO NOT `end()` the passed in span manually. It will be ended automatically when the `operation` returns. - /// - /// - Parameters: - /// - operationName: The name of the operation being traced. This may be a handler function, database call, ... - /// - kind: The ``SpanKind`` of the ``Span`` to be created. Defaults to ``SpanKind/internal``. - /// - operation: operation to wrap in a span start/end and execute immediately - /// - function: The function name in which the span was started. - /// - fileID: The `fileID` where the span was started. - /// - line: The file line where the span was started. - /// - Returns: the value returned by `operation` - /// - Throws: the error the `operation` has thrown (if any) - public func withSpan( + #else // TODO: remove this if/else when we require 5.7; it is only here to add @_unsafeInheritExecutor + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public static func withSpan( _ operationName: String, + baggage: @autoclosure () -> Baggage = .current ?? .topLevel, ofKind kind: SpanKind = .internal, + at time: DispatchWallTime = .now(), function: String = #function, file fileID: String = #fileID, line: UInt = #line, - _ operation: (Span) async throws -> T + _ operation: (any SpanProtocol) async throws -> T ) async rethrows -> T { - let span = self.startSpan( + try await InstrumentationSystem.legacyTracer.withAnySpan( operationName, - baggage: .current ?? .topLevel, + baggage: baggage(), ofKind: kind, + at: time, function: function, file: fileID, line: line - ) - defer { span.end() } - do { - return try await Baggage.$current.withValue(span.baggage) { - try await operation(span) - } - } catch { - span.recordError(error) - throw error // rethrow - } - } - - /// Execute the given async operation within a newly created `Span`, - /// started as a child of the passed in `Baggage` or as a root span if `nil`. - /// - /// DO NOT `end()` the passed in span manually. It will be ended automatically when the `operation` returns. - /// - /// - Parameters: - /// - operationName: The name of the operation being traced. This may be a handler function, database call, ... - /// - baggage: The baggage to be used for the newly created span. It may be obtained by the user manually from the `Baggage.current`, - // task local and modified before passing into this function. The baggage will be made the current task-local baggage for the duration of the `operation`. - /// - kind: The `SpanKind` of the `Span` to be created. Defaults to `.internal`. - /// - operation: operation to wrap in a span start/end and execute immediately - /// - function: The function name in which the span was started. - /// - fileID: The `fileID` where the span was started. - /// - line: The file line where the span was started. - /// - Returns: the value returned by `operation` - /// - Throws: the error the `operation` has thrown (if any) - public func withSpan( - _ operationName: String, - baggage: Baggage, - ofKind kind: SpanKind = .internal, - function: String = #function, - file fileID: String = #fileID, - line: UInt = #line, - _ operation: (Span) async throws -> T - ) async rethrows -> T { - let span = self.startSpan(operationName, baggage: baggage, ofKind: kind, function: function, file: fileID, line: line) - defer { span.end() } - do { - return try await Baggage.$current.withValue(span.baggage) { - try await operation(span) - } - } catch { - span.recordError(error) - throw error // rethrow + ) { anySpan in + try await operation(anySpan) } } + #endif } -#endif diff --git a/Sources/Tracing/TracerProtocol+Legacy.swift b/Sources/Tracing/TracerProtocol+Legacy.swift new file mode 100644 index 00000000..a2a5ad05 --- /dev/null +++ b/Sources/Tracing/TracerProtocol+Legacy.swift @@ -0,0 +1,427 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Distributed Tracing open source project +// +// Copyright (c) 2020-2022 Apple Inc. and the Swift Distributed Tracing project +// authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Dispatch +@_exported import Instrumentation +@_exported import InstrumentationBaggage + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage +public protocol LegacyTracerProtocol: InstrumentProtocol { + /// Start a new span returning an existential ``SpanProtocol`` reference. + /// + /// This API will be deprecated as soon as Swift 5.9 is released, and the Swift 5.7 requiring `TracerProtocol` + /// is recommended instead. + /// + /// The current task-local `Baggage` is picked up and provided to the underlying tracer. + /// It is also possible to pass a specific `baggage` explicitly, in which case attempting + /// to pick up the task-local baggage is prevented. This can be useful when we know that + /// we're about to start a top-level span, or if a span should be started from a different, + /// stored away previously, + /// + /// - Note: Legacy API, prefer using ``startSpan(_:baggage:ofKind:at: + /// + /// - Note: Prefer ``withSpan(_:baggage:ofKind:at:function:file:line:operation:)`` to start + /// a span as it automatically takes care of ending the span, and recording errors when thrown. + /// Use `startSpan` iff you need to pass the span manually to a different + /// location in your source code to end it. + /// + /// - Warning: You must `end()` the span when it the measured operation has completed explicitly, + /// otherwise the span object will potentially never be released nor reported. + /// + /// - Parameters: + /// - operationName: The name of the operation being traced. This may be a handler function, database call, ... + /// - baggage: The `Baggage` providing information on where to start the new ``Span``. + /// - kind: The ``SpanKind`` of the new ``Span``. + /// - time: The time at which to start the new ``Span``. + /// - function: The function name in which the span was started + /// - fileID: The `fileID` where the span was started. + /// - line: The file line where the span was started. + func startAnySpan( + _ operationName: String, + baggage: @autoclosure () -> Baggage, + ofKind kind: SpanKind, + at time: DispatchWallTime, + function: String, + file fileID: String, + line: UInt + ) -> any SpanProtocol + + /// Export all ended spans to the configured backend that have not yet been exported. + /// + /// This function should only be called in cases where it is absolutely necessary, + /// such as when using some FaaS providers that may suspend the process after an invocation, but before the backend exports the completed spans. + /// + /// This function should not block indefinitely, implementations should offer a configurable timeout for flush operations. + func forceFlush() +} + +// ==== ------------------------------------------------------------------ +// MARK: Legacy implementations for Swift 5.7 + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage +extension LegacyTracerProtocol { + /// Start a new span returning an existential ``SpanProtocol`` reference. + /// + /// This API will be deprecated as soon as Swift 5.9 is released, and the Swift 5.7 requiring `TracerProtocol` + /// is recommended instead. + /// + /// The current task-local `Baggage` is picked up and provided to the underlying tracer. + /// It is also possible to pass a specific `baggage` explicitly, in which case attempting + /// to pick up the task-local baggage is prevented. This can be useful when we know that + /// we're about to start a top-level span, or if a span should be started from a different, + /// stored away previously, + /// + /// - Note: Legacy API, prefer using ``startSpan(_:baggage:ofKind:at: + /// + /// - Note: Prefer ``withSpan(_:baggage:ofKind:at:function:file:line:operation:)`` to start + /// a span as it automatically takes care of ending the span, and recording errors when thrown. + /// Use `startSpan` iff you need to pass the span manually to a different + /// location in your source code to end it. + /// + /// - Warning: You must `end()` the span when it the measured operation has completed explicitly, + /// otherwise the span object will potentially never be released nor reported. + /// + /// - Parameters: + /// - operationName: The name of the operation being traced. This may be a handler function, database call, ... + /// - baggage: The `Baggage` providing information on where to start the new ``Span``. + /// - kind: The ``SpanKind`` of the new ``Span``. + /// - time: The time at which to start the new ``Span``. + /// - function: The function name in which the span was started + /// - fileID: The `fileID` where the span was started. + /// - line: The file line where the span was started. + public func startAnySpan( + _ operationName: String, + baggage: @autoclosure () -> Baggage = .current ?? .topLevel, + ofKind kind: SpanKind = .internal, + at time: DispatchWallTime = .now(), + function: String = #function, + file fileID: String = #fileID, + line: UInt = #line + ) -> any SpanProtocol { + self.startAnySpan( + operationName, + baggage: baggage(), + ofKind: kind, + at: time, + function: function, + file: fileID, + line: line + ) + } + + /// Start a new ``SpanProtocol`` and automatically end when the `operation` completes, + /// including recording the `error` in case the operation throws. + /// + /// This API will be deprecated as soon as Swift 5.9 is released, and the Swift 5.7 requiring `TracerProtocol` + /// is recommended instead. + /// + /// The current task-local `Baggage` is picked up and provided to the underlying tracer. + /// It is also possible to pass a specific `baggage` explicitly, in which case attempting + /// to pick up the task-local baggage is prevented. This can be useful when we know that + /// we're about to start a top-level span, or if a span should be started from a different, + /// stored away previously, + /// + /// - Note: Legacy API, prefer using ``startSpan(_:baggage:ofKind:at: + /// + /// - Note: Prefer ``withSpan(_:baggage:ofKind:at:function:file:line:operation:)`` to start + /// a span as it automatically takes care of ending the span, and recording errors when thrown. + /// Use `startSpan` iff you need to pass the span manually to a different + /// location in your source code to end it. + /// + /// - Warning: You must `end()` the span when it the measured operation has completed explicitly, + /// otherwise the span object will potentially never be released nor reported. + /// + /// - Parameters: + /// - operationName: The name of the operation being traced. This may be a handler function, database call, ... + /// - baggage: The `Baggage` providing information on where to start the new ``Span``. + /// - kind: The ``SpanKind`` of the new ``Span``. + /// - time: The time at which to start the new ``Span``. + /// - function: The function name in which the span was started + /// - fileID: The `fileID` where the span was started. + /// - line: The file line where the span was started. + /// - operation: The operation that this span should be measuring + /// - Returns: the value returned by `operation` + /// - Throws: the error the `operation` has thrown (if any) + public func withAnySpan( + _ operationName: String, + baggage: @autoclosure () -> Baggage = .current ?? .topLevel, + ofKind kind: SpanKind = .internal, + at time: DispatchWallTime = .now(), + function: String = #function, + file fileID: String = #fileID, + line: UInt = #line, + _ operation: (any SpanProtocol) throws -> T + ) rethrows -> T { + let span = self.startAnySpan(operationName, baggage: baggage(), ofKind: kind, at: time, function: function, file: fileID, line: line) + defer { span.end() } + do { + return try Baggage.$current.withValue(span.baggage) { + try operation(span) + } + } catch { + span.recordError(error) + throw error // rethrow + } + } + + /// Start a new ``SpanProtocol`` and automatically end when the `operation` completes, + /// including recording the `error` in case the operation throws. + /// + /// This API will be deprecated as soon as Swift 5.9 is released, and the Swift 5.7 requiring `TracerProtocol` + /// is recommended instead. + /// + /// The current task-local `Baggage` is picked up and provided to the underlying tracer. + /// It is also possible to pass a specific `baggage` explicitly, in which case attempting + /// to pick up the task-local baggage is prevented. This can be useful when we know that + /// we're about to start a top-level span, or if a span should be started from a different, + /// stored away previously, + /// + /// - Note: Legacy API, prefer using ``startSpan(_:baggage:ofKind:at: + /// + /// - Note: Prefer ``withSpan(_:baggage:ofKind:at:function:file:line:operation:)`` to start + /// a span as it automatically takes care of ending the span, and recording errors when thrown. + /// Use `startSpan` iff you need to pass the span manually to a different + /// location in your source code to end it. + /// + /// - Warning: You must `end()` the span when it the measured operation has completed explicitly, + /// otherwise the span object will potentially never be released nor reported. + /// + /// - Parameters: + /// - operationName: The name of the operation being traced. This may be a handler function, database call, ... + /// - baggage: The `Baggage` providing information on where to start the new ``Span``. + /// - kind: The ``SpanKind`` of the new ``Span``. + /// - time: The time at which to start the new ``Span``. + /// - function: The function name in which the span was started + /// - fileID: The `fileID` where the span was started. + /// - line: The file line where the span was started. + /// - operation: The operation that this span should be measuring + /// - Returns: the value returned by `operation` + /// - Throws: the error the `operation` has thrown (if any) + public func withAnySpan( + _ operationName: String, + baggage: @autoclosure () -> Baggage = .current ?? .topLevel, + ofKind kind: SpanKind = .internal, + at time: DispatchWallTime = .now(), + function: String = #function, + file fileID: String = #fileID, + line: UInt = #line, + _ operation: (any SpanProtocol) async throws -> T + ) async rethrows -> T { + let span = self.startAnySpan(operationName, baggage: baggage(), ofKind: kind, at: time, function: function, file: fileID, line: line) + defer { span.end() } + do { + return try await Baggage.$current.withValue(span.baggage) { + try await operation(span) + } + } catch { + span.recordError(error) + throw error // rethrow + } + } +} + +#if swift(>=5.7.0) +// Provide compatibility shims of the `...AnySpan` APIs to the 5.7 requiring `TracerProtocol`. + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension TracerProtocol { + /// Start a new span returning an existential ``SpanProtocol`` reference. + /// + /// This API will be deprecated as soon as Swift 5.9 is released, and the Swift 5.7 requiring `TracerProtocol` + /// is recommended instead. + /// + /// The current task-local `Baggage` is picked up and provided to the underlying tracer. + /// It is also possible to pass a specific `baggage` explicitly, in which case attempting + /// to pick up the task-local baggage is prevented. This can be useful when we know that + /// we're about to start a top-level span, or if a span should be started from a different, + /// stored away previously, + /// + /// - Note: Legacy API, prefer using ``startSpan(_:baggage:ofKind:at: + /// + /// - Note: Prefer ``withSpan(_:baggage:ofKind:at:function:file:line:operation:)`` to start + /// a span as it automatically takes care of ending the span, and recording errors when thrown. + /// Use `startSpan` iff you need to pass the span manually to a different + /// location in your source code to end it. + /// + /// - Warning: You must `end()` the span when it the measured operation has completed explicitly, + /// otherwise the span object will potentially never be released nor reported. + /// + /// - Parameters: + /// - operationName: The name of the operation being traced. This may be a handler function, database call, ... + /// - baggage: The `Baggage` providing information on where to start the new ``Span``. + /// - kind: The ``SpanKind`` of the new ``Span``. + /// - time: The time at which to start the new ``Span``. + /// - function: The function name in which the span was started + /// - fileID: The `fileID` where the span was started. + /// - line: The file line where the span was started. + public func startAnySpan( + _ operationName: String, + baggage: @autoclosure () -> Baggage = .current ?? .topLevel, + ofKind kind: SpanKind = .internal, + at time: DispatchWallTime = .now(), + function: String = #function, + file fileID: String = #fileID, + line: UInt = #line + ) -> any SpanProtocol { + self.startSpan( + operationName, + baggage: baggage(), + ofKind: kind, + at: time, + function: function, + file: fileID, + line: line + ) + } + + /// Start a new ``SpanProtocol`` and automatically end when the `operation` completes, + /// including recording the `error` in case the operation throws. + /// + /// This API will be deprecated as soon as Swift 5.9 is released, and the Swift 5.7 requiring `TracerProtocol` + /// is recommended instead. + /// + /// The current task-local `Baggage` is picked up and provided to the underlying tracer. + /// It is also possible to pass a specific `baggage` explicitly, in which case attempting + /// to pick up the task-local baggage is prevented. This can be useful when we know that + /// we're about to start a top-level span, or if a span should be started from a different, + /// stored away previously, + /// + /// - Note: Legacy API, prefer using ``startSpan(_:baggage:ofKind:at: + /// + /// - Note: Prefer ``withSpan(_:baggage:ofKind:at:function:file:line:operation:)`` to start + /// a span as it automatically takes care of ending the span, and recording errors when thrown. + /// Use `startSpan` iff you need to pass the span manually to a different + /// location in your source code to end it. + /// + /// - Warning: You must `end()` the span when it the measured operation has completed explicitly, + /// otherwise the span object will potentially never be released nor reported. + /// + /// - Parameters: + /// - operationName: The name of the operation being traced. This may be a handler function, database call, ... + /// - baggage: The `Baggage` providing information on where to start the new ``Span``. + /// - kind: The ``SpanKind`` of the new ``Span``. + /// - time: The time at which to start the new ``Span``. + /// - function: The function name in which the span was started + /// - fileID: The `fileID` where the span was started. + /// - line: The file line where the span was started. + /// - operation: The operation that this span should be measuring + /// - Returns: the value returned by `operation` + /// - Throws: the error the `operation` has thrown (if any) + public func withAnySpan( + _ operationName: String, + baggage: @autoclosure () -> Baggage = .current ?? .topLevel, + ofKind kind: SpanKind = .internal, + at time: DispatchWallTime = .now(), + function: String = #function, + file fileID: String = #fileID, + line: UInt = #line, + _ operation: (any SpanProtocol) throws -> T + ) rethrows -> T { + try self.withSpan( + operationName, + baggage: baggage(), + ofKind: kind, + at: time, + function: function, + file: fileID, + line: line + ) { span in + try operation(span) + } + } + + /// Start a new ``SpanProtocol`` and automatically end when the `operation` completes, + /// including recording the `error` in case the operation throws. + /// + /// This API will be deprecated as soon as Swift 5.9 is released, and the Swift 5.7 requiring `TracerProtocol` + /// is recommended instead. + /// + /// The current task-local `Baggage` is picked up and provided to the underlying tracer. + /// It is also possible to pass a specific `baggage` explicitly, in which case attempting + /// to pick up the task-local baggage is prevented. This can be useful when we know that + /// we're about to start a top-level span, or if a span should be started from a different, + /// stored away previously, + /// + /// - Note: Legacy API, prefer using ``startSpan(_:baggage:ofKind:at: + /// + /// - Note: Prefer ``withSpan(_:baggage:ofKind:at:function:file:line:operation:)`` to start + /// a span as it automatically takes care of ending the span, and recording errors when thrown. + /// Use `startSpan` iff you need to pass the span manually to a different + /// location in your source code to end it. + /// + /// - Warning: You must `end()` the span when it the measured operation has completed explicitly, + /// otherwise the span object will potentially never be released nor reported. + /// + /// - Parameters: + /// - operationName: The name of the operation being traced. This may be a handler function, database call, ... + /// - baggage: The `Baggage` providing information on where to start the new ``Span``. + /// - kind: The ``SpanKind`` of the new ``Span``. + /// - time: The time at which to start the new ``Span``. + /// - function: The function name in which the span was started + /// - fileID: The `fileID` where the span was started. + /// - line: The file line where the span was started. + /// - operation: The operation that this span should be measuring + /// - Returns: the value returned by `operation` + /// - Throws: the error the `operation` has thrown (if any) + #if swift(>=5.7.0) + @_unsafeInheritExecutor + public func withAnySpan( + _ operationName: String, + baggage: @autoclosure () -> Baggage = .current ?? .topLevel, + ofKind kind: SpanKind = .internal, + at time: DispatchWallTime = .now(), + function: String = #function, + file fileID: String = #fileID, + line: UInt = #line, + _ operation: (any SpanProtocol) async throws -> T + ) async rethrows -> T { + try await self.withSpan( + operationName, + baggage: baggage(), + ofKind: kind, + at: time, + function: function, + file: fileID, + line: line + ) { span in + try await operation(span) + } + } + #else // TODO: remove this if/else when we require 5.7; it is only here to add @_unsafeInheritExecutor + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public func withAnySpan( + _ operationName: String, + baggage: @autoclosure () -> Baggage = .current ?? .topLevel, + ofKind kind: SpanKind = .internal, + at time: DispatchWallTime = .now(), + function: String = #function, + file fileID: String = #fileID, + line: UInt = #line, + _ operation: (any SpanProtocol) async throws -> T + ) async rethrows -> T { + try await self.withSpan( + operationName, + baggage: baggage(), + ofKind: kind, + at: time, + function: function, + file: fileID, + line: line + ) { span in + try await operation(span) + } + } + #endif +} +#endif diff --git a/Sources/Tracing/TracerProtocol.swift b/Sources/Tracing/TracerProtocol.swift new file mode 100644 index 00000000..ee9d8560 --- /dev/null +++ b/Sources/Tracing/TracerProtocol.swift @@ -0,0 +1,226 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Distributed Tracing open source project +// +// Copyright (c) 2020-2022 Apple Inc. and the Swift Distributed Tracing project +// authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Dispatch +@_exported import Instrumentation +@_exported import InstrumentationBaggage + +// ==== ----------------------------------------------------------------------- +// MARK: Tracer protocol + +#if swift(>=5.7.0) +/// A tracer capable of creating new trace spans. +/// +/// A tracer is a special kind of instrument with the added ability to start a ``SpanProtocol``. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage +public protocol TracerProtocol: LegacyTracerProtocol { + /// The concrete type of span this tracer will be producing/ + associatedtype Span: SpanProtocol + + /// Start a new ``Span`` with the given `Baggage`. + /// + /// The current task-local `Baggage` is picked up and provided to the underlying tracer. + /// It is also possible to pass a specific `baggage` explicitly, in which case attempting + /// to pick up the task-local baggage is prevented. This can be useful when we know that + /// we're about to start a top-level span, or if a span should be started from a different, + /// stored away previously, + /// + /// - Note: Prefer ``withSpan(_:baggage:ofKind:at:function:file:line:operation:)`` to start + /// a span as it automatically takes care of ending the span, and recording errors when thrown. + /// Use `startSpan` iff you need to pass the span manually to a different + /// location in your source code to end it. + /// + /// - Warning: You must `end()` the span when it the measured operation has completed explicitly, + /// otherwise the span object will potentially never be released nor reported. + /// + /// - Parameters: + /// - operationName: The name of the operation being traced. This may be a handler function, database call, ... + /// - baggage: The `Baggage` providing information on where to start the new ``Span``. + /// - kind: The ``SpanKind`` of the new ``Span``. + /// - time: The time at which to start the new ``Span``. + /// - function: The function name in which the span was started + /// - fileID: The `fileID` where the span was started. + /// - line: The file line where the span was started. + func startSpan( + _ operationName: String, + baggage: @autoclosure () -> Baggage, + ofKind kind: SpanKind, + at time: DispatchWallTime, + function: String, + file fileID: String, + line: UInt + ) -> Self.Span +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage +extension TracerProtocol { + /// Start a new ``Span`` with the given `Baggage`. + /// + /// The current task-local `Baggage` is picked up and provided to the underlying tracer. + /// It is also possible to pass a specific `baggage` explicitly, in which case attempting + /// to pick up the task-local baggage is prevented. This can be useful when we know that + /// we're about to start a top-level span, or if a span should be started from a different, + /// stored away previously, + /// + /// - Note: Prefer ``withSpan(_:baggage:ofKind:at:function:file:line:operation:)`` to start + /// a span as it automatically takes care of ending the span, and recording errors when thrown. + /// Use `startSpan` iff you need to pass the span manually to a different + /// location in your source code to end it. + /// + /// - Warning: You must `end()` the span when it the measured operation has completed explicitly, + /// otherwise the span object will potentially never be released nor reported. + /// + /// - Parameters: + /// - operationName: The name of the operation being traced. This may be a handler function, database call, ... + /// - baggage: The `Baggage` providing information on where to start the new ``Span``. + /// - kind: The ``SpanKind`` of the new ``Span``. + /// - time: The time at which to start the new ``Span``. + /// - function: The function name in which the span was started + /// - fileID: The `fileID` where the span was started. + /// - line: The file line where the span was started. + public func startSpan( + _ operationName: String, + baggage: @autoclosure () -> Baggage = .current ?? .topLevel, + ofKind kind: SpanKind = .internal, + at time: DispatchWallTime = .now(), + function: String = #function, + file fileID: String = #fileID, + line: UInt = #line + ) -> Self.Span { + self.startSpan( + operationName, + baggage: baggage(), + ofKind: kind, + at: time, + function: function, + file: fileID, + line: line + ) + } +} + +// ==== ---------------------------------------------------------------------------------------------------------------- +// MARK: Starting spans: `withSpan` + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage +extension TracerProtocol { + /// Start a new ``Span`` and automatically end when the `operation` completes, + /// including recording the `error` in case the operation throws. + /// + /// The current task-local `Baggage` is picked up and provided to the underlying tracer. + /// It is also possible to pass a specific `baggage` explicitly, in which case attempting + /// to pick up the task-local baggage is prevented. This can be useful when we know that + /// we're about to start a top-level span, or if a span should be started from a different, + /// stored away previously, + /// + /// - Warning: You MUST NOT ``SpanProtocol/end()`` the span explicitly, because at the end of the `withSpan` + /// operation closure returning the span will be closed automatically. + /// + /// - Parameters: + /// - operationName: The name of the operation being traced. This may be a handler function, database call, ... + /// - baggage: The `Baggage` providing information on where to start the new ``Span``. + /// - kind: The ``SpanKind`` of the new ``Span``. + /// - time: The time at which to start the new ``Span``. + /// - function: The function name in which the span was started + /// - fileID: The `fileID` where the span was started. + /// - line: The file line where the span was started. + /// - operation: The operation that this span should be measuring + /// - Returns: the value returned by `operation` + /// - Throws: the error the `operation` has thrown (if any) + public func withSpan( + _ operationName: String, + baggage: @autoclosure () -> Baggage = .current ?? .topLevel, + ofKind kind: SpanKind = .internal, + at time: DispatchWallTime = .now(), + function: String = #function, + file fileID: String = #fileID, + line: UInt = #line, + _ operation: (Self.Span) throws -> T + ) rethrows -> T { + let span = self.startSpan( + operationName, + baggage: baggage(), + ofKind: kind, + at: time, + function: function, + file: fileID, + line: line + ) + defer { span.end() } + do { + return try Baggage.$current.withValue(span.baggage) { + try operation(span) + } + } catch { + span.recordError(error) + throw error // rethrow + } + } + + /// Start a new ``Span`` and automatically end when the `operation` completes, + /// including recording the `error` in case the operation throws. + /// + /// The current task-local `Baggage` is picked up and provided to the underlying tracer. + /// It is also possible to pass a specific `baggage` explicitly, in which case attempting + /// to pick up the task-local baggage is prevented. This can be useful when we know that + /// we're about to start a top-level span, or if a span should be started from a different, + /// stored away previously, + /// + /// - Warning: You MUST NOT ``SpanProtocol/end()`` the span explicitly, because at the end of the `withSpan` + /// operation closure returning the span will be closed automatically. + /// + /// - Parameters: + /// - operationName: The name of the operation being traced. This may be a handler function, database call, ... + /// - baggage: The `Baggage` providing information on where to start the new ``Span``. + /// - kind: The ``SpanKind`` of the new ``Span``. + /// - time: The time at which to start the new ``Span``. + /// - function: The function name in which the span was started + /// - fileID: The `fileID` where the span was started. + /// - line: The file line where the span was started. + /// - operation: The operation that this span should be measuring + /// - Returns: the value returned by `operation` + /// - Throws: the error the `operation` has thrown (if any) + @_unsafeInheritExecutor + public func withSpan( + _ operationName: String, + baggage: @autoclosure () -> Baggage = .current ?? .topLevel, + ofKind kind: SpanKind = .internal, + at time: DispatchWallTime = .now(), + function: String = #function, + file fileID: String = #fileID, + line: UInt = #line, + _ operation: (Self.Span) async throws -> T + ) async rethrows -> T { + let span = self.startSpan( + operationName, + baggage: baggage(), + ofKind: kind, + at: time, + function: function, + file: fileID, + line: line + ) + defer { span.end() } + do { + return try await Baggage.$current.withValue(span.baggage) { + try await operation(span) + } + } catch { + span.recordError(error) + throw error // rethrow + } + } +} + +#endif // Swift 5.7 diff --git a/Sources/_TracingBenchmarks/SpanAttributesDSLBenchmark.swift b/Sources/_TracingBenchmarks/SpanAttributesDSLBenchmark.swift index 0a930de0..18cca95a 100644 --- a/Sources/_TracingBenchmarks/SpanAttributesDSLBenchmark.swift +++ b/Sources/_TracingBenchmarks/SpanAttributesDSLBenchmark.swift @@ -69,10 +69,10 @@ public let SpanAttributesDSLBenchmarks: [BenchmarkInfo] = [ ), ] -private var span: Span! +private var span: (any SpanProtocol)! private func setUp() { - span = InstrumentationSystem.tracer.startSpan("something", baggage: .topLevel) + span = InstrumentationSystem.legacyTracer.startAnySpan("something", baggage: .topLevel) } private func tearDown() { @@ -86,14 +86,14 @@ func bench_empty(times: Int) throws {} func bench_makeSpan(times: Int) throws { for _ in 0 ..< times { - let span = InstrumentationSystem.tracer.startSpan("something", baggage: .topLevel) + let span = InstrumentationSystem.legacyTracer.startAnySpan("something", baggage: .topLevel) _ = span } } func bench_startSpan_end(times: Int) throws { for _ in 0 ..< times { - let span = InstrumentationSystem.tracer.startSpan("something", baggage: .topLevel) + let span = InstrumentationSystem.legacyTracer.startAnySpan("something", baggage: .topLevel) span.end() } } diff --git a/Tests/InstrumentationTests/InstrumentTests.swift b/Tests/InstrumentationTests/InstrumentTests.swift index aaea30ad..cada7ed7 100644 --- a/Tests/InstrumentationTests/InstrumentTests.swift +++ b/Tests/InstrumentationTests/InstrumentTests.swift @@ -52,7 +52,7 @@ private struct DictionaryExtractor: Extractor { } } -private final class FirstFakeTracer: Instrument { +private final class FirstFakeTracer: InstrumentProtocol { enum TraceIDKey: BaggageKey { typealias Value = String @@ -77,7 +77,7 @@ private final class FirstFakeTracer: Instrument { } } -private final class SecondFakeTracer: Instrument { +private final class SecondFakeTracer: InstrumentProtocol { enum TraceIDKey: BaggageKey { typealias Value = String diff --git a/Tests/InstrumentationTests/InstrumentationSystemTests.swift b/Tests/InstrumentationTests/InstrumentationSystemTests.swift index 87a7be5c..5f0a2c86 100644 --- a/Tests/InstrumentationTests/InstrumentationSystemTests.swift +++ b/Tests/InstrumentationTests/InstrumentationSystemTests.swift @@ -17,7 +17,7 @@ import InstrumentationBaggage import XCTest extension InstrumentationSystem { - public static func _instrument(of instrumentType: I.Type) -> I? where I: Instrument { + public static func _instrument(of instrumentType: I.Type) -> I? where I: InstrumentProtocol { self._findInstrument(where: { $0 is I }) as? I } } @@ -48,7 +48,7 @@ final class InstrumentationSystemTests: XCTestCase { } } -private final class FakeTracer: Instrument { +private final class FakeTracer: InstrumentProtocol { func inject( _ baggage: Baggage, into carrier: inout Carrier, @@ -68,7 +68,7 @@ private final class FakeTracer: Instrument { Carrier == Extract.Carrier {} } -private final class FakeInstrument: Instrument { +private final class FakeInstrument: InstrumentProtocol { func inject( _ baggage: Baggage, into carrier: inout Carrier, diff --git a/Tests/TracingTests/DynamicTracepointTracerTests.swift b/Tests/TracingTests/DynamicTracepointTracerTests.swift index 1291531b..be23c089 100644 --- a/Tests/TracingTests/DynamicTracepointTracerTests.swift +++ b/Tests/TracingTests/DynamicTracepointTracerTests.swift @@ -40,6 +40,16 @@ final class DynamicTracepointTracerTests: XCTestCase { // Effectively enabling tracepoints is similar to tracer bullets, tho bullets are generally "one off", // but here we could attach a trace-rate, so e.g.: control ` trace enable Sample:1234 .2` to set 20% sampling rate etc. + tracer.withAnySpan("dont") { _ in + // don't capture this span... + } + tracer.withAnySpan("yes", line: fakeLine) { _ in + // do capture this span, and all child spans of it! + tracer.withAnySpan("yes-inner", line: fakeNextLine) { _ in + // since the parent of this span was captured, this shall be captured as well + } + } + #if swift(>=5.7.0) tracer.withSpan("dont") { _ in // don't capture this span... } @@ -49,8 +59,14 @@ final class DynamicTracepointTracerTests: XCTestCase { // since the parent of this span was captured, this shall be captured as well } } + #endif + #if swift(>=5.7.0) + XCTAssertEqual(tracer.spans.count, 4) + #else XCTAssertEqual(tracer.spans.count, 2) + #endif + for span in tracer.spans { XCTAssertEqual(span.baggage.traceID, "trace-id-fake-\(fileID)-\(fakeLine)") } @@ -60,7 +76,6 @@ final class DynamicTracepointTracerTests: XCTestCase { } func test_adhoc_enableByFunction() { - #if swift(>=5.5) let tracer = DynamicTracepointTestTracer() InstrumentationSystem.bootstrapInternal(tracer) @@ -83,29 +98,39 @@ final class DynamicTracepointTracerTests: XCTestCase { } XCTAssertEqual(tracer.spans[0].baggage.spanID, "span-id-fake-\(fileID)-\(fakeLine)") XCTAssertEqual(tracer.spans[1].baggage.spanID, "span-id-fake-\(fileID)-\(fakeNextLine)") - #endif } func logic(fakeLine: UInt) { - #if swift(>=5.5) + #if swift(>=5.7) InstrumentationSystem.tracer.withSpan("\(#function)-dont", line: fakeLine) { _ in + // inside + } + #else + InstrumentationSystem.legacyTracer.withAnySpan("\(#function)-dont", line: fakeLine) { _ in + // inside } #endif } func traceMeLogic(fakeLine: UInt) { - #if swift(>=5.5) + #if swift(>=5.7) InstrumentationSystem.tracer.withSpan("\(#function)-yes", line: fakeLine) { _ in InstrumentationSystem.tracer.withSpan("\(#function)-yes-inside", line: fakeLine + 11) { _ in // inside } } + #else + InstrumentationSystem.legacyTracer.withAnySpan("\(#function)-yes", line: fakeLine) { _ in + InstrumentationSystem.legacyTracer.withAnySpan("\(#function)-yes-inside", line: fakeLine + 11) { _ in + // inside + } + } #endif } } /// Only intended to be used in single-threaded testing. -final class DynamicTracepointTestTracer: Tracer { +final class DynamicTracepointTestTracer: LegacyTracerProtocol { private(set) var activeTracepoints: Set = [] struct TracepointID: Hashable { @@ -139,26 +164,27 @@ final class DynamicTracepointTestTracer: Tracer { } private(set) var spans: [TracepointSpan] = [] - var onEndSpan: (Span) -> Void = { _ in + var onEndSpan: (SpanProtocol) -> Void = { _ in } - func startSpan(_ operationName: String, - baggage: InstrumentationBaggage.Baggage, - ofKind kind: Tracing.SpanKind, - at time: DispatchWallTime, - function: String, - file fileID: String, - line: UInt) -> Tracing.Span - { + func startAnySpan( + _ operationName: String, + baggage: @autoclosure () -> Baggage, + ofKind kind: SpanKind, + at time: DispatchWallTime, + function: String, + file fileID: String, + line: UInt + ) -> any SpanProtocol { let tracepoint = TracepointID(function: function, fileID: fileID, line: line) guard self.shouldRecord(tracepoint: tracepoint) else { - return NoOpTracer.NoOpSpan(operationName: operationName, baggage: baggage) + return TracepointSpan.notRecording(file: fileID, line: line) } let span = TracepointSpan( operationName: operationName, startTime: time, - baggage: baggage, + baggage: baggage(), kind: kind, file: fileID, line: line, @@ -229,7 +255,7 @@ final class DynamicTracepointTestTracer: Tracer { extension DynamicTracepointTestTracer { /// Only intended to be used in single-threaded testing. - final class TracepointSpan: Tracing.Span { + final class TracepointSpan: Tracing.SpanProtocol { private let kind: SpanKind private var status: SpanStatus? @@ -241,7 +267,21 @@ extension DynamicTracepointTestTracer { private(set) var baggage: Baggage private(set) var isRecording: Bool = false - let onEnd: (Span) -> Void + let onEnd: (TracepointSpan) -> Void + + static func notRecording(file fileID: String, line: UInt) -> TracepointSpan { + let span = TracepointSpan( + operationName: "", + startTime: .now(), + baggage: .topLevel, + kind: .internal, + file: fileID, + line: line, + onEnd: { _ in () } + ) + span.isRecording = false + return span + } init(operationName: String, startTime: DispatchWallTime, @@ -249,7 +289,7 @@ extension DynamicTracepointTestTracer { kind: SpanKind, file fileID: String, line: UInt, - onEnd: @escaping (Span) -> Void) + onEnd: @escaping (TracepointSpan) -> Void) { self.operationName = operationName self.startTime = startTime @@ -291,6 +331,38 @@ extension DynamicTracepointTestTracer { } } +#if compiler(>=5.7.0) +extension DynamicTracepointTestTracer: TracerProtocol { + typealias Span = TracepointSpan + + func startSpan(_ operationName: String, + baggage: @autoclosure () -> Baggage, + ofKind kind: Tracing.SpanKind, + at time: DispatchWallTime, + function: String, + file fileID: String, + line: UInt) -> TracepointSpan + { + let tracepoint = TracepointID(function: function, fileID: fileID, line: line) + guard self.shouldRecord(tracepoint: tracepoint) else { + return TracepointSpan.notRecording(file: fileID, line: line) + } + + let span = TracepointSpan( + operationName: operationName, + startTime: time, + baggage: baggage(), + kind: kind, + file: fileID, + line: line, + onEnd: onEndSpan + ) + self.spans.append(span) + return span + } +} +#endif + #if compiler(>=5.6.0) extension DynamicTracepointTestTracer: @unchecked Sendable {} // only intended for single threaded testing extension DynamicTracepointTestTracer.TracepointSpan: @unchecked Sendable {} // only intended for single threaded testing diff --git a/Tests/TracingTests/SpanTests.swift b/Tests/TracingTests/SpanTests.swift index 25aa30e8..1e76fe87 100644 --- a/Tests/TracingTests/SpanTests.swift +++ b/Tests/TracingTests/SpanTests.swift @@ -70,7 +70,7 @@ final class SpanTests: XCTestCase { } func testSpanAttributeIsExpressibleByArrayLiteral() { - let s = InstrumentationSystem.tracer.startSpan("", baggage: .topLevel) + let s = InstrumentationSystem.legacyTracer.startAnySpan("", baggage: .topLevel) s.attributes["hi"] = [42, 21] s.attributes["hi"] = [42.10, 21.0] s.attributes["hi"] = [true, false] diff --git a/Tests/TracingTests/TestTracer.swift b/Tests/TracingTests/TestTracer.swift index 1479e89e..72e8c6ff 100644 --- a/Tests/TracingTests/TestTracer.swift +++ b/Tests/TracingTests/TestTracer.swift @@ -19,23 +19,23 @@ import InstrumentationBaggage import Tracing /// Only intended to be used in single-threaded testing. -final class TestTracer: Tracer { +final class TestTracer: LegacyTracerProtocol { private(set) var spans = [TestSpan]() var onEndSpan: (TestSpan) -> Void = { _ in } - func startSpan( + func startAnySpan( _ operationName: String, - baggage: Baggage, + baggage: @autoclosure () -> Baggage, ofKind kind: SpanKind, at time: DispatchWallTime, function: String, file fileID: String, line: UInt - ) -> Span { + ) -> any SpanProtocol { let span = TestSpan( operationName: operationName, startTime: time, - baggage: baggage, + baggage: baggage(), kind: kind, onEnd: onEndSpan ) @@ -64,6 +64,30 @@ final class TestTracer: Tracer { } } +#if swift(>=5.7.0) +extension TestTracer: TracerProtocol { + func startSpan( + _ operationName: String, + baggage: @autoclosure () -> Baggage, + ofKind kind: SpanKind, + at time: DispatchWallTime, + function: String, + file fileID: String, + line: UInt + ) -> TestSpan { + let span = TestSpan( + operationName: operationName, + startTime: time, + baggage: baggage(), + kind: kind, + onEnd: onEndSpan + ) + self.spans.append(span) + return span + } +} +#endif + extension TestTracer { enum TraceIDKey: BaggageKey { typealias Value = String @@ -95,7 +119,7 @@ extension Baggage { } /// Only intended to be used in single-threaded testing. -final class TestSpan: Span { +final class TestSpan: SpanProtocol { private let kind: SpanKind private var status: SpanStatus? diff --git a/Tests/TracingTests/TracedLock.swift b/Tests/TracingTests/TracedLock.swift index a17003af..92043a4e 100644 --- a/Tests/TracingTests/TracedLock.swift +++ b/Tests/TracingTests/TracedLock.swift @@ -21,7 +21,7 @@ final class TracedLock { let name: String let underlyingLock: NSLock - var activeSpan: Span? + var activeSpan: SpanProtocol? init(name: String) { self.name = name @@ -31,7 +31,7 @@ final class TracedLock { func lock(baggage: Baggage) { // time here self.underlyingLock.lock() - self.activeSpan = InstrumentationSystem.tracer.startSpan(self.name, baggage: baggage) + self.activeSpan = InstrumentationSystem.legacyTracer.startAnySpan(self.name, baggage: baggage) } func unlock(baggage: Baggage) { diff --git a/Tests/TracingTests/TracedLockTests.swift b/Tests/TracingTests/TracedLockTests.swift index 3a5deddb..3edbeedf 100644 --- a/Tests/TracingTests/TracedLockTests.swift +++ b/Tests/TracingTests/TracedLockTests.swift @@ -59,21 +59,21 @@ enum TaskIDKey: BaggageKey { // MARK: PrintLn Tracer /// Only intended to be used in single-threaded testing. -private final class TracedLockPrintlnTracer: Tracer { - func startSpan( +private final class TracedLockPrintlnTracer: LegacyTracerProtocol { + func startAnySpan( _ operationName: String, - baggage: Baggage, + baggage: @autoclosure () -> Baggage, ofKind kind: SpanKind, at time: DispatchWallTime, function: String, file fileID: String, line: UInt - ) -> Span { + ) -> any SpanProtocol { TracedLockPrintlnSpan( operationName: operationName, startTime: time, kind: kind, - baggage: baggage + baggage: baggage() ) } @@ -97,7 +97,7 @@ private final class TracedLockPrintlnTracer: Tracer { Extract: Extractor, Carrier == Extract.Carrier {} - final class TracedLockPrintlnSpan: Span { + final class TracedLockPrintlnSpan: SpanProtocol { private let kind: SpanKind private var status: SpanStatus? @@ -160,6 +160,27 @@ private final class TracedLockPrintlnTracer: Tracer { } } +#if swift(>=5.7.0) +extension TracedLockPrintlnTracer: TracerProtocol { + func startSpan( + _ operationName: String, + baggage: @autoclosure () -> Baggage, + ofKind kind: SpanKind, + at time: DispatchWallTime, + function: String, + file fileID: String, + line: UInt + ) -> TracedLockPrintlnSpan { + TracedLockPrintlnSpan( + operationName: operationName, + startTime: time, + kind: kind, + baggage: baggage() + ) + } +} +#endif + #if compiler(>=5.6.0) extension TracedLockPrintlnTracer: Sendable {} extension TracedLockPrintlnTracer.TracedLockPrintlnSpan: @unchecked Sendable {} // only intended for single threaded testing diff --git a/Tests/TracingTests/TracerTests+XCTest.swift b/Tests/TracingTests/TracerTests+XCTest.swift index 1e6bca48..b5a8cf1b 100644 --- a/Tests/TracingTests/TracerTests+XCTest.swift +++ b/Tests/TracingTests/TracerTests+XCTest.swift @@ -35,6 +35,8 @@ extension TracerTests { ("testWithSpan_automaticBaggagePropagation_async", testWithSpan_automaticBaggagePropagation_async), ("testWithSpan_enterFromNonAsyncCode_passBaggage_asyncOperation", testWithSpan_enterFromNonAsyncCode_passBaggage_asyncOperation), ("testWithSpan_automaticBaggagePropagation_async_throws", testWithSpan_automaticBaggagePropagation_async_throws), + ("test_static_Tracer_withSpan_automaticBaggagePropagation_async_throws", test_static_Tracer_withSpan_automaticBaggagePropagation_async_throws), + ("test_static_Tracer_withSpan_automaticBaggagePropagation_throws", test_static_Tracer_withSpan_automaticBaggagePropagation_throws), ("testWithSpan_recordErrorWithAttributes", testWithSpan_recordErrorWithAttributes), ] } diff --git a/Tests/TracingTests/TracerTests.swift b/Tests/TracingTests/TracerTests.swift index fa65cefc..389cd85d 100644 --- a/Tests/TracingTests/TracerTests.swift +++ b/Tests/TracingTests/TracerTests.swift @@ -58,6 +58,9 @@ final class TracerTests: XCTestCase { } func testWithSpan_success() { + guard #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) else { + return + } let tracer = TestTracer() InstrumentationSystem.bootstrapInternal(tracer) defer { @@ -65,11 +68,19 @@ final class TracerTests: XCTestCase { } var spanEnded = false - tracer.onEndSpan = { _ in spanEnded = true } + tracer.onEndSpan = { _ in + spanEnded = true + } + #if swift(>=5.7.0) let value = tracer.withSpan("hello", baggage: .topLevel) { _ in "yes" } + #else + let value = tracer.withAnySpan("hello", baggage: .topLevel) { _ in + "yes" + } + #endif XCTAssertEqual(value, "yes") XCTAssertTrue(spanEnded) @@ -86,7 +97,7 @@ final class TracerTests: XCTestCase { tracer.onEndSpan = { _ in spanEnded = true } do { - _ = try tracer.withSpan("hello", baggage: .topLevel) { _ in + _ = try tracer.withAnySpan("hello", baggage: .topLevel) { _ in throw ExampleSpanError() } } catch { @@ -112,11 +123,11 @@ final class TracerTests: XCTestCase { var spanEnded = false tracer.onEndSpan = { _ in spanEnded = true } - func operation(span: Span) -> String { + func operation(span: SpanProtocol) -> String { "world" } - let value = tracer.withSpan("hello") { (span: Span) -> String in + let value = tracer.withAnySpan("hello") { (span: SpanProtocol) -> String in XCTAssertEqual(span.baggage.traceID, Baggage.current?.traceID) return operation(span: span) } @@ -141,12 +152,12 @@ final class TracerTests: XCTestCase { var spanEnded = false tracer.onEndSpan = { _ in spanEnded = true } - func operation(span: Span) throws -> String { + func operation(span: SpanProtocol) throws -> String { throw ExampleSpanError() } do { - _ = try tracer.withSpan("hello", operation) + _ = try tracer.withAnySpan("hello", operation) } catch { XCTAssertTrue(spanEnded) XCTAssertEqual(error as? ExampleSpanError, ExampleSpanError()) @@ -171,12 +182,12 @@ final class TracerTests: XCTestCase { var spanEnded = false tracer.onEndSpan = { _ in spanEnded = true } - func operation(span: Span) async throws -> String { + func operation(span: SpanProtocol) async throws -> String { "world" } try self.testAsync { - let value = try await tracer.withSpan("hello") { (span: Span) -> String in + let value = try await tracer.withAnySpan("hello") { (span: SpanProtocol) -> String in XCTAssertEqual(span.baggage.traceID, Baggage.current?.traceID) return try await operation(span: span) } @@ -188,7 +199,6 @@ final class TracerTests: XCTestCase { } func testWithSpan_enterFromNonAsyncCode_passBaggage_asyncOperation() throws { - #if swift(>=5.5) && canImport(_Concurrency) guard #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) else { throw XCTSkip("Task locals are not supported on this platform.") } @@ -202,14 +212,14 @@ final class TracerTests: XCTestCase { var spanEnded = false tracer.onEndSpan = { _ in spanEnded = true } - func operation(span: Span) async -> String { + func operation(span: SpanProtocol) async -> String { "world" } self.testAsync { var fromNonAsyncWorld = Baggage.topLevel fromNonAsyncWorld.traceID = "1234-5678" - let value = await tracer.withSpan("hello", baggage: fromNonAsyncWorld) { (span: Span) -> String in + let value = await tracer.withAnySpan("hello", baggage: fromNonAsyncWorld) { (span: SpanProtocol) -> String in XCTAssertEqual(span.baggage.traceID, Baggage.current?.traceID) XCTAssertEqual(span.baggage.traceID, fromNonAsyncWorld.traceID) return await operation(span: span) @@ -218,11 +228,9 @@ final class TracerTests: XCTestCase { XCTAssertEqual(value, "world") XCTAssertTrue(spanEnded) } - #endif } func testWithSpan_automaticBaggagePropagation_async_throws() throws { - #if swift(>=5.5) && canImport(_Concurrency) guard #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) else { throw XCTSkip("Task locals are not supported on this platform.") } @@ -236,13 +244,73 @@ final class TracerTests: XCTestCase { var spanEnded = false tracer.onEndSpan = { _ in spanEnded = true } - func operation(span: Span) async throws -> String { + func operation(span: SpanProtocol) async throws -> String { throw ExampleSpanError() } self.testAsync { do { - _ = try await tracer.withSpan("hello", operation) + _ = try await tracer.withAnySpan("hello", operation) + } catch { + XCTAssertTrue(spanEnded) + XCTAssertEqual(error as? ExampleSpanError, ExampleSpanError()) + return + } + XCTFail("Should have thrown") + } + } + + func test_static_Tracer_withSpan_automaticBaggagePropagation_async_throws() throws { + guard #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) else { + throw XCTSkip("Task locals are not supported on this platform.") + } + + let tracer = TestTracer() + InstrumentationSystem.bootstrapInternal(tracer) + defer { + InstrumentationSystem.bootstrapInternal(nil) + } + + var spanEnded = false + tracer.onEndSpan = { _ in spanEnded = true } + + func operation(span: SpanProtocol) async throws -> String { + throw ExampleSpanError() + } + + self.testAsync { + do { + _ = try await Tracer.withSpan("hello", operation) + } catch { + XCTAssertTrue(spanEnded) + XCTAssertEqual(error as? ExampleSpanError, ExampleSpanError()) + return + } + XCTFail("Should have thrown") + } + } + + func test_static_Tracer_withSpan_automaticBaggagePropagation_throws() throws { + guard #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) else { + throw XCTSkip("Task locals are not supported on this platform.") + } + + let tracer = TestTracer() + InstrumentationSystem.bootstrapInternal(tracer) + defer { + InstrumentationSystem.bootstrapInternal(nil) + } + + var spanEnded = false + tracer.onEndSpan = { _ in spanEnded = true } + + func operation(span: SpanProtocol) throws -> String { + throw ExampleSpanError() + } + + self.testAsync { + do { + _ = try Tracer.withSpan("hello", operation) } catch { XCTAssertTrue(spanEnded) XCTAssertEqual(error as? ExampleSpanError, ExampleSpanError()) @@ -250,7 +318,6 @@ final class TracerTests: XCTestCase { } XCTFail("Should have thrown") } - #endif } func testWithSpan_recordErrorWithAttributes() throws { @@ -271,7 +338,7 @@ final class TracerTests: XCTestCase { let errorToThrow = ExampleSpanError() let attrsForError: SpanAttributes = ["attr": "value"] - tracer.withSpan("hello") { span in + tracer.withAnySpan("hello") { span in span.recordError(errorToThrow, attributes: attrsForError) } @@ -343,12 +410,14 @@ struct FakeHTTPServer { } func receive(_ request: FakeHTTPRequest) { - let tracer = InstrumentationSystem.tracer - var baggage = Baggage.topLevel InstrumentationSystem.instrument.extract(request.headers, into: &baggage, using: HTTPHeadersExtractor()) - let span = tracer.startSpan("GET \(request.path)", baggage: baggage) + #if swift(>=5.7.0) + let span = InstrumentationSystem.legacyTracer.startAnySpan("GET \(request.path)", baggage: baggage) + #else + let span = InstrumentationSystem.legacyTracer.startAnySpan("GET \(request.path)", baggage: baggage) + #endif let response = self.catchAllHandler(span.baggage, request, self.client) span.attributes["http.status"] = response.status @@ -364,7 +433,11 @@ final class FakeHTTPClient { func performRequest(_ baggage: Baggage, request: FakeHTTPRequest) { var request = request - let span = InstrumentationSystem.tracer.startSpan("GET \(request.path)", baggage: baggage) + #if swift(>=5.7.0) + let span = InstrumentationSystem.legacyTracer.startAnySpan("GET \(request.path)", baggage: baggage) + #else + let span = InstrumentationSystem.legacyTracer.startAnySpan("GET \(request.path)", baggage: baggage) + #endif self.baggages.append(span.baggage) InstrumentationSystem.instrument.inject(baggage, into: &request.headers, using: HTTPHeadersInjector()) span.end() diff --git a/Tests/TracingTests/TracingInstrumentationSystemTests.swift b/Tests/TracingTests/TracingInstrumentationSystemTests.swift index 5e68ea5b..b0b9ec2a 100644 --- a/Tests/TracingTests/TracingInstrumentationSystemTests.swift +++ b/Tests/TracingTests/TracingInstrumentationSystemTests.swift @@ -17,11 +17,17 @@ import Tracing import XCTest extension InstrumentationSystem { - public static func _tracer(of tracerType: T.Type) -> T? where T: Tracer { + public static func _legacyTracer(of tracerType: T.Type) -> T? where T: LegacyTracerProtocol { self._findInstrument(where: { $0 is T }) as? T } - public static func _instrument(of instrumentType: I.Type) -> I? where I: Instrument { + #if swift(>=5.7.0) + public static func _tracer(of tracerType: T.Type) -> T? where T: TracerProtocol { + self._findInstrument(where: { $0 is T }) as? T + } + #endif + + public static func _instrument(of instrumentType: I.Type) -> I? where I: InstrumentProtocol { self._findInstrument(where: { $0 is I }) as? I } } @@ -35,22 +41,33 @@ final class TracingInstrumentationSystemTests: XCTestCase { func testItProvidesAccessToATracer() { let tracer = TestTracer() + XCTAssertNil(InstrumentationSystem._legacyTracer(of: TestTracer.self)) + #if swift(>=5.7.0) XCTAssertNil(InstrumentationSystem._tracer(of: TestTracer.self)) + #endif InstrumentationSystem.bootstrapInternal(tracer) XCTAssertFalse(InstrumentationSystem.instrument is MultiplexInstrument) XCTAssert(InstrumentationSystem._instrument(of: TestTracer.self) === tracer) XCTAssertNil(InstrumentationSystem._instrument(of: NoOpInstrument.self)) + XCTAssert(InstrumentationSystem._legacyTracer(of: TestTracer.self) === tracer) + XCTAssert(InstrumentationSystem.legacyTracer is TestTracer) + #if swift(>=5.7.0) XCTAssert(InstrumentationSystem._tracer(of: TestTracer.self) === tracer) XCTAssert(InstrumentationSystem.tracer is TestTracer) + #endif let multiplexInstrument = MultiplexInstrument([tracer]) InstrumentationSystem.bootstrapInternal(multiplexInstrument) XCTAssert(InstrumentationSystem.instrument is MultiplexInstrument) XCTAssert(InstrumentationSystem._instrument(of: TestTracer.self) === tracer) + XCTAssert(InstrumentationSystem._legacyTracer(of: TestTracer.self) === tracer) + XCTAssert(InstrumentationSystem.legacyTracer is TestTracer) + #if swift(>=5.7.0) XCTAssert(InstrumentationSystem._tracer(of: TestTracer.self) === tracer) XCTAssert(InstrumentationSystem.tracer is TestTracer) + #endif } }