Skip to content

Rename protocols, use associatedtype Span and add Tracer.current #84

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
572 changes: 5 additions & 567 deletions README.md

Large diffs are not rendered by default.

79 changes: 52 additions & 27 deletions Sources/Instrumentation/Instrument.swift
Original file line number Diff line number Diff line change
@@ -14,16 +14,52 @@

import InstrumentationBaggage

/// Typealias used to simplify Support of old Swift versions which do not have `Sendable` defined.
#if swift(>=5.6.0)
@preconcurrency public protocol _SwiftInstrumentationSendable: Sendable {}
#else
public protocol _SwiftInstrumentationSendable {}
#endif
// ===== ---------------------------------------------------------------------------------------------------------------
// MARK: Instrument

/// Convenience for accessing the globally bootstrapped ``InstrumentProtocol``.
///
/// Equivalent to ``InstrumentationSystem/instrument``.
public enum Instrument {
/// Convenience to access the globally bootstrapped instrument on ``InstrumentationSystem``.
///
/// Equivalent to ``InstrumentationSystem/instrument``.
public var current: InstrumentProtocol {
InstrumentationSystem.instrument
}
}

/// 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 InstrumentProtocol: _SwiftInstrumentationSendable {
/// Extract values from a `Carrier` by using the given extractor and inject them into the given `Baggage`.
/// 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.
/// - baggage: The `Baggage` into which these values should be injected.
/// - extractor: The ``Extractor`` that extracts values from the given `Carrier`.
func extract<Carrier, Extract>(_ carrier: Carrier, into baggage: inout Baggage, using extractor: Extract)
where Extract: Extractor, Extract.Carrier == Carrier

/// Extract values from a `Baggage` and inject them into the given `Carrier` using the given ``Injector``.
///
/// - Parameters:
/// - baggage: The `Baggage` from which relevant information will be extracted.
/// - carrier: The `Carrier` into which this information will be injected.
/// - injector: The ``Injector`` used to inject extracted `Baggage` into the given `Carrier`.
func inject<Carrier, Inject>(_ baggage: Baggage, into carrier: inout Carrier, using injector: Inject)
where Inject: Injector, Inject.Carrier == Carrier
}

// ===== ---------------------------------------------------------------------------------------------------------------
// MARK: Instrument

