cd /news/artificial-intelligence/instrument-an-llm-agent-with-opentel… · home topics artificial-intelligence article
[ARTICLE · art-80877] src=blog.triplecloud.tech ↗ pub= topic=artificial-intelligence verified=true sentiment=· neutral

Instrument an LLM Agent with OpenTelemetry · TripleCloud Blog

TripleCloud Blog published a guide on instrumenting an LLM agent with OpenTelemetry using GenAI semantic conventions, covering zero-code auto-instrumentation, manual spans, token usage, and multi-turn conversations. The conventions, supported by contributors from Amazon, Elastic, Google, IBM, Microsoft, and others, allow vendor-neutral tracing that works with any OTLP backend such as IceGate or Jaeger.

read23 min views3 publishedJul 30, 2026
Instrument an LLM Agent with OpenTelemetry · TripleCloud Blog
Image: source

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.

── more in #artificial-intelligence 4 stories · sorted by recency
── more on @triplecloud blog 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/instrument-an-llm-ag…] indexed:0 read:23min 2026-07-30 ·