Observability

OpenTelemetry GenAI spans are the missing layer in LLM observability — standardise them before you standardise agents

Teams are converging on OpenTelemetry’s GenAI semantic conventions, but traces still don’t line up across frameworks and vendors. The quickest way to stop breaking dashboards is to standardise GenAI spans first, then layer agent semantics on top.

Your LLM traces don’t line up, even though everyone is “using OpenTelemetry GenAI semantic conventions”. The result is broken dashboards, impossible cross-team comparisons, and agent observability work that never stabilises.

The situation in 2026: convergence on words, fragmentation in traces

The OpenTelemetry community has done the right thing: define a vendor-neutral vocabulary for GenAI spans, metrics, and events. Those conventions have now moved into a dedicated repository, with its own schema URL and release cadence. (github.com)

That split matters operationally. It’s a signal that GenAI instrumentation is moving fast enough to deserve its own lifecycle, and it’s a practical hint: you should treat GenAI semantics as a layer you can upgrade independently from your base HTTP / DB / messaging tracing.

But “GenAI semantic conventions” still isn’t one stable, interoperable schema in production. The repo explicitly covers multiple flavours (provider-specific conventions, MCP conventions, framework guidance), and the docs call out that gen_ai.provider.name is a discriminator for which flavour you’re emitting. (opentelemetry.io)

In other words: the spec is converging, but your reality is that different instrumentations make different choices about span names, parent/child relationships, attribute population, and what constitutes “an LLM call” versus “an agent step”. That’s where dashboards die.

The missing layer: normalised GenAI spans that survive framework churn

If you’re deciding where to invest, the key insight is this:

Agent observability is downstream of GenAI span correctness.

Agents create more spans (planning, tool calls, retrieval, memory, sub-agents), but they don’t remove the need for consistent LLM-call spans. If those base spans aren’t standardised, anything “agent-level” becomes an overlay of guesswork.

The OpenTelemetry GenAI conventions already include opt-in message capture fields (gen_ai.system_instructions, gen_ai.input.messages, gen_ai.output.messages) and guidance that instrumentations should not capture them by default, because they can contain secrets and PII. (github.com)

That opt-in nature is a good example of why you need a dedicated normalisation layer:

  • Some frameworks emit content by default (bad for risk).
  • Some never emit content even when you opt in (bad for debugging).
  • Some emit content, but in incompatible shapes (bad for downstream queries).

When you normalise, you can keep your security posture stable while you change frameworks.

Where traces stop lining up: the mapping gaps you will hit

These are the gaps that show up when you compare real traces across multiple libraries, vendors, and teams.

1) Span naming is inconsistent, so “LLM latency” graphs drift

The GenAI repo gives overall span naming guidelines and allows system/framework conventions to specify formats. (github.com)

That flexibility is reasonable, but it means a dashboard that expects one of:

  • gen_ai.chat
  • gen_ai.completion
  • gen_ai.request
  • provider-specific names

…will break the moment someone swaps an SDK, or adds an agent framework that wraps the SDK.

Failure mode: your “P95 model latency” chart becomes a partial view of traffic, because half the spans moved under a different name.

2) gen_ai.provider.name is treated as optional, but it’s a join key

The spec frames gen_ai.provider.name as a discriminator for the telemetry flavour, and it explicitly says providers like AWS Bedrock should set it (e.g. aws.bedrock) and then use provider-specific attributes rather than openai.* ones. (opentelemetry.io)

In practice, teams often omit it, or populate it with a human label (“OpenAI”, “Bedrock”), which makes cross-service queries brittle.

Failure mode: you can’t build one query that groups token usage and cost across providers because the key is inconsistent.

3) Content capture is opt-in, but “opt-in” is implemented three different ways

The conventions mark message-bearing fields as opt-in; instrumentations should not capture them by default, and may implement content uploading in a processing pipeline. (github.com)

Some ecosystems have already standardised an env var / config flag for this (for example, Python utilities describe OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT controlling capture). (github.com)

Failure modes:

  • Debugging runs in staging are useless because content isn’t present where you expected it.
  • Production accidentally captures user prompts due to a default change.
  • Downstream tooling assumes content exists and fails (or silently produces empty “prompt” panels).

4) Parent/child relationships are the first casualty of agent frameworks

Agent frameworks often do internal concurrency (parallel tool calls, speculative decoding, multiple model calls per “step”). If the framework doesn’t propagate context correctly, you get:

  • tool spans that aren’t children of the agent step
  • LLM spans that look like roots
  • retrieval spans attached to the wrong trace

Failure mode: you can’t answer “what happened in this run?” from a single trace tree, so the on-call engineer switches back to log spelunking.

Standardise GenAI spans first: a migration plan that won’t break dashboards

Here is a plan that works when you already have dashboards in Honeycomb/Tempo/Jaeger/Datadog/etc., and you can’t afford a “flip everything” migration.

Step 1: declare your internal GenAI contract (thin, strict, versioned)

Write down, in your own repo, a small contract that you require every LLM call span to satisfy. Keep it intentionally minimal.

