Skip to content

Commit b8021de

Browse files
committed
+API introduce Tracer and Instrument convenience APIs
1 parent 8daa244 commit b8021de

File tree

5 files changed

+143
-43
lines changed

5 files changed

+143
-43
lines changed

Sources/Instrumentation/Instrument.swift

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,35 @@
1414

1515
import InstrumentationBaggage
1616

17+
public enum Instrument {
18+
static var current: InstrumentProtocol {
19+
InstrumentationSystem.instrument
20+
}
21+
}
22+
23+
/// Conforming types are usually cross-cutting tools like tracers. They are agnostic of what specific `Carrier` is used
24+
/// to propagate metadata across boundaries, but instead just specify what values to use for which keys.
25+
public protocol InstrumentProtocol {
26+
/// Extract values from a `Carrier` by using the given extractor and inject them into the given `Baggage`.
27+
/// It's quite common for `Instrument`s to come up with new values if they weren't passed along in the given `Carrier`.
28+
///
29+
/// - Parameters:
30+
/// - carrier: The `Carrier` that was used to propagate values across boundaries.
31+
/// - baggage: The `Baggage` into which these values should be injected.
32+
/// - extractor: The ``Extractor`` that extracts values from the given `Carrier`.
33+
func extract<Carrier, Extract>(_ carrier: Carrier, into baggage: inout Baggage, using extractor: Extract)
34+
where Extract: Extractor, Extract.Carrier == Carrier
35+
36+
/// Extract values from a `Baggage` and inject them into the given `Carrier` using the given ``Injector``.
37+
///
38+
/// - Parameters:
39+
/// - baggage: The `Baggage` from which relevant information will be extracted.
40+
/// - carrier: The `Carrier` into which this information will be injected.
41+
/// - injector: The ``Injector`` used to inject extracted `Baggage` into the given `Carrier`.
42+
func inject<Carrier, Inject>(_ baggage: Baggage, into carrier: inout Carrier, using injector: Inject)
43+
where Inject: Injector, Inject.Carrier == Carrier
44+
}
45+
1746
/// Conforming types are used to extract values from a specific `Carrier`.
1847
public protocol Extractor {
1948
/// The carrier to extract values from.
@@ -40,26 +69,3 @@ public protocol Injector {
4069
/// - carrier: The `Carrier` to inject into.
4170
func inject(_ value: String, forKey key: String, into carrier: inout Carrier)
4271
}
43-
44-
/// Conforming types are usually cross-cutting tools like tracers. They are agnostic of what specific `Carrier` is used
45-
/// to propagate metadata across boundaries, but instead just specify what values to use for which keys.
46-
public protocol InstrumentProtocol {
47-
/// Extract values from a `Carrier` by using the given extractor and inject them into the given `Baggage`.
48-
/// It's quite common for `Instrument`s to come up with new values if they weren't passed along in the given `Carrier`.
49-
///
50-
/// - Parameters:
51-
/// - carrier: The `Carrier` that was used to propagate values across boundaries.
52-
/// - baggage: The `Baggage` into which these values should be injected.
53-
/// - extractor: The ``Extractor`` that extracts values from the given `Carrier`.
54-
func extract<Carrier, Extract>(_ carrier: Carrier, into baggage: inout Baggage, using extractor: Extract)
55-
where Extract: Extractor, Extract.Carrier == Carrier
56-
57-
/// Extract values from a `Baggage` and inject them into the given `Carrier` using the given ``Injector``.
58-
///
59-
/// - Parameters:
60-
/// - baggage: The `Baggage` from which relevant information will be extracted.
61-
/// - carrier: The `Carrier` into which this information will be injected.
62-
/// - injector: The ``Injector`` used to inject extracted `Baggage` into the given `Carrier`.
63-
func inject<Carrier, Inject>(_ baggage: Baggage, into carrier: inout Carrier, using injector: Inject)
64-
where Inject: Injector, Inject.Carrier == Carrier
65-
}

Sources/Tracing/Docs.docc/Guides/InstrumentYourLibrary.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ In such situations you can resort to using the ``TracerProtocol/startSpan(_:bagg
260260
// Callback heavy APIs may need to store and manage spans manually:
261261
var span: Span?
262262

263-
func startHandline(request: HTTPRequest) {
263+
func startHandling(request: HTTPRequest) {
264264
self.span = InstrumentationSystem.tracer.startSpan("\(request.path)")
265265

266266
userCode.handle(request)

Sources/Tracing/Docs.docc/TracerProtocol.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
## Topics
44

5-
6-
75
### Working with Spans
86

97
- ``withSpan(_:ofKind:_:)-11n3y``

Sources/Tracing/Tracer.swift

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,19 @@ import Dispatch
1616
@_exported import Instrumentation
1717
@_exported import InstrumentationBaggage
1818

19-
/// An `Instrument` with added functionality for distributed tracing. It uses the span-based tracing model and is
20-
/// based on the OpenTracing/OpenTelemetry spec.
21-
public protocol TracerProtocol: InstrumentProtocol {
19+
/// Convenience entry point to operations on the tracer configured
20+
/// on the global ``InstrumentationSystem`` using the `InstrumentationSystem/bootstrap(_:)` method.
21+
///
22+
/// If no tracer was bootstrapped, these operations will default to a no-op tracer (creating no spans).
23+
///
24+
/// For implementing a new tracer, see ``TracerProtocol``.
25+
enum Tracer: TracerStartSpanOps {
26+
func startSpan(_ operationName: String, baggage: Baggage, ofKind kind: SpanKind, at time: DispatchWallTime) -> Span {
27+
InstrumentationSystem.tracer.startSpan(operationName, baggage: baggage, ofKind: kind, at: time)
28+
}
29+
}
30+
31+
public protocol TracerStartSpanOps {
2232
/// Start a new ``Span`` with the given `Baggage` at a given time.
2333
///
2434
/// - Note: Prefer to use `withSpan` to start a span as it automatically takes care of ending the span,
@@ -36,17 +46,9 @@ public protocol TracerProtocol: InstrumentProtocol {
3646
ofKind kind: SpanKind,
3747
at time: DispatchWallTime
3848
) -> Span
39-
40-
/// Export all ended spans to the configured backend that have not yet been exported.
41-
///
42-
/// This function should only be called in cases where it is absolutely necessary,
43-
/// such as when using some FaaS providers that may suspend the process after an invocation, but before the backend exports the completed spans.
44-
///
45-
/// This function should not block indefinitely, implementations should offer a configurable timeout for flush operations.
46-
func forceFlush()
4749
}
4850

49-
extension TracerProtocol {
51+
extension TracerStartSpanOps {
5052
/// Start a new ``Span`` with the given `Baggage` starting at `DispatchWallTime.now()`.
5153
///
5254
/// - Parameters:
@@ -65,7 +67,7 @@ extension TracerProtocol {
6567
// ==== ----------------------------------------------------------------------------------------------------------------
6668
// MARK: Starting spans: `withSpan`
6769

68-
extension TracerProtocol {
70+
extension TracerStartSpanOps {
6971
/// Execute a specific task within a newly created ``Span``.
7072
///
7173
/// DO NOT `end()` the passed in span manually. It will be ended automatically when the `operation` returns.
@@ -92,14 +94,41 @@ extension TracerProtocol {
9294
throw error // rethrow
9395
}
9496
}
97+
98+
/// Execute a specific task within a newly created ``Span``.
99+
///
100+
/// DO NOT `end()` the passed in span manually. It will be ended automatically when the `operation` returns.
101+
///
102+
/// - Parameters:
103+
/// - operationName: The name of the operation being traced. This may be a handler function, database call, ...
104+
/// - baggage: Baggage potentially containing trace identifiers of a parent ``Span``.
105+
/// - kind: The ``SpanKind`` of the ``Span`` to be created. Defaults to ``SpanKind/internal``.
106+
/// - operation: operation to wrap in a span start/end and execute immediately
107+
/// - Returns: the value returned by `operation`
108+
/// - Throws: the error the `operation` has thrown (if any)
109+
public func withSpan<T>(
110+
_ operationName: String,
111+
baggage: Baggage,
112+
ofKind kind: SpanKind = .internal,
113+
_ operation: () throws -> T
114+
) rethrows -> T {
115+
let span = self.startSpan(operationName, baggage: baggage, ofKind: kind)
116+
defer { span.end() }
117+
do {
118+
return try operation()
119+
} catch {
120+
span.recordError(error)
121+
throw error // rethrow
122+
}
123+
}
95124
}
96125

97126
// ==== ----------------------------------------------------------------------------------------------------------------
98127
// MARK: Starting spans: Task-local Baggage propagation
99128

100129
#if swift(>=5.5) && canImport(_Concurrency)
101130
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
102-
extension TracerProtocol {
131+
extension TracerStartSpanOps {
103132
/// Execute the given operation within a newly created ``Span``,
104133
/// started as a child of the currently stored task local `Baggage.current` or as a root span if `nil`.
105134
///
@@ -123,6 +152,32 @@ extension TracerProtocol {
123152
}
124153
}
125154

155+
/// Execute the given operation within a newly created ``Span``,
156+
/// started as a child of the currently stored task local `Baggage.current` or as a root span if `nil`.
157+
///
158+
/// DO NOT `end()` the passed in span manually. It will be ended automatically when the `operation` returns.
159+
///
160+
/// - Parameters:
161+
/// - operationName: The name of the operation being traced. This may be a handler function, database call, ...
162+
/// - kind: The ``SpanKind`` of the ``Span`` to be created. Defaults to ``SpanKind/internal``.
163+
/// - operation: operation to wrap in a span start/end and execute immediately
164+
/// - Returns: the value returned by `operation`
165+
/// - Throws: the error the `operation` has thrown (if any)
166+
public func withSpan<T>(
167+
_ operationName: String,
168+
ofKind kind: SpanKind = .internal,
169+
_ operation: () throws -> T
170+
) rethrows -> T {
171+
try self.withSpan(operationName, baggage: .current ?? .topLevel, ofKind: kind) { span in
172+
try Baggage.$current.withValue(span.baggage) {
173+
try operation()
174+
}
175+
}
176+
}
177+
}
178+
179+
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
180+
extension TracerStartSpanOps {
126181
/// Execute the given async operation within a newly created ``Span``,
127182
/// started as a child of the currently stored task local `Baggage.current` or as a root span if `nil`.
128183
///
@@ -183,3 +238,18 @@ extension TracerProtocol {
183238
}
184239
}
185240
#endif
241+
242+
// ==== ----------------------------------------------------------------------------------------------------------------
243+
// MARK: TracerProtocol
244+
245+
/// An `Instrument` with added functionality for distributed tracing. It uses the span-based tracing model and is
246+
/// based on the OpenTracing/OpenTelemetry spec.
247+
public protocol TracerProtocol: TracerStartSpanOps, InstrumentProtocol {
248+
/// Export all ended spans to the configured backend that have not yet been exported.
249+
///
250+
/// This function should only be called in cases where it is absolutely necessary,
251+
/// such as when using some FaaS providers that may suspend the process after an invocation, but before the backend exports the completed spans.
252+
///
253+
/// This function should not block indefinitely, implementations should offer a configurable timeout for flush operations.
254+
func forceFlush()
255+
}

Tests/TracingTests/TracerTests.swift

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ final class TracerTests: XCTestCase {
116116
"world"
117117
}
118118

119-
let value = tracer.withSpan("hello") { (span: Span) -> String in
119+
let value = tracer.withSpan("hello") { span -> String in
120120
XCTAssertEqual(span.baggage.traceID, Baggage.current?.traceID)
121121
return operation(span: span)
122122
}
@@ -156,6 +156,32 @@ final class TracerTests: XCTestCase {
156156
#endif
157157
}
158158

159+
func testWithSpan_operation_withoutSpanPassed() throws {
160+
#if swift(>=5.5) && canImport(_Concurrency)
161+
guard #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) else {
162+
throw XCTSkip("Task locals are not supported on this platform.")
163+
}
164+
165+
let tracer = TestTracer()
166+
InstrumentationSystem.bootstrapInternal(tracer)
167+
defer {
168+
InstrumentationSystem.bootstrapInternal(nil)
169+
}
170+
171+
var spansEnded = 0
172+
tracer.onEndSpan = { _ in spansEnded += 1 }
173+
174+
tracer.withSpan("hello") {
175+
// ...
176+
}
177+
tracer.withSpan("hello-again", baggage: .topLevel) {
178+
// ...
179+
}
180+
181+
XCTAssertEqual(spansEnded, 2)
182+
#endif
183+
}
184+
159185
func testWithSpan_automaticBaggagePropagation_async() throws {
160186
#if swift(>=5.5) && canImport(_Concurrency)
161187
guard #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) else {
@@ -176,7 +202,7 @@ final class TracerTests: XCTestCase {
176202
}
177203

178204
try self.testAsync {
179-
let value = try await tracer.withSpan("hello") { (span: Span) -> String in
205+
let value = try await tracer.withSpan("hello") { span -> String in
180206
XCTAssertEqual(span.baggage.traceID, Baggage.current?.traceID)
181207
return try await operation(span: span)
182208
}
@@ -209,7 +235,7 @@ final class TracerTests: XCTestCase {
209235
self.testAsync {
210236
var fromNonAsyncWorld = Baggage.topLevel
211237
fromNonAsyncWorld.traceID = "1234-5678"
212-
let value = await tracer.withSpan("hello", baggage: fromNonAsyncWorld) { (span: Span) -> String in
238+
let value = await tracer.withSpan("hello", baggage: fromNonAsyncWorld) { span -> String in
213239
XCTAssertEqual(span.baggage.traceID, Baggage.current?.traceID)
214240
XCTAssertEqual(span.baggage.traceID, fromNonAsyncWorld.traceID)
215241
return await operation(span: span)

0 commit comments

Comments
 (0)