OpenTelemetry Instrumentation
This product is not supported for your selected
Datadog site. (
).
Overview
By using OpenTelemetry’s standardized semantic conventions for generative AI operations, you can instrument your LLM applications with any OpenTelemetry-compatible library or framework and visualize the traces in Agent Observability.
Agent Observability supports ingesting OpenTelemetry traces that follow the OpenTelemetry 1.37+ semantic conventions for generative AI. This allows you to send LLM traces directly from OpenTelemetry-instrumented applications to Datadog without requiring the Agent Observability SDK or a Datadog Agent.
Prerequisites
Supported features
Evaluations
To send external evaluations directly to the API for OpenTelemetry spans, include the source:otel tag in the evaluation. When referencing spans, provide span_id and trace_id as decimal strings. OpenTelemetry uses hexadecimal IDs natively, so convert them to decimal before submitting evaluations. For example, use Python’s int(hex_span_id, 16) to convert a hex span ID to its decimal equivalent.
Prompt Tracking
For information on using Prompt Tracking with OpenTelemetry spans, see Prompt Tracking - OpenTelemetry Instrumentation.
Experiments
You can use OpenTelemetry spans inside Agent Observability Experiments. By setting DD_TRACE_OTEL_ENABLED=1, OTel spans created inside an experiment task automatically appear as children of the experiment span.
Span links
Use OpenTelemetry span links on your GenAI spans to express non-parent-child relationships, such as when one span’s output feeds another span’s input. When two linked spans are in the same trace, the link appears as an edge in that trace’s Execution Graph, so you can see how data flows between sibling spans (for example, a tool’s output feeding a downstream LLM call).
Use from and to attributes to indicate the direction of the data flow:
from opentelemetry import trace
from opentelemetry.trace import Link
tracer = trace.get_tracer(__name__)
# A tool span whose output feeds a downstream LLM call.
with tracer.start_as_current_span("lookup_order") as tool_span:
tool_span.set_attribute("gen_ai.operation.name", "execute_tool")
tool_ctx = tool_span.get_span_context()
# The LLM span links back to the tool span: its output became this span's input.
link = Link(context=tool_ctx, attributes={"from": "output", "to": "input"})
with tracer.start_as_current_span("chat gpt-4o", links=[link]) as llm_span:
llm_span.set_attribute("gen_ai.operation.name", "chat")
A span link that points to a span in a different trace is stored, but is not drawn in the Execution Graph, which visualizes a single trace.
Setup
Any method Datadog supports for ingesting OpenTelemetry traces works with Agent Observability. For the full list of supported ingestion paths, see OpenTelemetry feature compatibility. The following is one way to configure it.
To send OpenTelemetry traces to Agent Observability, configure your OpenTelemetry exporter with the following settings:
Configuration
Set the following environment variables in your application:
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=
OTEL_EXPORTER_OTLP_TRACES_HEADERS=dd-api-key=<YOUR_API_KEY>,dd-otlp-source=llmobs
Replace <YOUR_API_KEY> with your Datadog API key.
If your framework previously supported a pre-1.37 OpenTelemetry specification version, you also need to set:
OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
This environment variable enables version 1.37+-compliant OpenTelemetry traces for frameworks that now support the version 1.37+ semantic conventions, but previously supported older versions (such as strands-agents).
Note:
- If you are using an OpenTelemetry library other than the default OpenTelemetry SDK, you may need to configure the endpoint, protocol, and headers differently depending on the library’s API. See your library’s documentation for the appropriate configuration method.
- When using OpenTelemetry instrumentation, some data sent to Agent Observability may also be written to the corresponding APM traces. If you are protecting sensitive data, consider also configuring a Restricted Dataset on APM to match your Agent Observability access controls. See Data Access Control for more information.
Using strands-agents
If you are using the strands-agents library, you need to set an additional environment variable to enable traces that are compliant with OpenTelemetry v1.37+:
OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
This environment variable ensures that strands-agents emits traces following the OpenTelemetry v1.37+ semantic conventions for generative AI, which are required by Agent Observability.
Instrumentation
To generate traces compatible with Agent Observability, do one of the following:
After your application starts sending data, the traces automatically appear in the Agent Observability Traces page. To search for your traces in the UI, use the ml_app attribute, which is automatically set to the value of your OpenTelemetry root span’s service attribute.
- OpenLLMetry version 0.47+ is supported. See the OpenLLMetry example.
- OpenInference is not supported.
- There may be a 3-5 minute delay between sending traces and seeing them appear on the Agent Observability Traces page. If you have APM enabled, traces appear immediately in the APM Traces page.
Tested frameworks and libraries
These frameworks and libraries have been tested with Agent Observability. Any framework that emits OpenTelemetry 1.37+ GenAI semantic convention-compliant spans is supported.
Examples
Using Strands Agents
The following example demonstrates a complete application using Strands Agents with the OpenTelemetry integration. This same approach works with any framework that supports OpenTelemetry version 1.37+ semantic conventions for generative AI.
from strands import Agent
from strands_tools import calculator, current_time
from strands.telemetry.config import StrandsTelemetry
import os
# Configure AWS credentials for Bedrock access
os.environ["AWS_PROFILE"] = "<YOUR_AWS_PROFILE>"
os.environ["AWS_DEFAULT_REGION"] = "<YOUR_AWS_REGION>"
# Enable latest GenAI semantic conventions (1.37)
os.environ["OTEL_SEMCONV_STABILITY_OPT_IN"] = "gen_ai_latest_experimental"
# Configure OTLP endpoint to send traces to Agent Observability
os.environ["OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"] = "http/protobuf"
os.environ["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"] = ""
os.environ["OTEL_EXPORTER_OTLP_TRACES_HEADERS"] = f"dd-api-key={os.getenv('DD_API_KEY')},dd-otlp-source=llmobs"
# Initialize telemetry with OTLP exporter
telemetry = StrandsTelemetry()
telemetry.setup_otlp_exporter()
# Create agent with tools
agent = Agent(tools=[calculator, current_time])
# Run the agent
if __name__ == "__main__":
result = agent("I was born in 1993, what is my age?")
print(f"Agent: {result}")
Custom OpenTelemetry instrumentation
The following example demonstrates how to instrument your LLM application using custom OpenTelemetry code. This approach gives you full control over the traces and spans emitted by your application.
import os
import json
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
from openai import OpenAI
# Configure OpenTelemetry to send traces to Datadog
os.environ["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"] = ""
os.environ["OTEL_EXPORTER_OTLP_TRACES_HEADERS"] = "dd-api-key=<YOUR_DATADOG_API_KEY>,dd-otlp-source=llmobs"
os.environ["OTEL_SEMCONV_STABILITY_OPT_IN"] = "gen_ai_latest_experimental"
# Initialize OpenTelemetry SDK
resource = Resource(attributes={SERVICE_NAME: "simple-llm-example"})
provider = TracerProvider(resource=resource)
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)
# Make LLM call with OpenTelemetry tracing
with tracer.start_as_current_span(
"chat gpt-4o",
kind=trace.SpanKind.CLIENT,
) as span:
model = "gpt-4o"
max_tokens = 1024
temperature = 0.7
messages = [{"role": "user", "content": "Explain OpenTelemetry in one sentence."}]
# Set request attributes
span.set_attribute("gen_ai.provider.name", "openai")
span.set_attribute("gen_ai.request.model", model)
span.set_attribute("gen_ai.operation.name", "chat")
span.set_attribute("gen_ai.request.max_tokens", max_tokens)
span.set_attribute("gen_ai.request.temperature", temperature)
# Add input messages as event
input_messages_parts = []
for msg in messages:
input_messages_parts.append({
"role": msg["role"],
"parts": [{"type": "text", "content": msg["content"]}]
})
span.add_event(
"gen_ai.client.inference.operation.details",
{
"gen_ai.input.messages": json.dumps(input_messages_parts)
}
)
# Make actual LLM call
client = OpenAI(api_key="<YOUR_OPENAI_API_KEY>")
response = client.chat.completions.create(
model=model,
max_tokens=max_tokens,
temperature=temperature,
messages=messages
)
# Set response attributes from actual data
span.set_attribute("gen_ai.response.id", response.id)
span.set_attribute("gen_ai.response.model", response.model)
span.set_attribute("gen_ai.response.finish_reasons", [response.choices[0].finish_reason])
span.set_attribute("gen_ai.usage.input_tokens", response.usage.prompt_tokens)
span.set_attribute("gen_ai.usage.output_tokens", response.usage.completion_tokens)
# Add output messages as event
output_text = response.choices[0].message.content
span.add_event(
"gen_ai.client.inference.operation.details",
{
"gen_ai.output.messages": json.dumps([{
"role": "assistant",
"parts": [{"type": "text", "content": output_text}],
"finish_reason": response.choices[0].finish_reason
}])
}
)
print(f"Response: {output_text}")
# Flush spans before exit
provider.force_flush()
After running this example, search for ml_app:simple-llm-example in the Agent Observability UI to find the generated trace.
Using OpenLLMetry
The following example demonstrates using OpenLLMetry to automatically instrument OpenAI calls with OpenTelemetry.
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.openai import OpenAIInstrumentor
import openai
from opentelemetry.sdk.resources import Resource
resource = Resource.create({
"service.name": "simple-openllmetry-test",
})
provider = TracerProvider(resource=resource)
trace.set_tracer_provider(provider)
exporter = OTLPSpanExporter(
endpoint="",
headers={
"dd-api-key": "<YOUR_DATADOG_API_KEY>",
"dd-ml-app": "simple-openllmetry-test",
"dd-otlp-source": "llmobs",
},
)
provider.add_span_processor(BatchSpanProcessor(exporter))
OpenAIInstrumentor().instrument()
# Make OpenAI call (automatically traced)
client = openai.OpenAI(api_key="<YOUR_OPENAI_API_KEY>")
client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "What is 15 multiplied by 7?"}]
)
provider.force_flush(timeout_millis=5000)
After running this example, search for ml_app:simple-openllmetry-test in the Agent Observability UI to find the generated trace.
Attribute mapping reference
This section provides the mapping between OpenTelemetry GenAI semantic conventions (v1.37+) as well as OpenLLMetry to Datadog’s Agent Observability span schema.
OpenTelemetry 1.37+ attribute mappings
Base span attributes
| OTLP Field | Agent Observability Field | Notes |
|---|
resource.attributes.service.name | ml_app, tags.service | |
name | name | Overridden by gen_ai.tool.name if present |
parent_span_id | parent_id | |
start_time_unix_nano | start_ns | |
end_time_unix_nano | duration | Calculated: end - start |
status.code | status | error if > 0, else ok |
status.message | meta.error.message | |
attributes.error.type | meta.error.type | |
Span kind resolution
gen_ai.operation.name | Agent Observability span.kind |
|---|
generate_content, chat, text_completion, completion | llm |
embeddings, embedding | embedding |
execute_tool | tool |
invoke_agent, create_agent | agent |
rerank, unknown, (default) | workflow |
| OTel Attribute | Agent Observability Field | Notes |
|---|
gen_ai.operation.name | meta.span.kind | See resolution table above |
gen_ai.provider.name | meta.model_provider | Falls back to gen_ai.system, then custom |
gen_ai.response.model | meta.model_name | |
gen_ai.request.model | meta.model_name | Fallback if response.model absent |
Token usage metrics
| OTel Attribute | Agent Observability Field |
|---|
gen_ai.usage.input_tokens | metrics.input_tokens |
gen_ai.usage.output_tokens | metrics.output_tokens |
gen_ai.usage.prompt_tokens | metrics.prompt_tokens |
gen_ai.usage.completion_tokens | metrics.completion_tokens |
gen_ai.usage.total_tokens | metrics.total_tokens |
Request parameters
All gen_ai.request.* parameters map to meta.metadata.* with the prefix stripped.
| OTel Attribute | Agent Observability Field |
|---|
gen_ai.request.seed | metadata.seed |
gen_ai.request.frequency_penalty | metadata.frequency_penalty |
gen_ai.request.max_tokens | metadata.max_tokens |
gen_ai.request.stop_sequences | metadata.stop_sequences |
gen_ai.request.temperature | metadata.temperature |
gen_ai.request.top_k | metadata.top_k |
gen_ai.request.top_p | metadata.top_p |
gen_ai.request.choice.count | metadata.choice.count |
| OTel Attribute | Agent Observability Field | Notes |
|---|
gen_ai.tool.name | name | Overrides span name |
gen_ai.tool.call.id | metadata.tool_id | |
gen_ai.tool.description | metadata.tool_description | |
gen_ai.tool.type | metadata.tool_type | |
gen_ai.tool.definitions | meta.tool_definitions | Parsed JSON array |
gen_ai.tool.call.arguments | input.value | |
gen_ai.tool.call.result | output.value | |
Session and conversation
| OTel Attribute | Agent Observability Field | Notes |
|---|
gen_ai.conversation.id | session_id | Also added to metadata.conversation_id and tags |
When an APM trace’s top-most span is not a gen_ai span (for example, an HTTP handler that invokes several LLMs in parallel), Agent Observability produces a separate Agent Observability trace for each top-level gen_ai span in that APM trace. To keep these split traces grouped together in the UI, set gen_ai.conversation.id to the same value on each gen_ai span within the APM trace: Agent Observability groups by session_id, so the resulting traces appear together even though they have distinct Agent Observability trace IDs. This is the same attribute used for cross-request conversation grouping.
Span links
Span links you set on a GenAI span appear as span_links on the corresponding Agent Observability span.
| OTel span link field | Agent Observability Field | Notes |
|---|
trace_id | span_links[].trace_id | 128-bit trace IDs are emitted as hex. A link to a span in the same trace resolves to that span’s Agent Observability trace ID. |
span_id | span_links[].span_id | Decimal |
attributes | span_links[].attributes | Dots in attribute keys are replaced with underscores (for example, messaging.operation becomes messaging_operation). |
Links between spans in the same trace are drawn as edges in that trace’s Execution Graph.
Response attributes
| OTel Attribute | Agent Observability Field |
|---|
gen_ai.response.model | meta.model_name |
gen_ai.response.finish_reasons | metadata.finish_reasons |
Input and output messages are extracted from the following sources, in priority order:
- Direct attributes:
gen_ai.input.messages, gen_ai.output.messages, gen_ai.system_instructions - Span events (
meta["events"]) with name gen_ai.client.inference.operation.details
| OTel Source | Agent Observability Field | Notes |
|---|
gen_ai.input.messages | meta.input.messages (llm) / meta.input.value (others) | |
gen_ai.output.messages | meta.output.messages (llm) / meta.output.value (others) | |
gen_ai.system_instructions | Prepended to input | Added as system role messages |
Embedding spans
| OTel Source | Agent Observability Field |
|---|
gen_ai.input.messages | meta.input.documents |
| N/A | meta.output.value = [N embedding(s) returned] |
Tags are placed directly on the span:
- Non-
gen_ai.* attributes are converted to key:value tags - Unknown
gen_ai.* keys are added with prefix stripped - Filtered out:
_dd.*, llm.*, ddtags, events, and already specifically mapped gen_ai.* keys
Any gen_ai.* attributes that are not explicitly mapped to Agent Observability span fields are placed in the LLM span's tags, with a 256 character limit per value. Values exceeding this limit are truncated. All other non-gen_ai attributes are dropped.
To add structured metadata to a span’s meta.metadata field instead of its tags, set the _dd.ml_obs.metadata attribute to a JSON object string. Its keys and values (including nested objects and arrays) are merged into meta.metadata and rendered as JSON in the UI.
import json
span.set_attribute("_dd.ml_obs.metadata", json.dumps({
"experiment": "a/b",
"config": {
"retry": {"max": 3, "backoff": "exp"},
"feature_flags": ["new_ranker", "fast_path"],
},
}))
Notes:
- Values may be arbitrarily nested; unlike tags, metadata is not subject to the 256 character per-value limit.
- Keys that collide with metadata derived from
gen_ai.* attributes (for example, temperature) are overwritten by your values, with the exception of model_name and model_provider, which are reserved. - The value must be a JSON object. A value that is not valid JSON, or that is a top-level array or scalar, is dropped.
OpenLLMetry attribute mappings
This section documents OpenLLMetry-specific attribute mappings that differ from or extend the standard OpenTelemetry GenAI semantic conventions.
Span kind resolution
llm.request.type is used as a fallback when gen_ai.operation.name is absent.
llm.request.type | Agent Observability span.kind |
|---|
chat | llm |
completion | llm |
embedding | embedding |
rerank | workflow |
unknown, (default) | workflow |
| OpenLLMetry Attribute | Agent Observability Field | Notes |
|---|
gen_ai.system | meta.model_provider | Fallback when gen_ai.provider.name absent |
Token usage metrics
| OpenLLMetry Attribute | Agent Observability Field | Notes |
|---|
llm.usage.total_tokens | metrics.total_tokens | Fallback when gen_ai.usage.total_tokens absent |
OpenLLMetry uses indexed attributes instead of JSON arrays. These are the lowest priority source and are only used when no OTel standard sources exist.
| OpenLLMetry Attribute | Description |
|---|
gen_ai.prompt.<index>.role | Message role (user, system, assistant, tool) |
gen_ai.prompt.<index>.content | Message content |
gen_ai.prompt.<index>.tool_call_id | Tool call ID for tool response messages |
Completion attributes (output)
| OpenLLMetry Attribute | Description |
|---|
gen_ai.completion.<index>.role | Message role |
gen_ai.completion.<index>.content | Message content |
gen_ai.completion.<index>.finish_reason | Completion finish reason |
Mapping
Messages are converted to OTel-compatible format and processed normally:
| OpenLLMetry Source | LLMObs Field |
|---|
gen_ai.prompt.* | meta.input.messages (llm) / meta.input.value (others) |
gen_ai.completion.* | meta.output.messages (llm) / meta.output.value (others) |
Tool calls are nested within completion attributes.
| OpenLLMetry Attribute | Maps To |
|---|
gen_ai.completion.<index>.tool_calls.<idx>.name | tool_calls[].name |
gen_ai.completion.<index>.tool_calls.<idx>.id | tool_calls[].tool_id |
gen_ai.completion.<index>.tool_calls.<idx>.arguments | tool_calls[].arguments |
When role = "tool" and tool_call_id are present, the message is converted to a tool result:
| OpenLLMetry Attribute | Maps To |
|---|
gen_ai.prompt.<index>.tool_call_id | tool_results[].tool_id |
gen_ai.prompt.<index>.content | tool_results[].result |
Embedding spans
For embedding spans, documents are extracted from prompt content attributes.
| OpenLLMetry Source | Agent Observability Field |
|---|
gen_ai.prompt.<index>.content | meta.input.documents[].text |
The following OpenLLMetry-specific attributes are filtered from tags:
gen_ai.prompt.*gen_ai.completion.*llm.*
Supported semantic conventions
Agent Observability supports spans that follow the OpenTelemetry 1.37+ semantic conventions for generative AI, including:
- LLM operations with
gen_ai.provider.name, "gen_ai.operation.name", gen_ai.request.model, and other gen_ai attributes - Operation inputs/outputs on direct span attributes or via span events
- Token usage metrics (
gen_ai.usage.input_tokens, gen_ai.usage.output_tokens) - Model parameters and metadata
For the complete list of supported attributes and their specifications, see the OpenTelemetry semantic conventions for generative AI documentation.
Disabling Agent Observability conversion
If you’d only like your generative AI spans to remain in APM and not appear in Agent Observability, you can disable the automatic conversion by setting the dd_llmobs_enabled attribute to false. Setting this attribute on any span in a trace prevents the entire trace from being converted to Agent Observability.
Using environment variables
Add the dd_llmobs_enabled=false attribute to your OTEL_RESOURCE_ATTRIBUTES environment variable:
OTEL_RESOURCE_ATTRIBUTES=dd_llmobs_enabled=false
Using code
You can also set the attribute programmatically on any span in your trace:
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("my-span") as span:
# Disable Agent Observability conversion for this entire trace
span.set_attribute("dd_llmobs_enabled", False)