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 theOTelOptionsyou 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.OTelOptionssees them alongside the SDK's own signals. - They count against the same per-session limits as
EmbraceIO.shared.createSpanandEmbraceIO.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
)
)
)
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.
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.