The OpenTelemetry GenAI semantic conventions are a shared vocabulary for telemetry about generative AI: the span names, attribute keys, metrics and events that describe a model call, an agent run, a tool execution or an MCP request. Every part is still marked Development. Prompts and completions are not captured by default, and that default is the most important security control in the whole set.
This post goes one level deeper than our pieces on what Claude Code logs through OpenTelemetry by default, the collector's redaction and filter processors, distributed tracing and trace context and agent planning patterns. Those explained the plumbing and the agent loop. This one explains the GenAI vocabulary itself, attribute by attribute, and then shows how to keep the sensitive parts out of your trace store. Everything below was checked against the GenAI conventions repository on 24 October 2026, and the walkthrough code was run against the OpenTelemetry Python SDK 1.45.1.
Key takeaways
- The GenAI conventions cover inference, embeddings, retrieval, memory, agents, workflows, tools, MCP, metrics and events, all at Development stability.
- In semantic conventions v1.42.0 (released 12 June 2026) the GenAI, OpenAI and MCP definitions moved out of the core repository into a dedicated
semantic-conventions-genairepository. Old links still work but point to a "Moved" notice. - A model call is a span named
{gen_ai.operation.name} {gen_ai.request.model}, for examplechat my-model, withgen_ai.provider.name, token usage, response model and finish reasons as attributes. - Agents get
invoke_agent,invoke_workflow,planandexecute_toolspans. Tool calls that run shell commands or agent skills have their own refinements. - Across MCP hops, trace context travels inside each JSON-RPC message in
params._meta, not only in HTTP headers. - Instructions, input messages, output messages, tool definitions, tool arguments and tool results are all Opt-In attributes. Instrumentation should not capture them unless you ask.
- Once content is on, your trace store holds prompts, personal data and pasted secrets. Redact in the SDK first and the collector second, keep retention short, and keep tenants apart.
What are OpenTelemetry semantic conventions?
OpenTelemetry gives you the plumbing: an API, SDKs, a wire protocol (OTLP) and the Collector. Semantic conventions are the agreed names and meanings for spans, attributes, metrics and events, so spans from different libraries can be queried the same way.
The HTTP conventions are the familiar example. Any HTTP client span records http.request.method, server.address and http.response.status_code, whatever the language or library. A dashboard that groups by status code works on all of them.
Generative AI needed the same thing. Every model SDK and agent framework had invented its own fields for model name, token counts and prompt text. The GenAI conventions give them one shape, so "input tokens per model per service" is one query across languages and frameworks.
Each convention defines a few properties that matter in practice:
- Span name. A low cardinality name, such as
chat my-modelorexecute_tool get_weather. - Span kind.
CLIENTfor a call to another process,INTERNALfor work inside the same process,SERVERfor the receiving side. - Attributes with requirement levels.
Required,Conditionally Required,RecommendedorOpt-In. Opt-In attributes are the ones an instrumentation must not record unless the user turns them on. - Stability. Development, then (eventually) Stable. Stable conventions come with compatibility promises. Development ones do not.
What does the GenAI set cover?
The GenAI index page in the new repository lists these documents, each with status Development:
| Document | Signals | What it describes |
|---|---|---|
| Client Inference | Spans, events, metrics | A call to a model that generates a response or asks for a tool call |
| Model spans | Spans | Embeddings, retrieval, fetch response, memory operations and the execute tool span |
| Agent spans | Spans | Creating and invoking agents, workflows, planning, skills and command execution |
| Events | Events | Evaluation results; inference detail events live in Client Inference |
| Exceptions | Events | Exceptions raised during GenAI operations |
| Metrics | Metrics | Client and server durations, agent, workflow and tool durations |
| Inference token metrics | Metrics | Input, output, cached and reasoning token counts |
| Model Context Protocol | Spans, metrics | MCP client and server operations and sessions, and how context propagates |
| Provider specific pages | Varies | Extra attributes for a few model providers and cloud platforms |
The well known values for gen_ai.operation.name show how wide the scope is: chat, text_completion, generate_content, embeddings, retrieval, fetch_response, create_agent, invoke_agent, invoke_workflow, plan, execute_tool, and seven memory operations (create_memory, update_memory, upsert_memory, delete_memory, search_memory, create_memory_store, delete_memory_store).
Stability status and the 2026 move to a separate repository
Two facts here trip people up.
| Date | Event | Source |
|---|---|---|
| 23 November 2021 | W3C Trace Context Level 1 becomes a W3C Recommendation | W3C |
| 30 May 2024 | W3C Baggage published as a Candidate Recommendation Snapshot | W3C |
| 24 January 2025 | Semantic conventions v1.30.0 released; the Python OpenAI instrumentation still defaults to this GenAI shape unless you opt in to newer conventions | semantic-conventions releases; instrumentation README |
| 25 August 2025 | v1.37.0: gen_ai.system renamed to gen_ai.provider.name; per message events replaced by gen_ai.system_instructions, gen_ai.input.messages and gen_ai.output.messages | semantic-conventions changelog |
| 5 May 2026 | open-telemetry/semantic-conventions-genai repository created | GitHub API |
| 12 June 2026 | v1.42.0: all gen_ai.*, OpenAI and MCP definitions deprecated in the core repository and moved to the GenAI repository | semantic-conventions changelog |
| 4 August 2026 | Core semantic conventions v1.44.0, the latest core release at the time of writing | semantic-conventions releases |
| 23 October 2026 | opentelemetry-instrumentation-genai-openai 1.3b0 published to PyPI | PyPI |
What this means for you:
- Nothing in the GenAI set is Stable. Every span, attribute and metric carries the Development badge. Expect renames. The v1.37.0 change from
gen_ai.systemtogen_ai.provider.namebroke many dashboards, and the move to new message attributes broke anything that parsed the old per message events. - Instrumentations pin to a version. The Python instrumentation README says that without an opt in it aligns with v1.30.0. To get the current shape, set
OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental. Two services with different settings will send different attribute names for the same thing. - The schema URL is not settled. At the time of writing, the new repository's README lists the schema URL as "TODO", and the v1.42.0 changelog tells instrumentations to refer to the new repository for the
schema_urlto use. - Old links mislead. The
docs/gen-aifolder in the core repository now holds short "Moved" stubs. If a blog post or internal wiki links there, it is pointing at a frozen copy.
The practical rule: record which conventions version each service emits, and write queries that tolerate old and new names until every service has moved.
Which attributes describe a model call?
The core span is gen_ai.client.inference. It represents a client call to a model or service that generates a response or requests a tool call. It covers streaming and non streaming variants of chat, completions, responses and multimodal generation APIs.
- Span name:
{gen_ai.operation.name} {gen_ai.request.model}, for examplechat my-model-1. - Span kind:
CLIENT. It may beINTERNALwhen the model runs in the same process. - Duration: from when the operation starts until the response is fully received or the call fails or is cancelled. Automatic retries belong inside the same span.
The attributes, grouped by purpose
| Purpose | Attribute | Requirement level | Notes |
|---|---|---|---|
| What kind of call | gen_ai.operation.name | Required | chat, generate_content, text_completion and so on |
| Who served it | gen_ai.provider.name | Required | Set from the instrumentation's best knowledge; a proxy may hide the real upstream |
| What was asked for | gen_ai.request.model | Conditionally Required, if available | The exact model name requested |
| What answered | gen_ai.response.model | Recommended | Often a dated or versioned name, different from the request |
| Response identity | gen_ai.response.id | Recommended | Lets you join the span to provider side logs |
| Why it stopped | gen_ai.response.finish_reasons | Recommended | Array; well known values include stop, length, content_filter, tool_call, compaction, error |
| Request parameters | gen_ai.request.max_tokens, temperature, top_p, top_k, stop_sequences, frequency_penalty, presence_penalty, seed, choice.count, stream, reasoning.level | Mostly Recommended | Some are Conditionally Required when present in the request |
| Token usage | gen_ai.usage.input_tokens, gen_ai.usage.output_tokens | Recommended | Plus cache read, cache write, reasoning, and per modality (text, image, audio) breakdowns |
| Streaming | gen_ai.response.time_to_first_chunk | Recommended for streaming requests | Seconds to the first chunk |
| Conversation | gen_ai.conversation.id | Conditionally Required | Only when a real conversation identifier exists |
| Prompt templates | gen_ai.prompt.name, gen_ai.prompt.version | Conditionally Required when a named template is used | gen_ai.prompt.variable is Opt-In |
| Output type | gen_ai.output.type | Conditionally Required | text, json, image |
| Endpoint | server.address, server.port | Recommended / Conditionally Required | Should show the server behind any proxy, if known |
| Failure | error.type | Conditionally Required on error | A low cardinality error class or provider error code |
| Content | gen_ai.system_instructions, gen_ai.input.messages, gen_ai.output.messages, gen_ai.tool.definitions | Opt-In | Not recorded unless the user turns content capture on |
Three details in the specification are worth reading twice.
Sampling attributes come first. The specification lists gen_ai.operation.name, gen_ai.provider.name, gen_ai.request.model, server.address and server.port as attributes that should be set at span creation time, because head samplers only see what exists when the span starts. If you add the model name after the call, a sampler that keeps "all calls to the expensive model" cannot see it.
The provider attribute is a discriminator, not proof. The specification says gen_ai.provider.name should be set from the instrumentation's best knowledge and may differ from the real upstream, for example when a client SDK talks to a proxy that relays to another provider. It also decides which provider specific attributes may appear. A span with gen_ai.provider.name set to aws.bedrock should carry aws.bedrock.* attributes and not openai.* ones. For security reviews, this means a trace tells you which SDK your code used, not which company processed the data. Data residency questions still need your gateway or proxy logs.
Conversation IDs must be real. The specification says that when no conversation identifier exists, instrumentations should not invent one. A new UUID, the trace ID or a hash of the request content are all named as things that should not be used as a fallback. This matters for privacy: a hash of the prompt is a fingerprint of the prompt.
Worked example: token usage arithmetic
The specification gives an example for a request with 100 text tokens, 40 of them cached, and 200 image tokens. The totals are supersets of the details:
| Attribute | Value | Why |
|---|---|---|
gen_ai.usage.input_tokens | 300 | 100 text + 200 image, cached tokens included |
gen_ai.usage.cache_read.input_tokens | 40 | The cached part, already inside the 300 |
gen_ai.usage.text.input_tokens | 100 | Text only |
gen_ai.usage.text.cache_read.input_tokens | 40 | Cached text |
gen_ai.usage.image.input_tokens | 200 | Image only |
Two consequences follow. First, never add cache_read to input_tokens when you compute cost; it is already counted. Second, when a provider reports both billed units and raw model tokens, the specification says to report the billed count so the number matches what you are charged for.
A second example from the walkthrough later in this post: an agent makes two chat calls. The first uses 412 input and 38 output tokens and ends with finish reason tool_call. The second uses 471 input and 22 output tokens and ends with stop. The agent span sums them: 883 input tokens and 60 output tokens. The second call's input is larger because it carries the first question, the model's tool call and the tool result. That growth per turn is why long agent runs get expensive, and why token metrics per agent invocation are worth having.
Metrics and events for model calls
Spans describe one call; metrics describe all of them cheaply. The metrics documents define, among others:
| Metric | What it measures |
|---|---|
gen_ai.client.inference.duration | Duration of inference calls from the client side |
gen_ai.client.inference.time_to_first_chunk | Time to the first streamed chunk |
gen_ai.client.inference.time_per_output_chunk | Time between streamed chunks |
gen_ai.client.inference.usage.input_tokens and .output_tokens | Token counts, with separate cache read, cache write and reasoning variants |
gen_ai.client.inference.operation.input_tokens and .output_tokens | Per operation token counts, recorded for every operation including failed ones (0 when no response arrived) |
gen_ai.client.operation.duration | Duration of other client GenAI operations |
gen_ai.server.request.duration, gen_ai.server.time_to_first_token, gen_ai.server.time_per_output_token | The same view from a model server you run yourself |
gen_ai.invoke_agent.duration, gen_ai.invoke_agent.inference_calls, gen_ai.invoke_agent.tool_calls | How long agent invocations take and how many model and tool calls each makes |
gen_ai.invoke_workflow.duration, gen_ai.execute_tool.duration | Workflow and tool durations |
Duration histograms use explicit bucket boundaries from 0.01 seconds doubling up to 81.92 seconds. That range fits model calls, which can take tens of seconds, much better than default HTTP buckets.
The main event is gen_ai.client.inference.operation.details. It carries the request parameters and, when content capture is on, the chat history and output. The specification says it can store input and output details independently from traces and should be logged at DEBUG severity (severity number 5). There is also gen_ai.evaluation.result for quality scores from an evaluator, parented to the span being evaluated, and gen_ai.client.operation.exception for errors.
The events route matters for security. Events travel through your logs pipeline, which may have different retention, different access controls and a different backend from your traces. Decide on purpose where content goes.
How are agent and tool executions represented?
An agent is a loop: think, call a model, maybe call a tool, feed the result back, repeat. The agent spans document gives each part of that loop a span.
| Span | Operation name | Kind | Span name | When it is used |
|---|---|---|---|---|
| Create agent | create_agent | CLIENT | create_agent {gen_ai.agent.name} | Registering an agent with a remote agent service |
| Invoke agent (client) | invoke_agent | CLIENT | invoke_agent {gen_ai.agent.name} | Calling an agent that runs as a remote service |
| Invoke agent (internal) | invoke_agent | INTERNAL | invoke_agent {gen_ai.agent.name} | Running an agent inside your own process, through a framework |
| Invoke workflow | invoke_workflow | INTERNAL | invoke_workflow {gen_ai.workflow.name} | A multi step or multi agent workflow |
| Plan | plan | INTERNAL | plan {gen_ai.agent.name} | A planning or task decomposition phase |
| Execute tool | execute_tool | INTERNAL | execute_tool {gen_ai.tool.name} | One tool call |
| Load skill, read skill resource | execute_tool refinements | INTERNAL | execute_tool {gen_ai.tool.name} {gen_ai.skill.name} and variants | Agent skills loaded or read as tools |
| Command execution | execute_tool refinement | INTERNAL | execute_tool {gen_ai.tool.name} {process.executable.name} and variants | A tool that runs shell commands or skill scripts |
When the agent name is not available, the invoke and plan span names fall back to the bare operation name.
The invoke agent spans carry the same request and usage attributes as an inference span, plus agent identity: gen_ai.agent.name, gen_ai.agent.id, gen_ai.agent.description and gen_ai.agent.version. Like the inference span, they list gen_ai.input.messages, gen_ai.output.messages, gen_ai.system_instructions and gen_ai.tool.definitions as Opt-In.
The execute tool span in detail
The execute tool span is where agents touch the real world, so it is the span security teams care about most.
| Attribute | Requirement level | What it holds |
|---|---|---|
gen_ai.operation.name | Required | execute_tool |
gen_ai.tool.name | Required | For example get_weather |
gen_ai.tool.call.id | Conditionally Required, only if the model supplied one | Joins the tool span to the model's tool call |
gen_ai.tool.type | Recommended | function (client side), extension (agent side call to external APIs) or datastore (retrieval) |
gen_ai.tool.description | Recommended | Marked in the specification as possibly sensitive |
gen_ai.agent.name, gen_ai.conversation.id | Conditionally Required | Which agent ran it, in which conversation |
gen_ai.tool.call.arguments | Opt-In | The arguments the model chose, marked as possibly sensitive |
gen_ai.tool.call.result | Opt-In | The tool's return value, marked as possibly sensitive |
error.type | Conditionally Required on error | Why the tool failed |
The specification asks framework instrumentations to trace tool calls, and encourages application developers to follow the same convention when their own code runs tools. It also warns against two spans for one call: if a framework and an MCP instrumentation both see the same tool call, only one should record it as the tool span.
The command execution refinement is new and important for coding agents. It applies when a tool runs one or more commands, including a skill's script. The specification gives examples from several agent SDKs, including a client bash tool in Anthropic's tool use documentation. It also says instrumentations may add CLI client spans for each observed command or process. That gives you a trace record of which commands an agent ran, which is useful for incident review and very sensitive in its own right, since command lines often contain file paths, hostnames and sometimes credentials.
What a full agent trace looks like
A simple run of an in-process agent that answers one question with one tool call produces a tree like this:
invoke_agent weather-agent INTERNAL gen_ai.usage.input_tokens=883
├── chat demo-model-1 CLIENT finish_reasons=["tool_call"] in=412 out=38
├── execute_tool get_weather INTERNAL gen_ai.tool.call.id=call_001
└── chat demo-model-1 CLIENT finish_reasons=["stop"] in=471 out=22
If the HTTP client library is also instrumented, each chat span gets a child HTTP span for the actual request. If the tool is an MCP tool, the execute_tool span (or the MCP client span, but not both) leads to an MCP server span in another process, joined by trace context.
Claude Code is a real example of this structure, with its own span names. Its monitoring documentation describes a claude_code.interaction root span for each user prompt, with claude_code.llm_request, claude_code.hook and claude_code.tool children. Each tool span has two children: claude_code.tool.blocked_on_user for the time spent waiting on a permission decision and claude_code.tool.execution for the work itself. Subagent spans nest under the parent's tool span. Tracing there is a beta feature and is off by default: you enable it with CLAUDE_CODE_ENABLE_TELEMETRY=1 and CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 and choose an exporter with OTEL_TRACES_EXPORTER.
How do traces link across MCP hops?
Trace context normally travels in HTTP headers. The W3C Trace Context traceparent header carries a version, a 16 byte trace ID, an 8 byte parent ID and trace flags, such as 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01. A server that receives it creates its span as a child.
MCP breaks that model in two ways. It runs over stdio as well as HTTP, and stdio has no headers. And with Streamable HTTP, one HTTP request can carry several MCP messages, while one MCP request can span several HTTP requests if it is retried. The MCP conventions therefore say:
- Instrumentations should inject trace context into each MCP request or notification, in the
params._metaproperty bag. - The keys are written unprefixed:
traceparent,tracestateandbaggage, even though MCP normally expects DNS prefixed keys in_meta. The MCP proposal SEP-414 defines this handling. - The MCP server should use the context from
params._metaas the parent of its server span, and should link (not parent) any ambient transport context, such as the HTTP request's own trace. - The MCP client span becomes the parent of the MCP server span regardless of transport.
A tool call request with context looks like this:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get-weather",
"arguments": { "city": "Lisbon" },
"_meta": {
"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}
}
}
MCP spans are named {mcp.method.name} {target}, with kind CLIENT on the client and SERVER on the server. The conventions recommend these MCP spans over the generic RPC conventions, because they record message exchanges that RPC and HTTP spans miss. There are also four metrics: mcp.client.operation.duration, mcp.server.operation.duration, mcp.client.session.duration and mcp.server.session.duration.
Claude Code follows the same idea outward. Its documentation says outbound HTTP MCP requests carry traceparent, model requests to the Anthropic API carry a traceparent header set to the claude_code.llm_request span's context, and commands Claude Code runs receive a TRACEPARENT environment variable so their own spans join the same trace. In Agent SDK and non interactive -p sessions it also reads TRACEPARENT and TRACESTATE from its environment, while interactive sessions ignore inbound TRACEPARENT to avoid inheriting stray values from CI or container environments. That last choice is a good pattern to copy.
And across A2A hops?
The Agent2Agent (A2A) protocol runs over HTTP based transports. Its specification lists tracing among the enterprise practices it aligns with, but defines no protocol field for trace context, and the GenAI repository has no A2A document today. In practice you use standard W3C Trace Context on the HTTP request: the caller's invoke_agent client span is the parent of the remote agent's server spans. If a hop strips headers, agree on your own carrier and document it.
How are prompts and completions captured, and why is content capture opt in?
The specification is blunt: model instructions, user messages and model outputs are considered sensitive and are often large. It names three problems with recording them in telemetry: storage cost, regulatory requirements, and the need for different access models for operational data and user data. So instrumentations should not capture content by default and should offer an opt in.
It then describes three patterns, in order of maturity:
| Pattern | What is recorded | Where the specification says it fits |
|---|---|---|
| 1. Default | No instructions, inputs or outputs | Everywhere, unless you have a reason |
| 2. Content on spans | gen_ai.system_instructions, gen_ai.input.messages, gen_ai.output.messages as span attributes | When volume is manageable and privacy rules do not apply or the store complies with them, for example pre-production |
| 3. External storage with references | Content uploaded elsewhere; the span records a reference | Recommended for production when volume is a concern or sensitive data must be handled securely, because external storage allows separate access controls |
The message format
When content is captured, it follows JSON schemas published with the conventions. Messages have a role (system, user, assistant, tool) and a list of parts. Part types include text, tool_call, tool_call_response, server_tool_call, server_tool_call_response, reasoning, compaction, blob, file and uri. A user turn and a tool round trip look like this:
[
{"role": "user", "parts": [{"type": "text", "content": "What is the weather in Lisbon?"}]},
{"role": "assistant", "parts": [{"type": "tool_call", "id": "call_001",
"name": "get_weather", "arguments": {"city": "Lisbon"}}]},
{"role": "tool", "parts": [{"type": "tool_call_response", "id": "call_001",
"response": {"temp_c": 21, "conditions": "sunny"}}]}
]
Note the reasoning part type. If your model returns reasoning text and content capture is on, that text lands in telemetry too. Reasoning often restates the user's data in new words, which defeats redaction rules that only look for the original strings.
Structured attribute values are supported on events (logs) but may not yet be supported on spans in every language. In that case the specification says to serialize the value to a JSON string on spans and keep the structured form on events. It also allows a truncation option that shortens individual message contents while keeping the JSON structure.
The upload hook
For pattern 3, instrumentations may support an in-process hook. The details matter:
- The hook works independently of the content capture flags.
- If configured, it is called regardless of the span sampling decision, with the instructions, inputs and outputs before they are serialized, plus the span.
- The hook may modify the span and the messages. If content attributes are also enabled, they are recorded after the hook runs, using the modified values.
That last point makes the hook the right place for in-process redaction: replace an email address in the message objects, and both the uploaded copy and any span attribute get the redacted version. The "regardless of sampling" point cuts the other way: a 1 percent trace sample does not mean 1 percent of prompts are uploaded. Every prompt goes to the upload target.
How real instrumentations expose the opt in
The OpenTelemetry Python GenAI instrumentations use one environment variable, OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT. In the current opentelemetry-instrumentation-genai-openai package the values are no_content (the default), span_only, event_only and span_and_event. The upload hook is enabled with OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK=upload and a destination in OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH, which accepts a local path or an fsspec compatible URI.
Packaging has moved too. The older opentelemetry-instrumentation-openai-v2 package is marked deprecated and only receives security patches; its README points to opentelemetry-instrumentation-genai-openai, in a separate opentelemetry-python-genai repository, with breaking changes. In the older package, true was a legacy value that put content on the old per message events, so an upgrade can move content between your logs and traces pipelines.
Claude Code has its own switches, which its documentation describes in detail. By default, user prompt events record only the prompt length. OTEL_LOG_USER_PROMPTS=1 adds prompt text, and the documentation notes that the text then appears in two attributes, prompt and prompt_text, so a collector rule must drop both. OTEL_LOG_ASSISTANT_RESPONSES adds response text, OTEL_LOG_TOOL_DETAILS adds tool parameters such as Bash commands and MCP tool names, and OTEL_LOG_RAW_API_BODIES emits full API request and response bodies, which the documentation says include the entire conversation history. The documentation also states that this telemetry goes only to the OpenTelemetry endpoint you configure, never to Anthropic.
What are the privacy and security risks?
Turning content capture on changes what your observability stack is. It stops being a store of timings and counters and becomes a store of conversations. OWASP's 2025 Top 10 for LLM applications lists sensitive information disclosure as LLM02 and names personal data, financial details, health records, confidential business data, security credentials and legal documents among the information at risk. Every one of those can arrive in a prompt.
Personal data in prompts and tool results
Users paste names, addresses, account numbers and support histories into assistants. Retrieval tools pull customer records into context. Tool results return database rows. With content capture on, all of it is copied into spans or events and shipped to wherever your telemetry goes. The OpenTelemetry project's own guidance on handling sensitive data starts with data minimisation: only collect data that serves an observability purpose, avoid personal information unless it is truly necessary, and regularly review collected attributes.
Secrets in prompts, arguments and command lines
Developers paste API keys and connection strings into coding assistants to ask why a call fails. Agents read configuration files and environment variables into context. The command execution span and tool argument attributes can hold full command lines. None of this is hypothetical; it is how people use these tools. Treat any content attribute as able to contain credentials, and make sure your trace store is not readable by more people than the systems those credentials unlock.
Retention and copies
Telemetry is copied often: SDK buffers, the collector's sending queue, debug file exporters, hot and cold backend storage, and analysis exports. Each copy has its own retention, so a prompt your application deletes after 30 days may live for a year in a trace archive. If you have deletion obligations, map every place a span can land.
Multi tenant collectors and backends
A shared collector that serves many teams or many customers is a mixing point. Without care, one tenant's prompts are visible in another tenant's trace search, or a single debug exporter writes everyone's content to one file. Keep content bearing pipelines separate, tag every span with a tenant attribute at the source, route by it, and check that your backend's access controls actually filter on it.
Trace context as an information channel
Trace context itself carries some risk, and the W3C specification is explicit about it:
- Correlation.
traceparentlets a downstream service correlate requests in one transaction. The specification calls this risk theoretical, but allows services to restart the trace at a boundary. - Information in
tracestate. The specification warns thattracestatemay reveal things like the monitoring software version, and says application owners should either keep confidential information out of it or not send it to external systems. - Baggage. W3C Baggage propagates key value pairs to every downstream service, including third party model and tool endpoints. The MCP conventions' own example shows
userId=alicein baggage. That is fine inside your network and a leak when the next hop is someone else's MCP server. - Denial of service. The specification notes that a public API which naively continues every incoming trace with the sampled flag set lets an attacker overwhelm it with tracing overhead. Agents that accept requests from the internet should not trust incoming sampling decisions blindly.
Telemetry as an injection surface
Content attributes hold text that a user or a model wrote. Dashboards and trace viewers render it. Treat that text as untrusted: a trace viewer that renders message content as HTML has the same problems as any other page that renders user input. And anything that reads traces back into a model, such as an "explain this trace" feature, is reading attacker controllable text. That is prompt injection by way of your observability stack.
How do redaction and filtering work in the collector?
The specification's ordering is the right one: do not capture what you do not need, redact in process where you can, and use the collector as a second line. The Collector's own redaction processor README says the processor is intended as "one line of defence rather than the only compliance measure in place."
There are five processors to know. All are in the contrib distribution.
| Processor | Stability (traces) | What it does | Use it for |
|---|---|---|---|
redaction | Beta (logs and metrics alpha) | Deletes attributes not on an allowlist; masks or hashes values that match blocked patterns | Fail closed allowlists; masking card numbers, emails, key shaped strings |
transform | Beta | Runs OTTL statements on spans, logs and metrics | Deleting specific keys, regex replacement, truncation, conditional edits |
attributes | Beta | Insert, update, delete or hash attributes by key or regex | Simple deletes and hashing |
filter | Alpha | Drops whole spans, logs or metric points that match conditions | Dropping noisy or content heavy records entirely |
tail_sampling | Beta | Buffers whole traces and decides which to keep | Keeping errors and slow runs, sampling the rest |
The redaction processor
Its design is allowlist first. If allowed_keys is empty and allow_all_keys is false, every attribute is removed. The README describes this as designed to fail closed. On the attributes that remain, blocked_values regular expressions mask matching parts of values, and blocked_key_patterns mask whole values whose keys match. allowed_values takes precedence over blocked_values, which is useful and also easy to get wrong.
Instead of masking with asterisks, you can set hash_function to md5, sha1, sha3, hmac-sha256 or hmac-sha512. The README recommends HMAC for low entropy data such as IP addresses, because a plain hash of a small input space can be reversed by trying every value. The same applies to email addresses and user IDs.
The summary option controls the diagnostic attributes the processor adds about what it removed. The README points out that in some contexts a list of redacted attribute names leaks information, so silent or info is safer in production than debug.
One detail for the logs pipeline: the processor also handles log records whose body is a map, and reports how many body keys it removed or masked. If an instrumentation puts content in a plain string body instead, test the processor against your real records, or handle the body with the transform processor.
The transform processor
OTTL statements are the precise tool for GenAI content. To drop all content attributes from spans:
processors:
transform/drop-genai-content:
error_mode: ignore
trace_statements:
- delete_key(span.attributes, "gen_ai.system_instructions")
- delete_key(span.attributes, "gen_ai.input.messages")
- delete_key(span.attributes, "gen_ai.output.messages")
- delete_key(span.attributes, "gen_ai.tool.definitions")
- delete_matching_keys(span.attributes, "^gen_ai\\.tool\\.call\\.(arguments|result)$")
To keep content but mask email addresses and shorten long values:
processors:
transform/mask-genai-content:
error_mode: ignore
trace_statements:
- replace_pattern(span.attributes["gen_ai.input.messages"], "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}", "[email]")
- replace_pattern(span.attributes["gen_ai.output.messages"], "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}", "[email]")
- truncate_all(span.attributes, 4096)
Be careful with truncate_all on JSON strings: it cuts at a byte count and can leave invalid JSON. The specification's own truncation option works inside the instrumentation and preserves structure, so prefer that where it exists. Also note that error_mode: ignore (the default when unset) means a broken statement is skipped with a log line rather than stopping the pipeline. That keeps telemetry flowing, and it also means a typo in a redaction rule fails open. Test rules with sample data before you rely on them.
What should be sampled or dropped?
| Data | Keep at full rate | Sample | Drop or keep out of the pipeline |
|---|---|---|---|
| GenAI metrics (durations, token counts) | Yes, they are cheap and aggregate | No | No |
| Inference and tool spans without content | Errors, slow runs, unusual finish reasons (content_filter, length, error) | Successful routine runs | No |
| Content attributes in production | Only with a written purpose and short retention | Not a substitute for redaction | By default |
| Content in pre-production | Acceptable with synthetic or test data | Optional | Real customer data |
| Tool arguments and results | When needed for incident review, behind restricted access | Optional | Arguments from tools that touch credentials or files |
| Raw API bodies and full command lines | Rarely; time boxed debugging only | No | By default |
tracestate and baggage to external endpoints | No | No | Strip at the boundary |
Tail sampling is the natural fit for agents, because you only know whether a run was interesting at the end. The tail sampling processor buffers spans per trace (decision_wait defaults to 30 seconds and num_traces to 50,000) and then applies policies such as status_code, latency and probabilistic. Two cautions: agent runs that last longer than decision_wait get decided before they finish, and the buffer holds content in memory if content capture is on. Redact before tail sampling, not after.
Step by step: instrument a small agent, export to a local collector and redact content
This walkthrough builds a tiny agent with one tool, instruments it by hand with the GenAI conventions, sends traces to a local Collector over OTLP, and removes content in the Collector. The model call is a stand-in function, so it runs offline without an API key. When you use a real provider SDK, an instrumentation package can create the inference spans for you; the attributes are the same.
Step 1: create an environment and install the SDK
python3 -m venv .venv
. .venv/bin/activate
pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
pip list | grep opentelemetry
We ran this with Python 3.14 and got opentelemetry-sdk 1.45.1. Pin exact versions in a lock file for anything you deploy.
Step 2: write the agent
Save this as agent.py. Content capture is controlled by AGENT_CAPTURE_CONTENT and is off unless you set it, which mirrors the convention's default.
import json
import os
import time
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
from opentelemetry.trace import SpanKind, Status, StatusCode
CAPTURE = os.getenv("AGENT_CAPTURE_CONTENT", "false").lower() == "true"
provider = TracerProvider(resource=Resource.create({"service.name": "weather-agent"}))
if os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"):
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
else:
provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("weather-agent", "0.1.0")
MODEL = "demo-model-1"
def fake_model(messages):
"""Stand-in for a provider SDK call, so the example runs offline."""
time.sleep(0.05)
if messages[-1]["role"] == "user":
return {"id": "resp_001", "model": MODEL + "-2026-01",
"finish_reason": "tool_call", "usage": {"in": 412, "out": 38},
"tool_call": {"id": "call_001", "name": "get_weather",
"arguments": {"city": "Lisbon"}}}
return {"id": "resp_002", "model": MODEL + "-2026-01",
"finish_reason": "stop", "usage": {"in": 471, "out": 22},
"text": "It is 21 C and sunny in Lisbon."}
def chat(messages):
with tracer.start_as_current_span(
f"chat {MODEL}",
kind=SpanKind.CLIENT,
attributes={
"gen_ai.operation.name": "chat",
"gen_ai.provider.name": "demo",
"gen_ai.request.model": MODEL,
"gen_ai.request.max_tokens": 512,
"server.address": "llm.internal.example",
"server.port": 443,
},
) as span:
if CAPTURE:
span.set_attribute("gen_ai.input.messages", json.dumps(messages))
r = fake_model(messages)
span.set_attribute("gen_ai.response.id", r["id"])
span.set_attribute("gen_ai.response.model", r["model"])
span.set_attribute("gen_ai.response.finish_reasons", [r["finish_reason"]])
span.set_attribute("gen_ai.usage.input_tokens", r["usage"]["in"])
span.set_attribute("gen_ai.usage.output_tokens", r["usage"]["out"])
if CAPTURE and "text" in r:
span.set_attribute("gen_ai.output.messages", json.dumps(
[{"role": "assistant", "parts": [{"type": "text", "content": r["text"]}]}]))
return r
def get_weather(call):
with tracer.start_as_current_span(
"execute_tool get_weather",
kind=SpanKind.INTERNAL,
attributes={
"gen_ai.operation.name": "execute_tool",
"gen_ai.tool.name": "get_weather",
"gen_ai.tool.type": "function",
"gen_ai.tool.call.id": call["id"],
},
) as span:
if CAPTURE:
span.set_attribute("gen_ai.tool.call.arguments", json.dumps(call["arguments"]))
result = {"temp_c": 21, "conditions": "sunny"}
if CAPTURE:
span.set_attribute("gen_ai.tool.call.result", json.dumps(result))
return result
def run(question):
with tracer.start_as_current_span(
"invoke_agent weather-agent",
kind=SpanKind.INTERNAL,
attributes={
"gen_ai.operation.name": "invoke_agent",
"gen_ai.provider.name": "demo",
"gen_ai.agent.name": "weather-agent",
"gen_ai.request.model": MODEL,
},
) as agent_span:
user = {"role": "user", "parts": [{"type": "text", "content": question}]}
try:
first = chat([user])
call = first["tool_call"]
result = get_weather(call)
final = chat([user,
{"role": "assistant", "parts": [{"type": "tool_call", "id": call["id"],
"name": call["name"],
"arguments": call["arguments"]}]},
{"role": "tool", "parts": [{"type": "tool_call_response",
"id": call["id"], "response": result}]}])
agent_span.set_attribute("gen_ai.usage.input_tokens",
first["usage"]["in"] + final["usage"]["in"])
agent_span.set_attribute("gen_ai.usage.output_tokens",
first["usage"]["out"] + final["usage"]["out"])
return final["text"]
except Exception as exc:
agent_span.set_status(Status(StatusCode.ERROR))
agent_span.set_attribute("error.type", type(exc).__name__)
raise
if __name__ == "__main__":
print(run("What is the weather in Lisbon? My email is jane@example.com"))
provider.shutdown()
A few choices in this code follow the specification directly:
- The sampling relevant attributes (
gen_ai.operation.name,gen_ai.provider.name,gen_ai.request.model,server.address,server.port) are passed when the span is created, not set afterwards. gen_ai.provider.nameisdemobecause the stand-in is not a real provider. Use a well known value such asopenaioranthropiconly when that is the SDK you are calling.- Messages use the
rolepluspartsshape from the conventions' JSON schemas, serialized to a JSON string on spans. - The tool span records
gen_ai.tool.call.idonly because the model supplied one. gen_ai.conversation.idis not set, because this agent has no real conversation identifier.
Step 3: run it with the console exporter
python agent.py
With no OTLP endpoint set, the script prints spans to the console. You should see four spans: one invoke_agent weather-agent root, two chat demo-model-1 children of kind CLIENT, and one execute_tool get_weather child. In our run, the first chat span showed 412 input and 38 output tokens with finish reason tool_call, the second 471 and 22 with stop, and the agent span 883 input tokens in total. No message content appears.
Now turn capture on and look again:
AGENT_CAPTURE_CONTENT=true python agent.py | grep -E 'input.messages|output.messages|tool.call'
The gen_ai.input.messages attribute now contains the full question, including the email address the user typed. The tool arguments and result appear as well. This is exactly the data the rest of the walkthrough removes.
Step 4: write a collector configuration that redacts
Save this as otelcol.yaml. It receives OTLP over HTTP, deletes content attributes, masks email addresses in anything that survives, and prints the result.
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
processors:
memory_limiter:
check_interval: 1s
limit_mib: 512
transform/drop-genai-content:
error_mode: ignore
trace_statements:
- delete_key(span.attributes, "gen_ai.system_instructions")
- delete_key(span.attributes, "gen_ai.input.messages")
- delete_key(span.attributes, "gen_ai.output.messages")
- delete_key(span.attributes, "gen_ai.tool.definitions")
- delete_matching_keys(span.attributes, "^gen_ai\\.tool\\.call\\.(arguments|result)$")
redaction/genai:
allow_all_keys: true
blocked_values:
- "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
summary: info
batch: {}
exporters:
debug:
verbosity: detailed
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, transform/drop-genai-content, redaction/genai, batch]
exporters: [debug]
allow_all_keys: true is used here so the redaction processor only masks values; the transform processor has already removed the content keys. For a stricter setup, set allow_all_keys: false and list every attribute you want to keep in allowed_keys. That fails closed when a new instrumentation version adds an attribute you have not reviewed.
Step 5: run the collector
Run the contrib distribution, which includes the transform and redaction processors. With a container runtime:
docker run --rm -p 127.0.0.1:4318:4318 \
-v "$PWD/otelcol.yaml:/etc/otelcol-contrib/config.yaml:ro" \
otel/opentelemetry-collector-contrib:0.163.2
The receiver listens on all interfaces inside the container, and the 127.0.0.1: prefix on -p keeps the port reachable only from your own machine. If you run the Collector binary directly instead, change the receiver endpoint to 127.0.0.1:4318 so it is not exposed to your network. Version 0.163.2 was the latest Collector release on 21 October 2026. Pin by digest in real deployments. If you installed the otelcol-contrib binary instead, validate and run with:
otelcol-contrib validate --config=otelcol.yaml
otelcol-contrib --config=otelcol.yaml
Step 6: send traces with content capture on, and check they are clean
export OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318
AGENT_CAPTURE_CONTENT=true python agent.py
The Python OTLP HTTP exporter appends /v1/traces to the base endpoint. In the Collector's debug output you should see the same four spans with token counts, finish reasons and tool names, and no gen_ai.input.messages, gen_ai.output.messages or tool argument attributes. If the user had typed an email address into an attribute you kept on purpose, it would show as asterisks.
Test the failure case too: remove the transform processor from the pipeline, restart, and run again. The content returns, which proves the rule is doing the work. Turn that into an automated test.
Step 7: move redaction into the process
The Collector is the second line. The first is the application. In this example, the simplest in-process control is the flag itself. With an instrumentation package, keep OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT at no_content in production, and if you need content for evaluation, use the upload hook to send redacted copies to a separate store with its own access policy. Content that never leaves the process cannot leak from the pipeline.
GenAI spans, HTTP client spans and vendor specific LLM logs compared
Many teams already have some LLM telemetry before they adopt the GenAI conventions. Here is how the three common sources compare by what they record.
| What you want to know | Ordinary HTTP client span | Vendor specific LLM log or SDK event | GenAI semantic convention spans |
|---|---|---|---|
| That a call happened, its duration and status | Yes | Yes | Yes |
| Endpoint host and port | Yes | Usually | Yes, server.address and server.port |
| Which model was requested and which answered | No, unless parsed from the body | Yes, in the vendor's field names | Yes, gen_ai.request.model and gen_ai.response.model |
| Token usage with cache and reasoning breakdown | No | Yes, vendor specific | Yes, standard names and arithmetic rules |
| Why generation stopped | No | Yes | Yes, gen_ai.response.finish_reasons |
| Agent, workflow and tool structure | No, just a list of HTTP calls | Sometimes, framework specific | Yes, invoke_agent, plan, execute_tool |
| Cross process linkage over MCP stdio | No | No | Yes, context in params._meta |
| Prompt and completion text | No (bodies are not recorded by HTTP conventions) | Often on by default or by a single switch | Opt-In only, with a defined schema |
| Portable across providers and backends | Yes | No | Yes, but at Development stability |
| Main risk | Too little context | Content captured by default; lock in | Version churn; content captured once someone flips the flag |
Keep HTTP spans (they show retries, TLS errors and proxies), add GenAI spans as their parents, and stop relying on vendor specific logs for cross provider queries.
Common mistakes
- Treating Development as Stable. Building alerts on attribute names without recording which convention version each service emits. The
gen_ai.systemtogen_ai.provider.namerename and the message attribute change will not be the last. - Linking to the old docs. The core repository's
docs/gen-aipages are now "Moved" stubs. Use the GenAI repository or the opentelemetry.io pages generated from it. - Turning content capture on in production to debug one issue, and leaving it on. Time box it, scope it to one service, and put an expiry in the ticket.
- Dropping only one of two copies. Content may be on spans, on events, or both, depending on
span_only,event_onlyorspan_and_event. Claude Code puts prompt text in bothpromptandprompt_text. Redact every copy. - Assuming sampling limits content exposure. Upload hooks run regardless of the sampling decision, and tail samplers buffer everything before they decide.
- Hashing low entropy values with plain hashes. A SHA hash of an email address or IP is reversible by guessing. Use HMAC with a secret key, or drop the value.
- Shipping redaction rules without a test.
allowed_valuesoverridesblocked_values, anderror_mode: ignoreskips a broken OTTL statement while data keeps flowing. Send a known sensitive string through the pipeline and fail the build if it comes out. - Inventing conversation IDs. Using the trace ID or a hash of the prompt as
gen_ai.conversation.idis explicitly discouraged, and the hash is a fingerprint of the content. - Propagating baggage to third party tools. User identifiers in baggage travel to every MCP server and model endpoint downstream. Strip it at the boundary.
- Reading
gen_ai.provider.nameas proof of where data went. It reflects the SDK, not the upstream behind a proxy. - Rendering message content as HTML in internal tools. Trace viewers and evaluation dashboards should treat content as untrusted text.
FAQ
Are the OpenTelemetry GenAI semantic conventions stable?
No. As of 24 October 2026, every GenAI document, span, metric and attribute is marked Development. Instrumentations usually need OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental to emit the newest shape, and the Python OpenAI instrumentation README warns that experimental features will bring breaking changes.
Where do the GenAI semantic conventions live now?
In the open-telemetry/semantic-conventions-genai GitHub repository. Semantic conventions v1.42.0 deprecated the GenAI, OpenAI and MCP definitions in the core repository and moved them there. The opentelemetry.io specification pages for GenAI are still the readable entry point.
What replaced gen_ai.system?
gen_ai.provider.name, from semantic conventions v1.37.0. Well known values include openai, anthropic, aws.bedrock, azure.ai.openai and gcp.vertex_ai, among others.
Does OpenTelemetry record my prompts by default?
Instrumentations that follow the conventions should not. Instructions, input messages, output messages, tool definitions, tool arguments and tool results are Opt-In. Check each library you use, because a library that does not follow the conventions may behave differently.
How do I turn on prompt capture in the Python GenAI instrumentation?
Set OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT to span_only, event_only or span_and_event. The default is no_content. For production, the conventions recommend storing content externally with references on spans, which the upload hook supports.
How does trace context pass through an MCP server?
The client injects traceparent, tracestate and baggage into the request's params._meta, unprefixed. The server uses that context as the parent of its server span and links any transport level context. This works over stdio, where there are no HTTP headers.
How do I remove prompts from traces in the OpenTelemetry Collector?
Use the transform processor with delete_key on gen_ai.input.messages, gen_ai.output.messages, gen_ai.system_instructions and gen_ai.tool.definitions, and delete_matching_keys for tool arguments and results. Add the redaction processor with an allowlist for a fail closed second layer, and remember to handle events in the logs pipeline as well.
Should I record token usage on spans or metrics?
Both, for different jobs. Metrics are cheap and good for cost and capacity dashboards. Span attributes let you find the single run that used 200,000 tokens. Remember that gen_ai.usage.input_tokens already includes cached tokens.
Can I trace Claude Code with OpenTelemetry?
Yes. Its documentation describes metrics and events, and a beta tracing mode enabled with CLAUDE_CODE_ENABLE_TELEMETRY=1, CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 and OTEL_TRACES_EXPORTER. Prompt text, response text, tool details and raw API bodies each have their own switch and are off by default.
Where Safeguard fits
Safeguard is not an observability product. It does not collect traces, run a collector or redact telemetry, and nothing in this post depends on it. The fit is with the software that does that work. OpenTelemetry SDKs, GenAI instrumentation packages, exporters and Collector images are open source dependencies, and in the GenAI area they are young, versioned as betas and changing quickly: the Python OpenAI instrumentation has already been deprecated in favour of a new package with breaking changes. Safeguard scans repositories, container images and SBOM files (safeguard scan --dir ., --image <name>, --sbom <file>), shows which projects carry a given component and version, matches them against vulnerability data and applies policies and gates. That tells you which services still run the deprecated instrumentation, or an old Collector image, when a fix ships. Remediation pull requests are available and switched off by default.
Fix it without waiting for upstream
Observability pipelines are built from open source parts: SDKs, instrumentation libraries, exporters and Collector distributions, all pulled in as dependencies and container images and rebuilt on every release. When one of those parts has a vulnerability, the fix path is the same as for any other dependency, so the habits below apply to your telemetry stack as much as to your application.
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
The first demo is a real session with the Safeguard CLI. It shows the assistant reading a finding, asking before each command, upgrading a dependency and running the tests, without pushing anything. The same flow applies when an OpenTelemetry instrumentation package or exporter in your agent service needs an upgrade.
Click through it at your own pace, or open it full screen.
The second demo sets up the local runner on your own machine, approves it with a device code and scans from the terminal. It is a good fit for checking the dependencies and images that make up a self hosted Collector before you deploy them.
Click through it at your own pace, or open it full screen.
The third demo connects Claude to the Safeguard MCP server with OAuth and lets you choose which tools Claude can use. Those tool calls are MCP requests, so they are exactly the kind of hop the MCP conventions above describe, and choosing a small tool set is the same least privilege habit that keeps tool arguments out of places they should not be.
Click through it at your own pace, or open it full screen.
Download the apps at safeguard.sh/download and read the CLI docs at docs.safeguard.sh/docs/cli.
Quick check
Your agent's traces are sampled at 1 percent and the upload hook is configured to send content to object storage. What share of prompts reaches the storage bucket?
All of them. The specification says the upload hook should be invoked regardless of the span sampling decision. Sampling controls how many spans you keep, not how much content you upload. Redact inside the hook, and give the bucket its own retention and access policy.
A collector rule deletes gen_ai.input.messages from spans, but prompt text still shows up in your logging backend. What is the likely cause?
Content is also being captured on events. With event_only or span_and_event, or with an event such as gen_ai.client.inference.operation.details, the same messages travel through the logs pipeline. Add matching rules to the logs pipeline, or set capture to no_content at the source.
Sources
- OpenTelemetry: Semantic conventions for generative AI systems
- GitHub: open-telemetry/semantic-conventions-genai repository
- GenAI conventions: index (docs/gen-ai/README.md)
- GenAI conventions: Client inference spans, metrics and events
- GenAI conventions: Model spans and content capture
- GenAI conventions: Agent spans
- GenAI conventions: Events
- GenAI conventions: Metrics
- GenAI conventions: Inference token metrics
- GenAI conventions: Model Context Protocol
- GenAI conventions: LLM call examples
- GenAI conventions: message models (models.py)
- Semantic conventions CHANGELOG (v1.37.0 and v1.42.0 entries)
- Semantic conventions: "Moved" notice in docs/gen-ai
- Semantic conventions releases
- W3C Trace Context (Recommendation)
- W3C Baggage
- OpenTelemetry: Handling sensitive data
- OpenTelemetry: Document status definitions
- OpenTelemetry Collector documentation
- Collector contrib: Redaction processor README
- Collector contrib: Transform processor README
- Collector contrib: Attributes processor README
- Collector contrib: Filter processor README
- Collector contrib: Tail sampling processor README
- OpenTelemetry Collector releases
- OpenTelemetry Python contrib: OpenAI instrumentation README (deprecated package)
- OpenTelemetry Python GenAI: opentelemetry-instrumentation-genai-openai README
- PyPI: opentelemetry-instrumentation-genai-openai
- Claude Code documentation: Monitoring
- A2A protocol specification
- OWASP Top 10 for LLM Applications 2025: LLM02 Sensitive Information Disclosure
Last reviewed: 24 October 2026. The GenAI semantic conventions are at Development stability and change often; check the GenAI repository and your instrumentation's changelog before you build dashboards or redaction rules on a specific attribute name.