Understanding OpenTelemetry: How It Works End to End
OpenTelemetry (OTel) is a vendor-neutral, open-source standard for collecting traces, metrics, and logs from an application — collectively called telemetry. Before OTel, every observability vendor (Datadog, New Relic, Jaeger, Prometheus) shipped its own instrumentation library, so switching vendors meant re-instrumenting your whole codebase. OpenTelemetry fixes this by splitting the problem into two halves: a standard API you instrument your code with once, and a swappable exporter that sends the resulting data to whichever backend you choose — without touching application code.
1. How OpenTelemetry Works
OpenTelemetry is made of four cooperating pieces:
| Component | Role |
|---|---|
| API | The interfaces your code calls — Tracer, Span, Meter. Stable across versions, so instrumented code never breaks. |
| SDK | The real implementation behind the API — creates spans, batches them, and hands them to an exporter. |
| Exporter | Converts finished spans/metrics into a wire format and ships them — e.g. OTLP, or straight to a console for debugging. |
| Collector (optional) | A separate process that receives telemetry over the network, can batch/filter/transform it, and forwards it to one or more backends (Jaeger, Prometheus, a vendor SaaS). |
Your application only ever talks to the API. The SDK, exporter, and Collector are all configuration — swap the exporter and the same instrumented code now reports to a different backend, with zero code changes.
2. Putting the OpenTelemetry Flow Together
End to end, a single request flows like this: your code calls the API → the SDK creates and manages the span → the exporter serializes finished spans → (optionally) the Collector receives, batches, and routes them → a backend (Jaeger, Grafana, a vendor) stores and visualizes them.
The example below wires up the SDK with a logging exporter — no Collector or external backend needed — so a span’s full lifecycle is visible directly on the console.
2.1 Maven Dependencies
Add the SDK and the logging exporter — this is the only pair of dependencies the example below needs, no Collector or external backend required:
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<version>1.42.1</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-logging</artifactId>
<version>1.42.1</version>
</dependency>2.2 Code Example
A tracer is built from the SDK and the logging exporter above, then used to wrap a single business method in a span:
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.exporter.logging.LoggingSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
public class OrderService {
public static void main(String[] args) {
// 1. SDK: build a tracer provider wired to the logging exporter
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(SimpleSpanProcessor.create(LoggingSpanExporter.create()))
.build();
OpenTelemetry otel = OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.build();
// 2. API: your application code only ever touches this Tracer
Tracer tracer = otel.getTracer("order-service");
placeOrder(tracer, 42);
tracerProvider.shutdown();
}
static void placeOrder(Tracer tracer, long orderId) {
Span span = tracer.spanBuilder("placeOrder")
.setAttribute("order.id", orderId)
.startSpan();
try {
// ... business logic would run here ...
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
span.end();
}
}
}2.3 Code Explanation
SdkTracerProvider is the SDK half of OpenTelemetry — it owns the pipeline that turns spans into exported data, and here it’s wired to SimpleSpanProcessor + LoggingSpanExporter, which just prints each finished span to stdout instead of sending it to a Collector or backend; otel.getTracer("order-service") is the only line application code ever needs, since the string is just a name identifying which part of the app produced the span, not a connection to any backend; and tracer.spanBuilder("placeOrder").startSpan() starts timing while setAttribute("order.id", orderId) attaches structured metadata so the span is searchable later, with span.end() in the finally block stopping the timer and handing the completed span to the processor — the exact moment the exporter fires.
Running OrderService.main() prints the completed span to the console:
Output:
'placeOrder' : ab12cd34ef56ab12cd34ef56ab12cd34 ab12cd34ef56ab12 INTERNAL [tracer: order-service:] AttributesMap{data={order.id=42}, capacity=128, totalAddedValues=1}
That one line is a full span: a trace ID, a span ID, the span name, its kind, and the attribute you attached. In a real deployment you’d swap LoggingSpanExporter for an OTLP exporter pointed at a Collector — the placeOrder method above would not change at all.
3. Do You Need Every OpenTelemetry Component?
No — most of the pipeline is optional depending on scale:
- API + SDK — always required; this is what actually produces telemetry.
- Exporter — always required too, but it can be as simple as the logging exporter above, or an OTLP exporter sending straight to a backend like Jaeger with no Collector in between.
- Collector — skip it for a single small service exporting directly to one backend. Add it once you need to batch/retry telemetry centrally, fan data out to multiple backends, or strip sensitive attributes before they leave your network — all without redeploying every service.
- Auto-instrumentation agents — optional. They attach at JVM startup and instrument common frameworks (Spring, JDBC, HTTP clients) with zero code changes. Use them to get traces fast; drop to manual API calls (like the example above) only for the business-specific spans auto-instrumentation can’t see.
4. Why Use OpenTelemetry?
- No vendor lock-in — instrument once with the API; change exporters, not code, to switch backends.
- One standard for traces, metrics, and logs — instead of three different vendor SDKs with three different conventions.
- Industry-wide adoption — a CNCF project with support baked into most major frameworks and observability platforms, so instrumentation you write today keeps working as the ecosystem around it evolves.
- Centralized control via the Collector — security/ops teams can filter, sample, or redact telemetry in one place instead of in every service.
5. Conclusion
OpenTelemetry’s whole design rests on one separation: your code only ever calls a stable API; everything that decides where telemetry ends up — the SDK’s processors, the exporter, and an optional Collector — lives in configuration you can change without touching that code. Start with just the API, SDK, and a simple exporter like the one above; add a Collector and swap exporters only once you actually need to fan data out or centralize processing.