/// Conforming types are used to extract values from a specific `Carrier`.
public protocol Extractor: _SwiftInstrumentationSendable {
/// The carrier to extract values from.
///
/// For example, this could be an "http request" type.
associatedtype Carrier

/// Extract the value for the given key from the `Carrier`.
@@ -37,6 +73,8 @@ public protocol Extractor: _SwiftInstrumentationSendable {
/// Conforming types are used to inject values into a specific `Carrier`.
public protocol Injector: _SwiftInstrumentationSendable {
/// The carrier to inject values into.
///
/// For example, this could be an "http request" type.
associatedtype Carrier

/// Inject the given value for the given key into the given `Carrier`.
@@ -48,25 +86,12 @@ public protocol Injector: _SwiftInstrumentationSendable {
func inject(_ value: String, forKey key: String, into carrier: inout Carrier)
}

/// 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 {
/// 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`.
///
/// - Parameters:
/// - carrier: The `Carrier` that was used to propagate values across boundaries.
/// - baggage: The `Baggage` into which these values should be injected.
/// - extractor: The ``Extractor`` that extracts values from the given `Carrier`.
func extract<Carrier, Extract>(_ carrier: Carrier, into baggage: inout Baggage, using extractor: Extract)
where Extract: Extractor, Extract.Carrier == Carrier
// ===== ---------------------------------------------------------------------------------------------------------------
// MARK: Sendable support

/// Extract values from a `Baggage` and inject them into the given `Carrier` using the given ``Injector``.
///
/// - Parameters:
/// - baggage: The `Baggage` from which relevant information will be extracted.
/// - carrier: The `Carrier` into which this information will be injected.
/// - injector: The ``Injector`` used to inject extracted `Baggage` into the given `Carrier`.
func inject<Carrier, Inject>(_ baggage: Baggage, into carrier: inout Carrier, using injector: Inject)
where Inject: Injector, Inject.Carrier == Carrier
}
/// Typealias used to simplify Support of old Swift versions which do not have `Sendable` defined.
#if swift(>=5.6.0)
@preconcurrency public protocol _SwiftInstrumentationSendable: Sendable {}
#else
public protocol _SwiftInstrumentationSendable {}
#endif
24 changes: 12 additions & 12 deletions Sources/Instrumentation/InstrumentationSystem.swift
Original file line number Diff line number Diff line change
@@ -15,23 +15,23 @@
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``.
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 +47,23 @@ public enum InstrumentationSystem {
/// For testing scenarios one may want to set instruments multiple times, rather than the set-once semantics enforced by ``bootstrap(_:)``.
///
/// - Parameter instrument: the instrument to boostrap the system with, if `nil` the ``NoOpInstrument`` is bootstrapped.
internal static func bootstrapInternal(_ instrument: Instrument?) {
internal static func bootstrapInternal(_ instrument: InstrumentProtocol?) {
self.lock.withWriterLock {
self._instrument = instrument ?? NoOpInstrument()
}
}

/// Returns the globally configured ``Instrument``.
/// Returns the globally configured ``InstrumentProtocol``.
///
/// Defaults to a no-op ``Instrument`` if ``bootstrap(_:)`` wasn't called before.
public static var instrument: Instrument {
/// Defaults to a no-op ``InstrumentProtocol`` if ``bootstrap(_:)`` wasn't called before.
public static var instrument: InstrumentProtocol {
self.lock.withReaderLock { self._instrument }
}
}

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)
12 changes: 6 additions & 6 deletions Sources/Instrumentation/MultiplexInstrument.swift
Original file line number Diff line number Diff line change
@@ -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<Carrier, Inject>(_ baggage: Baggage, into carrier: inout Carrier, using injector: Inject)
where Inject: Injector, Carrier == Inject.Carrier
{
4 changes: 2 additions & 2 deletions Sources/Instrumentation/NoOpInstrument.swift
Original file line number Diff line number Diff line change
@@ -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<Carrier, Inject>(_ baggage: Baggage, into carrier: inout Carrier, using injector: Inject)
24 changes: 12 additions & 12 deletions Sources/Tracing/Docs.docc/InDepthGuide.md
Original file line number Diff line number Diff line change
@@ -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: 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
@@ -248,7 +248,7 @@ For your library/framework to be able to carry `LoggingContext` across asynchron

When your library/framework can benefit from tracing, you should make use of it by integrating the `Tracing` library.

In order to work with the tracer configured by the end-user (see <doc:InDepthGuide#Bootstrapping-the-InstrumentationSystem>), it adds a property to `InstrumentationSystem` that gives you back a ``Tracer``. You can then use that tracer to start ``Span``s. In an HTTP client you e.g.
In order to work with the tracer configured by the end-user (see <doc:InDepthGuide#Bootstrapping-the-InstrumentationSystem>), it adds a property to `InstrumentationSystem` that gives you back a ``TracerProtocol``. You can then use that tracer to start ``Span``s. In an HTTP client you e.g.
should start a ``Span`` when sending the outgoing HTTP request:

```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 ``TracerProtocol`` in case you develop a tracer).
`InstrumentProtocol` is part of the `Instrumentation` library & `Tracing` contains the ``TracerProtocol`` protocol.

`Instrument` has two requirements:
`InstrumentProtocol` has two requirements:

1. A method to inject values inside a `LoggingContext` into a generic carrier (e.g. HTTP headers)
2. A method to extract values from a generic carrier (e.g. HTTP headers) and store them in a `LoggingContext`
@@ -296,11 +296,11 @@ act on the provided information or to add additional information to be carried a
> Check out the [`Baggage` documentation](https://github.com/apple/swift-distributed-tracing-baggage) for more information on
how to retrieve values from the `LoggingContext` and how to set values on it.

### Creating a `Tracer`
### Creating a `TracerProtocol`

When creating a tracer you need to create two types:

1. Your tracer conforming to ``Tracer``
1. Your tracer conforming to ``TracerProtocol``
2. A span class conforming to ``Span``

> ``Span`` conforms to the standard rules defined in [OpenTelemetry](https://github.com/open-telemetry/opentelemetry-specification/blob/v0.7.0/specification/trace/api.md#span), so if unsure about usage patterns, you can refer to this specification and examples referring to it.
Loading