OpenTelemetry GenAI Semantic Conventions Implementation Guide - Vendor-Neutral Instrumentation for LLM and Agent Applications
First Published:
Last Updated:
llm.model, another records model_name, a third stuffs the whole request body into a single attribute called payload. Six months later nobody can write a dashboard that spans two services, and switching observability backends means rewriting every query.Semantic conventions exist to stop that. OpenTelemetry's GenAI semantic conventions define the span names, attribute keys, metric instruments, and event names that instrumentation should emit for model invocations, tool executions, agent runs, retrieval, and memory operations. Get them right and a trace produced by a Python agent framework, a Java service calling a hosted model, and a self-written wrapper around a raw HTTP endpoint all line up in the same waterfall, with the same attribute keys, queryable by the same dashboard.
The problem is that the conventions are unusually hard to read right now, and for a reason that has nothing to do with their content: they moved. If you go to the page you would naturally go to, you land on a stub.
Conventions in this article were verified against the OpenTelemetry GenAI semantic conventions repository at commit
c739977ae690961f36e435504e5c1febaef1f7f3 (committed 2026-07-30) as of 2026-07-31. All attribute names, metric names, span name formats, and stability markers below were copied verbatim from that commit rather than written from memory. Every GenAI document in that repository carries Status: Development, which has a specific and consequential meaning covered in section 3.A pin is necessary rather than fussy, and the rate of change is easy to underestimate: within a day of that commit the repository had already added a
fetch_response operation, together with the gen_ai.response.status and gen_ai.request.stream_cursor attributes that go with retrieving a stored or background response. Read anything below as the state of the convention at the pinned commit, and re-read the registry against your own pin before you implement.1. Introduction
1.1 Where the conventions actually live
Until recently, the GenAI semantic conventions lived alongside every other convention inopen-telemetry/semantic-conventions, and were published at opentelemetry.io/docs/specs/semconv/gen-ai/. They no longer do. They now live in a dedicated repository, open-telemetry/semantic-conventions-genai, which was created on 2026-05-05.This matters more than a repository move usually would, because of what was left behind. As of the verification date:
- https://opentelemetry.io/docs/specs/semconv/gen-ai/ and every page under it return HTTP 200, but the body is a single notice: "GenAI semantic conventions have moved to the OpenTelemetry GenAI semantic conventions repository. This page has moved and is no longer maintained in this repository."
- The attribute registry page at https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/ still lists every
gen_ai.*key, but the stability column of each row now reads "Moved to the OpenTelemetry GenAI semantic conventions repository." rather than a stability level. Only the deprecated attribute tables on that page still carry real content. - Several official instrumentation package READMEs still link to the old docs page, so following the link from the library you are actually using can land you on the stub.
The practical consequence is that the human-readable conventions are, today, the Markdown files under
docs/gen-ai/ in the new repository, generated from the YAML models under model/. That is the source this article transcribes.1.2 Who this article is for
This is written for engineers, SREs, and platform teams who are adding instrumentation to LLM and agent applications and want traces and metrics that are portable across backends. It assumes you have used OpenTelemetry for ordinary services — you know what a span and an exporter are — but not that you have read the GenAI conventions.1.3 What this article covers, and what it does not
In scope: the structure of the conventions, the attribute and metric tables transcribed verbatim, how to shape spans for model calls, tool calls, agents and workflows, the privacy design around recording prompts and completions, the split between automatic and manual instrumentation, and how to survive convention changes.Out of scope, deliberately:
- Any specific observability product's setup. Backends appear only as parallel examples of OTLP destinations. The conventions are the subject; the backend is not.
- The AWS implementation architecture. How to wire this into a managed observability stack — collectors, log destinations, evaluation gates, IAM boundaries — is a separate topic with its own article. See LLMOps Observability and Evaluation Architecture on AWS for that side, and AWS Observability Architecture Guide for the general tracing and metrics substrate. This article stays on the convention.
- Evaluation methodology. The conventions define an event for carrying evaluation results (section 4.4), but what to evaluate and how to decide an output is acceptable is a different problem. See LLM Output Verification Patterns and Amazon Bedrock AgentCore Evaluations Practical Guide.
- Numbers I did not measure. No pricing, no benchmark scores, and no instrumentation-overhead figures appear anywhere below. Where the conventions themselves specify numeric values — histogram bucket boundaries, for example — those are quoted as specification content, not as measurements.
1.4 How to read the tables
Every attribute table in this article reproduces two things from the specification exactly: the key and its requirement level. A single character wrong in a key means your telemetry silently fails to match any convention-aware dashboard, so the keys are the part worth being pedantic about. Requirement levels (Required, Conditionally Required, Recommended, Opt-In) are defined in a stable core document and are explained in section 2.4.2. OpenTelemetry Refresher for GenAI Engineers
You do not need deep OpenTelemetry expertise to apply the GenAI conventions, but four concepts decide most of the design questions that come up. If these are already familiar, skip to section 3.2.1 Signals: traces, metrics, logs, and events
OpenTelemetry emits three signal types, and the GenAI conventions use all three.Traces are trees of spans. A span covers one logical operation with a start time, an end time, a status, and a bag of key/value attributes. The GenAI conventions define span shapes for model inference, embeddings, retrieval, memory operations, tool execution, agent invocation, workflow invocation, and planning.
Metrics are pre-aggregated numeric series. The GenAI conventions define histograms for token usage, operation duration, streaming timing, agent invocation duration, per-invocation call counts, and tool execution duration. Metrics are cheap to keep at full fidelity; traces usually are not, because they get sampled. Anything you need to be exactly right in aggregate belongs in a metric.
Logs and events. An event is a log record with a well-known name and a defined attribute set. The GenAI conventions define three:
gen_ai.client.inference.operation.details, gen_ai.evaluation.result, and gen_ai.client.operation.exception. Events exist mainly to carry payloads that are too large or too sensitive to belong on a span, and to carry structured values in languages where span attributes cannot yet hold complex types.That last point is a real constraint the specification calls out: recording structured attributes is supported on events and logs, but may not yet be supported on spans in a given language. Where structured span attributes are unavailable, the conventions say the value SHOULD be serialized to a JSON string on the span and recorded in structured form on the event.
2.2 Resource attributes versus span attributes
Resource attributes describe the producer of telemetry — the service name, the deployment environment, the host — and are attached once to everything a process emits. Span attributes describe a single operation.None of the
gen_ai.* attributes are resource attributes. Model names, token counts, and agent names all vary per call and belong on spans, metrics, and events. A common early mistake is to set the model name as a resource attribute because "this service only uses one model", which stops being true the first time someone adds a fallback model.2.3 Schema URLs, and why the GenAI ones are not usable yet
OpenTelemetry's Telemetry Schemas specification — itself Stable — defines a mechanism for decoupling producers from consumers: telemetry carries a schema URL identifying which version of the conventions it follows, and a published schema file describes the transformations between versions, so a collector or backend can rewrite old telemetry into new attribute names automatically.For the GenAI conventions this mechanism is not yet usable. The repository README lists the Schema URL as
TODO. The registry manifest at model/manifest.yaml declares stability: development and a development schema URL of https://opentelemetry.io/schemas/gen-ai-dev/1.42.0-dev, but no schema file is served at that address as of the verification date (the core conventions' equivalent, https://opentelemetry.io/schemas/1.43.0, does resolve). Plan migrations manually for now; section 11 covers how.2.4 Requirement levels
The four requirement levels come from a Stable core document, Attribute requirement levels, and they are the fastest way to read any convention table.* You can sort the table by clicking on the column name.
| Requirement level | Included by default | Can be added by config | Can be removed by config | Reading |
|---|---|---|---|---|
Required | Yes | n/a | No | Instrumentation MUST populate it. Consumers may use its presence to detect that a span follows the convention |
Conditionally Required | Yes, when the stated condition holds | No | No | The convention MUST state the condition. Absent condition, absent attribute |
Recommended | Yes | No | Yes | Populate unless you have a specific reason (cardinality, cost, privacy) not to |
Opt-In | No | Yes | Yes | Off unless the operator explicitly turns it on. Every attribute that can hold prompt or output content is here |
The
Required row has a useful consequence: because gen_ai.operation.name is Required on every GenAI span, its presence is a reliable detector that a span is a GenAI span at all. Backends use this, and so can your own queries.The
Opt-In row is the entire privacy design of these conventions in one line, and section 8 unpacks it.3. Stability Levels and Why They Matter Here
3.1 The maturity ladder
OpenTelemetry documents carry a status drawn from a project-wide maturity ladder defined in Definitions of Document Statuses. The relevant rungs, condensed from the official definitions:| Status | What the project guarantees |
|---|---|
| Development | Not all pieces are in place; the component SHOULD NOT be used in production; configuration options might break often; the component MAY be removed without prior notice |
| Alpha | Usable for limited non-critical production workloads; interfaces and generated telemetry might change without backward-compatibility guarantees |
| Beta | As Alpha, but the interfaces are treated as stable whenever possible; breaking changes minimized |
| Release Candidate | Feature-complete; breaking changes only under special circumstances, with prior notice where possible |
| Stable | Generally available; breaking changes only under special circumstances |
| Deprecated | Development halted; may be removed from distributions; must communicate the removal version |
3.2 Where the GenAI conventions sit
Every document in the GenAI conventions repository — spans, agent spans, metrics, events, exceptions, MCP, and each provider-specific document — is markedStatus: Development. So is essentially every individual gen_ai.* attribute, which carries a per-attribute Development badge in the generated tables.There are two important exceptions worth internalizing, because they are the parts of your GenAI telemetry that are actually safe:
- Attributes borrowed from the core conventions are Stable.
error.type,server.address,server.port,exception.type,exception.message, andexception.stacktraceall come fromopen-telemetry/semantic-conventionsv1.43.0 (released 2026-07-03), which the GenAI repository pins viaSEMCONV_VERSIONinversions.env. These carry Stable badges in the GenAI tables. - The framework around the conventions is Stable. Requirement levels, the Telemetry Schemas mechanism, OTLP, and the trace and metrics APIs themselves are stable. It is the GenAI vocabulary that is in flux, not the machinery carrying it.
There is also a formal signal that the conventions have not yet reached a release: the repository has no tagged releases, and
CHANGELOG.md contains only an ## Unreleased heading. The reference point you can cite is a commit, not a version.3.3 Translating "SHOULD NOT be used in production" into a design
Read literally, Development status says do not use this in production. Read usefully, it says: the vocabulary will change under you, so build as if it will. Teams are shipping GenAI-instrumented services today because the alternative — inventing a private vocabulary — is strictly worse. What the status changes is not whether you adopt, but how.Four practices follow from it, each of which gets its own treatment later:
- Pin and record what you built against. Instrumentation library version, and the convention commit it targeted. Without this you cannot reason about a discrepancy six months later.
- Do not let raw attribute keys reach human-facing artifacts. Dashboards and alerts should read from a thin mapping layer, so a rename is one edit rather than fifty (section 11.4).
- Expect renames, retypes, and removals — not just additions. Section 11.3 lists breaking changes that have already landed in this repository.
- Keep custom attributes in your own namespace. Never invent a
gen_ai.*key of your own; a future release may define the same key with different semantics (section 12.1).
4. The GenAI Attribute Model
4.1 The unit of instrumentation
The conventions do not model "an LLM call" as one thing. They model eight operation kinds, and the first design decision in any instrumentation is which one a given piece of code is.| Operation kind | Convention document | Typical emitter |
|---|---|---|
| Model inference | gen-ai-spans.md — Inference | Client SDK wrapping a chat or completion API |
| Embeddings | gen-ai-spans.md — Embeddings | Client SDK wrapping an embeddings API |
| Retrieval | gen-ai-spans.md — Retrievals | Vector store or search client, RAG framework |
| Memory | gen-ai-spans.md — Memory | Agent memory store client |
| Tool execution | gen-ai-spans.md — Execute tool span | Agent framework, or your own tool dispatcher |
| Agent creation and invocation | gen-ai-agent-spans.md | Agent framework, or a hosted agent service client |
| Workflow invocation | gen-ai-agent-spans.md — Invoke workflow span | Multi-agent orchestrator |
| Planning | gen-ai-agent-spans.md — Plan span | Agent framework with an explicit planning phase |
4.2 gen_ai.operation.name — the discriminator
gen_ai.operation.name is Required on every GenAI span and carries the operation kind. Its well-known values, transcribed verbatim, are:| Value | Meaning per the specification |
|---|---|
chat | Chat completion operation |
create_agent | Create GenAI agent |
create_memory | Create new memory records |
create_memory_store | Create or initialize a memory store |
delete_memory | Delete memory records |
delete_memory_store | Delete or deprovision a memory store |
embeddings | Embeddings operation |
execute_tool | Execute a tool |
generate_content | Multimodal content generation operation |
invoke_agent | Invoke GenAI agent |
invoke_workflow | Invoke GenAI workflow |
plan | Agent planning or task decomposition phase |
retrieval | Retrieval operation |
search_memory | Search/query memories from a memory store |
text_completion | Text completions operation |
update_memory | Update existing memory records |
upsert_memory | Create or update memory records without the caller choosing which |
All seventeen values are marked Development. The convention states that if one of the well-known values applies, that value MUST be used; otherwise a custom value MAY be used.
4.3 gen_ai.provider.name — the flavor discriminator
gen_ai.provider.name identifies the provider as seen by the instrumentation. Its role is subtler than it looks: the specification describes it as "a discriminator that identifies the GenAI telemetry format flavor specific to that provider", which is why it should be set consistently with provider-specific attributes. Telemetry with gen_ai.provider.name set to aws.bedrock is expected to carry aws.bedrock.* attributes and not openai.* ones.The well-known values, verbatim:
anthropic, aws.bedrock, azure.ai.inference, azure.ai.openai, cohere, deepseek, gcp.gemini, gcp.gen_ai, gcp.vertex_ai, groq, ibm.watsonx.ai, mistral_ai, moonshot_ai, openai, perplexity, x_ai.Two of the Google values are distinguished by endpoint rather than by product:
gcp.gemini is used when accessing the generativelanguage.googleapis.com endpoint, gcp.vertex_ai when accessing aiplatform.googleapis.com, and gcp.gen_ai may be used when the specific backend is unknown.The specification also acknowledges the proxy case explicitly: the attribute SHOULD be set based on the instrumentation's best knowledge and may differ from the actual upstream provider, because a client SDK may be pointed at a gateway that relays elsewhere. If you run an internal LLM gateway, your telemetry will say what the SDK believes, not what the gateway did.
4.4 The attribute namespaces
The sixty-one attributes currently in thegen_ai registry fall into a small number of namespaces, and knowing the namespaces is enough to guess where anything lives.| Namespace | Purpose | Representative keys |
|---|---|---|
gen_ai.operation.*, gen_ai.provider.* | Discriminators | gen_ai.operation.name, gen_ai.provider.name |
gen_ai.request.* | What was asked for | gen_ai.request.model, gen_ai.request.temperature, gen_ai.request.max_tokens, gen_ai.request.top_p, gen_ai.request.top_k, gen_ai.request.stream, gen_ai.request.seed, gen_ai.request.reasoning.level |
gen_ai.response.* | What came back | gen_ai.response.model, gen_ai.response.id, gen_ai.response.finish_reasons, gen_ai.response.time_to_first_chunk |
gen_ai.usage.* | Token accounting | gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, gen_ai.usage.reasoning.output_tokens, gen_ai.usage.cache_read.input_tokens, gen_ai.usage.cache_creation.input_tokens |
gen_ai.agent.*, gen_ai.workflow.* | Agent and orchestration identity | gen_ai.agent.id, gen_ai.agent.name, gen_ai.agent.version, gen_ai.agent.description, gen_ai.workflow.name |
gen_ai.tool.* | Tool identity, definitions, and payloads | gen_ai.tool.name, gen_ai.tool.type, gen_ai.tool.description, gen_ai.tool.call.id, gen_ai.tool.definitions, gen_ai.tool.call.arguments, gen_ai.tool.call.result |
gen_ai.prompt.* | Prompt template identity | gen_ai.prompt.name, gen_ai.prompt.version, gen_ai.prompt.variable |
gen_ai.conversation.* | Session correlation | gen_ai.conversation.id, gen_ai.conversation.compacted |
gen_ai.retrieval.*, gen_ai.memory.*, gen_ai.data_source.* | RAG and memory | gen_ai.retrieval.top_k, gen_ai.retrieval.documents, gen_ai.memory.store.id, gen_ai.memory.record.count, gen_ai.data_source.id |
gen_ai.evaluation.* | Evaluation results | gen_ai.evaluation.name, gen_ai.evaluation.score.value, gen_ai.evaluation.score.label, gen_ai.evaluation.explanation |
gen_ai.embeddings.* | Embedding shape | gen_ai.embeddings.dimension.count |
gen_ai.input.*, gen_ai.output.*, gen_ai.system_instructions | Content | gen_ai.input.messages, gen_ai.output.messages, gen_ai.output.type, gen_ai.system_instructions |
gen_ai.token.* | Metric dimension only | gen_ai.token.type (values input, output) |
The evaluation namespace deserves a note, because it is where verification work meets telemetry. The
gen_ai.evaluation.result event carries gen_ai.evaluation.name (Required), gen_ai.evaluation.score.value and gen_ai.evaluation.score.label (both Conditionally Required if applicable), and gen_ai.evaluation.explanation (Recommended). The event SHOULD be parented to the GenAI operation span being evaluated when possible, or carry gen_ai.response.id when the span ID is not available. The specification also asks that the score label stay low cardinality and that implementations document the possible values, since a numeric score means nothing without knowing the evaluator's scale.4.5 The prompt template attributes are worth adopting early
Three attributes get less attention than they deserve:gen_ai.prompt.name, gen_ai.prompt.version, and gen_ai.prompt.variable. The first two are Conditionally Required when a named prompt template is used and a version is available. Together they let you answer "did quality change when we shipped prompt v3?" from telemetry alone, without correlating deploy logs by timestamp.The version string may follow any scheme the application chooses — the specification gives
1.0.0, 2025-05-01, prod, and v2 as examples — and where a prompt management system is in use, it SHOULD match that system's identifier. If you already run prompt versioning (see Prompt Lifecycle Management on AWS for one implementation of that discipline), wiring these two attributes is a small change with disproportionate payoff.gen_ai.prompt.variable is different: it is Opt-In, and it is templated. A variable named user_name with value Alice is recorded as the attribute gen_ai.prompt.variable.user_name with value "Alice". Because template variables usually carry user data, the specification flags it as potentially sensitive.
invoke_agent span parents a model call, a tool execution, and a second model call, with the representative attributes each span carries. Section 6.3 walks through why the tree has that shape.5. Instrumenting Model Invocations
5.1 Span name, kind, and status
The inference span is the one you will emit most, and its shape is fixed by three rules:- Span name SHOULD be
{gen_ai.operation.name} {gen_ai.request.model}— for example,chat gpt-4. Conventions for individual systems MAY specify a different format but MUST follow the general span naming guidelines. Note what this excludes: no conversation ID, no user ID, no request ID in the span name. Span names are a metric dimension in most backends, so they must stay low cardinality. - Span kind SHOULD be
CLIENT, and MAY beINTERNALfor models running in the same process.CLIENTis recommended when the GenAI system usually runs in a different process, or when the call happens over an instrumented protocol such as HTTP. - Span status SHOULD follow the core Recording Errors document. In practice: leave it unset on success, set
ERRORwitherror.typeon failure.
One more rule shapes the span boundary: if a transient issue caused an automatic retry, the span SHOULD cover the whole logical operation including all retries. A span is the logical call, not the wire call.
5.2 Attributes on the inference span
Transcribed from the specification, grouped by requirement level.* You can sort the table by clicking on the column name.
| Attribute key | Requirement level | Type | Note |
|---|---|---|---|
gen_ai.operation.name | Required | string | The discriminator (section 4.2) |
gen_ai.provider.name | Required | string | Provider flavor (section 4.3) |
error.type | Conditionally Required if the operation ended in an error | string | Stable, from core conventions |
gen_ai.conversation.id | Conditionally Required when available | string | See the caution below |
gen_ai.output.type | Conditionally Required when applicable and if the request includes an output format | string | text, json, image, speech |
gen_ai.prompt.name | Conditionally Required when a named prompt template is used | string | |
gen_ai.prompt.version | Conditionally Required when gen_ai.prompt.name is set and a version is available | string | |
gen_ai.request.choice.count | Conditionally Required if available, in the request, and !=1 | int | |
gen_ai.request.model | Conditionally Required if available | string | Exact vendor model name |
gen_ai.request.seed | Conditionally Required if applicable and if the request includes a seed | int | |
gen_ai.request.stream | Conditionally Required if and only if the request is streaming | boolean | Unset means non-streaming |
gen_ai.request.top_k | Conditionally Required if applicable | int | Decoding parameter only — see caution below |
server.port | Conditionally Required if server.address is set | int | Stable |
gen_ai.conversation.compacted | Recommended when available | boolean | Set only to true, never to false |
gen_ai.request.frequency_penalty | Recommended | double | |
gen_ai.request.max_tokens | Recommended | int | |
gen_ai.request.presence_penalty | Recommended | double | |
gen_ai.request.previous_response.id | Recommended when available and if the request references a previous response | string | |
gen_ai.request.reasoning.level | Recommended when applicable | string | Exact string sent to the provider |
gen_ai.request.stop_sequences | Recommended | string[] | |
gen_ai.request.temperature | Recommended | double | |
gen_ai.request.top_p | Recommended | double | |
gen_ai.response.finish_reasons | Recommended | string[] | One entry per generation |
gen_ai.response.id | Recommended | string | |
gen_ai.response.model | Recommended | string | Model that actually answered |
gen_ai.response.time_to_first_chunk | Recommended if the request was a streaming request | double | Seconds |
gen_ai.usage.cache_creation.input_tokens | Recommended | int | Included in gen_ai.usage.input_tokens |
gen_ai.usage.cache_read.input_tokens | Recommended | int | Included in gen_ai.usage.input_tokens |
gen_ai.usage.input_tokens | Recommended | int | Includes cached tokens |
gen_ai.usage.output_tokens | Recommended | int | |
gen_ai.usage.reasoning.output_tokens | Recommended when applicable | int | Included in gen_ai.usage.output_tokens |
server.address | Recommended | string | Stable |
gen_ai.input.messages | Opt-In | any | Sensitive — section 8 |
gen_ai.output.messages | Opt-In | any | Sensitive — section 8 |
gen_ai.prompt.variable | Opt-In | string | Templated key; sensitive |
gen_ai.system_instructions | Opt-In | any | Sensitive — section 8 |
gen_ai.tool.definitions | Opt-In | any | Can be large |
Three cautions that are easy to get wrong.
gen_ai.conversation.id must not be synthesized. The specification is explicit: when no identifier for the conversation is available, instrumentations SHOULD NOT populate it, and a new UUID, a trace identifier, or a hash of request content SHOULD NOT be used as a fallback. If your framework does not have a real conversation identity, leave the attribute off. A synthetic per-request "conversation ID" is worse than nothing, because it silently breaks every conversation-level query while looking populated.
gen_ai.request.top_k is not top_logprobs. The specification names this trap directly:
top_k is a decoding/sampling parameter (Anthropic top_k, Cohere k, Google topK); OpenAI's top_logprobs controls how many per-token log-probabilities are returned and does not change generation, so it MUST NOT be reported as gen_ai.request.top_k.The cache and reasoning token counts are subsets, not addends.
gen_ai.usage.cache_read.input_tokens and gen_ai.usage.cache_creation.input_tokens are already included in gen_ai.usage.input_tokens; gen_ai.usage.reasoning.output_tokens is already included in gen_ai.usage.output_tokens. Summing them double-counts.5.3 Attributes that must be set at span creation
The conventions single out a set of attributes that SHOULD be provided at span creation time on the inference span, because samplers only see attributes that exist when the sampling decision is made:gen_ai.operation.name, gen_ai.provider.name, gen_ai.request.model, server.address, server.port.If your instrumentation creates the span, calls the model, and then sets the model name, a sampler that wanted to keep all spans for one model cannot do it. This is the single most common structural bug in hand-written GenAI instrumentation.
5.4 Streaming
Streaming is handled by three coordinated pieces rather than a separate span type:gen_ai.request.streamis set totrueif and only if the request is streaming. Unset means non-streaming.gen_ai.response.time_to_first_chunkrecords, in seconds, the time from request issuance to the first chunk arriving in the response stream.- The span still covers the whole operation — it ends when the response is fully received, not when the first chunk arrives.
Two client metrics complement these for aggregate views:
gen_ai.client.operation.time_to_first_chunk and gen_ai.client.operation.time_per_output_chunk (section 7). Note the honest naming: from the client side, the conventions count chunks, not tokens, because a client cannot generally see token boundaries. Only the server-side metrics use token wording.The "Streaming chunks" section of the spans document is currently a
TODO heading with no content — one of the concrete places where Development status is visible.5.5 Errors
Errors are recorded in two complementary ways.On the span, set
error.type — a Stable, low-cardinality classifier that SHOULD match the provider's or client library's error code, the canonical exception name, or another low-cardinality identifier. The only well-known value is _OTHER, a fallback for when instrumentation does not define a custom value. Instrumentations SHOULD document which errors they report.As an event,
gen_ai.client.operation.exception carries the detail: exception.message and exception.type (each Conditionally Required — required if the other is not set) and exception.stacktrace (Recommended). All three are Stable core attributes. Instrumentations SHOULD set the severity to WARN (severity number 13) when recording this event, and MAY offer an option to copy the corresponding span's GenAI attributes onto it.Note that
exception.message is flagged as potentially containing sensitive information — provider error messages sometimes echo request content back.6. Instrumenting Tool Calls and Agents
6.1 The five span shapes above the model call
Agent instrumentation is where teams diverge most, so it is worth laying the shapes side by side. All are defined ingen-ai-agent-spans.md except the tool span, which is shared with gen-ai-spans.md.* You can sort the table by clicking on the column name.
| Span | gen_ai.operation.name | Span name format | Span kind | Use when |
|---|---|---|---|---|
| Create agent | create_agent | create_agent {gen_ai.agent.name} | CLIENT | Creating an agent resource, usually on a remote agent service |
| Invoke agent (client) | invoke_agent | invoke_agent {gen_ai.agent.name}, or invoke_agent if the name is unavailable | CLIENT | Invoking an agent over a remote service |
| Invoke agent (internal) | invoke_agent | Same as above | INTERNAL | Invoking an agent running in the same process |
| Invoke workflow | invoke_workflow | invoke_workflow {gen_ai.workflow.name} | INTERNAL | An orchestration that coordinates multiple agents |
| Plan | plan | plan {gen_ai.agent.name}, or plan if the name is unavailable | INTERNAL | An explicit planning or task-decomposition phase |
| Execute tool | execute_tool | execute_tool {gen_ai.tool.name} | INTERNAL | Any tool execution |
The client/internal split on
invoke_agent is the distinction most worth getting right. A hosted agent service you call over the network is a CLIENT span, and carries provider and server attributes; an agent object running inside your own process is an INTERNAL span, and does not.That difference has been sharpened by recent changes:
gen_ai.provider.name was removed as a required attribute from the internal invoke_agent span, and gen_ai.agent.id and gen_ai.agent.version were removed from it as well. The reasoning for gen_ai.agent.id is stated in the specification: for hosted agents it SHOULD be the provider-assigned stable identifier — an agent resource ARN, a registry identifier — and it is NOT RECOMMENDED to record in-memory agent instance IDs there, because those are transient. An in-process agent has no stable ID to record, so the attribute does not belong on that span.6.2 The execute tool span
The tool span is small and worth memorizing.| Attribute key | Requirement level | Note |
|---|---|---|
gen_ai.operation.name | Required | execute_tool |
gen_ai.tool.name | Required | |
error.type | Conditionally Required if the operation ended in an error | Stable |
gen_ai.agent.name | Conditionally Required when applicable | The agent executing the tool |
gen_ai.tool.call.id | Recommended if available | Correlates with the model's tool call |
gen_ai.tool.description | Recommended if available | |
gen_ai.tool.type | Recommended if available | function, extension, datastore |
gen_ai.tool.call.arguments | Opt-In | Sensitive |
gen_ai.tool.call.result | Opt-In | Sensitive |
gen_ai.tool.type has three defined meanings that are easy to mix up. function is executed client-side: the model generates parameters and the client executes the logic. extension is executed agent-side, calling external APIs directly from within the agent's controlled environment. datastore is used for accessing and querying external data for retrieval-augmented tasks or knowledge updates.The sampling-relevant attributes for this span — to be set at creation time — are
gen_ai.operation.name, gen_ai.tool.name, and gen_ai.tool.type.The conventions also address ownership explicitly: GenAI instrumentations that can instrument tool execution SHOULD do so unless another instrumentation reliably covers all supported tool types, and application developers are encouraged to follow the convention for tools invoked by their own code and to manually instrument tool calls that automatic instrumentations do not cover. In practice, most teams have some tools dispatched by the framework and some dispatched by their own code; only the first gets instrumented for free.
6.3 Building the span tree
A single agent turn typically produces the shape in the figure in section 4:invoke_agent {agent}(INTERNAL) opens as the root of the agent's work.chat {model}(CLIENT) runs as its child. The model returns a tool call.execute_tool {tool}(INTERNAL) runs as a sibling of the firstchatspan, under the sameinvoke_agentparent.chat {model}(CLIENT) runs again with the tool result appended, producing the final answer.
Three properties of that shape are worth stating explicitly, because they are the ones that break.
Tool spans are children of the agent, not of the model call. The model span ends when the model returns its tool call request; tool execution happens afterwards, in your code. Nesting the tool span inside the model span misrepresents the timeline and inflates the model's apparent latency.
The step boundary is whatever your framework can reliably bound. The conventions are pragmatic here:
gen_ai.invoke_agent.duration is intended for instrumentations that can reliably bound a single agent invocation; if you can only measure a single provider-facing call, use gen_ai.client.operation.duration instead; if you can bound a higher-level orchestration, use gen_ai.workflow.duration for it. Emitting several of these for the same request path is allowed when more than one boundary is genuinely available. Do not invent a boundary you cannot measure.When
gen_ai.invoke_agent.duration accompanies an internal invoke_agent span, the metric value SHOULD equal the span duration. If your metric and your span disagree, one of them is instrumented at the wrong boundary.6.4 Tool calls that cross the Model Context Protocol
When tools are served over the Model Context Protocol, a separate set of conventions inmcp.md applies, and the two must be reconciled rather than stacked.MCP defines four attributes of its own —
mcp.method.name, mcp.protocol.version, mcp.resource.uri, mcp.session.id — and reuses gen_ai.tool.name, gen_ai.prompt.name, jsonrpc.request.id, rpc.response.status_code, and the network and server attributes. The MCP client span name SHOULD follow {mcp.method.name} {target}, where target SHOULD match {gen_ai.tool.name} or {gen_ai.prompt.name} when applicable, falling back to {mcp.method.name} alone when no low-cardinality target exists. Instrumentation MAY let users opt into including {mcp.resource.uri} as the target but SHOULD NOT do so by default, to avoid high-cardinality span names.Avoiding double-counted tool spans. The specification handles the overlap directly: if the MCP instrumentation can reliably detect that outer GenAI instrumentation is already tracing the tool execution, it SHOULD NOT create a separate span, and SHOULD instead add MCP-specific attributes to the existing tool execution span. Instrumentations that support this MAY offer a configuration option to enable it. If you see every MCP tool call appearing twice in your traces, this is why — check whether that option exists in your stack before writing a span processor to drop duplicates.
Context propagation is not automatic over MCP. HTTP trace context propagation covers the HTTP request but not the individual messages exchanged within request/response streams, so instrumentations SHOULD propagate context by injecting it into the MCP request's
params._meta property bag, and the receiver extracts it as the remote parent. The keys are written unprefixed — traceparent, tracestate, and baggage — even though MCP _meta keys are otherwise expected to be DNS-prefixed. MCP server instrumentation SHOULD by default use the context extracted from params._meta as the parent of the MCP server span, and SHOULD link the current ambient context if present.A tool call request with injected W3C Trace Context looks like this:
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "get-weather",
"_meta": {
"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
"tracestate": "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"
}
},
"id": 1
}
The transport nuance behind that design: MCP and its underlying transport contexts are independent. One MCP request can be served by multiple HTTP requests (retries), and one streamable HTTP request can serve more than one MCP message. So the MCP client span parents the MCP server span regardless of transport, and span links record the transport context. For background on MCP versions themselves, see Model Context Protocol Specification Version Timeline; for exercising an MCP server under test, MCP Server Testing and Debugging Guide.
7. Metrics
7.1 The full instrument list
Every GenAI metric currently defined is a Histogram. Transcribed verbatim:* You can sort the table by clicking on the column name.
| Metric name | Instrument | Unit (UCUM) | Meaning |
|---|---|---|---|
gen_ai.client.token.usage | Histogram | {token} | Number of input and output tokens used |
gen_ai.client.operation.duration | Histogram | s | GenAI operation duration |
gen_ai.client.operation.time_to_first_chunk | Histogram | s | Time from request issuance to the first chunk received in the response stream |
gen_ai.client.operation.time_per_output_chunk | Histogram | s | Time per output chunk, recorded for each chunk after the first |
gen_ai.server.request.duration | Histogram | s | Model server request duration, such as time-to-last-byte or last output token |
gen_ai.server.time_to_first_token | Histogram | s | Time to generate the first token, for successful responses |
gen_ai.server.time_per_output_token | Histogram | s | Time per output token generated after the first, for successful responses |
gen_ai.workflow.duration | Histogram | s | GenAI workflow duration |
gen_ai.invoke_agent.duration | Histogram | s | End-to-end duration of a single in-process agent invocation |
gen_ai.invoke_agent.inference_calls | Histogram | {inference_call} | Number of inference (model) calls an agent makes during a single invocation |
gen_ai.invoke_agent.tool_calls | Histogram | {tool_call} | Number of tool calls an agent makes during a single invocation |
gen_ai.execute_tool.duration | Histogram | s | Duration of a single tool execution |
The specification attaches a disclaimer to the client metrics section: these are initial instruments and attributes, and more may be added.
7.2 Client versus server, and why both exist
Thegen_ai.client.* and gen_ai.server.* families measure the same phenomenon from opposite ends, and the naming difference is not cosmetic.Client metrics are emitted by the caller and include network time, queueing at the provider, and everything else between your process and the model. Server metrics are emitted by a model server — if you run your own inference server — and measure only what happens inside it. The client family counts chunks where the server family counts tokens, for the reason given in section 5.4: a client cannot see token boundaries, a server can.
If you both call and host models, expect the two families side by side, and expect client duration to exceed server duration by the network and queueing time.
7.3 The two agent counters
gen_ai.invoke_agent.inference_calls and gen_ai.invoke_agent.tool_calls are the least familiar instruments here and the most useful for agent reliability work. Each is a Histogram scoped to a single agent invocation — so the distribution answers "how many model calls does one turn take?" rather than "how many model calls happened this minute."That distribution is the shape of your agent's behavior. A long tail on
inference_calls is a loop that does not terminate cleanly. A bimodal tool_calls distribution usually means two distinct user intents are being served by one agent. Neither is visible in a simple counter. Both metrics are recommended for instrumentations of agent frameworks, and the specification says each SHOULD be emitted together with the other.7.4 Aggregation dimensions and cardinality
The attribute sets on the metrics are deliberately much smaller than on the corresponding spans, and the reason is cardinality: metric attributes become time series, and time series multiply.| Metric | Required attributes | Other dimensions |
|---|---|---|
gen_ai.client.token.usage | gen_ai.operation.name, gen_ai.provider.name, gen_ai.token.type | gen_ai.request.model, gen_ai.response.model, server.address, server.port |
gen_ai.client.operation.duration | gen_ai.operation.name | error.type, gen_ai.provider.name, gen_ai.request.model, gen_ai.response.model, server.address, server.port |
gen_ai.server.* | gen_ai.operation.name, gen_ai.provider.name | error.type (on request duration), model and server attributes |
gen_ai.workflow.duration | — | error.type, gen_ai.workflow.name |
gen_ai.invoke_agent.duration | — | error.type, gen_ai.agent.name, gen_ai.request.model |
gen_ai.invoke_agent.inference_calls / .tool_calls | — | gen_ai.agent.name |
gen_ai.execute_tool.duration | gen_ai.tool.name | error.type, gen_ai.agent.name, gen_ai.tool.type |
gen_ai.token.type — with values input and output — is a metric-only dimension. It exists so one histogram carries both token directions rather than requiring two instruments.Note what is absent from every one of these lists:
gen_ai.conversation.id, gen_ai.response.id, gen_ai.prompt.variable.*, and every content attribute. Adding a per-conversation or per-request identifier as a metric dimension creates one time series per conversation. Section 12.2 covers what that does to a metrics backend.There is one nuance on the model dimension of
gen_ai.invoke_agent.duration: gen_ai.request.model SHOULD be populated if and only if the instrumented library allows setting only a single model per agent, and SHOULD NOT be populated for agents that support multiple models or dynamic selection. An agent that switches models mid-run has no single model to attribute its duration to.7.5 Bucket boundaries
Because these are Histograms, bucket boundaries determine what your percentiles can actually resolve. The specification supplies explicit advisory boundaries via theExplicitBucketBoundaries instrument advisory parameter, and the values differ meaningfully by instrument:| Instrument family | Advised boundaries |
|---|---|
gen_ai.client.token.usage | 1, 4, 16, 64, 256, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864 |
Client and server operation duration, chunk timing, gen_ai.execute_tool.duration | 0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28, 2.56, 5.12, 10.24, 20.48, 40.96, 81.92 |
gen_ai.server.time_per_output_token | 0.01, 0.025, 0.05, 0.075, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.75, 1.0, 2.5 |
gen_ai.server.time_to_first_token | 0.001, 0.005, 0.01, 0.02, 0.04, 0.06, 0.08, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0 |
gen_ai.invoke_agent.duration | 0.1, 0.2, 0.4, 0.8, 1.6, 3.2, 6.4, 12.8, 25.6, 51.2, 102.4, 204.8, 409.6 |
gen_ai.invoke_agent.inference_calls, gen_ai.invoke_agent.tool_calls | 1, 2, 4, 8, 16, 32, 64, 128 |
gen_ai.workflow.duration | 1, 5, 10, 30, 60, 120, 300, 600, 1800, 3600, 7200 |
The pattern is deliberate, and it runs in both directions. Agent invocations start an order of magnitude coarser than single model calls, and workflows an order of magnitude coarser again, because a workflow that runs for an hour is normal and a chat call that runs for an hour is not. In the other direction, the two server-side token-timing histograms are the finest-grained instruments in the set —
gen_ai.server.time_to_first_token starts at a millisecond and tops out at ten seconds — because a per-token interval is measured on a scale where the whole-operation boundaries would resolve nothing. Note in particular that these two do not share the whole-operation boundaries, even though their names sit next to the client chunk-timing metrics that do. If you accept your SDK's default boundaries instead, long-running agent percentiles collapse into the top bucket and per-token intervals collapse into the bottom one, and both stop being informative.For the model-call side of latency — what actually drives time-to-first-token and throughput, and which levers change it — see Amazon Bedrock Inference Throughput and Latency Optimization.
8. Recording Prompts and Completions
8.1 The default is "do not record"
This is the part of the conventions with the largest blast radius if you get it wrong, and the specification's position is unambiguous: model instructions, user messages, and model outputs are considered sensitive and are often large. OpenTelemetry instrumentations SHOULD NOT capture them by default, but SHOULD provide an option for users to opt in.That position is enforced structurally, not just by advice. Every content-bearing attribute is
Opt-In, the one requirement level that is not included by default and can be added only through configuration — and instrumentation that does not support configuration "MUST NOT populate Opt-In attributes". There is no path where content lands in your telemetry because someone forgot a flag.The content attributes, with the specification's own warnings:
| Attribute | Where it appears | Specification warning |
|---|---|---|
gen_ai.input.messages | Inference and agent spans, inference details event | "likely to contain sensitive information including user/PII data" |
gen_ai.output.messages | Inference and agent spans, inference details event | "likely to contain sensitive information including user/PII data" |
gen_ai.system_instructions | Inference, agent, and create-agent spans | "may contain sensitive information" |
gen_ai.tool.call.arguments | Execute tool span, MCP client and server spans | "may contain sensitive information" |
gen_ai.tool.call.result | Execute tool span, MCP client and server spans | "may contain sensitive information" |
gen_ai.prompt.variable | Inference span, MCP client span | "may contain sensitive information" |
gen_ai.retrieval.query.text, gen_ai.retrieval.documents | Retrieval span | Opt-In |
gen_ai.memory.query.text, gen_ai.memory.records | Memory span | Instrumentations SHOULD NOT capture by default; capture SHOULD be gated |
gen_ai.tool.definitions | Inference and agent spans | Large; not recommended to populate non-required properties by default |
Note that
gen_ai.tool.definitions is on this list for a different reason — size rather than sensitivity. Tool definitions with full JSON Schema parameter descriptions can dwarf the rest of a span.8.2 The three usage patterns
The conventions describe three patterns and say to choose based on application needs and maturity. This is the decision to make deliberately, once, at platform level — not per service.Pattern 1 (the default): record nothing. No content in telemetry. You still get every timing, token count, error, and structural attribute. This is enough to answer almost all operational questions: is it slow, is it erroring, how many tool calls did it take, which model answered.
Pattern 2: record content on span attributes. Content lands in
gen_ai.system_instructions, gen_ai.input.messages, gen_ai.output.messages. The specification scopes this pattern precisely: best suited where telemetry volume is manageable and either privacy regulations do not apply or the telemetry storage complies with them — for example, in pre-production environments. That parenthetical is the specification's own example, and it is the right default reading. Your trace store now contains user data, and inherits every retention, access-control, and residency obligation that comes with it.Pattern 3: store content externally and record references on the span. The specification recommends this in production environments where telemetry volume is a concern or sensitive data needs to be handled securely, because external storage enables separate access controls. This is the pattern that scales: the operational telemetry stays queryable by everyone who debugs, while the content sits behind its own authorization boundary with its own lifecycle.
The mechanism is a hook. Instrumentations MAY support user-defined in-process hooks to handle content upload; the hook SHOULD operate independently of the opt-in flags, and SHOULD be invoked regardless of the span sampling decision, receiving the instructions, inputs, and outputs objects before JSON serialization, plus the span instance. The hook implementation is allowed to enrich and modify both the span and the message objects, which is where redaction naturally belongs. If content attributes are also being recorded, instrumentation SHOULD record the potentially-modified values from the hook.
One honest gap: the specification currently carries a
TODO: document a common approach to record references to externally stored content. There is no standardized attribute for "the content is over there" yet. Whatever you choose today for that pointer, keep it in your own namespace and expect to migrate it.8.3 What this looks like in an implementation
Because the conventions define the vocabulary but not the configuration surface, the actual switches live in the instrumentation libraries. Taking the official Python GenAI instrumentation as a concrete example, message content is not captured by default and is enabled with a single environment variable,OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, with these values:| Value | Effect |
|---|---|
no_content | Do not capture content (the default) |
span_only | Capture content on span attributes |
event_only | Capture content on event attributes |
span_and_event | Capture content on both |
The
event_only value is worth noticing, because it maps to a design that is easy to miss: events and spans usually go to different backends with different retention and access policies. Sending content to events only, while spans stay content-free, gives you pattern-2 richness with pattern-3-like separation, without building an upload pipeline.The same package exposes the upload hook through
OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK=upload together with OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH pointing at a storage location. Other languages and third-party instrumentations use different names; check yours rather than assuming these transfer.8.4 Sampling, retention, and the questions to settle first
Three decisions determine whether content capture is sustainable, and none of them are technical.Who can read it. If content lands in the same store as operational telemetry, everyone who can debug can read user conversations. Pattern 3 exists to break that coupling.
How long it lives. Trace retention is usually set for debugging — days to weeks. Conversation content may be subject to deletion requests, data residency rules, or contractual retention limits that have nothing to do with debugging windows.
What fraction you keep. Content volume scales with traffic, and prompts are large. Head-based sampling reduces it proportionally but loses exactly the rare failures you wanted content for; tail-based sampling in a collector keeps the interesting traces but needs the content to transit the collector first. There is no free option — but note that the upload hook is invoked regardless of the sampling decision, which means content capture and trace sampling can be tuned independently if you use the hook path.
For where content controls sit relative to the rest of an agent's defenses, see AI Agent Defense in Depth Model (AIDDM).
9. Manual versus Automatic Instrumentation
9.1 What automatic instrumentation covers today
The GenAI conventions repository maintains something unusual and very useful: a reference compliance matrix, generated by running reference implementations of real client libraries against a deterministic local mock server and validating the emitted telemetry against the conventions. It is published inreference/README.md and regenerated from committed scenario data.Summarizing that matrix as of the verification date, by span type:
| Span type | Libraries with reference coverage |
|---|---|
| Inference | 22 |
| Execute tool | 13 |
| Embeddings | 9 |
| Invoke agent (internal) | 6 |
| Invoke agent (client) | 4 |
| Retrieval | 3 |
| Create agent | 3 |
| Memory | 2 |
| Invoke workflow | 2 |
| Plan | 2 |
For events, the inference operation details event has coverage across 15 libraries and the evaluation result event across 3. For metrics, only the two agent counters appear in the matrix, each with a single library.
The shape of that distribution is the practical takeaway, and it is stable enough to plan around even as the numbers change: inference is well covered, and everything above it is thin. If your application is "call a model", automatic instrumentation will probably give you correct conventional telemetry with no code. If your application is an agent with tools, memory, and a planner, expect to write the upper half yourself.
Because the matrix is regenerated from committed data in the repository, it is worth re-reading rather than trusting this summary; it is the only authoritative statement of what actually emits what.
9.2 Where the language implementations live
The GenAI instrumentation packages are being reorganized alongside the conventions, and the destination differs by language.For Python, a dedicated repository,
open-telemetry/opentelemetry-python-genai, now hosts the GenAI instrumentations, including packages for OpenAI, Anthropic, Google GenAI, LangChain, LlamaIndex, OpenAI Agents, Agno, Claude Agent SDK, and Weaviate. The older opentelemetry-instrumentation-openai-v2 package in opentelemetry-python-contrib carries a migration note pointing at its replacement, opentelemetry-instrumentation-genai-openai.For JavaScript and Java, GenAI instrumentations remain in the existing contrib repositories, and the set is smaller:
opentelemetry-js-contrib carries OpenAI and LangChain instrumentation packages, and opentelemetry-java-instrumentation carries an OpenAI one.If you are adopting now, check which package your language and library combination actually has before designing around automatic instrumentation. The gap between "the convention defines this span" and "an instrumentation emits this span in my language" is currently wide.
9.3 The manual half
Whatever your framework does not cover, you write. Three rules make hand-written GenAI instrumentation match automatic instrumentation instead of fighting it:- Set the sampling-relevant attributes at span creation (sections 5.3 and 6.2). This cannot be fixed afterwards.
- Use the exact span name formats.
chat gpt-4, notchat: gpt-4orLLM call. Backends key GenAI views off these. - Do not populate
Conditionally Requiredattributes when the condition is false. Absence is meaningful. Populatinggen_ai.request.streamasfalseon non-streaming requests, or synthesizinggen_ai.conversation.id, produces telemetry that looks conformant and queries wrong.
Note also that the specification explicitly invites application-level instrumentation of tool calls: application developers are encouraged to follow the tool convention for tools invoked by their own code. Your dispatcher is a legitimate instrumentation point, not a workaround. For agent runtimes built on a vendor SDK, see Claude Agent SDK Complete Guide for where the agent loop boundaries actually fall.
10. Exporting and Backends
10.1 OTLP is the portability boundary
The conventions define what attributes mean; OTLP defines how the data moves. Together they are what makes backend choice reversible: if your application emits convention-conformant telemetry over OTLP, changing where it lands is a configuration change, not an instrumentation project.Two export topologies cover most deployments.
Direct from SDK to backend. The application's OTLP exporter points at a backend endpoint. Fewest moving parts, and the right starting point. The costs are that every export decision — retry, batching, redaction, sampling — is compiled into the application, and changing any of them means redeploying.
Through a collector. The application exports to a local or regional collector, which processes and fans out. This buys tail-based sampling (which needs the whole trace before deciding), attribute redaction and filtering as a pipeline stage rather than application code, and multiple destinations from one pipeline. For GenAI telemetry the redaction point matters especially: a collector processor is a natural place to enforce that no content attribute leaves a boundary, independent of whether some service enabled it.

