|
1 | 1 | # Swift (Server) Tracing
|
2 | 2 |
|
3 | 3 | [](https://swift.org/download/)
|
| 4 | +[](https://swift.org/download/) |
| 5 | +[](https://swift.org/download/) |
4 | 6 | [](https://github.com/slashmo/gsoc-swift-tracing/actions?query=workflow%3ACI)
|
5 | 7 |
|
6 |
| -This is a WIP collection of Swift libraries enabling Tracing for your Swift (Server) systems. Check out the [GSoC project overview](https://summerofcode.withgoogle.com/projects/#6092707967008768) to learn more. For a more detailed project plan please take a look [at this Google doc](https://docs.google.com/document/d/19j9x515dR0IAwF3Zoevxoj6jvMdGpP4UuyGhmEXO_B8). |
| 8 | +This is a collection of Swift libraries enabling the instrumentation of your server side applications using tools such |
| 9 | +as tracers. Our goal is to provide a common foundation that allows you too freely choose how to instrument your system |
| 10 | +with minimal changes to your actual code. |
7 | 11 |
|
8 |
| -## Dependencies |
| 12 | +While Swift Tracing allows building all kinds of _instruments_ which can co-exist in applications transparently, |
| 13 | +it's primary use is instrumenting multi-threaded and distributed systems with Distributed Traces. |
9 | 14 |
|
10 |
| -The Swift packages contained in this repository make use of the library package `Baggage`, which can be found in a separate repository: https://github.com/slashmo/gsoc-swift-baggage-context |
| 15 | +> The tracing API is compatible with the [Open Telemetry specification](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/api.md). |
| 16 | +
|
| 17 | +--- |
| 18 | + |
| 19 | +## Instrumentation |
| 20 | + |
| 21 | +When instrumenting server applications there are typically three parties involved: |
| 22 | + |
| 23 | +1. [Application developers](#application-developers-setting-up-instruments) creating server-side applications |
| 24 | +2. [Library/Framework developers](#libraryframework-developers-instrumenting-your-software) providing building blocks to create these applications |
| 25 | +3. [Instrument developers](#instrument-developers-creating-an-instrument) providing tools to collect distributed metadata about your application |
| 26 | + |
| 27 | +For applications to be instrumented correctly these three parts have to play along nicely. |
| 28 | + |
| 29 | +### Use-case Example |
| 30 | + |
| 31 | +Let's say you build an API for a fruit store that has two services, one for ordering goods and one that checks what |
| 32 | +items are available in storage. Your frontend might make an HTTP request to the order service which then makes a |
| 33 | +subsequent request to the storage service, just to find out that your application isn't behaving not as it should. Now |
| 34 | +you could debug each service individually to figure out where the bug is located. If you'd instead had instrumentation |
| 35 | +in place you could take a look take at the bigger picture instead. |
| 36 | + |
| 37 | +One specific type of instrument that might be helpful in this case is called *Tracer*. It collects pieces of metadata |
| 38 | +about your system (spans) and then links them to present the entire call from start to finish (trace). This metadata |
| 39 | +contains, depending on the piece being instrumented, e.g. the request URL or an SQL query. The following image shows |
| 40 | +one such trace presented in [Zipkin](https://zipkin.io): |
| 41 | + |
| 42 | + |
| 43 | + |
| 44 | +**TODO**: Replace with fruit store example screenshot |
| 45 | + |
| 46 | +In the fruit store example, we'd want a span for both the incoming HTTP request to the order service, its outgoing |
| 47 | +request to the storage service, one for handling that request, and a span respresenting the querying of a database. |
| 48 | +Spans naturally form a parent-child relationship, and by visually analyzing a trace we can easily spot bottlenecks for |
| 49 | +performance profiling use-cases, and for failure debugging situations we can benefit from the application specific |
| 50 | +metadata that spans can carry (such as noticing that failures only occur e.g. when the username contains some illegal |
| 51 | +character or some other otherwise hard to notice situation). |
| 52 | + |
| 53 | +### Context propagation |
| 54 | + |
| 55 | +For instrumentation and tracing to work, certain pieces of metadata (usually in the form of identifiers), must be |
| 56 | +carried throughout the entire system–including across process and service boundaries. Because of that, it's essential |
| 57 | +for a context object to be passed around your application and the libraries/frameworks you depend on, but also carried |
| 58 | +over asynchronous boundaries like an HTTP call to another service of your app. |
| 59 | + |
| 60 | +In Swift this is done by passing a **`BaggageContext`** explicitly through APIs which participate in |
| 61 | +instrumentation/tracing. It's vendored in its own library, |
| 62 | +[Baggage](https://github.com/slashmo/gsoc-swift-baggage-context). |
| 63 | + |
| 64 | +> We intentionally didn't call this something like TraceContext as we aim on supporting any kind of instrument, |
| 65 | +not only tracers. |
| 66 | + |
| 67 | +For each party involved we offer different libraries that all make use of `BaggageContext`. |
| 68 | + |
| 69 | +--- |
| 70 | + |
| 71 | +## Adoption |
| 72 | + |
| 73 | +The following libraries already support swift tracing or baggage context in their APIs: |
| 74 | + |
| 75 | +| Library | Integrates | Status | |
| 76 | +| --- | --- | --- | |
| 77 | +| Swift NIO | `Baggage` | [PoC #1574](https://github.com/apple/swift-nio/pull/1574) | |
| 78 | +| AsyncHTTPClient | `Tracing` | [PoC #289](https://github.com/swift-server/async-http-client/pull/289) | |
| 79 | +| Swift gRPC | `Tracing` | [PoC #941](https://github.com/grpc/grpc-swift/pull/941) | |
| 80 | + |
| 81 | +If you know of any other library please send in a PR to add it to the list, thank you! |
| 82 | + |
| 83 | +--- |
| 84 | + |
| 85 | +## Application Developers: Setting up instruments |
| 86 | + |
| 87 | +As an end-user building server applications you get to choose what instruments to use to instrument your system. Here's |
| 88 | +all the steps you need to take to get up and running: |
| 89 | + |
| 90 | +Add a package dependency for this repository in your `Package.swift` file, and one for the specific instrument you want |
| 91 | +to use, in this case `FancyInstrument`: |
| 92 | + |
| 93 | +```swift |
| 94 | +.package(url: "https://github.com/slashmo/gsoc-swift-tracing.git", .branch("main")), |
| 95 | +.package(url: "<https://repo-of-fancy-instrument.git>", from: "<4.2.0>"), |
| 96 | +``` |
| 97 | + |
| 98 | +To your main target, add a dependency on the `Instrumentation library` and the instrument you want to use: |
| 99 | + |
| 100 | +```swift |
| 101 | +.target(name: "MyApplication", dependencies: ["Instrumentation", "FancyInstrument"]), |
| 102 | +``` |
| 103 | + |
| 104 | +Then, [bootstrap the instrumentation system](#Bootstrapping-the-Instrumentation-System) to use `FancyInstrument`. |
| 105 | + |
| 106 | +### Passing BaggageContext |
| 107 | + |
| 108 | +`BaggageContext` should always be passed around explicitly, so it's very likely for the libraries you use to expect |
| 109 | +a `BaggageContext`. Make sure to always pass along the context that's previously handed to you. E.g., when making an |
| 110 | +HTTP request using `AsyncHTTPClient` in a `NIO` handler, you can use the `ChannelHandlerContext`s `baggage` property to |
| 111 | +access the `BaggageContext`. |
| 112 | + |
| 113 | +[Check out the `BaggageContext` repository](https://github.com/slashmo/gsoc-swift-baggage-context) for detailed |
| 114 | +documentation about context passing. |
| 115 | + |
| 116 | +> Note that instrumentation of `AsyncHTTPClient` and the `baggage` property of `ChannelHandlerContext` are still WIP. |
| 117 | +
|
| 118 | +## Library/Framework developers: Instrumenting your software |
| 119 | + |
| 120 | +### Extracting & injecting BaggageContext |
| 121 | + |
| 122 | +When hitting boundaries like an outgoing HTTP request you call out to the [configured instrument(s)](#Bootstrapping-the-Instrumentation-System): |
| 123 | + |
| 124 | +An HTTP client e.g. should inject the given `BaggageContext` into the HTTP headers of its outbound request: |
| 125 | + |
| 126 | +```swift |
| 127 | +func get(url: String, context: BaggageContextCarrier) { |
| 128 | + var request = HTTPRequest(url: url) |
| 129 | + InstrumentationSystem.instrument.inject( |
| 130 | + context.baggage, |
| 131 | + into: &request.headers, |
| 132 | + using: HTTPHeadersInjector() |
| 133 | + ) |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +On the receiving side, an HTTP server should use the following `Instrument` API to extract the HTTP headers of the given |
| 138 | +`HTTPRequest` into: |
| 139 | + |
| 140 | +```swift |
| 141 | +func handler(request: HTTPRequest, context: BaggageContextCarrier) { |
| 142 | + InstrumentationSystem.instrument.extract( |
| 143 | + request.headers, |
| 144 | + into: &context.baggage, |
| 145 | + using: HTTPHeadersExtractor() |
| 146 | + ) |
| 147 | + // ... |
| 148 | +} |
| 149 | +``` |
| 150 | + |
| 151 | +> In case your library makes use of the `NIOHTTP1.HTTPHeaders` type we already have an `HTTPHeadersInjector` & |
| 152 | +`HTTPHeadersExtractor` available as part of the `NIOInstrumentation` library. |
| 153 | + |
| 154 | +For your library/framework to be able to carry `BaggageContext` across asynchronous boundaries, it's crucial that you |
| 155 | +carry the context throughout your entire call chain in order to avoid dropping metadata. |
| 156 | + |
| 157 | +> For more information on `BaggageContext` & `BaggageContextCarrier` check out the |
| 158 | +[Baggage library's documentation](https://github.com/slashmo/gsoc-swift-baggage-context). |
| 159 | + |
| 160 | +### Tracing your library |
| 161 | + |
| 162 | +When your library/framework can benefit from tracing, you should make use of it by addentionally integrating the |
| 163 | +`Tracing` library. In order to work with the tracer |
| 164 | +[configured by the end-user](#Bootstrapping-the-Instrumentation-System), it adds a property to `InstrumentationSystem` |
| 165 | +that gives you back a `Tracer`. You can then use that tracer to start `Span`s. In an HTTP client you e.g. |
| 166 | +should start a `Span` when sending the outgoing HTTP request: |
| 167 | + |
| 168 | +```swift |
| 169 | +func get(url: String, context: BaggageContextCarrier) { |
| 170 | + var request = HTTPRequest(url: url) |
| 171 | + |
| 172 | + // inject the request headers into the baggage context as explained above |
| 173 | + |
| 174 | + // start a span for the outgoing request |
| 175 | + let tracer = InstrumentationSystem.tracer |
| 176 | + var span = tracer.startSpan(named: "HTTP GET", context: context, ofKind: .client) |
| 177 | + |
| 178 | + // set attributes on the span |
| 179 | + span.attributes.http.method = "GET" |
| 180 | + // ... |
| 181 | + |
| 182 | + self.execute(request).always { _ in |
| 183 | + // set some more attributes & potentially record an error |
| 184 | + |
| 185 | + // end the span |
| 186 | + span.end() |
| 187 | + } |
| 188 | +} |
| 189 | +``` |
| 190 | + |
| 191 | +> ⚠️ Make sure to ALWAYS end spans. Ensure that all paths taken by the code will result in ending the span. |
| 192 | +> Make sure that error cases also set the error attribute and end the span. |
| 193 | +
|
| 194 | +> In the above example we used the semantic `http.method` attribute that gets exposed via the |
| 195 | +`OpenTelemetryInstrumentationSupport` library. |
| 196 | + |
| 197 | +## Instrument developers: Creating an instrument |
| 198 | + |
| 199 | +Creating an instrument means adopting the `Instrument` protocol (or `Tracer` in case you develop a tracer). |
| 200 | +`Instrument` is part of the `Instrumentation` library & `Tracing` contains the `Tracer` protocol. |
| 201 | + |
| 202 | +`Instrument` has two requirements: |
| 203 | + |
| 204 | +1. A method to inject values inside a `BaggageContext` into a generic carrier (e.g. HTTP headers) |
| 205 | +2. A method to extract values from a generic carrier (e.g. HTTP headers) and store them in a `BaggageContext` |
| 206 | + |
| 207 | +The two methods will be called by instrumented libraries/frameworks at asynchronous boundaries, giving you a chance to |
| 208 | +act on the provided information or to add additional information to be carried across these boundaries. |
| 209 | + |
| 210 | +> Check out the [`Baggage` documentation](https://github.com/slashmo/gsoc-swift-baggage-context) for more information on |
| 211 | +how to retrieve values from the `BaggageContext` and how to set values on it. |
| 212 | + |
| 213 | +### Creating a `Tracer` |
| 214 | + |
| 215 | +When creating a tracer you need to create two types: |
| 216 | + |
| 217 | +1. Your tracer conforming to `Tracer` |
| 218 | +2. A span class conforming to `Span` |
| 219 | + |
| 220 | +> The `Span` conforms to the standard rules defined in [OpenTelemetry](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/api.md#span), so if unsure about usage patterns, you can refer to this specification and examples referring to it. |
| 221 | +
|
| 222 | +--- |
| 223 | + |
| 224 | +## Bootstrapping the Instrumentation System |
| 225 | + |
| 226 | +Instead of providing each instrumented library with a specific instrument explicitly, you *bootstrap* the |
| 227 | +`InstrumentationSystem` which acts as a singleton that libraries/frameworks access when calling out to the configured |
| 228 | +`Instrument`: |
| 229 | + |
| 230 | +```swift |
| 231 | +InstrumentationSystem.bootstrap(FancyInstrument()) |
| 232 | +``` |
| 233 | + |
| 234 | +### Bootstrapping multiple instruments using MultiplexInstrument |
| 235 | + |
| 236 | +It is important to note that `InstrumentationSystem.bootstrap(_: Instrument)` must only be called once. In case you |
| 237 | +want to bootstrap the system to use multiple instruments, you group them in a `MultiplexInstrument` first, which you |
| 238 | +then pass along to the `bootstrap` method like this: |
| 239 | + |
| 240 | +```swift |
| 241 | +InstrumentationSystem.bootstrap(MultiplexInstrument([FancyInstrument(), OtherFancyInstrument()])) |
| 242 | +``` |
| 243 | + |
| 244 | +`MultiplexInstrument` will then call out to each instrument it has been initialized with. |
11 | 245 |
|
12 | 246 | ## Discussions
|
13 | 247 |
|
14 |
| -Discussions about this topic are **more than welcome**. During this project we'll use a mixture of [GitHub issues](https://github.com/slashmo/gsoc-swift-tracing/issues) and [Swift forum posts](https://forums.swift.org/c/server/serverdev/14). |
| 248 | +Discussions about this topic are **more than welcome**. During this project we'll use a mixture of |
| 249 | +[GitHub issues](https://github.com/slashmo/gsoc-swift-tracing/issues) |
| 250 | +and [Swift forum posts](https://forums.swift.org/c/server/serverdev/14). |
15 | 251 |
|
16 | 252 | ## Contributing
|
17 | 253 |
|
|
0 commit comments