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.chatgen_ai.completiongen_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.namealways 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=ERRORvs 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:
- Standardise the span tree shape for a run (root run span → step spans → tool/retrieval/model spans)
- Add a stable run identifier (trace-level correlation plus your own run ID)
- Add agent identity only at boundaries where it changes (root span and sub-agent spans), not on every leaf span
- 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.



