Skip to main content

OpenTelemetry Providers

OpenTelemetry Providers​

The Embrace Apple SDK is built on OpenTelemetry, and 7.x gives you access to the TracerProvider and LoggerProvider that back the Embrace pipeline. Use them when you already have OpenTelemetry instrumentation — your own, or a third-party library's — that should emit through Embrace instead of through a separate pipeline of its own.

Signals created through these providers are handled exactly like the ones the SDK creates itself: Embrace captures them, and forwards them to every processor and exporter you registered through EmbraceIO.OTelOptions.

For instrumentation you write yourself, prefer the Embrace API (EmbraceIO.shared.createSpan(...) and EmbraceIO.shared.log(...)). It is the more ergonomic surface, and it doesn't require you to import OpenTelemetryApi. The providers exist for the cases that API doesn't cover.

Enabling the Providers​

The providers only exist when you start the SDK with an EmbraceIO.OTelOptions. You don't need any exporters for them — an empty OTelOptions is enough:

try EmbraceIO.start(
options: EmbraceIO.Options.withAppId(
"YOUR_APP_ID",
otel: EmbraceIO.OTelOptions()
)
)

Every accessor returns nil when there is no Embrace OTel pipeline to hand back:

  • You haven't called start(options:) yet.
  • You started the SDK without EmbraceIO.OTelOptions.
  • start(options:) threw.
  • An earlier start(options:) already created the SDK client, so the OTelOptions you passed to a later call were ignored.

Nothing is substituted in place of a missing pipeline. The SDK returns nil rather than a no-op provider, so an absent pipeline shows up at the call site instead of quietly swallowing your telemetry.

Getting a Tracer or a Logger​

EmbraceIO.shared exposes convenience accessors that return a Tracer or a Logger attached to the Embrace pipeline:

import EmbraceIO
import OpenTelemetryApi

guard let tracer = EmbraceIO.shared.tracer(
instrumentationName: "com.mycompany.checkout",
instrumentationVersion: "1.0.0"
) else {
return
}

let span = tracer.spanBuilder(spanName: "checkout").startSpan()
span.setAttribute(key: "cart.items", value: 3)
span.end()
guard let logger = EmbraceIO.shared.logger(
instrumentationScopeName: "com.mycompany.checkout"
) else {
return
}

logger.logRecordBuilder()
.setBody(.string("Payment declined"))
.setSeverity(.error)
.emit()

instrumentationName and instrumentationScopeName identify your instrumentation, not the library it instruments. instrumentationVersion is optional in both calls.

If you need options these accessors don't cover — a schema URL, instrumentation scope attributes, or control over whether the trace context is attached to log records automatically — reach for the providers directly:

let tracer = EmbraceIO.shared.tracerProvider?.get(
instrumentationName: "com.mycompany.checkout",
instrumentationVersion: "1.0.0",
schemaUrl: "https://opentelemetry.io/schemas/1.30.0",
attributes: ["checkout.flow": .string("express")]
)

let logger = EmbraceIO.shared.loggerProvider?
.loggerBuilder(instrumentationScopeName: "com.mycompany.checkout")
.setSchemaUrl("https://opentelemetry.io/schemas/1.30.0")
.setIncludeTraceContext(false)
.build()

Both accessors return the OpenTelemetry API protocols (TracerProvider, LoggerProvider), not the underlying SDK classes. You can create tracers and loggers from them, but you can't add processors to Embrace's pipeline, change its sampler or resource, or shut it down — configure all of that through EmbraceIO.OTelOptions when you start the SDK.

What Happens to These Signals​

  • Embrace captures them. They are treated like the spans and logs the SDK generates itself, and are uploaded to Embrace (or exported only, if you configured the SDK with withLocalConfiguration).
  • Your processors and exporters receive them. Anything you registered through EmbraceIO.OTelOptions sees them alongside the SDK's own signals.
  • They count against the same per-session limits as EmbraceIO.shared.createSpan and EmbraceIO.shared.log, and are dropped once that budget is exhausted. Chatty instrumentation can therefore consume the budget your own calls draw from.

Providers and the SDK Lifecycle​

A non-nil provider means you supplied OTelOptions, not that the SDK is currently running. The providers outlive EmbraceIO.stop(), and signals created after the SDK stops are stamped with an empty session id and belong to no session. Check EmbraceIO.shared.state or EmbraceIO.shared.isSDKEnabled when that distinction matters to you.

Registering the Embrace Providers Globally​

By default, the Embrace providers are reachable only through EmbraceIO.shared, and the process-wide OpenTelemetry providers are left untouched. Set registersGlobalProviders to true to install Embrace's providers as the global ones, so that any instrumentation resolving OpenTelemetry.instance — including third-party OpenTelemetry libraries you don't configure yourself — emits through the Embrace pipeline:

try EmbraceIO.start(
options: EmbraceIO.Options.withAppId(
"YOUR_APP_ID",
otel: EmbraceIO.OTelOptions(
spanExporters: [myCustomSpanExporter],
registersGlobalProviders: true
)
)
)
warning

Global registration is a process-wide, one-way change. Before you enable it, keep in mind:

  • It only takes effect when this start(options:) call is the one that creates the SDK client. The value passed to any later call is ignored.
  • Registration is last-writer-wins. An app that registers its own providers after the Embrace SDK starts displaces Embrace's, and vice versa.
  • Registration is never undone, not even by EmbraceIO.stop(). OpenTelemetry offers no way to deregister a provider, so the providers stay installed for the remaining lifetime of the process.
  • Every span and log arriving this way counts against the same per-session limits as your own calls.
Changed in 7.x

The 6.x SDK always registered its providers as the process-wide OpenTelemetry providers. In 7.x that registration is opt-in through registersGlobalProviders, so the SDK no longer changes global state you didn't ask it to change.