From 8b522dce2951f258e0ae96da7882ac33a049197b Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Mon, 13 Mar 2023 16:01:27 +0900 Subject: [PATCH 1/5] associated type Span, Tracer as short-hand, and *Protocol types **Motivation:** This is a revival of https://github.com/apple/swift-distributed-tracing/pull/84 where we try to KEEP compatibility with versions below 5.7 with a compatibility "legacy" tracer type, but otherwise move towards requiring 5.7 for all the "nice" apis that use associated types and `any TracerProtocol` and friends **Modifications:** - `Tracer` -> `TracerProtocol` - `Tracer` is now a namespace in order to `Tracer.withSpan {}` easily - `Span` -> `SpanProtocol` - Introduce `LegacyTracerProtocol` which does not make use of associated type Span, and can be used in 5.6 libraries; they can deprecate and move away form it ASAP as they start requiring 5.7 **Result:** Offer the APIs we want in 5.7 but remain compatible with 5.6 until we drop it as soon as 5.9 is released as stable - this allows us to adopt eagerly in libraries without having to wait for 5.9 to drop. --- Package.swift | 6 + Package@swift-5.2.swift | 6 + Package@swift-5.3.swift | 6 + Package@swift-5.4.swift | 6 + Package@swift-5.5.swift | 6 + README.md | 24 +- Sources/Instrumentation/Instrument.swift | 4 +- .../InstrumentationSystem.swift | 26 +- .../Instrumentation/MultiplexInstrument.swift | 12 +- Sources/Instrumentation/NoOpInstrument.swift | 4 +- Sources/Tracing/Docs.docc/InDepthGuide.md | 18 +- Sources/Tracing/Docs.docc/index.md | 2 +- .../InstrumentationSystem+Tracing.swift | 20 +- Sources/Tracing/NoOpTracer.swift | 59 ++-- .../{Span.swift => SpanProtocol.swift} | 29 +- Sources/Tracing/Tracer.swift | 311 +++++------------ Sources/Tracing/TracerProtocol+Legacy.swift | 203 +++++++++++ Sources/Tracing/TracerProtocol.swift | 317 ++++++++++++++++++ .../SpanAttributesDSLBenchmark.swift | 8 +- .../InstrumentTests.swift | 4 +- .../InstrumentationSystemTests.swift | 6 +- .../DynamicTracepointTracerTests.swift | 80 ++++- Tests/TracingTests/SpanTests.swift | 2 +- Tests/TracingTests/TestTracer.swift | 32 +- Tests/TracingTests/TracedLock.swift | 4 +- Tests/TracingTests/TracedLockTests.swift | 29 +- Tests/TracingTests/TracerTests.swift | 51 ++- .../TracingInstrumentationSystemTests.swift | 21 +- 28 files changed, 939 insertions(+), 357 deletions(-) rename Sources/Tracing/{Span.swift => SpanProtocol.swift} (95%) create mode 100644 Sources/Tracing/TracerProtocol+Legacy.swift create mode 100644 Sources/Tracing/TracerProtocol.swift diff --git a/Package.swift b/Package.swift index 2366ce3..435c511 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 index 111d4ff..6715c23 100644 --- a/Package@swift-5.2.swift +++ b/Package@swift-5.2.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.3.swift b/Package@swift-5.3.swift index 111d4ff..6715c23 100644 --- a/Package@swift-5.3.swift +++ b/Package@swift-5.3.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.4.swift b/Package@swift-5.4.swift index 111d4ff..6715c23 100644 --- a/Package@swift-5.4.swift +++ b/Package@swift-5.4.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.5.swift b/Package@swift-5.5.swift index 111d4ff..6715c23 100644 --- a/Package@swift-5.5.swift +++ b/Package@swift-5.5.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/README.md b/README.md index 8e67175..15453b5 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,8 @@ This project uses the context progagation type defined independently in: + [Instrumenting your software](#library-framework-developers--instrumenting-your-software) + [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) +* In-Depth Guide for: **InstrumentProtocol developers** + + [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 @@ -261,7 +261,7 @@ When instrumenting server applications there are typically three parties involve 1. [Application developers](#application-developers-setting-up-instruments) creating server-side applications 2. [Library/Framework developers](#libraryframework-developers-instrumenting-your-software) providing building blocks to create these applications -3. [Instrument developers](#instrument-developers-creating-an-instrument) providing tools to collect distributed metadata about your application +3. [InstrumentProtocol developers](#instrument-developers-creating-an-instrument) providing tools to collect distributed metadata about your application For applications to be instrumented correctly these three parts have to play along nicely. @@ -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 @@ -536,12 +536,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/Instrumentation/Instrument.swift b/Sources/Instrumentation/Instrument.swift index 7224b7e..59ff81c 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 9c512a8..90000e7 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: any 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 6679357..fd96c19 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 437b31b..040da89 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 052f9f2..9dd1eeb 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 f541c06..db770d3 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 1be4949..8d62ea7 100644 --- a/Sources/Tracing/InstrumentationSystem+Tracing.swift +++ b/Sources/Tracing/InstrumentationSystem+Tracing.swift @@ -21,7 +21,23 @@ extension InstrumentationSystem { /// 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() + #if swift(>=5.7.0) + 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 8d18a87..7527f9a 100644 --- a/Sources/Tracing/NoOpTracer.swift +++ b/Sources/Tracing/NoOpTracer.swift @@ -16,52 +16,50 @@ 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: Baggage, + ofKind kind: SpanKind, + at time: DispatchWallTime, + function: String, + file fileID: String, + line: UInt) -> any SpanProtocol { + NoOpSpan(baggage: baggage) } public func forceFlush() {} public func inject(_ baggage: Baggage, into carrier: inout Carrier, using injector: Inject) - where Inject: Injector, Carrier == Inject.Carrier - { + where Inject: Injector, Carrier == Inject.Carrier { // no-op } public func extract(_ carrier: Carrier, into baggage: inout Baggage, using extractor: Extract) - where Extract: Extractor, Carrier == Extract.Carrier - { + where Extract: Extractor, Carrier == Extract.Carrier { // 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 +85,20 @@ public struct NoOpTracer: Tracer { } } } + +#if swift(>=5.7.0) +extension NoOpTracer: TracerProtocol { + + public func startSpan( + _ operationName: String, + baggage: Baggage, + ofKind kind: SpanKind, + at time: DispatchWallTime, + function: String, + file fileID: String, + line: UInt + ) -> Self.Span { + NoOpSpan(baggage: baggage) + } +} +#endif \ No newline at end of file diff --git a/Sources/Tracing/Span.swift b/Sources/Tracing/SpanProtocol.swift similarity index 95% rename from Sources/Tracing/Span.swift rename to Sources/Tracing/SpanProtocol.swift index ff271a7..dcfc73a 100644 --- a/Sources/Tracing/Span.swift +++ b/Sources/Tracing/SpanProtocol.swift @@ -25,8 +25,17 @@ import struct Dispatch.DispatchWallTime /// /// Creating a `Span` is delegated to a ``Tracer`` and end users should never create them directly. /// +/// ### Reference semantics +/// A span always must exhibit reference semantics. Passing around a `span` must allow other pieces of code +/// modify it safely. The span must therefore employ synchronization techniques adequate to ensure this. +/// +/// It is allowed to implement `SpanProtocol` using a `struct` or `enum`, however the type must still exhibit reference +/// semantics. This is useful especially for implementing efficient "no-op" or "tracing is disabled" span implementations, +/// which can be almost empty struct instances, while their "tracing is enabled" versions should refer all state to an +/// underlying reference type "box" which contains all the spans data. +/// /// - 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 +57,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 +80,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) @@ -85,10 +97,12 @@ public protocol Span: AnyObject, _SwiftTracingSendableSpan { /// - Parameter time: The `DispatchWallTime` at which the span ended. /// /// - SeeAlso: `Span.end()` which automatically uses the "current" time. + // @available(*, deprecated, message: "Use Clock based `end(at:)` instead") func end(at time: DispatchWallTime) + } -extension Span { +extension SpanProtocol { /// End this `Span` at the current time. /// /// ### Rules about ending Spans @@ -108,14 +122,21 @@ extension Span { } /// Adds a ``SpanLink`` between this `Span` and the given `Span`. + /// + /// ### Reference semantics + /// This setter `nonmutating` on purpose, a span may be implemented using a `struct`. + /// All state mutations performed on a struct must behave using reference semantics: + /// sharing a span with various pieces of code, must all be mutating the same underlying + /// reference semantics storage. + /// /// - 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 e0b7eb2..03737c8 100644 --- a/Sources/Tracing/Tracer.swift +++ b/Sources/Tracing/Tracer.swift @@ -16,309 +16,148 @@ 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. - /// - /// - 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. - /// - /// - 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: 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() +@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 { - #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( + static func startSpan( _ operationName: String, baggage: Baggage, - ofKind kind: SpanKind = .internal, + ofKind kind: SpanKind, + 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(), + at: time, 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( - operationName, - baggage: baggage, - ofKind: kind, - at: .now(), - function: function, - file: file, - line: line + #else + InstrumentationSystem.legacyTracer.startAnySpan( + operationName, + baggage: baggage, + ofKind: kind, + at: time, + function: function, + 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``. - /// - /// 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. - /// - 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( + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage + static func startSpan( _ operationName: String, - baggage: Baggage, - ofKind kind: SpanKind = .internal, + ofKind kind: SpanKind, function: String = #function, file fileID: String = #fileID, - line: UInt = #line, - _ operation: (Span) throws -> T - ) rethrows -> T { - let span = self.startSpan( + line: UInt = #line + ) -> 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, + baggage: .current ?? .topLevel, 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 - } - } - #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 + InstrumentationSystem.legacyTracer.startAnySpan( operationName, - baggage: baggage, + baggage: .current ?? .topLevel, 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 - } + #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`. - /// - /// 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( + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public static func withSpan( _ operationName: String, 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 { - try self.withSpan( + #if swift(>=5.7.0) + try InstrumentationSystem.legacyTracer.withAnySpan( operationName, - baggage: .current ?? .topLevel, ofKind: kind, function: function, file: fileID, - line: line - ) { span in - try Baggage.$current.withValue(span.baggage) { - try operation(span) - } + line: line) { anySpan in + try operation(anySpan) } + #else + try InstrumentationSystem.legacyTracer.withAnySpan( + operationName, + ofKind: kind, + function: function, + file: fileID, + line: line) { anySpan in + try operation(anySpan) + } + #endif } - /// 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( + #if swift(>=5.7.0) + @_unsafeInheritExecutor + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public static func withSpan( _ operationName: String, ofKind kind: SpanKind = .internal, 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.tracer.withAnySpan( operationName, - baggage: .current ?? .topLevel, 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 + line: line) { anySpan in + try await operation(anySpan) } } - - /// 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( + #else + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public static 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 + _ operation: (any SpanProtocol) 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 + try await InstrumentationSystem.legacyTracer.withAnySpan( + operationName, + ofKind: kind, + function: function, + file: fileID, + line: line) { 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 0000000..dbec053 --- /dev/null +++ b/Sources/Tracing/TracerProtocol+Legacy.swift @@ -0,0 +1,203 @@ +//===----------------------------------------------------------------------===// +// +// 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(*, deprecated, message: "Use 'TracerProtocol' instead") +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage +public protocol LegacyTracerProtocol: InstrumentProtocol { + + // @available(*, deprecated, message: "Use 'TracerProtocol/startSpan' instead") + func startAnySpan( + _ operationName: String, + baggage: 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 { + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage + 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) + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage + public func withAnySpan( + _ operationName: String, + baggage: @autoclosure () -> Baggage = .current ?? .topLevel, + ofKind kind: SpanKind = .internal, + 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: .now(), 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 + } + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public func withAnySpan( + _ operationName: String, + baggage: @autoclosure () -> Baggage = .current ?? .topLevel, + ofKind kind: SpanKind = .internal, + 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: .now(), 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) +extension TracerProtocol { + public func startAnySpan( + _ operationName: String, + baggage: Baggage, + ofKind kind: SpanKind, + at time: DispatchWallTime, + function: String, + file fileID: String, + line: UInt + ) -> any SpanProtocol { + self.startSpan( + operationName, + baggage: baggage, + ofKind: kind, + at: time, + function: function, + file: fileID, + line: line + ) + } + + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + // @available(*, deprecated, message: "Use 'TracerProtocol/withSpan' instead") + public func withAnySpan( + _ operationName: String, + ofKind kind: SpanKind, + function: String, + file fileID: String, + line: UInt, + _ operation: (any SpanProtocol) throws -> T + ) rethrows -> T { + try self.withSpan( + operationName, + ofKind: kind, + function: function, + file: fileID, + line: line) { span in + try operation(span) + } + } + + #if swift(>=5.7.0) + @_unsafeInheritExecutor + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + // @available(*, deprecated, message: "Use 'TracerProtocol/withSpan' instead") + public func withAnySpan( + _ operationName: String, + ofKind kind: SpanKind, + function: String, + file fileID: String, + line: UInt, + _ operation: (any SpanProtocol) async throws -> T + ) async rethrows -> T { + try await self.withSpan( + operationName, + ofKind: kind, + function: function, + file: fileID, + line: line) { span in + try await operation(span) + } + } + #else + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + // @available(*, deprecated, message: "Use 'TracerProtocol/withSpan' instead") + public func withAnySpan( + _ operationName: String, + ofKind kind: SpanKind, + function: String, + file fileID: String, + line: UInt, + _ operation: (any SpanProtocol) async throws -> T + ) async rethrows -> T { + try await self.withSpan( + operationName, + ofKind: kind, + function: function, + file: fileID, + line: line) { span in + try await operation(span) + } + } + #endif + +} +#endif \ No newline at end of file diff --git a/Sources/Tracing/TracerProtocol.swift b/Sources/Tracing/TracerProtocol.swift new file mode 100644 index 0000000..f124626 --- /dev/null +++ b/Sources/Tracing/TracerProtocol.swift @@ -0,0 +1,317 @@ +//===----------------------------------------------------------------------===// +// +// 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) +/// An `InstrumentProtocol` with added functionality for distributed tracing. It uses the span-based tracing model and is +/// based on the OpenTracing/OpenTelemetry spec. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage +public protocol TracerProtocol: LegacyTracerProtocol { + associatedtype Span: SpanProtocol + + /// Start a new ``Span`` with the given `Baggage` at a given time. + /// + /// - 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. + /// + /// - 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: Baggage, + ofKind kind: SpanKind, + at time: DispatchWallTime, + function: String, + file fileID: String, + line: UInt + ) -> Self.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() +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage +extension TracerProtocol { + /// 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, + ofKind kind: SpanKind = .internal, + function: String = #function, + file fileID: String = #fileID, + line: UInt = #line + ) -> Self.Span { + self.startSpan( + operationName, + baggage: baggage, + ofKind: kind, + at: .now(), + 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 { + /// 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. + /// - 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) + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage + public func withSpan( + _ operationName: String, + baggage: Baggage, + ofKind kind: SpanKind = .internal, + 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: .now(), + 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 + } + } +} + +// ==== ---------------------------------------------------------------------------------------------------------------- +// MARK: Starting spans: Task-local Baggage propagation + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage +extension TracerProtocol { + /// 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`. + /// + /// 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( + _ operationName: String, + ofKind kind: SpanKind = .internal, + function: String = #function, + file fileID: String = #fileID, + line: UInt = #line, + _ operation: (Self.Span) throws -> T + ) rethrows -> T { + try self.withSpan( + operationName, + baggage: .current ?? .topLevel, + ofKind: kind, + function: function, + file: fileID, + line: line + ) { span in + try Baggage.$current.withValue(span.baggage) { + try operation(span) + } + } + } + + /// 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) + #if swift(>=5.7.0) + @_unsafeInheritExecutor + public func withSpan( + _ operationName: String, + ofKind kind: SpanKind = .internal, + 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: .current ?? .topLevel, + 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 + } + } + #else + public func withSpan( + _ operationName: String, + ofKind kind: SpanKind = .internal, + 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: .current ?? .topLevel, + 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 + } + } + #endif + + /// 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) + #if swift(>=5.7.0) + @_unsafeInheritExecutor + public func withSpan( + _ operationName: String, + baggage: Baggage, + ofKind kind: SpanKind = .internal, + function: String = #function, + file fileID: String = #fileID, + line: UInt = #line, + _ operation: (SpanProtocol) 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 + } + } + #else + public func withSpan( + _ operationName: String, + baggage: Baggage, + ofKind kind: SpanKind = .internal, + 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, 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 +} + +#endif // Swift 5.7 diff --git a/Sources/_TracingBenchmarks/SpanAttributesDSLBenchmark.swift b/Sources/_TracingBenchmarks/SpanAttributesDSLBenchmark.swift index 0a930de..18cca95 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 aaea30a..cada7ed 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 87a7be5..5f0a2c8 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 1291531..61d38ae 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)") } @@ -87,14 +103,14 @@ final class DynamicTracepointTracerTests: XCTestCase { } func logic(fakeLine: UInt) { - #if swift(>=5.5) + #if swift(>=5.7) InstrumentationSystem.tracer.withSpan("\(#function)-dont", line: fakeLine) { _ in } #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 @@ -105,7 +121,7 @@ final class DynamicTracepointTracerTests: XCTestCase { } /// 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,20 +155,20 @@ final class DynamicTracepointTestTracer: Tracer { } private(set) var spans: [TracepointSpan] = [] - var onEndSpan: (Span) -> Void = { _ in + var onEndSpan: (SpanProtocol) -> Void = { _ in } - func startSpan(_ operationName: String, + func startAnySpan(_ operationName: String, baggage: InstrumentationBaggage.Baggage, ofKind kind: Tracing.SpanKind, at time: DispatchWallTime, function: String, file fileID: String, - line: UInt) -> Tracing.Span + 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( @@ -229,7 +245,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 +257,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 +279,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 +321,36 @@ extension DynamicTracepointTestTracer { } } +#if compiler(>=5.7.0) +extension DynamicTracepointTestTracer: TracerProtocol { + typealias Span = TracepointSpan + func startSpan(_ operationName: String, + baggage: InstrumentationBaggage.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 25aa30e..1e76fe8 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 1479e89..b3cf32d 100644 --- a/Tests/TracingTests/TestTracer.swift +++ b/Tests/TracingTests/TestTracer.swift @@ -19,11 +19,11 @@ 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, ofKind kind: SpanKind, @@ -31,7 +31,7 @@ final class TestTracer: Tracer { function: String, file fileID: String, line: UInt - ) -> Span { + ) -> any SpanProtocol { let span = TestSpan( operationName: operationName, startTime: time, @@ -64,6 +64,30 @@ final class TestTracer: Tracer { } } +#if swift(>=5.7.0) +extension TestTracer: TracerProtocol { + func startSpan( + _ operationName: String, + baggage: 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 a17003a..92043a4 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 3a5dedd..d08d2b1 100644 --- a/Tests/TracingTests/TracedLockTests.swift +++ b/Tests/TracingTests/TracedLockTests.swift @@ -59,8 +59,8 @@ 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, ofKind kind: SpanKind, @@ -68,7 +68,7 @@ private final class TracedLockPrintlnTracer: Tracer { function: String, file fileID: String, line: UInt - ) -> Span { + ) -> any SpanProtocol { TracedLockPrintlnSpan( operationName: operationName, startTime: time, @@ -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: 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.swift b/Tests/TracingTests/TracerTests.swift index fa65cef..8259049 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) } @@ -202,14 +213,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) @@ -236,13 +247,13 @@ 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()) @@ -271,7 +282,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 +354,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 +377,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 5e68ea5..b0b9ec2 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 } } From cb24ba3b5ddac7a518985ab80a9becd60f678104 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Mon, 13 Mar 2023 22:04:07 +0900 Subject: [PATCH 2/5] formatting --- .../InstrumentationSystem.swift | 2 +- .../InstrumentationSystem+Tracing.swift | 2 +- Sources/Tracing/NoOpTracer.swift | 12 +++++---- Sources/Tracing/SpanProtocol.swift | 1 - Sources/Tracing/Tracer.swift | 26 +++++++++++-------- Sources/Tracing/TracerProtocol+Legacy.swift | 20 +++++++------- .../DynamicTracepointTracerTests.swift | 25 +++++++++--------- Tests/TracingTests/TracerTests.swift | 2 +- 8 files changed, 47 insertions(+), 43 deletions(-) diff --git a/Sources/Instrumentation/InstrumentationSystem.swift b/Sources/Instrumentation/InstrumentationSystem.swift index 90000e7..ac72be2 100644 --- a/Sources/Instrumentation/InstrumentationSystem.swift +++ b/Sources/Instrumentation/InstrumentationSystem.swift @@ -57,7 +57,7 @@ public enum InstrumentationSystem { /// Returns the globally configured ``InstrumentProtocol``. /// /// Defaults to a no-op ``InstrumentProtocol`` if ``bootstrap(_:)`` wasn't called before. - public static var instrument: any InstrumentProtocol { + public static var instrument: InstrumentProtocol { self.lock.withReaderLock { self._instrument } } } diff --git a/Sources/Tracing/InstrumentationSystem+Tracing.swift b/Sources/Tracing/InstrumentationSystem+Tracing.swift index 8d62ea7..b3e50c0 100644 --- a/Sources/Tracing/InstrumentationSystem+Tracing.swift +++ b/Sources/Tracing/InstrumentationSystem+Tracing.swift @@ -25,7 +25,7 @@ extension InstrumentationSystem { public static var tracer: any TracerProtocol { let found: (any TracerProtocol)? = (self._findInstrument(where: { $0 is (any TracerProtocol) }) as? (any TracerProtocol)) - return found ?? NoOpTracer() + return found ?? NoOpTracer() } #endif diff --git a/Sources/Tracing/NoOpTracer.swift b/Sources/Tracing/NoOpTracer.swift index 7527f9a..46f1f0f 100644 --- a/Sources/Tracing/NoOpTracer.swift +++ b/Sources/Tracing/NoOpTracer.swift @@ -28,19 +28,22 @@ public struct NoOpTracer: LegacyTracerProtocol { at time: DispatchWallTime, function: String, file fileID: String, - line: UInt) -> any SpanProtocol { + line: UInt) -> any SpanProtocol + { NoOpSpan(baggage: baggage) } public func forceFlush() {} public func inject(_ baggage: Baggage, into carrier: inout Carrier, using injector: Inject) - where Inject: Injector, Carrier == Inject.Carrier { + where Inject: Injector, Carrier == Inject.Carrier + { // no-op } public func extract(_ carrier: Carrier, into baggage: inout Baggage, using extractor: Extract) - where Extract: Extractor, Carrier == Extract.Carrier { + where Extract: Extractor, Carrier == Extract.Carrier + { // no-op } @@ -88,7 +91,6 @@ public struct NoOpTracer: LegacyTracerProtocol { #if swift(>=5.7.0) extension NoOpTracer: TracerProtocol { - public func startSpan( _ operationName: String, baggage: Baggage, @@ -101,4 +103,4 @@ extension NoOpTracer: TracerProtocol { NoOpSpan(baggage: baggage) } } -#endif \ No newline at end of file +#endif diff --git a/Sources/Tracing/SpanProtocol.swift b/Sources/Tracing/SpanProtocol.swift index dcfc73a..30b535a 100644 --- a/Sources/Tracing/SpanProtocol.swift +++ b/Sources/Tracing/SpanProtocol.swift @@ -99,7 +99,6 @@ public protocol SpanProtocol: AnyObject, _SwiftTracingSendableSpan { /// - SeeAlso: `Span.end()` which automatically uses the "current" time. // @available(*, deprecated, message: "Use Clock based `end(at:)` instead") func end(at time: DispatchWallTime) - } extension SpanProtocol { diff --git a/Sources/Tracing/Tracer.swift b/Sources/Tracing/Tracer.swift index 03737c8..6cf1f6d 100644 --- a/Sources/Tracing/Tracer.swift +++ b/Sources/Tracing/Tracer.swift @@ -46,13 +46,13 @@ extension Tracer { ) #else InstrumentationSystem.legacyTracer.startAnySpan( - operationName, - baggage: baggage, - ofKind: kind, - at: time, - function: function, - file: fileID, - line: line + operationName, + baggage: baggage, + ofKind: kind, + at: time, + function: function, + file: fileID, + line: line ) #endif } @@ -105,7 +105,8 @@ extension Tracer { ofKind: kind, function: function, file: fileID, - line: line) { anySpan in + line: line + ) { anySpan in try operation(anySpan) } #else @@ -114,7 +115,8 @@ extension Tracer { ofKind: kind, function: function, file: fileID, - line: line) { anySpan in + line: line + ) { anySpan in try operation(anySpan) } #endif @@ -136,7 +138,8 @@ extension Tracer { ofKind: kind, function: function, file: fileID, - line: line) { anySpan in + line: line + ) { anySpan in try await operation(anySpan) } } @@ -155,7 +158,8 @@ extension Tracer { ofKind: kind, function: function, file: fileID, - line: line) { anySpan in + line: line + ) { anySpan in try await operation(anySpan) } } diff --git a/Sources/Tracing/TracerProtocol+Legacy.swift b/Sources/Tracing/TracerProtocol+Legacy.swift index dbec053..025cfad 100644 --- a/Sources/Tracing/TracerProtocol+Legacy.swift +++ b/Sources/Tracing/TracerProtocol+Legacy.swift @@ -19,7 +19,6 @@ import Dispatch // @available(*, deprecated, message: "Use 'TracerProtocol' instead") @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage public protocol LegacyTracerProtocol: InstrumentProtocol { - // @available(*, deprecated, message: "Use 'TracerProtocol/startSpan' instead") func startAnySpan( _ operationName: String, @@ -38,7 +37,6 @@ public protocol LegacyTracerProtocol: InstrumentProtocol { /// /// This function should not block indefinitely, implementations should offer a configurable timeout for flush operations. func forceFlush() - } // ==== ------------------------------------------------------------------ @@ -46,7 +44,6 @@ public protocol LegacyTracerProtocol: InstrumentProtocol { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage extension LegacyTracerProtocol { - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage public func startAnySpan( _ operationName: String, @@ -64,7 +61,8 @@ extension LegacyTracerProtocol { at: time, function: function, file: fileID, - line: line) + line: line + ) } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage @@ -110,7 +108,6 @@ extension LegacyTracerProtocol { throw error // rethrow } } - } #if swift(>=5.7.0) @@ -135,7 +132,6 @@ extension TracerProtocol { ) } - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // @available(*, deprecated, message: "Use 'TracerProtocol/withSpan' instead") public func withAnySpan( @@ -151,7 +147,8 @@ extension TracerProtocol { ofKind: kind, function: function, file: fileID, - line: line) { span in + line: line + ) { span in try operation(span) } } @@ -173,7 +170,8 @@ extension TracerProtocol { ofKind: kind, function: function, file: fileID, - line: line) { span in + line: line + ) { span in try await operation(span) } } @@ -193,11 +191,11 @@ extension TracerProtocol { ofKind: kind, function: function, file: fileID, - line: line) { span in + line: line + ) { span in try await operation(span) } } #endif - } -#endif \ No newline at end of file +#endif diff --git a/Tests/TracingTests/DynamicTracepointTracerTests.swift b/Tests/TracingTests/DynamicTracepointTracerTests.swift index 61d38ae..3205564 100644 --- a/Tests/TracingTests/DynamicTracepointTracerTests.swift +++ b/Tests/TracingTests/DynamicTracepointTracerTests.swift @@ -159,12 +159,12 @@ final class DynamicTracepointTestTracer: LegacyTracerProtocol { } func startAnySpan(_ operationName: String, - baggage: InstrumentationBaggage.Baggage, - ofKind kind: Tracing.SpanKind, - at time: DispatchWallTime, - function: String, - file fileID: String, - line: UInt) -> any SpanProtocol + baggage: InstrumentationBaggage.Baggage, + ofKind kind: Tracing.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 { @@ -325,12 +325,13 @@ extension DynamicTracepointTestTracer { extension DynamicTracepointTestTracer: TracerProtocol { typealias Span = TracepointSpan func startSpan(_ operationName: String, - baggage: InstrumentationBaggage.Baggage, - ofKind kind: Tracing.SpanKind, - at time: DispatchWallTime, - function: String, - file fileID: String, - line: UInt) -> TracepointSpan { + baggage: InstrumentationBaggage.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) diff --git a/Tests/TracingTests/TracerTests.swift b/Tests/TracingTests/TracerTests.swift index 8259049..930f9d9 100644 --- a/Tests/TracingTests/TracerTests.swift +++ b/Tests/TracingTests/TracerTests.swift @@ -60,7 +60,7 @@ 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 { From d214dcc61da2a10a20dfa66b217ab2eeddf5a5ec Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Mon, 13 Mar 2023 22:13:46 +0900 Subject: [PATCH 3/5] fix test --- Tests/TracingTests/DynamicTracepointTracerTests.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Tests/TracingTests/DynamicTracepointTracerTests.swift b/Tests/TracingTests/DynamicTracepointTracerTests.swift index 3205564..78daf43 100644 --- a/Tests/TracingTests/DynamicTracepointTracerTests.swift +++ b/Tests/TracingTests/DynamicTracepointTracerTests.swift @@ -105,6 +105,11 @@ final class DynamicTracepointTracerTests: XCTestCase { func logic(fakeLine: UInt) { #if swift(>=5.7) InstrumentationSystem.tracer.withSpan("\(#function)-dont", line: fakeLine) { _ in + // inside + } + #else + InstrumentationSystem.legacyTracer.withAnySpan("\(#function)-dont", line: fakeLine) { _ in + // inside } #endif } @@ -116,6 +121,12 @@ final class DynamicTracepointTracerTests: XCTestCase { // inside } } + #else + InstrumentationSystem.legacyTracer.withAnySpan("\(#function)-yes", line: fakeLine) { _ in + InstrumentationSystem.legacyTracer.withAnySpan("\(#function)-yes-inside", line: fakeLine + 11) { _ in + // inside + } + } #endif } } From 765f8453c40ec858cbf97606a88cf4898c6a364a Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Wed, 15 Mar 2023 12:43:07 +0900 Subject: [PATCH 4/5] cleanup --- .../Tracing/InstrumentationSystem+Tracing.swift | 2 +- Sources/Tracing/SpanProtocol.swift | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/Sources/Tracing/InstrumentationSystem+Tracing.swift b/Sources/Tracing/InstrumentationSystem+Tracing.swift index b3e50c0..a67710e 100644 --- a/Sources/Tracing/InstrumentationSystem+Tracing.swift +++ b/Sources/Tracing/InstrumentationSystem+Tracing.swift @@ -15,13 +15,13 @@ @_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. - #if swift(>=5.7.0) public static var tracer: any TracerProtocol { let found: (any TracerProtocol)? = (self._findInstrument(where: { $0 is (any TracerProtocol) }) as? (any TracerProtocol)) diff --git a/Sources/Tracing/SpanProtocol.swift b/Sources/Tracing/SpanProtocol.swift index 30b535a..316d155 100644 --- a/Sources/Tracing/SpanProtocol.swift +++ b/Sources/Tracing/SpanProtocol.swift @@ -25,15 +25,6 @@ import struct Dispatch.DispatchWallTime /// /// Creating a `Span` is delegated to a ``Tracer`` and end users should never create them directly. /// -/// ### Reference semantics -/// A span always must exhibit reference semantics. Passing around a `span` must allow other pieces of code -/// modify it safely. The span must therefore employ synchronization techniques adequate to ensure this. -/// -/// It is allowed to implement `SpanProtocol` using a `struct` or `enum`, however the type must still exhibit reference -/// semantics. This is useful especially for implementing efficient "no-op" or "tracing is disabled" span implementations, -/// which can be almost empty struct instances, while their "tracing is enabled" versions should refer all state to an -/// underlying reference type "box" which contains all the spans data. -/// /// - 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 SpanProtocol: AnyObject, _SwiftTracingSendableSpan { /// The read-only `Baggage` of this `Span`, set when starting this `Span`. @@ -122,12 +113,6 @@ extension SpanProtocol { /// Adds a ``SpanLink`` between this `Span` and the given `Span`. /// - /// ### Reference semantics - /// This setter `nonmutating` on purpose, a span may be implemented using a `struct`. - /// All state mutations performed on a struct must behave using reference semantics: - /// sharing a span with various pieces of code, must all be mutating the same underlying - /// reference semantics storage. - /// /// - Parameter other: The `Span` to link to. /// - Parameter attributes: The ``SpanAttributes`` describing this link. Defaults to no attributes. public func addLink(_ other: SpanProtocol, attributes: SpanAttributes = [:]) { From 3395e41d4dbcf3ba0ce944b95e885199fdf9888f Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Wed, 15 Mar 2023 14:17:43 +0900 Subject: [PATCH 5/5] simplify delegation scheme --- Sources/Tracing/SpanProtocol.swift | 1 - Sources/Tracing/Tracer.swift | 142 +++++---- Sources/Tracing/TracerProtocol+Legacy.swift | 308 ++++++++++++++++---- Sources/Tracing/TracerProtocol.swift | 246 +++++----------- Tests/TracingTests/TracerTests+XCTest.swift | 2 + Tests/TracingTests/TracerTests.swift | 64 +++- 6 files changed, 479 insertions(+), 284 deletions(-) diff --git a/Sources/Tracing/SpanProtocol.swift b/Sources/Tracing/SpanProtocol.swift index 316d155..2c44def 100644 --- a/Sources/Tracing/SpanProtocol.swift +++ b/Sources/Tracing/SpanProtocol.swift @@ -88,7 +88,6 @@ public protocol SpanProtocol: AnyObject, _SwiftTracingSendableSpan { /// - Parameter time: The `DispatchWallTime` at which the span ended. /// /// - SeeAlso: `Span.end()` which automatically uses the "current" time. - // @available(*, deprecated, message: "Use Clock based `end(at:)` instead") func end(at time: DispatchWallTime) } diff --git a/Sources/Tracing/Tracer.swift b/Sources/Tracing/Tracer.swift index 6cf1f6d..12b8806 100644 --- a/Sources/Tracing/Tracer.swift +++ b/Sources/Tracing/Tracer.swift @@ -16,6 +16,9 @@ import Dispatch @_exported import Instrumentation @_exported import InstrumentationBaggage +/// 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 @@ -23,10 +26,35 @@ public enum 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. + /// + /// 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. static func startSpan( _ operationName: String, - baggage: Baggage, - ofKind kind: SpanKind, + baggage: @autoclosure () -> Baggage = .current ?? .topLevel, + ofKind kind: SpanKind = .internal, at time: DispatchWallTime = .now(), function: String = #function, file fileID: String = #fileID, @@ -37,9 +65,6 @@ extension Tracer { #if swift(>=5.7.0) InstrumentationSystem.tracer.startSpan( operationName, - baggage: baggage, - ofKind: kind, - at: time, function: function, file: fileID, line: line @@ -57,42 +82,32 @@ extension Tracer { #endif } - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage - static func startSpan( - _ operationName: String, - ofKind kind: SpanKind, - function: String = #function, - file fileID: String = #fileID, - line: UInt = #line - ) -> 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: .current ?? .topLevel, - ofKind: kind, - at: .now(), - function: function, - file: fileID, - line: line - ) - #else - InstrumentationSystem.legacyTracer.startAnySpan( - operationName, - baggage: .current ?? .topLevel, - ofKind: kind, - at: .now(), - function: function, - file: fileID, - line: line - ) - #endif - } - - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + /// 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 static func withSpan( _ operationName: String, + baggage: @autoclosure () -> Baggage = .current ?? .topLevel, ofKind kind: SpanKind = .internal, function: String = #function, file fileID: String = #fileID, @@ -122,12 +137,36 @@ extension Tracer { #endif } - #if swift(>=5.7.0) + /// 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 @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, @@ -135,7 +174,9 @@ extension Tracer { ) async rethrows -> T { try await InstrumentationSystem.tracer.withAnySpan( operationName, + baggage: baggage(), ofKind: kind, + at: time, function: function, file: fileID, line: line @@ -143,25 +184,4 @@ extension Tracer { try await operation(anySpan) } } - #else - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) - public static func withSpan( - _ operationName: String, - ofKind kind: SpanKind = .internal, - function: String = #function, - file fileID: String = #fileID, - line: UInt = #line, - _ operation: (any SpanProtocol) async throws -> T - ) async rethrows -> T { - try await InstrumentationSystem.legacyTracer.withAnySpan( - operationName, - ofKind: kind, - function: function, - file: fileID, - line: line - ) { anySpan in - try await operation(anySpan) - } - } - #endif } diff --git a/Sources/Tracing/TracerProtocol+Legacy.swift b/Sources/Tracing/TracerProtocol+Legacy.swift index 025cfad..81288c6 100644 --- a/Sources/Tracing/TracerProtocol+Legacy.swift +++ b/Sources/Tracing/TracerProtocol+Legacy.swift @@ -16,13 +16,40 @@ import Dispatch @_exported import Instrumentation @_exported import InstrumentationBaggage -// @available(*, deprecated, message: "Use 'TracerProtocol' instead") @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage public protocol LegacyTracerProtocol: InstrumentProtocol { - // @available(*, deprecated, message: "Use 'TracerProtocol/startSpan' instead") + /// 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: Baggage, + baggage: @autoclosure () -> Baggage, ofKind kind: SpanKind, at time: DispatchWallTime, function: String, @@ -44,7 +71,35 @@ public protocol LegacyTracerProtocol: InstrumentProtocol { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage extension LegacyTracerProtocol { - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage + /// 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, @@ -65,17 +120,50 @@ extension LegacyTracerProtocol { ) } - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage + /// 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: .now(), function: function, file: fileID, line: line) + 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) { @@ -87,21 +175,57 @@ extension LegacyTracerProtocol { } } - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + /// 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: .now(), function: function, file: fileID, line: line) + print("HI: \(Self.self) \(#function) @ \(#file):\(#line)") + + 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) + print("OP: \(#function)") + return try await operation(span) } } catch { span.recordError(error) @@ -111,19 +235,52 @@ extension LegacyTracerProtocol { } #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: Baggage, - ofKind kind: SpanKind, - at time: DispatchWallTime, - function: String, - file fileID: String, - line: UInt + 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, + baggage: baggage(), ofKind: kind, at: time, function: function, @@ -132,19 +289,55 @@ extension TracerProtocol { ) } - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) - // @available(*, deprecated, message: "Use 'TracerProtocol/withSpan' instead") + + /// 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, - ofKind kind: SpanKind, - function: String, - file fileID: String, - line: UInt, + 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 @@ -153,42 +346,56 @@ extension TracerProtocol { } } - #if swift(>=5.7.0) + + /// 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) @_unsafeInheritExecutor - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) - // @available(*, deprecated, message: "Use 'TracerProtocol/withSpan' instead") - public func withAnySpan( - _ operationName: String, - ofKind kind: SpanKind, - function: String, - file fileID: String, - line: UInt, - _ operation: (any SpanProtocol) async throws -> T - ) async rethrows -> T { - try await self.withSpan( - operationName, - ofKind: kind, - function: function, - file: fileID, - line: line - ) { span in - try await operation(span) - } - } - #else - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) - // @available(*, deprecated, message: "Use 'TracerProtocol/withSpan' instead") public func withAnySpan( _ operationName: String, - ofKind kind: SpanKind, - function: String, - file fileID: String, - line: UInt, + 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 @@ -196,6 +403,5 @@ extension TracerProtocol { try await operation(span) } } - #endif } #endif diff --git a/Sources/Tracing/TracerProtocol.swift b/Sources/Tracing/TracerProtocol.swift index f124626..85d0860 100644 --- a/Sources/Tracing/TracerProtocol.swift +++ b/Sources/Tracing/TracerProtocol.swift @@ -20,17 +20,30 @@ import Dispatch // MARK: Tracer protocol #if swift(>=5.7.0) -/// An `InstrumentProtocol` with added functionality for distributed tracing. It uses the span-based tracing model and is -/// based on the OpenTracing/OpenTelemetry spec. +/// 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` at a given time. + /// 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 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. + /// - 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, ... @@ -50,39 +63,48 @@ public protocol TracerProtocol: LegacyTracerProtocol { line: UInt ) -> Self.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() } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage extension TracerProtocol { - /// Start a new ``Span`` with the given `Baggage` starting "now". + /// 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: 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. + /// - 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: Baggage, + 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, + baggage: baggage(), ofKind: kind, - at: .now(), + at: time, function: function, file: fileID, line: line @@ -95,25 +117,34 @@ extension TracerProtocol { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage extension TracerProtocol { - /// 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. + /// + /// 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, ... - /// - 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) - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage public func withSpan( _ 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, @@ -121,9 +152,9 @@ extension TracerProtocol { ) rethrows -> T { let span = self.startSpan( operationName, - baggage: baggage, + baggage: baggage(), ofKind: kind, - at: .now(), + at: time, function: function, file: fileID, line: line @@ -138,68 +169,36 @@ extension TracerProtocol { throw error // rethrow } } -} -// ==== ---------------------------------------------------------------------------------------------------------------- -// MARK: Starting spans: Task-local Baggage propagation - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage -extension TracerProtocol { - /// 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. /// - /// 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( - _ operationName: String, - ofKind kind: SpanKind = .internal, - function: String = #function, - file fileID: String = #fileID, - line: UInt = #line, - _ operation: (Self.Span) throws -> T - ) rethrows -> T { - try self.withSpan( - operationName, - baggage: .current ?? .topLevel, - ofKind: kind, - function: function, - file: fileID, - line: line - ) { span in - try Baggage.$current.withValue(span.baggage) { - try operation(span) - } - } - } - - /// 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`. + /// 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) - #if swift(>=5.7.0) @_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, @@ -207,35 +206,9 @@ extension TracerProtocol { ) async rethrows -> T { let span = self.startSpan( operationName, - baggage: .current ?? .topLevel, - 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 - } - } - #else - public func withSpan( - _ operationName: String, - ofKind kind: SpanKind = .internal, - 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: .current ?? .topLevel, + baggage: baggage(), ofKind: kind, + at: time, function: function, file: fileID, line: line @@ -250,68 +223,7 @@ extension TracerProtocol { throw error // rethrow } } - #endif - /// 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) - #if swift(>=5.7.0) - @_unsafeInheritExecutor - public func withSpan( - _ operationName: String, - baggage: Baggage, - ofKind kind: SpanKind = .internal, - function: String = #function, - file fileID: String = #fileID, - line: UInt = #line, - _ operation: (SpanProtocol) 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 - } - } - #else - public func withSpan( - _ operationName: String, - baggage: Baggage, - ofKind kind: SpanKind = .internal, - 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, 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 } #endif // Swift 5.7 diff --git a/Tests/TracingTests/TracerTests+XCTest.swift b/Tests/TracingTests/TracerTests+XCTest.swift index 1e6bca4..b5a8cf1 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 930f9d9..389cd85 100644 --- a/Tests/TracingTests/TracerTests.swift +++ b/Tests/TracingTests/TracerTests.swift @@ -199,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.") } @@ -229,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.") } @@ -261,7 +258,66 @@ final class TracerTests: XCTestCase { } XCTFail("Should have thrown") } - #endif + } + + 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()) + return + } + XCTFail("Should have thrown") + } } func testWithSpan_recordErrorWithAttributes() throws {