Skip to content

Commit 9114e51

Browse files
committed
docs revamp on implementing tracers
1 parent 6ff184d commit 9114e51

File tree

1 file changed

+60
-36
lines changed

1 file changed

+60
-36
lines changed

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

Lines changed: 60 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ In order to implement an instrument you need to implement the `Instrument` proto
2525
The two methods will be called by instrumented libraries/frameworks at asynchronous boundaries, giving you a chance to
2626
act on the provided information or to add additional information to be carried across these boundaries.
2727

28-
> The [`ServiceContext` documentation](https://github.com/apple/swift-service-context) type is declared in the swift-service-context package.
28+
> The [`ServiceContext`](https://swiftpackageindex.com/apple/swift-service-context/documentation/servicecontextmodule) type is declared in the swift-service-context package.
2929
3030
### Creating a `Tracer`
3131

@@ -36,7 +36,7 @@ When creating a tracer you will need to implement two types:
3636

3737
> ``Span`` largely resembles span concept as defined [OpenTelemetry](https://github.com/open-telemetry/opentelemetry-specification/blob/v0.7.0/specification/trace/api.md#span), however this package does not enforce certain rules specified in there - that is left up to a specific open telemetry tracer implementation.
3838
39-
### Defining, injecting and extracting ServiceContext
39+
### Defining, injecting and extracting `ServiceContext`
4040

4141
In order to be able to extract and inject values into the `ServiceContext` which is the value that is "carried around" across asynchronous contexts,
4242
we need to declare a `ServiceContextKey`. ServiceContext generally acts as a type-safe dictionary and declaring the keys this way allows us to perform lookups
@@ -118,52 +118,76 @@ func handler(request: HTTPRequest) async throws {
118118

119119
For your library/framework to be able to carry `ServiceContext` across asynchronous boundaries, it's crucial that you carry the context throughout your entire call chain in order to avoid dropping metadata.
120120

121-
## Creating an instrument
121+
### Starting and ending spans
122122

123-
Creating an instrument means adopting the `Instrument` protocol (or `Tracer` in case you develop a tracer).
124-
`Instrument` is part of the `Instrumentation` library & `Tracing` contains the `Tracer` protocol.
123+
The primary goal and user-facing API of a ``Tracer`` is to create spans.
125124

126-
`Instrument` has two requirements:
125+
While you will need to implement all methods of the tracer protocol, the most important one is `startSpan`:
127126

128-
1. A method to inject values inside a `ServiceContext` into a generic carrier (e.g. HTTP headers)
129-
2. A method to extract values from a generic carrier (e.g. HTTP headers) and store them in a `ServiceContext`
127+
```swift
128+
#if swift(>=5.7.0)
129+
extension MyTracer: Tracer {
130+
func startSpan<Instant: TracerInstant>(
131+
_ operationName: String,
132+
context: @autoclosure () -> ServiceContext,
133+
ofKind kind: SpanKind,
134+
at instant: @autoclosure () -> Instant,
135+
function: String,
136+
file fileID: String,
137+
line: UInt
138+
) -> MySpan {
139+
let span = MySpan(
140+
operationName: operationName,
141+
startTime: instant(),
142+
context: context(),
143+
kind: kind,
144+
onEnd: self.onEndSpan
145+
)
146+
self.spans.append(span)
147+
return span
148+
}
149+
}
150+
#endif
151+
```
130152

131-
The two methods will be called by instrumented libraries/frameworks at asynchronous boundaries, giving you a chance to
132-
act on the provided information or to add additional information to be carried across these boundaries.
153+
If you can require Swift 5.7 prefer doing so, and return the concrete ``Span`` type from the `startSpan` method.
154+
This allows users who decide to use your tracer explicitly, and not via the global bootstrapped system to avoid
155+
wrapping tracers in existentials which can be beneficial in some situations.
133156

134-
> Check out the [`ServiceContext` documentation](https://github.com/apple/swift-service-context) for more information on
135-
how to retrieve values from the `ServiceContext` and how to set values on it.
157+
Next, eventually the user started span will be ended and the `Span/end()` method will be invoked:
136158

137-
### Creating a `Tracer`
159+
```swift
160+
public struct MySpan: Tracing.Span {
161+
// ... implement all protocol requirements of Span ...
138162

139-
When creating a tracer you need to create two types:
163+
public func end<Instant: TracerInstant>(at instant: @autoclosure () -> Instant) {
164+
// store the `endInstant`
165+
self.tracer.emit(self)
166+
}
167+
}
168+
```
140169

141-
1. Your tracer conforming to `Tracer`
142-
2. A span class conforming to `Span`
170+
It is possible to implement a span as a struct or a class, but a ``Span`` **must exhibit reference type behavior**.
171+
In other words, adding an attribute to one reference of a span must be visible in other references to it.
143172

144-
> 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.
173+
The ability to implement a span using a struct comes in handy when implementing a "Noop" (do nothing) span and avoids heap allocations. Normal spans though generally will be backed by `class` based storage and should flush themselfes to the owning tracer once the span has been ended.
145174

146-
### Defining, injecting and extracting ServiceContext
175+
#### Detecting not-ended spans
147176

148-
```swift
149-
import Tracing
177+
It is a programmer error to NOT `end()` a span (be it by using `withSpan` or calling `startSpan` paired with `Span.end()`),
178+
and a tracer MAY choose to detect and error or fatal error in such situations.
150179

151-
private enum TraceIDKey: ServiceContextKey {
152-
typealias Value = String
153-
}
180+
It is possible to detect when a span has not been properly ended if the span's storage `deinit` is executed,
181+
however `end()` has not been called on it explicitly, like this:
154182

155-
extension ServiceContext {
156-
var traceID: String? {
157-
get {
158-
return self[TraceIDKey.self]
159-
}
160-
set {
161-
self[TraceIDKey.self] = newValue
183+
```swift
184+
struct MySpan: Tracing.Span {
185+
final class _Storage {
186+
var endInstant: Instant? = nil
187+
188+
deinit {
189+
precondition(endInstant != nil, "Span [...] was not properly ended before it was deinitialized!")
190+
}
162191
}
163-
}
164192
}
165-
166-
var context = ServiceContext.topLevel(logger: ...)
167-
context.context.traceID = "4bf92f3577b34da6a3ce929d0e0e4736"
168-
print(context.context.traceID ?? "new trace id")
169-
```
193+
```

0 commit comments

Comments
 (0)