The OpenTelemetrySdk provides limited functionality in its default configuration. For more useful functionality, some configuration is required.
The default registered TracerProvider
and MetricProvider
are not configured with an exporter. There are several exporters available depending on your needs. Below we will explore configuring the OTLP exporter, which can be used for sending data to the opentelemetry-collector.
import GRPC
import OpenTelemetryApi
import OpenTelemetrySdk
import OpenTelemetryProtocolExporter
// initalize the OtlpTraceExporter
let otlpConfiguration = OtlpConfiguration(timeout: OtlpConfiguration.DefaultTimeoutInterval)
let grpcChannel = ClientConnection.usingPlatformAppropriateTLS(for: MultiThreadedEventLoopGroup(numberOfThreads:1))
.connect(host: <collector host>, port: <collector port>)
let traceExporter = OtlpTraceExporter(channel: grpcChannel
config: otlpConfiguration)
// build & register the Tracer Provider using the built otlp trace exporter
OpenTelemetry.registerTracerProvider(tracerProvider: TracerProviderBuilder()
.add(spanProcessor:SimpleSpanProcessor(spanExporter: traceExporter))
.with(resource: Resource())
.build())
A similar pattern is used for the OtlpMetricExporter:
// otlpConfiguration & grpcChannel can be reused
OpenTelemetry.registerMeterProvider(meterProvider: MeterProviderBuilder()
.with(processor: MetricProcessorSdk())
.with(exporter: OtlpMetricExporter(channel: channel, config: otlpConfiguration))
.with(resource: Resource())
.build())
After configuring the MeterProvider & TracerProvider all subsequently initialized instrumentation will be exporting using this OTLP exporter.
To do tracing, you will need a tracer. A tracer is acquired through the tracer provider and is responsible for creating spans. The OpenTelementrySdk manages the tracer provider as we defined and registered above. A tracer requires an instrumentation name, and an optional version to be created:
let tracer = OpenTelemetrySDK.instance.tracerProvider.get(instrumentationName: "instrumentation-library-name", instrumentationVersion: "1.0.0")
A span represents a unit of work or operation. Spans are the building blocks of Traces. To create a span use the span builder associated with the tracer:
let span = let builder = tracer.spanBuilder(spanName: "\(name)").startSpan()
...
span.end()
It is required to call end()
to end the span.
Spans are used to build relationship between operations. Below is an example of how we can manually build relationship between spans.
Below we have parent()
calling child()
and how to manually link spans of each of these methods.
func parent() {
let parentSpan = someTracer.spanBuilder(spanName: "parent span").startSpan()
child(span: parentSpan)
parentSpan.end()
}
func child(parentSpan: Span) {
let childSpan = someTracer.spanBuilder(spanName: "child span")
.setParent(parentSpan)
.startSpan()
// do work
childSpan.end()
}
The parent-child relationship will be automatically linked if activeSpan
is used:
func parent() {
let parentSpan = someTracer.spanBuilder(spanName: "parent span")
.setActive(true) // automatically sets context
.startSpan()
child()
parentSpan.end()
}
func child() {
let childSpan = someTracer.spanBuilder(spanName: "child span")
.startSpan() //automatically captures `active span` as parent
// do work
childSpan.end()
}
Sometimes it’s useful to do something with the current/active span. Here’s how to access the current span from an arbitrary point in your code.
let currentSpan = OpenTelemetry.instance.contextProvider.activeSpan
Spans can also be annotated with additional attributes. All spans will be automatically annotated with the Resource
attributes attached to the tracer provider.
The Opentelementry-swift sdk already provides instrumentation of common attributes in the SDKResourceExtension
instrumentation.
In this example a span for a network request capturing details about that request using existing semantic conventions.
let span = tracer.spanBuilder("/resource/path").startSpan()
span.setAttribute("http.method", "GET");
span.setAttribute("http.url", url.toString());
A Span Event can be thought of as a structured log message (or annotation) on a Span, typically used to denote a meaningful, singular point in time during the Span’s duration.
let attributes = [
"key" : AttributeValue.string("value"),
"result" : AttributeValue.int(100)
]
span.addEvent(name: "computation complete", attributes: attributes)
A status can be set on a span, typically used to specify that a span has not completed successfully - SpanStatus.Error
. In rare scenarios, you could override the Error status with OK, but don’t set OK on successfully-completed spans.
The status can be set at any time before the span is finished:
func myFunction() {
let span = someTracer.spanBuilder(spanName: "my span").startSpan()
defer {
span.end()
}
guard let criticalData = get() else {
span.status = .error(description: "something bad happened")
return
}
// do somthing
}
Semantic conventions provide special demarcation for events that record exceptions:
let span = someTracer.spanBuilder(spanName: "my span").startSpan()
do {
try throwingFunction()
} catch {
span.addEvent(name: SemanticAttributes.exception.rawValue,
attributes: [SemanticAttributes.exceptionType.rawValue: AttributeValue.string(String(describing: type(of: error))),
SemanticAttributes.exceptionEscaped.rawValue: AttributeValue.bool(false),
SemanticAttributes.exceptionMessage.rawValue: AttributeValue.string(error.localizedDescription)])
})
span.status = .error(description: error.localizedDescription)
}
span.end()
Different Span processors are offered by OpenTelemetry-swift. The SimpleSpanProcessor
immediately forwards ended spans to the exporter, while the BatchSpanProcessor
batches them and sends them in bulk. Multiple Span processors can be configured to be active at the same time using the MultiSpanProcessor
.
For example, you may create a SimpleSpanProcessor
that exports to a logger, and a BatchSpanProcesssor
that exports to a OpenTelementry-Collector:
let otlpConfiguration = OtlpConfiguration(timeout: OtlpConfiguration.DefaultTimeoutInterval)
let grpcChannel = ClientConnection.usingPlatformAppropriateTLS(for: MultiThreadedEventLoopGroup(numberOfThreads:1))
.connect(host: <collector host>, port: <collector port>)
let traceExporter = OtlpTraceExporter(channel: grpcChannel
config: otlpConfiguration)
// build & register the Tracer Provider using the built otlp trace exporter
OpenTelemetry.registerTracerProvider(tracerProvider: TracerProviderBuilder()
.add(spanProcessor:BatchSpanProcessor(spanExporter: traceExporter))
.add(spanProcessor:SimpleSpanProcessor(spanExporter: StdoutExporter))
.with(resource: Resource())
.build())
The batch span processor allows for a variety of parameters for customization including.
OpenTelemetry-Swift provides the following exporters:
InMemoryExporter
: Keeps the span data in memory. This is useful for testing and debugging.DatadogExporter
: Converts OpenTelemetry span data to Datadog traces & span Events to Datadog logs.JaegerExporter
: Converts OpenTelemetry span data to Jaeger format and exports to a Jaeger endpoint.PrometheusExporter
: Converts metric data to Prometheus format and exports to a Prometheus endpoint.StdoutExporter
: Exports span data to Stdout. Useful for debugging.ZipkinTraceExporter
: Exports span data to Zipkin format to a zipkin endpoint.