OpenTelemetry Fundamentals: the Vendor-Neutral Standard¶
What was run¶
A real, minimal OpenTelemetry Python SDK setup (opentelemetry-sdk 1.45.1), creating one real span with a real
attribute and exporting it with the SDK's own ConsoleSpanExporter — the simplest possible real exporter, which
just prints the span's real data instead of sending it anywhere.
A real span, before any backend exists¶
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("test")
with tracer.start_as_current_span("test-span") as span:
span.set_attribute("real", True)
{
"name": "test-span",
"context": {
"trace_id": "0x81c8bc990b2c7966069c8ab091a7a342",
"span_id": "0xec8b6751e6b03adf"
},
"attributes": {"real": true},
"resource": {
"attributes": {
"telemetry.sdk.language": "python",
"telemetry.sdk.name": "opentelemetry",
"telemetry.sdk.version": "1.45.1",
"service.name": "unknown_service:python3"
}
}
}
A real, valid, 128-bit trace ID and 64-bit span ID were generated, with real start and end timestamps, without a
single network call made, without any tracing backend (Jaeger, Tempo, a vendor APM) configured or even running.
Also worth noticing honestly: service.name defaulted to "unknown_service:python3" — nothing in the minimal
setup above told OpenTelemetry what to call this service, a real, easy-to-miss gap this course's
Lesson 2 fixes directly.
How It Actually Works¶
OpenTelemetry is deliberately split into two layers, and the example above only exercises the first:
- The API (
opentelemetry.trace, used above astracer.start_as_current_span) is what your application code calls — a stable, vendor-neutral interface for creating spans, adding attributes, and propagating context. Code written against the API doesn't know or care what happens to the data afterward. - The SDK (
opentelemetry.sdk, used above asTracerProviderandSimpleSpanProcessor) is the real implementation underneath the API — it decides how spans are processed, batched, and exported. The SDK is swappable independently of your instrumented application code, which is the entire point: the realConsoleSpanExporterused here could be replaced with a real OTLP exporter sending to Jaeger, Tempo, or any of dozens of real commercial backends, without touching a single line of the actualwith tracer.start_as_current_span(...)instrumentation code shown above.
This vendor-neutral split is OpenTelemetry's actual value proposition, distinct from any specific backend: an
organization can instrument its real code once, against the stable API, and change where the data goes — self-hosted,
a specific vendor, a different vendor later — as a configuration change to the SDK's exporter, not a rewrite of
instrumented application code. This is architecturally the same separation Prometheus's Mount pattern gave
Level 1's metrics endpoint from its scrape configuration, applied
here to traces instead of metrics.
This course uses the real ConsoleSpanExporter throughout Level 3, deliberately, to make the real span data
directly visible without needing a full tracing backend running locally — a real, honest scope limitation stated
here plainly: a production system would send this data to a real queryable backend (Jaeger, Tempo, a vendor),
which this course did not run, in favor of showing the underlying mechanism clearly instead.
Common mistakes¶
- Instrumenting code directly against a specific vendor's SDK instead of the OpenTelemetry API, which locks instrumented code to that vendor — exactly the coupling the API/SDK split exists to avoid.
- Leaving
service.nameat its real, unhelpful default (unknown_service:python3, confirmed directly above) — every real span needs a meaningful resource identity to be useful once more than one service's spans start mixing together, the exact scenario Lesson 2 builds for real. - Assuming a span "didn't work" if no backend shows it — as this lesson demonstrates, span creation, attributes,
and timing are all real and complete the moment the
withblock exits, entirely independent of whether anything is actually consuming the exported data yet.
Exercise¶
Set a real, meaningful service.name using Resource.create({"service.name": "my-real-service"}) passed to
TracerProvider, create two more real nested spans inside the first one (a parent and a child, using nested
with tracer.start_as_current_span(...) blocks), and confirm in the real console output that the child span's
parent_id correctly references the parent span's span_id — the real mechanism Lesson 2
builds on directly.