SherlockLiu Logo SherlockLiu
Back to all posts
Engineering

Practical OpenTelemetry — Part 3: The Spec, Signals, and Stability

SL
Aug 15, 2026 13 min read
Practical OpenTelemetry — Part 3: The Spec, Signals, and Stability

If Part 2 was the why, this is the what. Chapter 3 of Practical OpenTelemetry lays out the project’s anatomy: the specification, the four signals, the API/SDK split, semantic conventions, and the stability promises that let an entire industry build on top of it.

This is the chapter to get right before the hands-on parts, because every later decision — what to instrument, how to name it, whether to use the SDK directly — traces back to these fundamentals.

The Specification: A Contract, Not a Codebase

OpenTelemetry is, at its core, a specification — a living document that captures the requirements any component must meet to be considered compliant. The actual code (SDKs in a dozen languages, the Collector) is an implementation of that spec.

Two things about the spec are worth internalizing:

Stability is a first-class property of the spec — at two different granularities. Individual spec documents carry a status: No explicit status (equivalent to Experimental), Experimental (breaking changes allowed), Stable (no breaking changes), or Deprecated (editorial changes only) — plus an orthogonal feature-freeze flag that can attach to a document at any of those stages, signaling maintainers aren’t accepting new features to it so the community can focus elsewhere. Zoom out one level and a whole signal (Tracing, Metrics, Logs, Baggage) has its own, slightly different life cycle: Experimental (alpha/beta/RC — breaking changes can land on a minor version bump) → Stable (no breaking changes without a major version bump) → Deprecated (once a replacement signal reaches Stable) → Removed (a breaking change, so it needs a major version bump). This is how the project ships new signals without destabilizing existing users: a signal moving through its own life cycle never forces stable signals to move with it.

Diagram of the OpenTelemetry signal life cycle: Experimental progresses to Stable, which can progress to Deprecated once a replacement is Stable, which can progress to Removed on a major version bump. A separate feature-freeze flag can attach to a spec document at any of these stages. The Signal Life Cycle Experimental alpha / beta / RC breaking OK on minor bump Stable long-term dependencies OK breaking needs major bump Deprecated after replacement is Stable editorial changes only Removed breaking change needs major bump feature-freeze — an orthogonal flag, not a stage can attach to a spec document at Experimental, Stable, or Deprecated — no new features while it's set A signal moving through this life cycle never forces already-Stable signals to move with it.

Figure: the signal life cycle (Experimental → Stable → Deprecated → Removed) is a separate axis from feature-freeze, which can pause any spec document at any stage.

Big changes go through OTEPs. Small changes are ordinary GitHub issues and PRs. Larger, cross-language or cross-component changes require an OpenTelemetry Enhancement Proposal (OTEP) — modeled explicitly on the Kubernetes Enhancement Proposal process (and the Rust RFC process). This is how major architectural decisions get standardized rather than forked: OTEP PR #111, for instance, is the real proposal that standardized automatic resource detection — the mechanism Part 4 covers in depth — by taking pre-existing, incompatible approaches from OpenCensus and individual language SDKs and proposing one common design. Once an OTEP is approved and integrated into the specification, the process doesn’t stop at the paper stage: issues get created in every component the proposal affects, and as each one lands, its compliance status is updated against a specific spec version — the same document-status vocabulary from the diagram above.

Diagram of how a change becomes part of the OpenTelemetry specification: small changes go directly through a GitHub issue or pull request, while large cross-language or cross-component changes require an OTEP proposal, review and approval, integration into the specification, per-component implementation issues, and a final status update against a spec version. How a Change Becomes Part of the Spec Is it a large, cross-language or cross-component change? No — small change Ordinary GitHub issue or PR Reviewed & merged Yes — large change OTEP proposal modeled on KEP / Rust RFC Reviewed & approved Integrated into the specification Issues created in every affected component Implemented per component, status updated per spec version OTEP PR #111 (resource detection) took this exact right-hand path — Part 4 covers what it standardized.

Figure: small changes merge like any GitHub PR; large cross-cutting changes go through the OTEP process before landing as per-component implementation work.

The version that matters historically: spec v1.0.0 shipped in February 2021, marking Tracing, Context, and Baggage stable. The long-term support policy is the kind of boring promise that makes boring enterprise adoption possible: no 2.x is planned, and if one ever ships, stable APIs are supported for at least three years, SDKs and contrib packages for at least one.

Four Signals, One Context

OpenTelemetry organizes telemetry into signals, each covering an observability area:

  • Traces — the path of a single transaction across services, built from spans
  • Metrics — aggregated measurements over time (the oldest signal)
  • Logs — the rawest form of telemetry (the newest addition to the spec)
  • Baggage — user-defined key-value pairs propagated alongside requests
Signal What It Captures Built From Status (API/SDK) Auto-Correlated to a Trace?
Traces The path of one transaction across services Spans, linked into a trace by trace ID Stable since spec v1.0.0 (Feb 2021) Yes — trace ID and span ID travel in context by design
Metrics Aggregated measurements over time, as time series Six instrument types (Part 7) Stable Yes, but opt-in — exemplars attach a trace ID to a data point
Logs Discrete, human-readable events LogRecords — timestamp, severity, body, attributes (Part 8) Data model and OTLP wire format are stable; the API/SDK is still Experimental — the newest signal Only if enriched — an appender or MDC context injector adds trace_id/span_id, nothing does it automatically
Baggage User-defined key-value context Key-value pairs on the W3C baggage header (Part 5) Stable since spec v1.0.0 (Feb 2021) No — must be read out explicitly; never auto-added to spans, metrics, or logs

