OpenTelemetry
For a decade, instrumenting for traces meant marrying a vendor. Zipkin libraries spoke Zipkin, Jaeger clients spoke Jaeger, Datadog's agent spoke Datadog — and switching backends meant re-instrumenting every service, which is why nobody ever switched. OpenTelemetry ended that war by splitting the problem into an API you code against, an SDK that implements it, and OTLP, one wire protocol every backend now accepts.
The consequence for Harborline is the whole sales pitch: instrument the five services once, and the telemetry can go to Tempo today, Jaeger tomorrow, or Datadog next year — by changing an endpoint, not a line of application code.
How the Wars Ended
Two competing open instrumentation standards, OpenTracing and OpenCensus, merged into OpenTelemetry under the CNCF in 2019. All three signals — traces, metrics, and logs — are stable today, and OTel is the second-most-active CNCF project after Kubernetes itself. Seeing either ancestor's name in a library's documentation dates it; adopting OTel in 2026 is the safe default, not a bet.
API, SDK, OTLP
The three layers have a deliberate seam. The API is what application and library code calls — tracer.start_span — and is inert by itself: no exporter, no network, no cost. The SDK is the concrete implementation wired up at process startup, carrying the samplers, processors, and exporters. OTLP is the wire protocol the result travels on — gRPC on port 4317, HTTP/protobuf on 4318. The seam exists so a library can instrument itself by depending only on the API, without dragging an exporter into every application that imports it.
Auto-Instrumentation
The fastest path to a full waterfall requires no code changes at all. The opentelemetry-instrument wrapper starts bookings with instrumentation for every library it recognizes: the FastAPI integration opens a server span per request, and the requests, psycopg, and redis integrations open client spans around every outbound call.
# wrap bookings without touching its code pip install opentelemetry-distro opentelemetry-exporter-otlp opentelemetry-bootstrap -a install # detects installed libs, adds integrations OTEL_SERVICE_NAME=bookings \ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 \ opentelemetry-instrument gunicorn main:app
Two installs, one wrapped launch command, and the entire Topic 38 waterfall exists — including the cache-01 client spans that will matter enormously in Topic 43. The bookings team ships this in an afternoon, which is the point: the expensive part of tracing used to be the code changes, and for the standard library surface that cost is now zero.
Manual Spans
Auto-instrumentation cannot know that "reserve the seat, then charge the card" is one business operation. Between the clean HTTP and database spans sits a void where the business logic lives, so the bookings team brackets it with one manual span carrying the attributes that matter to Harborline.
with tracer.start_as_current_span("checkout.confirm") as span:
span.set_attribute("harborline.route_id", route_id)
span.set_attribute("harborline.passengers", len(passengers))
reserve_seat(booking)
charge_card(booking)
Unlike metric labels, span attributes are per-request data, not new time series. A booking ID or route ID that would detonate Prometheus's cardinality (Chapter 5) is legitimate — routine, even — on a span. The cost model differs by signal, and this is the signal where high-cardinality context belongs.
Resource Attributes
Resource attributes describe the emitter, stamped once onto all telemetry a process produces. service.name is the one non-negotiable: leave it unset and the backend files everything under unknown_service:gunicorn, service graphs collapse into one blob, and Topic 43's correlation jumps have nothing to key on. Harborline sets it to match the Prometheus job label, adds the service version, and stamps an environment attribute matching the env="prod" convention — so the three signals join on shared identity instead of diverging.
The Narrow Waist
Restate the spine, because it decides architecture arguments: instrumentation is the expensive, invasive part — code changes across five services — so you do it once, against a neutral standard. Backends compete on storage, query, and price behind OTLP. Harborline's choice of Tempo in Topic 43 is a swappable decision, not a marriage, and that asymmetry is the entire reason OTel won.
- Instrumenting with a vendor's proprietary SDK in 2026 because the onboarding wizard suggested it — the telemetry works until the contract negotiation, and the exit price is re-instrumenting every service instead of editing one exporter endpoint.
- Deploying with
service.nameunset — every service reports asunknown_service:gunicorn, service graphs collapse into one blob, and the trace-to-metrics correlation has no join key. - Relying on auto-instrumentation alone — you get clean HTTP, database, and Redis spans and a business-logic void between them; the 9-second gap in a checkout trace is visible but unnamed until a manual span brackets the operation that owns it.
- Importing SDK classes inside application libraries instead of the API — the library now dictates exporter and sampler wiring, and two libraries doing this fight over global configuration at startup.
- Exporting OTLP from every process straight to the backend across the network — no local buffering or enrichment, so a 30-second Tempo restart becomes dropped spans in every service; the Collector (Topic 41) exists to absorb exactly this.
- Instrument against the OTel API and ship OTLP, whatever backend you run today — the once-instrumented, export-anywhere seam is the whole point of the standard.
- Start every service with auto-instrumentation, then add manual spans only where a business operation spans multiple downstream calls — checkout gets one; a plain CRUD handler doesn't need any.
- Set
service.name, the service version, and the environment as resource attributes on day one, matching the Prometheusjobandenvconventions so signals join instead of diverging. - Push rich per-request context into span attributes, not metric labels — the cardinality cost model differs by signal, and spans are where a booking ID belongs.
Knowledge Check
What problem does OpenTelemetry solve that no trace backend can solve on its own?
- It makes trace storage cheap enough to keep every request
- It makes instrumentation portable, so changing backends means changing an endpoint
- It removes the need for sampling by compressing spans
- It provides one universal query language that supersedes every backend's own dialect
Why must a library that instruments itself depend only on the OTel API, never the SDK?
- The API is inert and leaves exporter and sampler wiring to the application
- The SDK is too large to ship inside a library package
- Only the API can export spans over OTLP
- The SDK's license forbids redistribution inside libraries, unlike the API's terms
After wrapping bookings with opentelemetry-instrument, which span will still be missing from a checkout trace?
- The server span for the incoming POST /checkout request to the FastAPI handler
- The client span around the cache-01 session lookup
- The client span around the db-01 INSERT
- A span bracketing the reserve-seat-then-charge-card business operation
A route ID appears on every checkout. Why is it fine as a span attribute but dangerous as a Prometheus label (Chapter 5)?
- Sampling deletes most spans anyway, so the values never accumulate
- Span attributes are compressed while labels are stored raw
- A label value creates a persistent time series; an attribute is per-request data
- Prometheus rejects non-numeric label values like route IDs
You got correct