OpenTelemetry GenAI Semantic Conventions Implementation Guide - Vendor-Neutral Instrumentation for LLM and Agent Applications

First Published:
Last Updated:

Every team that ships an LLM or agent application eventually asks the same question: what exactly should a span for a model call contain? Left to itself, each team invents its own answer. One service records 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 in open-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 levelIncluded by defaultCan be added by configCan be removed by configReading
RequiredYesn/aNoInstrumentation MUST populate it. Consumers may use its presence to detect that a span follows the convention
Conditionally RequiredYes, when the stated condition holdsNoNoThe convention MUST state the condition. Absent condition, absent attribute
RecommendedYesNoYesPopulate unless you have a specific reason (cardinality, cost, privacy) not to
Opt-InNoYesYesOff 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:
StatusWhat the project guarantees
DevelopmentNot 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
AlphaUsable for limited non-critical production workloads; interfaces and generated telemetry might change without backward-compatibility guarantees
BetaAs Alpha, but the interfaces are treated as stable whenever possible; breaking changes minimized
Release CandidateFeature-complete; breaking changes only under special circumstances, with prior notice where possible
StableGenerally available; breaking changes only under special circumstances
DeprecatedDevelopment 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 marked Status: 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:
  1. Attributes borrowed from the core conventions are Stable. error.type, server.address, server.port, exception.type, exception.message, and exception.stacktrace all come from open-telemetry/semantic-conventions v1.43.0 (released 2026-07-03), which the GenAI repository pins via SEMCONV_VERSION in versions.env. These carry Stable badges in the GenAI tables.
  2. 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:
  1. 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.
  2. 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).
  3. Expect renames, retypes, and removals — not just additions. Section 11.3 lists breaking changes that have already landed in this repository.
  4. 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 kindConvention documentTypical emitter
Model inferencegen-ai-spans.md — InferenceClient SDK wrapping a chat or completion API
Embeddingsgen-ai-spans.md — EmbeddingsClient SDK wrapping an embeddings API
Retrievalgen-ai-spans.md — RetrievalsVector store or search client, RAG framework
Memorygen-ai-spans.md — MemoryAgent memory store client
Tool executiongen-ai-spans.md — Execute tool spanAgent framework, or your own tool dispatcher
Agent creation and invocationgen-ai-agent-spans.mdAgent framework, or a hosted agent service client
Workflow invocationgen-ai-agent-spans.md — Invoke workflow spanMulti-agent orchestrator
Planninggen-ai-agent-spans.md — Plan spanAgent 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:
ValueMeaning per the specification
chatChat completion operation
create_agentCreate GenAI agent
create_memoryCreate new memory records
create_memory_storeCreate or initialize a memory store
delete_memoryDelete memory records
delete_memory_storeDelete or deprovision a memory store
embeddingsEmbeddings operation
execute_toolExecute a tool
generate_contentMultimodal content generation operation
invoke_agentInvoke GenAI agent
invoke_workflowInvoke GenAI workflow
planAgent planning or task decomposition phase
retrievalRetrieval operation
search_memorySearch/query memories from a memory store
text_completionText completions operation
update_memoryUpdate existing memory records
upsert_memoryCreate 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 the gen_ai registry fall into a small number of namespaces, and knowing the namespaces is enough to guess where anything lives.
NamespacePurposeRepresentative keys
gen_ai.operation.*, gen_ai.provider.*Discriminatorsgen_ai.operation.name, gen_ai.provider.name
gen_ai.request.*What was asked forgen_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 backgen_ai.response.model, gen_ai.response.id, gen_ai.response.finish_reasons, gen_ai.response.time_to_first_chunk
gen_ai.usage.*Token accountinggen_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 identitygen_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 payloadsgen_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 identitygen_ai.prompt.name, gen_ai.prompt.version, gen_ai.prompt.variable
gen_ai.conversation.*Session correlationgen_ai.conversation.id, gen_ai.conversation.compacted
gen_ai.retrieval.*, gen_ai.memory.*, gen_ai.data_source.*RAG and memorygen_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 resultsgen_ai.evaluation.name, gen_ai.evaluation.score.value, gen_ai.evaluation.score.label, gen_ai.evaluation.explanation
gen_ai.embeddings.*Embedding shapegen_ai.embeddings.dimension.count
gen_ai.input.*, gen_ai.output.*, gen_ai.system_instructionsContentgen_ai.input.messages, gen_ai.output.messages, gen_ai.output.type, gen_ai.system_instructions
gen_ai.token.*Metric dimension onlygen_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.