At minimum:

  • Span name set from a small allowed set (your choice), regardless of framework
  • gen_ai.provider.name always present and from a controlled vocabulary (opentelemetry.io)
  • Model identifier always present (whatever attribute your chosen semconv version specifies)
  • Token usage fields populated when available (or explicit “unknown” behaviour)
  • Error mapping rules (what counts as status=ERROR vs a successful response with a refusal)

Do not start by standardising “agent identity” or “tool semantics”. If you can’t trust the base LLM spans, every agent field becomes contentious.

Step 2: normalise at the edge (Collector processor), not in every service

If you have more than a handful of services, normalising in application code leads to drift. Put the mapping logic in one of:

  • an OpenTelemetry Collector processor
  • a gateway sidecar that you own

Your goal is to accept “whatever the framework emits today” and output “your internal contract”.

This also lets you run two schemas side-by-side during the migration:

  • Keep existing dashboards working (old names/attributes preserved)
  • Emit the new canonical spans/attributes in parallel

Step 3: dual-write during migration, then cut over queries

For a period (weeks, not quarters), emit both:

  • the original framework/vendor attributes (for debugging unexpected differences)
  • your canonical gen_ai.* fields and names

Then update dashboards to read the canonical fields first, with fallbacks.

Do not do this if your backend pricing or storage model makes high-cardinality attributes prohibitively expensive and you’re planning to capture message content. Content capture is opt-in for a reason; if you need it, implement sampling and redaction from day one. (github.com)

Step 4: enforce the contract with tests, not docs

Treat your GenAI span contract like an API.

  • Add an integration test that runs a representative prompt through each supported framework
  • Assert span name, required attributes, and parent/child structure
  • Fail CI if a library upgrade changes semantics

This is the only scalable way to cope with the fact that GenAI conventions are still evolving and libraries will keep changing under you.

Then add agents: what to standardise (and what not to)

The OpenTelemetry community is actively discussing agent observability as a layer above LLM calls, and there are public drafts and community posts pushing in that direction. (opentelemetry.io)

Once your GenAI spans are stable, add agent semantics carefully. The mistake is to jump straight to “agent identity on every span” without having a consistent span hierarchy.

A pragmatic ordering:

  1. Standardise the span tree shape for a run (root run span → step spans → tool/retrieval/model spans)
  2. Add a stable run identifier (trace-level correlation plus your own run ID)
  3. Add agent identity only at boundaries where it changes (root span and sub-agent spans), not on every leaf span
  4. Standardise tool call spans and retrieval spans

Why avoid agent identity everywhere?

  • It increases cardinality and cost in backends
  • It creates misleading “who did this?” answers when spans are emitted by shared libraries
  • It encourages people to encode organisational concepts (“team”, “service”, “user”) into agent labels, which will churn

The rule of thumb: identity belongs on the smallest span that actually owns the decision.

A short word on building vs buying (and where codeversols fits)

If you have one LLM integration and one service, you can probably standardise in-app and be fine.

If you have multiple services, multiple frameworks, or you’re mid-migration (OpenAI SDK → Bedrock, LangChain → something else, MCP adoption), the normalisation layer becomes real engineering work: schema versioning, collector processors, redaction, sampling, and dashboard migration.

codeversols can help if you want this done as an engineering project rather than a tooling experiment: implement the collector-side normaliser, define the contract and tests, and migrate dashboards without breaking production. We’re not an observability vendor; we build and integrate the pieces you’ll keep owning.

Close

If your team is debating “agent observability fields”, you’re probably already feeling the pain one layer below: inconsistent GenAI spans. Standardise those spans first — names, provider discriminator, hierarchy, token fields, and safe content capture — and agent semantics becomes an additive step rather than a never-ending schema argument.

More From Our Blog

Android 16 (API 36) edge-to-edge is enforced: the window insets contract your UI must satisfy
Mobile Engineering • Oct 16, 2026

Android 16 (API 36) edge-to-edge is enforced: the window insets contract your UI must satisfy

Targeting Android 16 (API 36) removes the practical opt-out from enforced edge-to-edge. The only stable fix is to define a single owner for each inset (status bars, navigation bars, display cutout, IME) and test for double-application in CI.

Read more →
TestFlight ‘Waiting for Review’ for days? Ship anyway with a dual‑lane release pipeline (and what to automate in App Store Connect)
Mobile Engineering • Oct 15, 2026

TestFlight ‘Waiting for Review’ for days? Ship anyway with a dual‑lane release pipeline (and what to automate in App Store Connect)

When TestFlight Beta App Review and App Store Review queues stall, you don’t need to miss your launch date. This playbook describes a dual‑lane pipeline (internal TestFlight vs external TestFlight vs App Store), deterministic rollout rules, and the App Store Connect automation to keep shipping under review latency.

Read more →
Swift 6 strict concurrency default isolation footguns: how teams accidentally @MainActor their whole app (and a CI check to prove it)
iOS / Swift • Oct 14, 2026

Swift 6 strict concurrency default isolation footguns: how teams accidentally @MainActor their whole app (and a CI check to prove it)

Swift 6’s strict concurrency checks get teams over the line, but one build setting can quietly turn large parts of your app into MainActor-only code. Here’s how that happens, how it shows up as UI jank, and a simple CI audit that catches “main-actor creep” early.

Read more →