OpenTelemetry for Swift
An OpenTelemetry client implementation for Swift.
"OpenTelemetry Swift" builds on top of Swift Distributed Tracing by implementing its instrumentation & tracing API. This means that any library instrumented using Swift Distributed Tracing will automatically work with "OpenTelemetry Swift".
In this guide we'll create a service called "onboarding". It won't do anything other than starting a couple of spans and exporting them, but it highlights the key aspects of "OpenTelemetry Swift" and how to set it up. To wet your appetite, here are screenshots from both Jaeger & Zipkin displaying a trace created by "onboarding":
You can find the source code of the "onboarding" example here.
To add "OpenTelemetry Swift" to our project, we first need to include it as a package dependency:
.package(url: "https://github.com/slashmo/opentelemetry-swift.git", from: "0.3.0"),
Then we add
OpenTelemetry to our executable target:
.product(name: "OpenTelemetry", package: "opentelemetry-swift"),
Now that we installed "OpenTelemetry Swift", it's time to bootstrap the instrumentation system to use OpenTelemetry.
Before we can retrieve a tracer we need to configure and start the main object
import NIO import OpenTelemetry import Tracing let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) let otel = OTel(serviceName: "onboarding", eventLoopGroup: group) try otel.start().wait() InstrumentationSystem.bootstrap(otel.tracer())
We should also not forget to shutdown
OTel and the
try otel.shutdown().wait() try group.syncShutdownGracefully()
⚠️With this setup, ended spans will be ignored and not exported to a tracing backend. Read on to learn more about how to configure processing & exporting.
Configuring processing and exporting
To start processing and exporting spans, we must pass a processor to the
"OpenTelemetry Swift" comes with a number of built in processors and you can even build your own.
Check out the "Span Processors" section to learn more.
For now, we're going to use the
SimpleSpanProcessor. As the name suggests, this processor doesn't do much except
for forwarding ended spans to an exporter one by one. This exporter must be injected when initializing
Starting the collector
We want to export our spans to both Jaeger and Zipkin. The OpenTelemetry project provides the "OpenTelemetry Collector" which acts as a middleman between clients such as "OpenTelemetry Swift" and tracing backends such as Jaeger and Zipkin. We won't go into much detail on how to configure the collector in this guide, but instead focus on our "onboarding" service.
We use Docker to run the OTel collector, Jaeger, and Zipkin locally. Both
collector-config.yaml are located in the "docker" folder of the "onboarding" example.
# In Examples/Onboarding docker-compose -f docker/docker-compose.yaml up --build
After a couple of seconds everything should be up-and-running. Let's go ahead and
configure OTel to export to the collector. "OpenTelemetry Swift" contains a second library called
"OtlpGRPCSpanExporting", providing the necessary span exporter. We need to also include it in our target in
.product(name: "OtlpGRPCSpanExporting", package: "opentelemetry-swift"),
On to the fun part - Configuring the
let exporter = OtlpGRPCSpanExporter( config: OtlpGRPCSpanExporter.Config( eventLoopGroup: group ) )
As mentioned above we need to inject this exporter into a processor:
let processor = OTel.SimpleSpanProcessor(exportingTo: exporter)
The only thing left to do is to tell
OTel to use this processor:
- let otel = OTel(serviceName: "onboarding", eventLoopGroup: group) + let otel = OTel(serviceName: "onboarding", eventLoopGroup: group, processor: processor)
Our demo application creates two spans:
world. To make things even more realistic we'll add an event to
let rootSpan = InstrumentationSystem.tracer.startSpan("hello", baggage: .topLevel) sleep(1) rootSpan.addEvent(SpanEvent( name: "Discovered the meaning of life", attributes: ["meaning_of_life": 42] )) let childSpan = InstrumentationSystem.tracer.startSpan("world", baggage: rootSpan.baggage) sleep(1) childSpan.end() sleep(1) rootSpan.end()
Note that we retrieve the the tracer through
InstrumentationSystem.tracerinstead of directly using
otel.tracer(). This allows us to easily switch out the bootstrapped tracer in the future. It's also how frameworks/libraries implement tracing support without even knowing about
Finally, because the demo app start shutting down right after the last span was ended, we should add another delay to give the exporter a chance to finish its work:
+ sleep(1) try otel.shutdown().wait() try group.syncShutdownGracefully()
View the complete example here.
To learn more about the
InstrumentationSystem, check out the Swift Distributed Tracing docs on the subject.
To learn more about instrumenting your Swift code, check out the Swift Distributed Tracing docs on "instrumenting your code".
The "OpenTelemetry Collector" has many more configuration options. Check them out here.
"OpenTelemetry Swift" is designed to be easily customizable. This sections goes over the different moving parts that may be switched out with other non-default implementations.
Generating trace & span ids
When starting spans, the OTel Tracer will generate IDs uniquely identifying each trace/span. Creating a root span generates both trace and span ID. Creating a child span re-uses the parent's trace ID and only generates a new span ID.
A "W3C TraceContext" compatible
RandomIDGenerator is used
for this by default. As the name suggests, it generates completely random IDs.
To create your own ID generator you need to implement the
Using a custom ID generator
Simply pass a different ID generator when initializing
OTel like this:
let otel = OTel( serviceName: "service", eventLoopGroup: group, idGenerator: MyAwesomeIDGenerator() )
If your application creates a large amount of spans you might want to look into sampling out certain spans. By default, "OpenTelemetry Swift" ships with a "parent-based" sampler, configured to always sample root spans using a "constant sampler". Parent-based means that this sampler takes into account whether the parent span was sampled.
To create your own sampler you need to implement the
Using a custom sampler
OTel initializer allows you to inject a sampler:
let otel = OTel( serviceName: "service", eventLoopGroup: group, sampler: ConstantSampler(isOn: false) )
The above configuration would sample out each span, i.e. no span would ever be exported.
🔗 📖API Docs: OTelSampler 📖API Docs: OTel.ParentBasedSampler 📖API Docs: OTel.ConstantSampler 📖OpenTelemetry Specification: Sampling
Processing ended spans
Span processors get passed read-only ended spans. The most common use-case of this is to forward the ended span to an
exporter. The built-in
SimpleSpanProcessor forwards them immediately one-by-one.
To create your own span processor you need to implement the
Using a custom span processor
To configure which span processor should be used, pass it along to the
let otel = OTel( serviceName: "service", eventLoopGroup: group, processor: MyAwesomeSpanProcessor() )
🔗 📖API Docs: OTelSpanProcessor 📖API Docs: OTel.SimpleSpanProcessor 📖OpenTelemetry Specification: Span Processor
Exporting processed spans
To actually send span data to a tracing backend like Jaeger, spans need to be
OtlpGRPCSpanExporting, which is a library included in this package
implements exporting using the OpenTelemetry protocol (OTLP) by sending span data via gRPC to the
OpenTelemetry collector. The collector can then be
configured to forward received spans to tracing backends.
To create your own span exporter you need to implement the
Using a custom span exporter
Instead of passing the exporter directly to
OTel, you need to wrap it inside a
let otel = OTel( serviceName: "service", eventLoopGroup: group, processor: SimpleSpanProcessor( exportingTo: MyAwesomeSpanExporter() ) )
🔗 📖API Docs: OTelSpanExporter 📖API Docs: OtlpGRPCSpanExporter 📖OpenTelemetry Collector 📖OpenTelemetry Specification: Span Exporter
Propagating span context
OpenTelemetry uses the W3C TraceContext format to propagate
span context across HTTP requests by default. Some tracing backends may not fully support this standard and need to use
a custom propagator. X-Ray e.g. propagates using the
X-Amzn-Trace-Id header. Support for this header is implemented
in the X-Ray support library.
To create your own propagator you need to implement the
Using a custom propagator
Pass your propagator of choice to the
OTel initializer like this:
let otel = OTel( serviceName: "service", eventLoopGroup: group, propagator: MyAwesomePropagator() )
Detecting resource information
When investigating traces it is often helpful to not only see insights about your application but also about the system
(resource) it's running on. One option of including such information would be to set a bunch of span attributes on every
span. But this would be cumbersome and inefficient. Therefore, OpenTelemetry has the concept of resource detection.
Resource detectors run once on start-up, detect some attributes collected in a
Resource and hand them off to
From then on, the resulting
Resource will be passed along to span exporters for them to include these attributes.
"OpenTelemetry Swift" comes with two built-in resource detectors which are enabled by default:
This detector collects information such as the process ID and executable name.
This detector allows you to specify resource attributes through an environment variable. This comes in handy for attributes that you don't know yet at built-time.
To create your own resource detector you need to implement the
Using a custom resource detector
There are three possible settings for resource detection represented by the
// 1. Automatic, the default OTel.ResourceDetection.automatic( additionalDetectors: [MyAwesomeDetector()] ) // 2. Manual OTel.ResourceDetection.manual( OTel.Resource(attributes: ["key": "value"]) ) // 3. None, i.e. disabled OTel.ResourceDetection.none
🔗 📖API Docs: OTelResourceDetector 📖API Docs: OTelResourceDetection 📖OpenTelemetry Specification: Resource
To ensure a consistent code style we use SwiftFormat.
To automatically run it before you push to GitHub, you may define a
pre-push Git hook executing
the soundness script:
echo './scripts/soundness.sh' > .git/hooks/pre-push chmod +x .git/hooks/pre-push