10.2 Tuning the pipeline for GenAI volume
Two adjustments are specific to this telemetry rather than generic advice.First, if you enable content capture at all, the specification itself flags the consequence: given the potential data volume, it is RECOMMENDED to tune batching and export settings accordingly in the OpenTelemetry SDK pipeline. Default batch sizes and attribute value limits are sized for ordinary spans, not for spans carrying an entire conversation. Truncation at the SDK's attribute-length limit produces invalid JSON in a
gen_ai.input.messages string — silently.Second, instrumentation MAY provide a configuration option to truncate properties such as individual message contents while preserving JSON structure. If yours offers that, prefer it to blunt length limits, for exactly the reason above.
10.3 Backends
Backend choice is out of scope here by design, and the honest summary is short: any backend that accepts OTLP and preserves your attributes can serve GenAI telemetry, and a growing number of them ship purpose-built GenAI views keyed offgen_ai.operation.name. Open-source trace and metric stores, self-hosted observability platforms, commercial APM products, and cloud-provider observability services all sit in that category.If your destination is a managed cloud stack, the wiring — collectors, log destinations, evaluation gates, permissions, and dashboards — is a substantial topic of its own, and it is covered separately in LLMOps Observability and Evaluation Architecture on AWS and, for agent runtimes specifically, Amazon Bedrock AgentCore Production Operations Guide. For the vocabulary of the surrounding observability stack, AWS Observability Glossary and AI Agent Engineering Glossary cover the terms this article assumes. The point of the conventions is that those articles describe a destination, not a lock-in.
11. Migration and Versioning Strategy
11.1 Attributes that have already been replaced
The deprecated tables on the opentelemetry.io registry page are the one part of that page that still carries real content, and they are your migration checklist. Verbatim:* You can sort the table by clicking on the column name.
| Deprecated attribute | Replacement |
|---|---|
gen_ai.system | gen_ai.provider.name |
gen_ai.usage.prompt_tokens | gen_ai.usage.input_tokens |
gen_ai.usage.completion_tokens | gen_ai.usage.output_tokens |
gen_ai.prompt | Removed, no replacement at this time |
gen_ai.completion | Removed, no replacement at this time |
gen_ai.openai.request.response_format | gen_ai.output.type |
gen_ai.openai.request.seed | gen_ai.request.seed |
gen_ai.openai.request.service_tier | openai.request.service_tier |
gen_ai.openai.response.service_tier | openai.response.service_tier |
gen_ai.openai.response.system_fingerprint | openai.response.system_fingerprint |
The two rows without replacements are the ones that catch people.
gen_ai.prompt and gen_ai.completion were flat string attributes holding the whole prompt and completion; they were not renamed but removed, because the structured gen_ai.input.messages and gen_ai.output.messages model replaced the concept rather than the key. A mechanical rename will not migrate them; the shape of the data changed.If you built dashboards during the earlier generation of GenAI instrumentation,
gen_ai.system is the one to search for first. It appeared on essentially every span.11.2 The repository move is itself a migration
Beyond attribute renames, the move to a separate repository changes what you should be pinning and linking to:- Bookmarks and internal documentation pointing at
opentelemetry.io/docs/specs/semconv/gen-ai/*now resolve to stubs. Repoint them at the repository. - Version references cannot use a semconv version number for the GenAI parts, because the GenAI repository has no releases. Cite a commit.
- Core-attribute references still work normally.
error.type,server.*, andexception.*continue to be versioned bysemantic-conventionsreleases — v1.43.0 at the time of writing.
11.3 Breaking changes happen — recent examples
It is worth seeing that Development status is not theoretical. The unreleased changelog fragments in the repository record breaking changes that have already landed:| Change | What it breaks |
|---|---|
gen_ai.request.top_k changed type from double to int and was scoped to decoding only; retrieval spans now use the new gen_ai.retrieval.top_k | Any consumer that parsed top_k as a float, and any retrieval instrumentation that reused gen_ai.request.top_k |
gen_ai.agent.id scope clarified and removed from internal agent spans | Dashboards grouping in-process agent spans by agent ID |
gen_ai.provider.name removed as a required attribute from the internal invoke_agent span | Queries that assumed provider is always present on agent spans |
gen_ai.agent.version removed from the internal invoke_agent span; invoke_agent and execute_tool attributes aligned across spans and duration metrics | Version-based comparisons on in-process agents |
gen_ai.system_instructions part types limited to text | Instrumentation emitting non-text system instruction parts |
None of these are exotic. They are the ordinary consequence of a vocabulary being refined in public, and they will keep happening until the conventions reach a stable release.
11.4 A migration-tolerant design
Five practices, in rough order of payoff.- Record what you built against. Emit your instrumentation library version as a resource attribute and note the convention commit in your repository. When a metric shifts, the first question is always "did the convention change or did we?", and this answers it in seconds.
- Put a mapping layer between attributes and dashboards. Define your dashboard queries against named concepts — "input tokens", "model name" — resolved through one configuration file that maps concept to attribute key. A rename becomes one edit. Without this, every rename is a hunt through dashboards, alerts, saved queries, and notebooks.
- Dual-emit across a rename, briefly. When an attribute is renamed, emit both keys for one retention window, then drop the old one. This lets historical dashboards keep working while new ones move over, and it is cheap because it is a single extra attribute for a bounded time. It is not cheap if you leave it on forever, so put an end date on it.
- Keep your custom attributes in your own namespace. Anything you invent goes under a prefix you control — your company or product name — never under
gen_ai.*. Two reasons: a future convention release may define your key with different semantics, and convention-aware tooling may validate or transformgen_ai.*attributes in ways that mangle yours. - Watch the changelog, not the docs site. Because the docs site is currently a stub, the only signal for convention changes is the repository —
CHANGELOG.mdand thechangelog.d/fragments, which are categorized asbreaking,enhancement,bugfix, andclarification. Thebreakingfragments are the ones to read.
12. Failure Modes and Anti-Patterns
12.1 Inventing gen_ai.* attributes
The most tempting mistake, because the namespace is right there and your attribute really is a GenAI attribute. Suppose you addgen_ai.request.tenant_id. Two things can go wrong: a future release may define that exact key with different semantics, silently changing what your dashboards mean; and convention-aware tooling may validate or rewrite gen_ai.* attributes according to the published model, which does not include yours.Instead: use a prefix you control. If the attribute is genuinely general, propose it to the conventions repository — the whole point of a shared vocabulary is that additions are welcome.
12.2 Cardinality explosion in metrics
Every distinct combination of metric attribute values is a separate time series. Addinggen_ai.conversation.id as a metric dimension creates one series per conversation, and a metrics backend does not degrade gracefully under that — it either rejects writes, drops series, or bills you for a scale you did not intend.The conventions already encode the safe answer: the metric attribute lists in section 7.4 are short, and none of them contain a request-scoped or user-scoped identifier. Trust them. Spans are where high-cardinality identifiers belong; that is what traces are for.
The same logic applies to span names, which most backends treat as a metric dimension. This is why the conventions specify
execute_tool {gen_ai.tool.name} and not execute_tool {gen_ai.tool.call.id}, and why MCP instrumentation is told not to put mcp.resource.uri in span names by default.12.3 Recording prompts and completions unconditionally
Turning on content capture globally because it was useful in one debugging session is how user conversation data ends up in a trace store with a retention policy nobody reviewed and an access list that includes everyone who can read dashboards.Instead: decide the pattern at platform level (section 8.2), default production to pattern 1 or 3, and if you use pattern 2 anywhere, be able to state where the data goes, who can read it, and how long it lives — before enabling it.
A subtler variant: enabling content capture in pre-production only, but with production data copied into pre-production. The environment name does not change the sensitivity of the data.
12.4 Broken trace continuity
Symptom: the agent span and the tool span appear as separate traces, or the MCP server's work shows up detached from the client that requested it. Almost always a propagation gap.Over MCP specifically, HTTP context propagation does not cover individual messages within a stream, which is why the conventions specify injecting
traceparent into params._meta (section 6.4). Over ordinary async boundaries — queues, background workers, thread pools — the cause is usually that context did not cross the boundary and nobody noticed, because a detached trace still looks like a trace.Instead: assert on trace structure in tests, not just on attribute presence. "The tool span has the agent span as an ancestor" is a testable property and the one that actually breaks.
12.5 Double-counted tool spans
Symptom: every MCP tool call appears twice, and tool call counts are exactly double what they should be. Cause: both GenAI instrumentation and MCP instrumentation created a span for the same execution.Instead: check whether your MCP instrumentation implements the behavior the conventions describe — detecting that outer GenAI instrumentation is already tracing the tool execution and adding MCP attributes to the existing span instead of creating a new one — and whether it exposes the configuration option to enable it. Deduplicating in a span processor is a workaround, not a fix, and it will break when either instrumentation changes.
12.6 Treating Development as Stable
Symptom: an SDK upgrade changes an attribute and a dashboard silently goes flat, or an alert stops firing because its filter no longer matches anything.The dangerous failure here is not the error, it is the silence. A renamed attribute does not throw; a query that matches nothing returns zero, and zero looks like health.
Instead: alert on the absence of data, not only on bad values. A rule that fires when a GenAI metric reports no data points for an interval catches every rename, every instrumentation regression, and every deployment that dropped the instrumentation — none of which a threshold alert on the metric's value will ever catch.
12.7 Reading the stub as the specification
Finally, the failure mode this article exists to prevent.opentelemetry.io/docs/specs/semconv/gen-ai/ returns HTTP 200 and looks like documentation. It is a redirect notice. Anyone — or any tool — that fetches it and sees a successful response without reading the body concludes that the GenAI conventions are nearly empty.Instead: treat the repository as the source, and re-derive attribute tables from it rather than from any secondary summary, this article included.
13. Frequently Asked Questions
13.1 Should I adopt conventions that are marked Development?
Yes, with the four practices in section 3.3. The alternative is not "wait for stable" — it is "invent a private vocabulary in the meantime", which gives you a guaranteed migration later instead of a possible one, and no interoperability in the interim. Adopt, pin, and put a mapping layer between the attribute keys and anything a human reads.13.2 Where do I actually read the conventions?
github.com/open-telemetry/semantic-conventions-genai, under docs/gen-ai/. The five signal documents are gen-ai-spans.md, gen-ai-agent-spans.md, gen-ai-metrics.md, gen-ai-events.md, and gen-ai-exceptions.md; mcp.md covers Model Context Protocol; per-provider documents cover Anthropic, AWS Bedrock, Azure AI Inference, and OpenAI. The attribute registry is under docs/registry/attributes/.13.3 What version should I cite?
A commit. The repository has no tagged releases and its changelog contains only an## Unreleased section, so there is no version number to cite for the GenAI parts. Core attributes borrowed into these conventions (error.type, server.*, exception.*) are versioned normally by semantic-conventions releases.13.4 Can I use the schema URL mechanism to migrate automatically?
Not yet for GenAI. The Telemetry Schemas specification is Stable and the mechanism works, but the GenAI repository's README lists its Schema URL asTODO, and the development schema URL declared in model/manifest.yaml is not served as of the verification date. Plan migrations manually (section 11.4).13.5 Do I have to instrument agents, or is instrumenting model calls enough?
Model calls alone tell you latency, tokens, and errors per call. They cannot tell you how many model calls one user turn took, whether a loop terminated, or which tool failed. Those are exactly the agent reliability questions, and they needinvoke_agent, execute_tool, and the two agent counters. If you run agents in production, the upper spans are the point. Designing what to do with those signals once you have them is the subject of Agent Reliability Engineering Design Guide.13.6 What is the difference between the client and server metric families?
Perspective.gen_ai.client.* is emitted by the caller and includes network and queueing time; gen_ai.server.* is emitted by a model server and measures only its internal work. The client family counts chunks, the server family counts tokens, because clients cannot see token boundaries. Most application teams emit only the client family.13.7 Should I record prompts and completions?
Default to no in production. If you need content, prefer external storage with references on the span (pattern 3) so that access control and retention are separate from operational telemetry. Content on span attributes (pattern 2) is scoped by the specification to cases where volume is manageable and privacy obligations are satisfied — its own example is pre-production. Section 8.2.13.8 How do I stop MCP tool calls from being traced twice?
Look for the configuration option in your MCP instrumentation that suppresses its span when outer GenAI instrumentation is already tracing the tool execution — the conventions specify that behavior, including that MCP-specific attributes should be added to the existing span instead. Sections 6.4 and 12.5.13.9 Which attributes must be set before the span starts?
On the inference span:gen_ai.operation.name, gen_ai.provider.name, gen_ai.request.model, server.address, server.port. On the execute tool span: gen_ai.operation.name, gen_ai.tool.name, and gen_ai.tool.type. On the create agent span, additionally gen_ai.agent.name. Samplers only see what exists at creation time.13.10 How do I tell whether my library emits conventional telemetry?
Read the reference compliance matrix inreference/README.md in the conventions repository (section 9.1). It is generated by actually running each library against a mock server and validating the output against the model, so it is the only claim about coverage that is mechanically checked rather than asserted.13.11 Does any of this depend on a particular cloud or vendor?
No. The conventions are provider-neutral by construction; provider-specific documents exist to pin down provider-specific extensions, andgen_ai.provider.name exists precisely to make the provider a data value rather than a schema difference. Where a managed stack is involved, the implementation details live in a separate article by design (section 10.3).14. Summary
The OpenTelemetry GenAI semantic conventions give LLM and agent telemetry a shared vocabulary: seventeen operation names, sixty-onegen_ai.* attributes, eight span shapes, twelve metric instruments, and three events, all provider-neutral and all carried over OTLP. Adopting them is what makes a trace from one team's Python agent and another team's Java service line up in the same waterfall, and what makes backend choice reversible.Three things are worth carrying away.
The conventions are not where you would look for them. They live in
open-telemetry/semantic-conventions-genai, and every corresponding page on opentelemetry.io is now a redirect notice that still returns HTTP 200. Point your bookmarks, your internal docs, and any tooling that fetches specifications at the repository.Development status is a design input, not a veto. Ship, but pin your instrumentation version and the convention commit, keep raw attribute keys out of dashboards behind a thin mapping layer, dual-emit briefly across renames, keep your own attributes in your own namespace, and alert on missing data rather than only on bad values. Breaking changes are already landing — a type change on
gen_ai.request.top_k, attributes removed from internal agent spans, part types restricted on system instructions.The privacy model is opt-in by construction, and worth preserving. Every attribute that can hold prompt or output content is
Opt-In, which means content never appears because someone forgot a flag. Choose one of the three usage patterns deliberately at platform level, default production to recording nothing or to external storage with references, and if you record content on spans anywhere, know where it goes, who can read it, and how long it lives before you turn it on.The vocabulary will keep moving. Instrument as if it will, and the movement is a maintenance task rather than a rewrite.
15. References
OpenTelemetry GenAI semantic conventions (primary source)
- OpenTelemetry GenAI Semantic Conventions repository
- Semantic conventions for generative AI systems
- Semantic conventions for generative client AI spans
- Semantic Conventions for GenAI agent and framework spans
- Semantic conventions for generative AI metrics
- Semantic conventions for Generative AI events
- Semantic conventions for Generative AI exceptions
- Semantic conventions for Model Context Protocol (MCP)
- Gen AI attribute registry
- MCP attribute registry
- Reference implementations and compliance matrix
- Registry manifest (schema URL and stability)
- Changelog fragments
OpenTelemetry specification and core conventions
- Definitions of Document Statuses
- Telemetry Schemas
- Attribute requirement levels
- OpenTelemetry Semantic Conventions repository
- Generative AI semantic conventions page on opentelemetry.io (moved notice)
- Gen AI attribute registry on opentelemetry.io (deprecated attributes)
Instrumentation libraries
- OpenTelemetry Python GenAI Instrumentations
- OpenTelemetry Python contrib
- OpenTelemetry JavaScript contrib
- OpenTelemetry Java instrumentation
Related Articles on This Site
- LLMOps Observability and Evaluation Architecture on AWS
The managed-stack implementation side of this article: collectors, log destinations, evaluation gates, and dashboards. - AWS Observability Architecture Guide
The general tracing, metrics, and logs substrate that GenAI telemetry rides on. - AWS Observability Glossary
Terminology for the surrounding observability stack. - Agent Reliability Engineering Design Guide
The consumer of this instrumentation: budgets, loop detection, retry semantics, and escalation are only operable once the spans and metrics described here exist. - AI Agent Engineering Glossary
Memory, tools, orchestration, and protocol terms this article assumes. - AI Agent Defense in Depth Model (AIDDM)
Where content controls sit relative to the rest of an agent's defenses. - Amazon Bedrock AgentCore Evaluations Practical Guide
What to evaluate and how, once evaluation results have somewhere to go. - Amazon Bedrock AgentCore Production Operations Guide
Observability for a managed agent runtime specifically. - Prompt Lifecycle Management on AWS
The versioning discipline behind gen_ai.prompt.name and gen_ai.prompt.version. - Model Context Protocol Specification Version Timeline
Background for the mcp.protocol.version attribute. - MCP Server Testing and Debugging Guide
Exercising an MCP server, including trace continuity checks. - Amazon Bedrock Inference Throughput and Latency Optimization
What the latency metrics in section 7 are actually measuring. - Claude Agent SDK Complete Guide
Where the agent loop boundaries fall in a concrete SDK.
References:
Tech Blog with curated related content
Written by Hidekazu Konishi