Skip to content

Commit

Permalink
Introduce top level withSpan; reclaim Tracer and Instrument for proto…
Browse files Browse the repository at this point in the history
…cols (#113)
  • Loading branch information
ktoso authored Mar 31, 2023
1 parent db10407 commit 63a9c24
Show file tree
Hide file tree
Showing 21 changed files with 488 additions and 361 deletions.
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ This project uses the context progagation type defined independently in:
+ [Extracting & injecting Baggage](#extracting--injecting-baggage)
+ [Tracing your library](#tracing-your-library)
* In-Depth Guide for: **Instrument developers**
+ [Creating an `InstrumentProtocol`](#instrument-developers--creating-an-instrument)
+ [Creating an `Instrument`](#instrument-developers--creating-an-instrument)
+ [Creating a `Tracer`](#creating-a--tracer-)
* [Contributing](#contributing)

Expand Down Expand Up @@ -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 `InstrumentProtocol` 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 `Instrument` and can be offered used to globally bootstrap the tracing system, like this:


```swift
Expand Down Expand Up @@ -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
`InstrumentProtocol`:
`Instrument`:

```swift
InstrumentationSystem.bootstrap(FancyInstrument())
Expand All @@ -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(_: InstrumentProtocol)` must only be called once. In case you
It is important to note that `InstrumentationSystem.bootstrap(_: Instrument)` 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:

Expand Down Expand Up @@ -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: any TracerProtocol
let tracer: any Tracer

func makeDinner(context: LoggingContext) async throws -> Meal {
tracer.withSpan(operationName: "makeDinner", context) {
Expand Down Expand Up @@ -481,7 +481,7 @@ func get(url: String, context: LoggingContext) {
}
```

On the receiving side, an HTTP server should use the following `InstrumentProtocol` API to extract the HTTP headers of the given
On the receiving side, an HTTP server should use the following `Instrument` API to extract the HTTP headers of the given
`HTTPRequest` into:

```swift
Expand Down Expand Up @@ -538,10 +538,10 @@ func get(url: String, context: LoggingContext) {

## Instrument developers: Creating an instrument

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.
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.

`InstrumentProtocol` has two requirements:
`Instrument` 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`
Expand All @@ -556,7 +556,7 @@ how to retrieve values from the `LoggingContext` and how to set values on it.

When creating a tracer you need to create two types:

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

> The `Span` conforms to the standard rules defined in [OpenTelemetry](https://github.com/open-telemetry/opentelemetry-specification/blob/v0.7.0/specification/trace/api.md#span), so if unsure about usage patterns, you can refer to this specification and examples referring to it.
Expand Down
4 changes: 2 additions & 2 deletions Sources/Instrumentation/Instrument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 InstrumentProtocol: _SwiftInstrumentationSendable {
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 `InstrumentProtocol`s to come up with new values if they weren't passed along in the given `Carrier`.
/// 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.
Expand Down
24 changes: 12 additions & 12 deletions Sources/Instrumentation/InstrumentationSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +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 ``InstrumentProtocol`` implementation.
/// It is set up just once in a given program to select the desired ``Instrument`` implementation.
///
/// # Bootstrap multiple Instruments
/// If you need to use more that one cross-cutting tool you can do so by using ``MultiplexInstrument``.
///
/// # Access the InstrumentProtocol
/// ``instrument``: Returns whatever you passed to ``bootstrap(_:)`` as an ``InstrumentProtocol``.
/// # Access the Instrument
/// ``instrument``: Returns whatever you passed to ``bootstrap(_:)`` as an ``Instrument``.
@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: InstrumentProtocol = NoOpInstrument()
private static var _instrument: Instrument = NoOpInstrument()
private static var isInitialized = false

/// Globally select the desired ``InstrumentProtocol`` implementation.
/// Globally select the desired ``Instrument`` implementation.
///
/// - Parameter instrument: The ``InstrumentProtocol`` you want to share globally within your system.
/// - Parameter instrument: The ``Instrument`` 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: InstrumentProtocol) {
public static func bootstrap(_ instrument: Instrument) {
self.lock.withWriterLock {
precondition(
!self.isInitialized, """
Expand All @@ -48,24 +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: InstrumentProtocol?) {
internal static func bootstrapInternal(_ instrument: Instrument?) {
self.lock.withWriterLock {
self._instrument = instrument ?? NoOpInstrument()
}
}

/// Returns the globally configured ``InstrumentProtocol``.
/// Returns the globally configured ``Instrument``.
///
/// Defaults to a no-op ``InstrumentProtocol`` if ``bootstrap(_:)`` wasn't called before.
public static var instrument: InstrumentProtocol {
/// Defaults to a no-op ``Instrument`` if ``bootstrap(_:)`` wasn't called before.
public static var instrument: Instrument {
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: (InstrumentProtocol) -> Bool) -> InstrumentProtocol? {
public static func _findInstrument(where predicate: (Instrument) -> Bool) -> Instrument? {
self.lock.withReaderLock {
if let multiplex = self._instrument as? MultiplexInstrument {
return multiplex.firstInstrument(where: predicate)
Expand Down
12 changes: 6 additions & 6 deletions Sources/Instrumentation/MultiplexInstrument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,27 @@

import InstrumentationBaggage

/// A pseudo-``InstrumentProtocol`` that may be used to instrument using multiple other ``InstrumentProtocol``s across a
/// A pseudo-``Instrument`` that may be used to instrument using multiple other ``Instrument``s across a
/// common `Baggage`.
public struct MultiplexInstrument {
private var instruments: [InstrumentProtocol]
private var instruments: [Instrument]

/// Create a ``MultiplexInstrument``.
///
/// - Parameter instruments: An array of ``InstrumentProtocol``s, each of which will be used to ``InstrumentProtocol/inject(_:into:using:)`` or ``InstrumentProtocol/extract(_:into:using:)``
/// - Parameter instruments: An array of ``Instrument``s, each of which will be used to ``Instrument/inject(_:into:using:)`` or ``Instrument/extract(_:into:using:)``
/// through the same `Baggage`.
public init(_ instruments: [InstrumentProtocol]) {
public init(_ instruments: [Instrument]) {
self.instruments = instruments
}
}

extension MultiplexInstrument {
func firstInstrument(where predicate: (InstrumentProtocol) -> Bool) -> InstrumentProtocol? {
func firstInstrument(where predicate: (Instrument) -> Bool) -> Instrument? {
self.instruments.first(where: predicate)
}
}

extension MultiplexInstrument: InstrumentProtocol {
extension MultiplexInstrument: Instrument {
public func inject<Carrier, Inject>(_ baggage: Baggage, into carrier: inout Carrier, using injector: Inject)
where Inject: Injector, Carrier == Inject.Carrier
{
Expand Down
4 changes: 2 additions & 2 deletions Sources/Instrumentation/NoOpInstrument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@

import InstrumentationBaggage

/// A "no op" implementation of an ``InstrumentProtocol``.
public struct NoOpInstrument: InstrumentProtocol {
/// A "no op" implementation of an ``Instrument``.
public struct NoOpInstrument: Instrument {
public init() {}

public func inject<Carrier, Inject>(_ baggage: Baggage, into carrier: inout Carrier, using injector: Inject)
Expand Down
18 changes: 9 additions & 9 deletions Sources/Tracing/Docs.docc/InDepthGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. **InstrumentProtocol developers** provide tools to collect distributed metadata about your application
3. **Instrument developers** provide tools to collect distributed metadata about your application

For applications to be instrumented correctly these three parts have to play along nicely.

Expand Down Expand Up @@ -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
`InstrumentProtocol`:
`Instrument`:

```swift
InstrumentationSystem.bootstrap(FancyInstrument())
Expand All @@ -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(_: InstrumentProtocol)` must only be called once. In case you
It is important to note that `InstrumentationSystem.bootstrap(_: Instrument)` 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:

Expand Down Expand Up @@ -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: any TracerProtocol
let tracer: any Tracer

func makeDinner(context: LoggingContext) async throws -> Meal {
tracer.withSpan(operationName: "makeDinner", context) {
Expand Down Expand Up @@ -225,7 +225,7 @@ func get(url: String, context: LoggingContext) {
}
```

On the receiving side, an HTTP server should use the following `InstrumentProtocol` API to extract the HTTP headers of the given
On the receiving side, an HTTP server should use the following `Instrument` API to extract the HTTP headers of the given
`HTTPRequest` into:

```swift
Expand Down Expand Up @@ -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.

## InstrumentProtocol developers: Creating an instrument
## Instrument developers: Creating an instrument

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.
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.

`InstrumentProtocol` has two requirements:
`Instrument` 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`
Expand Down
2 changes: 1 addition & 1 deletion Sources/Tracing/Docs.docc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `InstrumentProtocol` 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 `Instrument` and can be offered used to globally bootstrap the tracing system, like this:


```swift
Expand Down
12 changes: 6 additions & 6 deletions Sources/Tracing/InstrumentationSystem+Tracing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ 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: any TracerProtocol {
let found: (any TracerProtocol)? =
(self._findInstrument(where: { $0 is (any TracerProtocol) }) as? (any TracerProtocol))
public static var tracer: any Tracer {
let found: (any Tracer)? =
(self._findInstrument(where: { $0 is (any Tracer) }) as? (any Tracer))
return found ?? NoOpTracer()
}
#endif
Expand All @@ -35,9 +35,9 @@ 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 legacyTracer: any LegacyTracerProtocol {
let found: (any LegacyTracerProtocol)? =
(self._findInstrument(where: { $0 is (any LegacyTracerProtocol) }) as? (any LegacyTracerProtocol))
public static var legacyTracer: any LegacyTracer {
let found: (any LegacyTracer)? =
(self._findInstrument(where: { $0 is (any LegacyTracer) }) as? (any LegacyTracer))
return found ?? NoOpTracer()
}
}
4 changes: 2 additions & 2 deletions Sources/Tracing/NoOpTracer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import Dispatch

/// Tracer that ignores all operations, used when no tracing is required.
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal Baggage
public struct NoOpTracer: LegacyTracerProtocol {
public struct NoOpTracer: LegacyTracer {
public typealias TracerSpan = NoOpSpan

public init() {}
Expand Down Expand Up @@ -91,7 +91,7 @@ public struct NoOpTracer: LegacyTracerProtocol {
}

#if swift(>=5.7.0)
extension NoOpTracer: TracerProtocol {
extension NoOpTracer: Tracer {
public func startSpan<Clock: TracerClock>(
_ operationName: String,
baggage: @autoclosure () -> Baggage,
Expand Down
Loading

0 comments on commit 63a9c24

Please sign in to comment.