Span tree of a single agent invocation with GenAI semantic convention attributes
Span tree of a single agent invocation with GenAI semantic convention attributes
The figure shows one agent turn as the conventions model it: an 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 be INTERNAL for models running in the same process. CLIENT is 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 ERROR with error.type on 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 keyRequirement levelTypeNote
gen_ai.operation.nameRequiredstringThe discriminator (section 4.2)
gen_ai.provider.nameRequiredstringProvider flavor (section 4.3)
error.typeConditionally Required if the operation ended in an errorstringStable, from core conventions
gen_ai.conversation.idConditionally Required when availablestringSee the caution below
gen_ai.output.typeConditionally Required when applicable and if the request includes an output formatstringtext, json, image, speech
gen_ai.prompt.nameConditionally Required when a named prompt template is usedstring
gen_ai.prompt.versionConditionally Required when gen_ai.prompt.name is set and a version is availablestring
gen_ai.request.choice.countConditionally Required if available, in the request, and !=1int
gen_ai.request.modelConditionally Required if availablestringExact vendor model name
gen_ai.request.seedConditionally Required if applicable and if the request includes a seedint
gen_ai.request.streamConditionally Required if and only if the request is streamingbooleanUnset means non-streaming
gen_ai.request.top_kConditionally Required if applicableintDecoding parameter only — see caution below
server.portConditionally Required if server.address is setintStable
gen_ai.conversation.compactedRecommended when availablebooleanSet only to true, never to false
gen_ai.request.frequency_penaltyRecommendeddouble
gen_ai.request.max_tokensRecommendedint
gen_ai.request.presence_penaltyRecommendeddouble
gen_ai.request.previous_response.idRecommended when available and if the request references a previous responsestring
gen_ai.request.reasoning.levelRecommended when applicablestringExact string sent to the provider
gen_ai.request.stop_sequencesRecommendedstring[]
gen_ai.request.temperatureRecommendeddouble
gen_ai.request.top_pRecommendeddouble
gen_ai.response.finish_reasonsRecommendedstring[]One entry per generation
gen_ai.response.idRecommendedstring
gen_ai.response.modelRecommendedstringModel that actually answered
gen_ai.response.time_to_first_chunkRecommended if the request was a streaming requestdoubleSeconds
gen_ai.usage.cache_creation.input_tokensRecommendedintIncluded in gen_ai.usage.input_tokens
gen_ai.usage.cache_read.input_tokensRecommendedintIncluded in gen_ai.usage.input_tokens
gen_ai.usage.input_tokensRecommendedintIncludes cached tokens
gen_ai.usage.output_tokensRecommendedint
gen_ai.usage.reasoning.output_tokensRecommended when applicableintIncluded in gen_ai.usage.output_tokens
server.addressRecommendedstringStable
gen_ai.input.messagesOpt-InanySensitive — section 8
gen_ai.output.messagesOpt-InanySensitive — section 8
gen_ai.prompt.variableOpt-InstringTemplated key; sensitive
gen_ai.system_instructionsOpt-InanySensitive — section 8
gen_ai.tool.definitionsOpt-InanyCan 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:
  1. gen_ai.request.stream is set to true if and only if the request is streaming. Unset means non-streaming.
  2. gen_ai.response.time_to_first_chunk records, in seconds, the time from request issuance to the first chunk arriving in the response stream.
  3. 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 in gen-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.
