OpenTelemetry's most important design decision is splitting the API from the SDK. Understanding why makes everything else — instrumentation, the Collector, exporters — click into place.
Why split API from SDK
Instrumentation code (application code, or a library like a database driver) calls the API to create spans and record metrics. It never talks to the SDK directly. This matters for one big reason: library authors can instrument their code without forcing a dependency on any particular telemetry backend or even on the SDK being present. If no SDK is registered, API calls are safe no-ops with near-zero overhead. If an application later installs and configures an SDK, that same library code suddenly starts producing real telemetry — with no code changes.
This mirrors SLF4J in the Java logging world: libraries log against a thin facade; the application chooses and wires up the actual logging backend. OTel does the same for telemetry.
The API
The API package defines the stable, minimal interfaces your code calls:
- Tracer — creates spans (
tracer.startSpan()). - Meter — creates instruments like counters and histograms.
- Logger — emits log records (via the Logs Bridge API, often wired to an existing logging library).
- Context & Propagation — carries the active span/baggage across function calls and network boundaries.
The API is versioned for long-term stability — breaking changes are rare and heavily guarded, because thousands of libraries depend on it staying backward compatible.
The SDK
The SDK is the concrete implementation that plugs into the API. It's where the real work happens:
- Sampling — deciding which traces to keep.
- Resource detection — attaching service name, host, cloud metadata.
- Batching — grouping spans/metrics before sending, for efficiency.
- Export — handing off the finished, processed telemetry to one or more destinations.
An application wires up the SDK once, near startup:
// Node.js — simplified SDK bootstrap
const sdk = new NodeSDK({
resource: resourceFromAttributes({ 'service.name': 'checkout-api' }),
traceExporter: new OTLPTraceExporter({ url: 'http://otel-collector:4318/v1/traces' }),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
Everything after this — every tracer.startSpan() call anywhere in the process, including inside third-party libraries — now flows through this configured pipeline.
Processors and exporters
Inside the SDK, telemetry flows through a small pipeline before leaving the process:
| Stage | Job |
|---|---|
| Processor | Batches spans/metrics, optionally filters or enriches them, before handing off to an exporter. |
| Exporter | Serializes telemetry into a wire format and sends it somewhere — OTLP over gRPC/HTTP, or a vendor-specific format. |
You can register multiple processors/exporters — e.g. export traces to your Collector via OTLP while also printing them to the console during local development with a ConsoleSpanExporter.
OTLP: the wire protocol
The OpenTelemetry Protocol (OTLP) is the standard format telemetry travels in between the SDK, the Collector, and backends. It's defined via Protocol Buffers and supports two transports: gRPC (default port 4317) and HTTP/protobuf or HTTP/JSON (default port 4318). OTLP is vendor-neutral by design — any backend that speaks OTLP can receive telemetry from any OTel SDK, with no per-vendor exporter needed.
Unless you have a specific reason to export directly in a vendor's proprietary format, export via OTLP to a Collector, and let the Collector's exporters handle vendor-specific translation. This keeps your application code fully vendor-agnostic.
Putting it together
The full path a span takes, end to end:
- Application or library code calls the API (
tracer.startSpan()). - The registered SDK implementation actually creates the span, applies sampling, and tracks context.
- On span end, a processor batches it.
- An exporter serializes it to OTLP and ships it — usually to a local Collector.
- The Collector receives, optionally transforms, and forwards it to one or more backends (Jaeger, Tempo, a SaaS vendor).