How to instrument an LLM agent with OpenTelemetry

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 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 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)
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
# Read when the instrumentation loads, so it has to be set BEFORE the process
# starts. Exporting it from Python is too late. Emits the latest GenAI
# conventions instead of the frozen v1.30 default.
export OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
# OFF by default. This is an ENUM, not a boolean: NO_CONTENT, SPAN_ONLY,
# EVENT_ONLY, SPAN_AND_EVENT. Any other value (including `true`) logs a
# warning and silently falls back to NO_CONTENT. See Step 4 to opt in.
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()
# ... your app runs here ...
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 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 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 TypeScript
The steps above are Python. The JS path differs in three ways that the rest of this guide doesn't cover:
@opentelemetry/instrumentation-openaideclaressupportedVersions: ">=4.19.0 <7"(checked against 0.19.0). Withopenai7.x installed it silently never patches: your app runs fine and emits no spans at all. Pinopenaito 6.x.- It has no
OTEL_SEMCONV_STABILITY_OPT_INsupport, so its Chat Completions path emits the deprecatedgen_ai.system. The Step 2 advice below aboutgen_ai.provider.namecan't be followed in JS today. - Under Node ESM, calling
registerInstrumentations()at runtime is not enough. The loader hook has to be registered at process start:node --import tsx --experimental-loader=@opentelemetry/instrumentation/hook.mjs src/index.ts
Our TypeScript recipe carries both fixes: the openai 6.49.0 pin, and the loader flag in its start script.
Step 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",
},
):
# 1) model decides what to do; auto-instrumented as a child `chat` span
plan = call_model(user_msg)
# 2) wrap each tool call in its own `execute_tool` span
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)
# 3) model writes the final answer, another child `chat` span
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 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 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.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)
# The wrapper owns the whole SpanProcessor contract, so the remaining
# hooks have to reach `inner`. Drop these and batching never flushes.
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_ids (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 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.
FAQ
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?
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?
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?
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?
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?
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?
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?
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 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.