Spangen_ai.operation.nameSpan name formatSpan kindUse when
Create agentcreate_agentcreate_agent {gen_ai.agent.name}CLIENTCreating an agent resource, usually on a remote agent service
Invoke agent (client)invoke_agentinvoke_agent {gen_ai.agent.name}, or invoke_agent if the name is unavailableCLIENTInvoking an agent over a remote service
Invoke agent (internal)invoke_agentSame as aboveINTERNALInvoking an agent running in the same process
Invoke workflowinvoke_workflowinvoke_workflow {gen_ai.workflow.name}INTERNALAn orchestration that coordinates multiple agents
Planplanplan {gen_ai.agent.name}, or plan if the name is unavailableINTERNALAn explicit planning or task-decomposition phase
Execute toolexecute_toolexecute_tool {gen_ai.tool.name}INTERNALAny 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 keyRequirement levelNote
gen_ai.operation.nameRequiredexecute_tool
gen_ai.tool.nameRequired
error.typeConditionally Required if the operation ended in an errorStable
gen_ai.agent.nameConditionally Required when applicableThe agent executing the tool
gen_ai.tool.call.idRecommended if availableCorrelates with the model's tool call
gen_ai.tool.descriptionRecommended if available
gen_ai.tool.typeRecommended if availablefunction, extension, datastore
gen_ai.tool.call.argumentsOpt-InSensitive
gen_ai.tool.call.resultOpt-InSensitive

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:
  1. invoke_agent {agent} (INTERNAL) opens as the root of the agent's work.
  2. chat {model} (CLIENT) runs as its child. The model returns a tool call.
  3. execute_tool {tool} (INTERNAL) runs as a sibling of the first chat span, under the same invoke_agent parent.
  4. 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 in mcp.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 nameInstrumentUnit (UCUM)Meaning
gen_ai.client.token.usageHistogram{token}Number of input and output tokens used
gen_ai.client.operation.durationHistogramsGenAI operation duration
gen_ai.client.operation.time_to_first_chunkHistogramsTime from request issuance to the first chunk received in the response stream
gen_ai.client.operation.time_per_output_chunkHistogramsTime per output chunk, recorded for each chunk after the first
gen_ai.server.request.durationHistogramsModel server request duration, such as time-to-last-byte or last output token
gen_ai.server.time_to_first_tokenHistogramsTime to generate the first token, for successful responses
gen_ai.server.time_per_output_tokenHistogramsTime per output token generated after the first, for successful responses
gen_ai.workflow.durationHistogramsGenAI workflow duration
gen_ai.invoke_agent.durationHistogramsEnd-to-end duration of a single in-process agent invocation
gen_ai.invoke_agent.inference_callsHistogram{inference_call}Number of inference (model) calls an agent makes during a single invocation
gen_ai.invoke_agent.tool_callsHistogram{tool_call}Number of tool calls an agent makes during a single invocation
gen_ai.execute_tool.durationHistogramsDuration 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