Binding them together is a shared context subsystem that propagates data across a distributed transaction, enabling the cross-service correlation that Part 1 argued is the whole point.

One design decision deserves emphasis, because it explains a lot of the project’s stability: each signal is structured as four separate packages:

  • API — the public interfaces; application and library code depends only on this
  • Semantic conventions — standard attribute names, auto-generated from YAML in the spec repo
  • SDK — the reference implementation, with plugin hooks for exporters and processors
  • Contrib — community-maintained instrumentation and exporters

The book’s tip is pointed: minimize SDK touch points in your applications. Instrumentation that only touches the API survives SDK upgrades, backend changes, and even vendor migrations. The SDK is a deployment-time concern, not a code-time one.

Diagram of OpenTelemetry's API/SDK split: application code depends only on the stable API, the SDK sits below as a swappable implementation, and telemetry exits either through OTLP to a Collector or through direct exporters. OpenTelemetry Architecture: The API/SDK Split Your Application depends ONLY on the API OTel API (stable, backward-compatible, no-op without SDK) Traces · Metrics · Logs · Baggage · Context OTel SDK (reference implementation, chosen at deploy time) processors · samplers · exporters · resource OTLP → Collector (default) Direct exporters Jaeger, Zipkin, Prometheus… Swap the SDK, the exporter, or the backend — application code never changes.

Figure: the API/SDK split. Instrumentation couples to the API; everything below the line is a deployment decision.

The No-Op Guarantee

One subtle but crucial property: APIs return no-op implementations when no SDK is configured. This is what makes “instrument once” safe for library authors — a library can be fully instrumented, and users who never configure OpenTelemetry pay nothing and break nothing. The instrumented code simply does nothing until an SDK arrives at runtime.

The runtime guidelines flow from the same defensive philosophy: no unhandled exceptions, safe defaults when misused, low overhead, never block applications, never consume unbounded memory. A telemetry library that can take down the application it’s observing is worse than useless.

Semantic Conventions: The Naming Problem, Solved by YAML

Part 2 mentioned the naming problem (region vs cloud.region vs data_center). The solution is semantic conventions: a set of standard keys and values for describing telemetry across signals and languages. The critical implementation detail: conventions are defined in YAML files in the spec repository, and language constants are auto-generated from them. Go, Java, and Python can’t drift apart because they’re all compiled from the same source.

The most important conventions to know:

Resource Conventions

Resource conventions identify who produced the telemetry:

  • service.namerequired; unique within a namespace; identical across replicas
  • service.namespace — groups related services (team, project)
  • service.instance.id — one specific replica
  • service.version — the running version
  • Plus environment groups: cloud.*, container.*, host.*, k8s.*, process.*

If service.name is missing, the SDK defaults to unknown_service:java — a real signal you’ve misconfigured something. In Java, resource conventions ship as AttributeKey constants in a ResourceAttributes class, so identifying a producer looks like this — real code from the book, a running example (“lemonade-stand” / “juice-service”) it returns to across several chapters:

Resource resource = Resource.create(
    Attributes.of(
        ResourceAttributes.SERVICE_NAME, "juice-service",
        ResourceAttributes.SERVICE_NAMESPACE, "lemonade-stand"));

Tracing Conventions

Tracing conventions govern span attributes. One naming rule matters more than the rest: server span names must not include URL parameters. /api/customer/1763/pet/3 fragments spans into useless statistical groups. The instrumentation extracts path variables instead: /api/customer/{customer_id}/pet/{pet_id}, so every call to the same operation shares a statistically significant name. High-cardinality names kill both cost and usability. Applying a convention to the current span in Java looks like this:

Span span = Span.current();
span.setAttribute(
    SemanticAttributes.NET_PEER_NAME,
    "my-dependency.example.com");

Metric Conventions

Metric conventions share the same philosophy with an extra rule: metric names have global scope. http.client.duration means the same thing regardless of client library. Units live in the data model, not the name (http.client.duration — never duration_ms). And no pluralization unless the value is genuinely countable (errors, not error). The book’s own anti-pattern example is worth keeping in mind before you name your first custom metric: prod.my-svc.okhttp.duration bakes environment and service identity into the metric name itself, which means every environment and every service invents its own metric — exactly the fragmentation semantic conventions exist to prevent. Environment and service identity belong on the Resource, not the metric name. (Metric conventions are, at time of writing, the least mature of the three — not yet part of the opentelemetry-semconv package.)

Signals at a Glance

The book’s advice for choosing between signals is the same advice repeated throughout the series: use the right signal for the right purpose.

Signal Strength Weakness Use for
Metrics Cheap, stable, always-on Cardinality explodes with unique attributes SLIs, dashboards, alerting
Traces High granularity, transactional Expensive, sampled (biased toward errors) Debugging, root-cause analysis
Logs Universal, human-readable Unstructured, hard to correlate Startup/shutdown, legacy code, RUM
Baggage Custom correlation dimensions Not auto-added to signals; header-borne Feature flags, session IDs, A/B tests

The chapter closes with telemetry schemas — an experimental mechanism (schema_url) that lets semantic conventions evolve without breaking portability, by describing the transformations needed to convert telemetry between versions. It’s the project acknowledging something mature standards do: naming will need to change someday, and we need a plan for when it does.


Next: Practical OpenTelemetry — Part 4: Auto-Instrumentation, Zero Code Changes — the Java agent, bytecode injection, and how to get spans, metrics, and context propagation without touching application code.


References

Comments