Agent Observability Is a Must-Have for LLM Applications
Originally published on Medium.
When something goes wrong, identifying the actual cause isn’t always straightforward due to non-deterministic nature of LLM applications.
Adding logs helps, but logs alone often aren’t enough for LLM applications.
You might have no visibility of the complete workflow if you are using logs alone.
This is where Datadog Agent Observability comes in.
Datadog gives you end-to-end visibility into your AI application’s execution.
Instead of guessing what happened, you can trace a user’s request through out the complete execution flow.
Implementing Datadog agent observability involves 3 core components:
⚪ Traces — shows the complete journey of a single request.
⚪🟡 Spans — represent individual operations within a trace.
⚪🟡⚪ Instrumentation is the process of capturing this information automatically or manually and sending it to Datadog for analysis.
Press enter or click to view image in full size
1. TRACE
A trace represents the complete execution path of a single request handled by the LLM application.
A trace captures everything that happens while the application processes that request, including LLM calls, agent decisions, tool calls, and other operations.
A trace can contain multiple spans, where each span represents an individual operation within the overall request.
LLM Agent workflow
An agent trace groups together all workflows and contextual operations such as the tool calls and reasoning steps executed by an agent to complete a user request.
For example, a user asks:
"What's the weather in Nairobi?"
The AI agent might:
Receive the user’s request.
Call an LLM to determine what to do.
Call a weather tool.
Send the weather data back to the LLM.
Generate the final response.
All of these operations can be represented within the same trace.
Trace Structure Example:
Press enter or click to view image in full size
TRACE
────────────────────────────────
trace_id: trace_001
────────────────────────────────
Agent / Workflow Span
span_id: span_001
trace_id: trace_001
│
├── LLM Span
│ span_id: span_002
│ trace_id: trace_001
│
│ Input:
│ "What's the weather in Nairobi?"
│
│ Output:
│ "I need to check the weather."
│
├── Tool Span
│ span_id: span_003
│ trace_id: trace_001
│
│ Input:
│ {
│ "location": "Nairobi"
│ }
│
│ Output:
│ {
│ "temperature": 23,
│ "condition": "Partly cloudy"
│ }
│
└── LLM Span
span_id: span_004
trace_id: trace_001
Input:
Weather data from tool
Output:
"It's 23°C and partly cloudy in Nairobi."
────────────────────────────────
TRACE SAMPLING
Trace Sampling is a method datadog uses to determine the number of traces to retain for observability.
Agent Observability billing is based on the volume of spans sent.
Setting a sample rate is a direct way to control the cost for agent obervability.
Example:
Suppose your application receives:
10,000 agent requests
And each request generates approximately:
10 spans
You could therefore send to DataDog approximately:
10,000 × 10 = 100,000 spans
Since Agent Observability billing is based on the volume of spans sent, this can become expensive at scale.
This is where trace sampling comes in.
You can configure the sample rate (a float between 0.0 and 1.0)
SETTING A SAMPLE RATE
Suppose we configure:
sample rate = 0.5
The above sample rate means approximately 50% of traces (50,000) will be retained instead of 100,000.
While trace sampling allows you to control the ingestion volume and storage costs for your individual traces and spans, it does not impact these operational metrics
Such metrics includes:
🟡 Token metrics
🟡🟡 Cost metrics
🟡🟡🟡 Performance metrics
Press enter or click to view image in full size
2. SPAN
A span represents a single operation in a trace.
A span serves as the fundamental building block of a trace
Press enter or click to view image in full size
1. SPAN ATTRIBUTES
A span attribute describes the operation represented by the span.
Span attributes represent the content of the work being done within the span
For example:
Suppose the span represents this operation:
SELECT * FROM users WHERE id = 123;
The span could contain the following span attributes:
@db.system:postgres
@db.operation:SELECT
@db.table:users
WHERE DO SPAN ATTRIBUTES COME FROM?
Automatic instrumentation
Datadog’s APM libraries can automatically collect information.
This means there is no need to write code to record values for such fields.
For example, your application makes an HTTP request:
GET /products
The instrumentation may automatically record things like:
@http.method:GET
@http.route:/products
2. Manual/custom instrumentation
This means we have to explicitly specify the fields to record.
This is useful because it allows you to put business-specific information into your traces.
For example:
Suppose your application calculates the shopping cart value:
cart_total = 1299.99
You might instrument your application so the span contains:
@cart.total:1299.99
Example:
from ddtrace import tracer
# Get the current active span
span = tracer.current_span()
if span:
# Manually set a custom attribute
span.set_tag('cart.total', 1299.99)
RESERVED SPAN ATTRIBUTES
Datadog has certain reserved span attributes
Reserved span attributes are automatically instrumented and are available on every span.
You do not need to perform any manual setup to ensure these attributes are collected for your traces:
env: environment name (e.g. production) where the service is running.
service: The name of the service that generated the span.
operation_name: The specific name of the operation being performed.
resource_name: A human-readable name for the resource associated with the operation (e.g., an endpoint path or a database query).
status: The operational status of the span (e.g., ok or error).
span_id: The unique identifier for that specific span.
trace_id: The unique identifier that groups all spans belonging to the same request or workflow.
timestamp: The exact time the span occurred.
type: The category of operation (e.g., web, db, or cache).
link: References connecting the span to other spans or traces.
ingestion_reason: The reason why the trace or span was retained by the Datadog ingestion pipeline.
When working with reserved attributes, don’t put @ in front.
For example:
service:checkout
env:production
status:error
rather than:
@service:checkout
@env:production
2. SPAN TAGS
Span tags are contextual information about where the span came from for example infrastructure/environment context.
Tags don’t require @.
For example:
hostname:web-server-17
availability-zone:us-east-1a
cluster-name:production-cluster
container_name:api-frontend
These tags provide context about the infrastructure or orchestration environment where the service is running
3. ANNOTATIONS
Annotations are additional pieces of context that you can attach to a span to make the trace more useful for analysis.
Annotations describe business-specific context about an operation.
This helps explain operations from a business perspective.
Example:
A checkout span could have annotations like customer_tier=premium or order_type=subscription.
Spans automatically capture common information such as: duration, status, errors, input, output
But sometimes you want to add your own context.
Example:
Suppose you’re building a customer-support AI.
You may want to know additional information about each request, such as:
In Datadog, annotations are filtered using attribute search syntax, where attributes are prefixed with an @ symbol.
@customer_tier:premium
@language:en
@ticket_category:refund
@department:electronics
Using ddtrace, you can attach this business context to the current span with LLMObs.annotate():
from ddtrace.llmobs import LLMObs
from ddtrace.llmobs.decorators import tool
@tool
def handle_support_request(message, customer_id):
response = process_support_request(message, customer_id)
LLMObs.annotate(
tags={
"customer_tier": "premium",
"language": "en",
"ticket_category": "refund",
"department": "electronics"
}
)
return response
The resulting span now contains both its normal observability information and your additional business context:
Tool Span: handle_support_request
────────────────────────────────────────
Duration:
4.2 sec
Status:
OK
Input:
"I want a refund for my headphones."
Output:
"I'll help you process your refund..."
Annotations:
customer_tier: premium
language: en
ticket_category: refund
department: electronics
────────────────────────────────────────
Using annotations, allow you to filter, group, and analyze traces based on your application’s specific business context.
For example, you could filter traces where:
@customer_tier:premium
to investigate premium-customer requests.
SPANS CATEGORIZATION
Datadog allows spans to be categorized according to the type of operation they represent.
Instead of representing every operation as a generic span (Span), Datadog allows spans to be categorized according to the type of operation they represent:
Categorizing spans makes your traces:
🟢- Easier to understand
🟢🟢- Easier to troubleshoot
🟢🟢🟢- Easier to analyze
Press enter or click to view image in full size
TYPES OF SPANS
Press enter or click to view image in full size
1. Agent Span
An Agent is an AI system that can decide what actions it needs to take and in what order to accomplish a goal.
An Agent span represents the complete execution of an AI agent.
Example:
Consider a travel-planning agent that helps a user plan a trip.
The agent can be implemented as:
from ddtrace.llmobs.decorators import agent
from openai import OpenAI
llm = OpenAI()
# Define tools
def search_flights(destination):
# Tool execution logic
pass
def search_hotels(destination):
# Tool execution logic
pass
def check_weather(destination):
# Tool execution logic
pass
@agent
def plan_trip(customer_query):
# The LLM analyzes the request and decides which tools to use
llm_response = llm.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": customer_query}],
tools=[
search_flights,
search_hotels,
check_weather
]
)
return llm_response.choices[0].message.content
2. Root Span
A root span is defined by its position in the trace hierarchy.
A span is considered a root span, if it is the span at the very top, with no parent span.
An agent span may be the root span, particularly when the agent execution is the top-level operation you’re tracing.
But if the agent is itself invoked by another operation — such as a workflow — it can be a child span.
Example:
def travel_workflow(customer_query):
validate_request(customer_query)
plan_trip(customer_query)
save_itinerary()
return "Workflow complete"
Conceptually, the trace looks like this:
ROOT SPAN: travel_workflow
│
├── Span: validate_request
│
├── Agent Span: plan_trip
│ │
│ ├── LLM Span: understand_requirements
│ │
│ ├── Tool Span: search_flights
│ │
│ ├── Tool Span: search_hotels
│ │
│ ├── LLM Span: compare_options
│ │
│ ├── Tool Span: check_weather
│ │
│ └── LLM Span: generate_itinerary
│
└── Span: save_itinerary
3. Workflow Span
A workflow span represents the execution of a complete, high-level process within an application.
A workflow in Agents Observability is a predefined sequence of operations that may include LLM calls, tool calls, data processing, and other steps required to complete a task.
A Workflow Span represents the complete execution of the workflow.
Unlike an Agent, which can autonomously decide which actions to take, a workflow generally follows a predefined sequence of operations defined by the application.
Example:
Consider a customer support ticket workflow:
The workflow can be implemented using the @workflow decorator:
from ddtrace.llmobs.decorators import workflow, llm, tool, retrieval, task
@tool
def retrieve_customer_data(customer_id):
# Logic to fetch customer information from an external database
pass
@llm
def classify_ticket(ticket):
# LLM call to categorize the support ticket
pass
@retrieval
def search_knowledge_base(category):
# Logic to search a vector database or external KB
pass
@llm
def generate_response(ticket, customer_data, knowledge):
# LLM call to generate the ticket response
pass
@task
def send_response(response):
# Logic to format/send the response (no external call)
pass
@workflow
def process_support_ticket(ticket):
customer_data = retrieve_customer_data(ticket.customer_id)
ticket_category = classify_ticket(ticket)
knowledge = search_knowledge_base(ticket_category)
response = generate_response(ticket, customer_data, knowledge)
send_response(response)
return response
4. Task Span
A task span represents an individual processing operation performed by an application that don’t call an external service.
It is typically used for application-level logic that happens within the application itself, rather than an operation that calls an external service.
Example:
Consider an application that calculates a discount for a customer.
The task can be implemented using the @task decorator:
from ddtrace.llmobs.decorators import task
@task
def calculate_discount(price, customer_type):
if customer_type == "premium":
return price * 0.80
return price
5. LLM Span
An LLM span represents an LLM call.
It captures the model invocation, including the model used, input, output, token usage, latency, and status.
For example:
from ddtrace.llmobs.decorators import llm
@llm(model_name="gpt-4") # model_name is optional
def generate_product_description(product_data):
response = llm.complete(
prompt=f"Generate description for: {product_data['name']}"
)
return response.text
The above example could be represented as:
6. Tool Span
A tool represents calls to external services like databases, APIs made by an AI application.
A tool span represents the details of the external operation, such as what tool was called, the input provided, the result returned, and how long the operation took.
Example:
Consider an AI customer-support assistant, that calls an external API to fetch order details:
from ddtrace.llmobs.decorators import tool
@tool
def fetch_inventory_status(product_id):
inventory = database.query(
"SELECT quantity, location FROM inventory WHERE product_id = ?",
product_id
)
return inventory
7. Embedding span
An Embedding Span represents the operation of converting text or other data into an embedding vector using an embedding model.
Example:
from ddtrace.llmobs.decorators import embedding
@embedding(model_name="text-embedding-ada-002") # model_name is required, model_provider is optional
def generate_product_embedding(product_text):
vector = embedding_model.encode(product_text)
return vector.tolist()
8. Retrieval Span
A retrieval span represents the operation of searching for and retrieving relevant information from a data source, such as a vector database.
The retrieval span captures the search operation and the information returned by it.
Example:
For example, in an AI shopping assistant, a user might ask:
“Find me a black running shoe under $100.”
The retrieval operation can be instrumented using:
from ddtrace.llmobs.decorators import retrieval
@retrieval
def search_product_catalog(query):
query_vector = generate_embedding(query)
results = vector_db.similarity_search(query_vector, top_k=5)
return results
Conclusion
Agent observability helps you see what happens inside your AI application when it processes a request.
Datadog uses traces, spans, and instrumentation to help you follow a request from beginning to end, inspect individual steps, and find out why something failed or took too long.
Logs are useful, but observability gives you a clearer picture of how the different parts of your application work together.
Top comments (0)