The gen_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.
MetricRequired attributesOther dimensions
gen_ai.client.token.usagegen_ai.operation.name, gen_ai.provider.name, gen_ai.token.typegen_ai.request.model, gen_ai.response.model, server.address, server.port
gen_ai.client.operation.durationgen_ai.operation.nameerror.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.nameerror.type (on request duration), model and server attributes
gen_ai.workflow.durationerror.type, gen_ai.workflow.name
gen_ai.invoke_agent.durationerror.type, gen_ai.agent.name, gen_ai.request.model
gen_ai.invoke_agent.inference_calls / .tool_callsgen_ai.agent.name
gen_ai.execute_tool.durationgen_ai.tool.nameerror.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 the ExplicitBucketBoundaries instrument advisory parameter, and the values differ meaningfully by instrument:
Instrument familyAdvised boundaries
gen_ai.client.token.usage1, 4, 16, 64, 256, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864
Client and server operation duration, chunk timing, gen_ai.execute_tool.duration0.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_token0.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_token0.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.duration0.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_calls1, 2, 4, 8, 16, 32, 64, 128
gen_ai.workflow.duration1, 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:
AttributeWhere it appearsSpecification warning
gen_ai.input.messagesInference and agent spans, inference details event"likely to contain sensitive information including user/PII data"
gen_ai.output.messagesInference and agent spans, inference details event"likely to contain sensitive information including user/PII data"
gen_ai.system_instructionsInference, agent, and create-agent spans"may contain sensitive information"
gen_ai.tool.call.argumentsExecute tool span, MCP client and server spans"may contain sensitive information"
gen_ai.tool.call.resultExecute tool span, MCP client and server spans"may contain sensitive information"
gen_ai.prompt.variableInference span, MCP client span"may contain sensitive information"
gen_ai.retrieval.query.text, gen_ai.retrieval.documentsRetrieval spanOpt-In
gen_ai.memory.query.text, gen_ai.memory.recordsMemory spanInstrumentations SHOULD NOT capture by default; capture SHOULD be gated
gen_ai.tool.definitionsInference and agent spansLarge; 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:
ValueEffect
no_contentDo not capture content (the default)
span_onlyCapture content on span attributes
event_onlyCapture content on event attributes
span_and_eventCapture 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 in reference/README.md and regenerated from committed scenario data.

Summarizing that matrix as of the verification date, by span type:
Span typeLibraries with reference coverage
Inference22
Execute tool13
Embeddings9
Invoke agent (internal)6
Invoke agent (client)4
Retrieval3
Create agent3
Memory2
Invoke workflow2
Plan2

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:
  1. Set the sampling-relevant attributes at span creation (sections 5.3 and 6.2). This cannot be fixed afterwards.
  2. Use the exact span name formats. chat gpt-4, not chat: gpt-4 or LLM call. Backends key GenAI views off these.
  3. Do not populate Conditionally Required attributes when the condition is false. Absence is meaningful. Populating gen_ai.request.stream as false on non-streaming requests, or synthesizing gen_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.

Vendor-neutral instrumentation and export pipeline for GenAI telemetry
Vendor-neutral instrumentation and export pipeline for GenAI telemetry

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 off gen_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 attributeReplacement
gen_ai.systemgen_ai.provider.name
gen_ai.usage.prompt_tokensgen_ai.usage.input_tokens
gen_ai.usage.completion_tokensgen_ai.usage.output_tokens
gen_ai.promptRemoved, no replacement at this time
gen_ai.completionRemoved, no replacement at this time
gen_ai.openai.request.response_formatgen_ai.output.type
gen_ai.openai.request.seedgen_ai.request.seed
gen_ai.openai.request.service_tieropenai.request.service_tier
gen_ai.openai.response.service_tieropenai.response.service_tier
gen_ai.openai.response.system_fingerprintopenai.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.*, and exception.* continue to be versioned by semantic-conventions releases — 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:
ChangeWhat 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_kAny 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 spansDashboards grouping in-process agent spans by agent ID
gen_ai.provider.name removed as a required attribute from the internal invoke_agent spanQueries 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 metricsVersion-based comparisons on in-process agents
gen_ai.system_instructions part types limited to textInstrumentation 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.
  1. 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.
  2. 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.
  3. 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.
  4. 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 transform gen_ai.* attributes in ways that mangle yours.
  5. 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.md and the changelog.d/ fragments, which are categorized as breaking, enhancement, bugfix, and clarification. The breaking fragments 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 add gen_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. Adding gen_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 as TODO, 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 need invoke_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 in reference/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, and gen_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-one gen_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 specification and core conventions


Instrumentation libraries


Related Articles on This Site


References:
Tech Blog with curated related content

Written by Hidekazu Konishi