If you run an LLM in production, OpenTelemetry can trace it the same way it traces the rest of your stack. This guide shows how to instrument an LLM agent using the GenAI semantic conventions, the shared vocabulary that every major vendor now maps to: the gen_ai.*
attributes plus the chat
, execute_tool
and invoke_agent
spans. It covers zero-code auto-instrumentation, manual spans for agent and tool structure, token usage and cost, grouping multi-turn conversations, and where to send the result. None of it ties you to a vendor, so the data stays yours.
Every step below was run end to end against a local model and a local IceGate, and the runnable recipes are public in our integrations cookbook. IceGate is our open-source observability engine: it receives OTLP natively and stores traces as Apache Parquet and Iceberg files on object storage you own. It is where the traces land in this guide. Any other OTLP backend works the same way, so you can follow along with Jaeger or whatever you already run.
Why trace an LLM agent with OpenTelemetryWhy trace an LLM agent with OpenTelemetry
To ordinary tracing, an LLM call is a black box. You see a slow HTTP request. You do not see which model was called, how many tokens it burned, or whether it looped on a tool. The OpenTelemetry GenAI semantic conventions fix that by standardizing how LLM operations are recorded: the model, the token counts, the finish reason, and, if you opt in, the full content of prompts, completions and tool calls.
The reason to bet on them is who is behind them. The working group's contributors include people from Amazon, Elastic, Google, IBM, Microsoft, Langtrace, OpenLIT, Scorecard and Traceloop, and vendors like Datadog already consume the conventions natively. When that many implementers agree on a schema before it is even finalized, adopting it early is a small risk.
The practical payoff is that you instrument once. Emit standard gen_ai.*
telemetry and you can point it at any OTLP backend, then switch backends later without touching the instrumentation.
Where the spec stands does affect how you pin things. The GenAI conventions are still in Development status and most attributes are experimental, so pin your instrumentation versions and expect occasional breaking changes as the spec settles. They also live in their own repository now, open-telemetry/semantic-conventions-genai; the copies still sitting in the main
semantic-conventions
repo are marked as moved. That split doesn't change any of the attribute names below, but it is the repo to watch.The GenAI semantic conventions in one tableThe GenAI semantic conventions in one table
Almost everything you'll see on a GenAI span comes from this short vocabulary:
| What | Attribute / span | Example |
|---|---|---|
| Operation (also the span name prefix) | gen_ai.operation.name |
|
chat , execute_tool , invoke_agent , embeddings |
||
| Provider | gen_ai.system (renamed gen_ai.provider.name in newer versions) |
|
openai , anthropic , aws.bedrock |
||
| Requested model | gen_ai.request.model |
|
gpt-4o-mini |
||
| Tokens in / out | gen_ai.usage.input_tokens , gen_ai.usage.output_tokens |
|
412 / 96 |
||
| Why it stopped | gen_ai.response.finish_reasons |
|
["stop"] , ["tool_calls"] |
||
| Agent | gen_ai.agent.name , gen_ai.agent.id |
|
travel-concierge |
||
| Tool | gen_ai.tool.name , gen_ai.tool.type , gen_ai.tool.call.id |
|
get_weather / function |
||
| Content (opt-in) | gen_ai.input.messages , gen_ai.output.messages , gen_ai.system_instructions |
|
| full prompt / response |
Span names follow {operation} {model-or-name}
, which gives you chat gpt-4o-mini
, execute_tool get_weather
, invoke_agent travel-concierge
. There are also two client metrics: gen_ai.client.operation.duration
(a latency histogram, in seconds) and gen_ai.client.token.usage
(a token histogram, split by gen_ai.token.type
).
There is no combined total-tokens attribute. The conventions define input and output only, so if you want a total, sum the two.
Step 1: Auto-instrument the OpenAI SDK in Python (no code changes)Step 1: Auto-instrument the OpenAI SDK in Python (no code changes)
This step is Python, and so is the application code in every step after it. The package names, the launcher and the environment-variable handling below are all specific to the Python instrumentation. If you're on Node or TypeScript, three of those details change, and the last section of this step covers them.
The official OpenTelemetry instrumentation for the OpenAI SDK is opentelemetry-instrumentation-openai-v2, part of
opentelemetry-python-contrib
and published on PyPI. Install it alongside the OTLP exporter, pinned:
pip install opentelemetry-distro==0.65b0 \
opentelemetry-sdk==1.44.0 \
opentelemetry-exporter-otlp-proto-grpc==1.44.0 \
opentelemetry-instrumentation-openai-v2==2.4b0 \
openai==2.50.0
These five were verified working together on 2026-07-29 under Python 3.14.5. They are pre-1.0 packages, so re-check the set before you pin it into your own build.
Something has to be listening on the other end. The endpoint below is http://localhost:4317
, so an OTLP receiver has to be running on that port before any of this produces a visible trace. A Jaeger, Grafana Tempo or Aspire dashboard container covers local debugging; an OpenTelemetry Collector, a vendor's agent or IceGate covers the rest, and Step 6 goes into the choice. Point the exporter at a port with nothing behind it and your app keeps working, because the exporter retries and logs the failure instead of raising. What you get is export warnings on stderr and no traces at the far end, which looks exactly like broken instrumentation when the instrumentation is fine.
That is why the recipe's prerequisites list two processes rather than one: Ollama running locally with the model already pulled, and IceGate listening on 4317
for OTLP gRPC. Swap in whichever OTLP backend you already have for the second.
Configure export and behavior through environment variables, without writing any code:
export OTEL_SERVICE_NAME=my-llm-app
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=NO_CONTENT
Port, protocol and installed exporter package all have to agree. 4317
is the OTLP/gRPC port and pairs with grpc
, which is what the pinned opentelemetry-exporter-otlp-proto-grpc
speaks. To export over HTTP instead, move the endpoint to 4318
, set OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
, and install opentelemetry-exporter-otlp-proto-http, which is the step people skip. It is a
separate distribution, and with only the gRPC package installed the launcher cannot resolve the HTTP exporter and fails at startup.
Then run your app under the OpenTelemetry launcher. Every OpenAI call now produces a chat
span automatically:
opentelemetry-instrument python app.py
We verified that on a file importing nothing from OpenTelemetry. Zero code changes is not zero dependencies, though. The distro and instrumentation packages above still have to be installed before the launcher has anything to discover.
If you'd rather wire it up in code, OpenAIInstrumentor().instrument()
is the call that patches the SDK:
from openai import OpenAI
from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor
OpenAIInstrumentor().instrument() # every OpenAI call now emits a `chat` span
client = OpenAI()
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Summarize today's incidents."}],
)
That alone will not export anything. instrument()
creates spans, but it has no opinion about where they go. Under opentelemetry-instrument
the launcher was doing that half of the job, building a TracerProvider
with an OTLP exporter out of the environment variables above. Drop the launcher, run plain python app.py
, and the global tracer stays the default no-op: the app runs normally and no span ever leaves the process. Nothing in the logs hints at why, because no exporter was ever built to fail at sending one.
So replacing the launcher means doing both halves yourself:
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
provider = TracerProvider(resource=Resource.create({"service.name": "my-llm-app"}))
provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True))
)
trace.set_tracer_provider(provider) # has to happen before instrument()
OpenAIInstrumentor().instrument()
provider.shutdown() # flush before exit
The shutdown()
call at the end fails the same quiet way when you forget it. BatchSpanProcessor
queues spans and flushes on a timer, so a short script that does its work and exits takes the pending batch with it, and you are back to an empty backend. Our recipe's telemetry.py handles both halves in one place and returns the provider so the caller can shut it down.
One constraint does not move into code at all. OTEL_SEMCONV_STABILITY_OPT_IN
is read when the instrumentation module loads, so setting os.environ[...]
above instrument()
is already too late. You silently get the v1.30 names back (gen_ai.system
instead of gen_ai.provider.name
) and nothing raises to tell you. Export it in your shell or .env
before the process starts. That same telemetry.py
exits loudly on a missing opt-in instead of quietly emitting the old names.
By default the instrumentation records metadata only (model, token counts, finish reason, duration) and leaves message content out, which is the right default for production. Step 4 covers turning content on safely.
Which package to installWhich package to install
Use opentelemetry-instrumentation-openai-v2
, the official OTel implementation. The older opentelemetry-instrumentation-openai
is Traceloop's community package (OpenLLMetry). It's still maintained, but it isn't the standard. For Anthropic, Bedrock, Vertex and others you have a choice: their OpenAI-compatible endpoints, a community or framework instrumentor (OpenLLMetry, or framework-native ones for LangChain, LlamaIndex, the OpenAI Agents SDK, CrewAI), or the dedicated agents instrumentation. All of them emit the same gen_ai.*
attributes, so everything below is identical regardless of provider.
Pointing at an OpenAI-compatible endpointPointing at an OpenAI-compatible endpoint
In practice the SDK requires two things, and the model name has to move with them:
import os
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:11434/v1", # any OpenAI-compatible endpoint
api_key="ollama", # required by the SDK; ignored by the server
)
resp = client.chat.completions.create(
model=os.environ.get("OLLAMA_MODEL", "gemma4:12b-mlx"),
messages=[{"role": "user", "content": "Summarize today's incidents."}],
)
Change only the base_url
and you get a model-not-found error instead of a trace. A local Ollama serves exactly the models you have pulled onto that machine, and gpt-4o-mini
is not one of them, so the model name has to change along with the endpoint. Our recipe reads it from OLLAMA_MODEL
, which is why its prerequisites start with ollama pull gemma4:12b-mlx
.
The emitted provider value describes the SDK and protocol surface, not the backend actually serving the request. Point the OpenAI SDK at Ollama and you get gen_ai.provider.name = "openai"
, not "ollama"
. That matters if you group cost or latency by provider.
If you're on Node or TypeScriptIf you're on Node or TypeScript
The steps above are Python. The JS path differs in three ways that the rest of this guide doesn't cover:
declares@opentelemetry/instrumentation-openai
supportedVersions: ">=4.19.0 <7"
(checked against 0.19.0). Withopenai
7.x installed it silently never patches: your app runs fine and emits no spans at all. Pinopenai
to 6.x.- It has no
OTEL_SEMCONV_STABILITY_OPT_IN
support, so its Chat Completions path emits the deprecatedgen_ai.system
. The Step 2 advice below aboutgen_ai.provider.name
can't be followed in JS today. - Under Node ESM, calling
registerInstrumentations()
at runtime is not enough. The hook has to be registered at process start:node --import tsx --experimental-=@opentelemetry/instrumentation/hook.mjs src/index.ts
Our TypeScript recipe carries both fixes: the openai
6.49.0 pin, and the flag in its start
script.
Step 2: Add agent and tool spans with invoke_agent and execute_toolStep 2: Add agent and tool spans with invoke_agent and execute_tool
Auto-instrumentation gives you a flat list of chat
spans. What it can't know is your agent's structure: which calls belong to which task, and when a tool ran. You add that by wrapping the run loop in an invoke_agent span and each tool call in an
execute_tool
span. The auto-instrumented chat
spans then nest underneath on their own.The sketch below is illustrative pseudocode: call_model
, dispatch_tool
and the shape of plan
are yours to supply. For a complete runnable version, with a real client call, real tool dispatch and real control flow around the tool-call decision, see agent.py in the cookbook. The one trap it has to handle comes up after the trace diagram.
from opentelemetry import trace
from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor
OpenAIInstrumentor().instrument()
tracer = trace.get_tracer("travel.concierge")
AGENT = "travel-concierge"
AGENT_ID = "travel-concierge-001"
def run_agent(user_msg):
with tracer.start_as_current_span(
f"invoke_agent {AGENT}",
kind=trace.SpanKind.INTERNAL,
attributes={
"gen_ai.operation.name": "invoke_agent",
"gen_ai.agent.name": AGENT,
"gen_ai.agent.id": AGENT_ID,
"gen_ai.provider.name": "openai",
},
):
plan = call_model(user_msg)
for call in plan.tool_calls:
with tracer.start_as_current_span(
f"execute_tool {call.name}",
attributes={
"gen_ai.operation.name": "execute_tool",
"gen_ai.tool.name": call.name,
"gen_ai.tool.type": "function",
"gen_ai.tool.call.id": call.id,
},
):
call.result = dispatch_tool(call.name, call.args)
return call_model(user_msg, tool_results=plan.tool_calls)
Note gen_ai.provider.name
rather than gen_ai.system
. Once you have opted into gen_ai_latest_experimental
in Step 1, that is the current name for this attribute; gen_ai.system
is the name that convention deprecated. Most backends still accept the old spelling through a deprecation fallback, so nothing breaks if you use it, but it contradicts the opt-in you just set.
gen_ai.agent.id
and gen_ai.tool.call.id
are worth setting too. The provider already returns a tool-call id, and backends with typed columns for these can only populate them if you emit them.
The result is a trace with the same shape as the run:
With that structure in place you can tell where a 45-second response went: into a slow tool, into a retry loop, or into the model call itself.
The trap is tool_choice
. Set it to "required"
on the planning call if you want the tool step to be deterministic, and make sure you don't set it on the final call. Left on, it forces the model to request another tool and the loop never terminates.
Step 3: From token usage to LLM cost per requestStep 3: From token usage to LLM cost per request
The instrumentation records gen_ai.usage.input_tokens
and gen_ai.usage.output_tokens
on every chat
span, so per-request cost is those counts multiplied by your model's price per token. The gen_ai.client.token.usage
metric lets you spot token-hungry prompts before they reach production, and gen_ai.response.finish_reasons
tells you when a call ended on tool_calls
rather than stop
, which is how you catch runaway tool loops.
The token counts land automatically. Currency does not, because that needs a price table, and a fresh deployment may not have one. On IceGate the built-in prices
table is filled by a crawler that ships disabled, so cost columns read NULL
until you turn it on. Our cost query is the one a real deployment should run, and on a stock install it returns true token counts next to NULL
costs.
To get real rates, turn the crawler on with maintain.pricing.enabled
in IceGate's Helm values. It ships off because it is the one maintain workload that makes outbound internet calls, and granting the pod egress should be a deliberate choice rather than a default someone inherits. The shipped source list already covers OpenRouter and LiteLLM, so there is usually nothing else to configure, but prices
has to exist before the first crawl: the crawler appends to that table and will not create it. Run migrate
first.
If you only need the arithmetic, skip the table and supply the rates inline. Our worked example runs the same calculation against a WITH rates AS (...)
CTE, so it returns real numbers on a stock install with an empty prices
table. That is also the query for a local model, which has no upstream rate card at all: the token counts are genuine measurements, and you are pricing them as if a paid model had served them.
These chat
spans are also big, which matters later. Prompts, completions and tool payloads make a single chat
span roughly fifty times the size of an ordinary HTTP or database span, the ~20 KB per span our own pricing model assumes. That size gap is why volume-priced observability gets expensive so fast, and we work the numbers through in our companion piece on why LLM observability bills explode at scale.
Step 4: Capture prompts and responses safely without leaking PIIStep 4: Capture prompts and responses safely without leaking PII
Content capture is off by default, because prompts and responses routinely contain PII. Turn it on with the environment variable from Step 1:
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_ONLY
The value is an enum, not a boolean: NO_CONTENT
, SPAN_ONLY
, EVENT_ONLY
or SPAN_AND_EVENT
. SPAN_ONLY
gives you the on-span attributes described below. true
and false
belong to the older pre-opt-in path and are rejected once gen_ai_latest_experimental
is set. The library logs a warning and falls back to NO_CONTENT
, so you get no content at all and one log line explaining why.
With gen_ai_latest_experimental
enabled, the instrumentation records content as structured span attributes (gen_ai.input.messages
, gen_ai.output.messages
, gen_ai.system_instructions
), so you can replay the exact conversation when debugging. In legacy mode it emits the same content as log events instead.
In production these attributes are both large and sensitive. Redact at the OpenTelemetry Collector before the data leaves your network, or keep full content only in storage you control.
Step 5: Group multi-turn conversations with gen_ai.conversation.idStep 5: Group multi-turn conversations with gen_ai.conversation.id
Everything so far covers a single turn. A real agent fields many turns from one user over time, and each turn is its own trace with its own root span. Without a shared key, a backend shows you a pile of unrelated traces that happen to share an agent name, instead of a conversation you can read end to end.
The attribute for this is gen_ai.conversation.id, which the spec defines as a "unique identifier for a conversation (session, thread)". It is a span attribute, not a resource attribute. OpenInference's equivalent is
session.id
. IceGate populates its typed operations.conversation_id
column from it, and that column exists to group a conversation's operations across traces.The obvious approach does not work. Setting the id on the invoke_agent
span you already create in Step 2 leaves it on that one span, because span attributes don't inherit. Every auto-instrumented chat
span nested underneath stays empty, which defeats the grouping exactly where it matters: the model calls are the spans you most want to line up across turns.
A resource attribute would cover every span for free, and it is still wrong. Resources are process-wide and immutable once the TracerProvider
is built; conversations are neither. One process serves many conversations, sequentially or concurrently, and one conversation can span many processes. Bake a conversation id into the resource and it breaks the first time either of those is true.
What works is a span processor. Register one whose on_start
hook (onStart
in Node) reads the active conversation id and stamps it on each span as the span opens. Because on_start
runs while the span is still live, span.set_attribute()
here is ordinary public API rather than a reach into private SDK internals or into a span that has already ended. Register it as the outermost processor on the TracerProvider
and coverage becomes uniform, since every span the process creates passes through the same hook, whether your own code created it or an auto-instrumentation did, deep inside a library call.
The active id itself comes from a ContextVar
in Python or AsyncLocalStorage
in Node. Both are the standard mechanism in their language for ambient per-call context that survives async boundaries.
Unlike the sketch in Step 2, this is real working code, taken from conversation.py in the cookbook (the Node port is
conversation.ts
from contextlib import contextmanager
from contextvars import ContextVar
from opentelemetry.sdk.trace import SpanProcessor
_current_conversation_id: ContextVar[str | None] = ContextVar(
"icegate_conversation_id", default=None
)
def current_id() -> str | None:
"""The active conversation id, or None outside any `conversation()` block."""
return _current_conversation_id.get()
@contextmanager
def conversation(conversation_id: str | None):
"""Make `conversation_id` the active conversation for the block."""
token = _current_conversation_id.set(conversation_id)
try:
yield conversation_id
finally:
_current_conversation_id.reset(token) # restores on exception too
class ConversationSpanProcessor(SpanProcessor):
"""Stamps `gen_ai.conversation.id` on every span started while a conversation is active."""
def __init__(self, inner: SpanProcessor):
self._inner = inner
def on_start(self, span, parent_context=None):
conversation_id = current_id() # reads the ContextVar above
if conversation_id is not None:
span.set_attribute("gen_ai.conversation.id", conversation_id)
self._inner.on_start(span, parent_context=parent_context)
def on_end(self, span):
self._inner.on_end(span)
def shutdown(self):
self._inner.shutdown()
def force_flush(self, timeout_millis=30000):
return self._inner.force_flush(timeout_millis=timeout_millis)
Wrap the work in conversation("conv-bc3ceb84")
and every span opened inside the block carries the id, including the ones auto-instrumentation creates deep inside a library call.
Two turns then share one id while staying two separate traces:
A real two-turn run, queried straight out of IceGate's operations
table, gives 8 spans with two distinct trace_id
s (d1161d75…
and e0fd6bbe…
) and one shared conversation_id
, conv-bc3ceb84…
, on every one of them. Four of those spans are the auto-instrumented chat
spans, which the recipe never sets gen_ai.conversation.id
on directly. The span processor is what reached them, because application code never touches those spans at all.
The recipe runs the agent twice on purpose. A single turn is a single trace, which would demonstrate nothing about grouping across traces.
Step 6: Where to send your gen_ai telemetryStep 6: Where to send your gen_ai telemetry
Any OTLP-compatible backend can receive this telemetry. For local debugging, a Jaeger, Grafana Tempo or Aspire dashboard container works. In production, the vendors that consume the GenAI conventions (Datadog, SigNoz, Uptrace and others) all accept the same data. That is what instrumenting to the standard buys you. Changing where the data goes is a one-line change instead of a re-instrumentation project.
The cost problem from Step 3 gets resolved at the same layer. Point the same OTLP exporter at IceGate, our open-source observability engine, and those heavy chat
spans land in open Parquet and Iceberg files on your own object storage, queryable with plain SQL. Because retention sits on cheap object storage rather than a proprietary indexed store, you can keep full-fidelity traces, prompts and tool calls included, instead of sampling them away. Nothing about your instrumentation changes: you keep the open OpenTelemetry data you just emitted, and it stays yours. Our companion piece on the observability data lake covers that architecture in depth.
IceGate is Apache-2.0 and speaks OTLP natively. Star it on GitHub, point your collector at it, and watch your agent's traces land in open formats. Every step above has a runnable, verified recipe in the integrations cookbook, in both Python and TypeScript.
FAQFAQ
What are the OpenTelemetry GenAI semantic conventions?What are the OpenTelemetry GenAI semantic conventions?
A standard set of span names, attributes (gen_ai.*
), metrics and events for recording generative-AI operations: the model, tokens, finish reason, agent and tool structure, and optional prompt and response content. Emitting them keeps AI telemetry consistent across frameworks and vendors.
How do I instrument an LLM with OpenTelemetry without rewriting my app?How do I instrument an LLM with OpenTelemetry without rewriting my app?
Install the provider's OTel instrumentation (for OpenAI, opentelemetry-instrumentation-openai-v2
), set the OTLP environment variables, and run under opentelemetry-instrument
. Every model call is traced automatically; add manual invoke_agent
and execute_tool
spans only where you want agent and tool structure.
Which OTLP port and protocol should I use?Which OTLP port and protocol should I use?
Match all three: 4317
with OTEL_EXPORTER_OTLP_PROTOCOL=grpc
and the opentelemetry-exporter-otlp-proto-grpc
package, or 4318
with http/protobuf
and opentelemetry-exporter-otlp-proto-http
. The two exporters are separate distributions, so selecting a protocol whose package isn't installed makes the launcher fail at startup.
Which gen_ai attributes matter most?Which gen_ai attributes matter most?
gen_ai.request.model
, gen_ai.usage.input_tokens
/ output_tokens
(for cost), and gen_ai.response.finish_reasons
(for loop and error detection). Add gen_ai.agent.name
and gen_ai.tool.name
to give traces structure.
Which convention version am I emitting?Which convention version am I emitting?
By default the OpenAI-v2 instrumentation follows conventions around v1.30 and won't record newer details. Set OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
to emit the latest version, including on-attribute content capture. Set it in the environment before the process starts: the instrumentation reads it at import time, so setting it from Python is too late. The provider attribute gen_ai.system
is also being renamed to gen_ai.provider.name
in newer versions, so if you opt in, use the new name in your manual spans too.
Do I have to capture prompt content?Do I have to capture prompt content?
No. It's off by default, and only metadata is recorded. Enable it deliberately with SPAN_ONLY
(not true
, since the setting is an enum), redact at the Collector, and prefer keeping full content in storage you control.
Does this work for Anthropic, Bedrock, or my agent framework?Does this work for Anthropic, Bedrock, or my agent framework?
Yes. Use OpenAI-compatible endpoints, a community or framework instrumentor, or the dedicated agents instrumentation. All of them emit the same gen_ai.*
attributes, so your dashboards and queries don't change when you switch providers. Language is a different matter: see the Node and TypeScript note in Step 1.
How do I group a multi-turn conversation into one view?How do I group a multi-turn conversation into one view?
Stamp gen_ai.conversation.id
on every span, not just the agent span. Span attributes do not inherit, so an id set only on invoke_agent
leaves every auto-instrumented chat
span without it. The pattern that covers all of them is a span processor that reads the active conversation id from a ContextVar
(Python) or AsyncLocalStorage
(Node) in its on_start
hook, as in Step 5.
Trademarks and disclaimerTrademarks and disclaimer
Trademarks. OpenTelemetry and Jaeger are trademarks of the Cloud Native Computing Foundation (CNCF) / The Linux Foundation. Grafana® and Tempo® are trademarks of Raintank, Inc. (Grafana Labs). Apache®, Apache Iceberg™, and Apache Parquet™ are trademarks of The Apache Software Foundation. OpenAI® is a trademark of OpenAI OpCo, LLC. Anthropic® is a trademark of Anthropic PBC. Amazon S3®, Amazon Bedrock®, and AWS® are trademarks of Amazon Technologies, Inc. Google® and Vertex AI® are trademarks of Google LLC. Microsoft® is a trademark of Microsoft Corporation. IBM® is a trademark of International Business Machines Corporation. Elasticsearch® is a trademark of Elasticsearch B.V. / Elastic N.V. Datadog® is a trademark of Datadog, Inc. Docker® is a trademark of Docker, Inc. Python® is a trademark of the Python Software Foundation. Node.js® is a trademark of the OpenJS Foundation. SigNoz, Uptrace, Ollama, LangChain, LlamaIndex, CrewAI, Traceloop / OpenLLMetry, OpenLIT, Langtrace, and Scorecard are designations of their respective owners. All other product names, logos, and brands are the property of their respective owners. Names are used solely for identification and do not imply affiliation, sponsorship, or endorsement by the trademark holders.
Disclaimer. IceGate and TripleCloud are not affiliated with or endorsed by the Cloud Native Computing Foundation (CNCF), The Apache Software Foundation, OpenAI, Anthropic, Amazon Web Services, Google, Microsoft, IBM, Elastic, Grafana Labs, Datadog, or any of the other companies mentioned here. The instructions and version pins above reflect the packages and configurations described, verified on the dates stated, and may not account for newer releases; the OpenTelemetry GenAI semantic conventions are still experimental and subject to breaking change. Corrections and clarifications are welcome, and we will fix any inaccuracies.