Skip to content

Commit 6e7f196

Browse files
jssmithclaude
andcommitted
Add OpenTelemetry tracing examples for OpenAI Agents workflows
This commit adds a comprehensive set of OTEL tracing examples demonstrating three progressive patterns: 1. Basic: Pure automatic instrumentation - plugin handles everything 2. Custom Spans: trace() + custom_span() for logical grouping 3. Direct API: trace() + custom_span() + direct OTEL tracer for detailed instrumentation Key additions: - openai_agents/otel_tracing/ directory with README and three workflow examples - Worker and client scripts for each pattern - Documentation covering setup, configuration, and troubleshooting - Added required OTEL dependencies to pyproject.toml Pattern clarification: - Never use trace() in client code - Use trace() in workflows when using custom_span() (patterns 2 & 3) - Plugin automatically creates root trace for pure automatic instrumentation Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 230fdf8 commit 6e7f196

11 files changed

Lines changed: 780 additions & 1 deletion
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
# OpenTelemetry (OTEL) Tracing for OpenAI Agents
2+
3+
This example demonstrates how to instrument OpenAI Agents workflows with OpenTelemetry (OTEL) for distributed tracing and observability.
4+
5+
Traces can be exported to any OTEL-compatible backend such as Jaeger, Grafana Tempo, Datadog, New Relic, or other tracing systems.
6+
7+
## Overview
8+
9+
The Temporal OpenAI Agents SDK provides built-in OTEL integration that:
10+
- **Automatically instruments** agent runs, model calls, and activities
11+
- **Is replay-safe** - spans are only exported when workflows actually complete (not during replay)
12+
- **Provides deterministic IDs** - consistent span/trace IDs across workflow replays
13+
- **Supports multiple exporters** - send traces to multiple backends simultaneously
14+
15+
## Two Instrumentation Patterns
16+
17+
### 1. Automatic Instrumentation (Recommended for Most Users)
18+
19+
The SDK automatically creates spans for:
20+
- Agent execution
21+
- Model invocations (as Temporal activities)
22+
- Tool/activity calls
23+
- Workflow lifecycle events
24+
25+
**Use this when:** You want visibility into agent behavior without custom instrumentation.
26+
27+
See `run_otel_basic.py` for an example.
28+
29+
### 2. Direct OTEL API Usage (Advanced)
30+
31+
You can use the OpenTelemetry API directly in workflows to:
32+
- Instrument custom business logic
33+
- Add domain-specific spans and attributes
34+
- Integrate with organization-wide OTEL conventions
35+
- Monitor performance of specific operations
36+
37+
**Use this when:** You need to trace custom workflow logic beyond agent/model calls.
38+
39+
See `run_otel_direct_api.py` for an example.
40+
41+
## Quick Start
42+
43+
### Prerequisites
44+
45+
Ensure you have an OTEL collector or backend running. For local testing with Grafana Tempo:
46+
47+
```bash
48+
git clone https://github.com/grafana/tempo.git
49+
cd tempo/example/docker-compose/local
50+
mkdir tempo-data/
51+
docker compose up -d
52+
```
53+
54+
View traces at: http://localhost:3000/explore
55+
56+
### Install Dependencies
57+
58+
```bash
59+
uv sync
60+
```
61+
62+
### Start the Worker
63+
64+
```bash
65+
uv run openai_agents/otel_tracing/run_worker.py
66+
```
67+
68+
### Run Examples
69+
70+
In separate terminals:
71+
72+
**Basic automatic instrumentation:**
73+
```bash
74+
uv run openai_agents/otel_tracing/run_otel_basic.py
75+
```
76+
77+
**Direct OTEL API usage:**
78+
```bash
79+
uv run openai_agents/otel_tracing/run_otel_direct_api.py
80+
```
81+
82+
## Implementation Details
83+
84+
### Plugin Configuration
85+
86+
Configure OTEL exporters in the `OpenAIAgentsPlugin`:
87+
88+
```python
89+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
90+
from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
91+
92+
client = await Client.connect(
93+
"localhost:7233",
94+
plugins=[
95+
OpenAIAgentsPlugin(
96+
model_params=ModelActivityParameters(
97+
start_to_close_timeout=timedelta(seconds=30)
98+
),
99+
otel_exporters=[
100+
OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True)
101+
],
102+
add_temporal_spans=False, # Optional: exclude Temporal internal spans
103+
),
104+
],
105+
)
106+
```
107+
108+
### Key Parameters
109+
110+
**`otel_exporters`**: List of OTEL span exporters
111+
- `OTLPSpanExporter` - OTLP protocol (most common)
112+
- `InMemorySpanExporter` - For testing
113+
- `ConsoleSpanExporter` - Debug output
114+
- Multiple exporters supported simultaneously
115+
116+
**`add_temporal_spans`**: Whether to include Temporal internal spans (default: `True`)
117+
- `False` - Cleaner traces focused on agent logic
118+
- `True` - Full visibility including Temporal internals (startWorkflow, executeActivity, etc.)
119+
120+
### Direct OTEL API Usage
121+
122+
**CRITICAL REQUIREMENTS** for using `opentelemetry.trace` API directly in workflows:
123+
124+
#### 1. Wrap in `custom_span()` from Agents SDK
125+
126+
Direct OTEL calls **MUST** be wrapped in `custom_span()` to establish the bridge between Agent SDK trace context and OTEL context:
127+
128+
```python
129+
from agents import custom_span
130+
import opentelemetry.trace
131+
132+
@workflow.defn
133+
class MyWorkflow:
134+
@workflow.run
135+
async def run(self):
136+
# ✅ CORRECT - custom_span establishes OTEL context
137+
with custom_span("My workflow logic"):
138+
tracer = opentelemetry.trace.get_tracer(__name__)
139+
with tracer.start_as_current_span("custom-instrumentation") as span:
140+
span.set_attribute("business.metric", 42)
141+
# This span will be properly parented
142+
result = await my_business_logic()
143+
144+
# ❌ WRONG - becomes orphaned root span, disconnected from trace
145+
tracer = opentelemetry.trace.get_tracer(__name__)
146+
with tracer.start_as_current_span("orphaned-span"):
147+
# No connection to the agent trace!
148+
pass
149+
```
150+
151+
#### 2. Configure Sandbox Passthrough
152+
153+
The `opentelemetry` module must be allowed in the workflow sandbox:
154+
155+
```python
156+
from temporalio.worker import Worker
157+
from temporalio.worker.workflow_sandbox import (
158+
SandboxedWorkflowRunner,
159+
SandboxRestrictions,
160+
)
161+
162+
worker = Worker(
163+
client,
164+
task_queue="otel-task-queue",
165+
workflows=[MyWorkflow],
166+
workflow_runner=SandboxedWorkflowRunner(
167+
SandboxRestrictions.default.with_passthrough_modules("opentelemetry")
168+
),
169+
)
170+
```
171+
172+
### Trace Context Propagation
173+
174+
Traces automatically propagate through the system:
175+
176+
```
177+
Client trace
178+
└─> Workflow execution
179+
├─> Agent span
180+
│ └─> Model activity
181+
├─> Custom span (if using direct API)
182+
└─> Tool activity
183+
```
184+
185+
- **Client → Workflow**: Trace context propagates when starting workflow within `trace()` block
186+
- **Workflow → Activity**: Context automatically propagates to activities
187+
- **Replay-safe**: Spans only export on actual completion, not during replay
188+
189+
### Environment Configuration
190+
191+
Set the OTEL service name (optional):
192+
```bash
193+
export OTEL_SERVICE_NAME=my-agent-service
194+
```
195+
196+
## Common Use Cases
197+
198+
### Basic Monitoring
199+
Use automatic instrumentation to:
200+
- Monitor agent performance
201+
- Debug agent behavior
202+
- Track model API usage
203+
- Identify bottlenecks
204+
205+
### Custom Business Logic
206+
Use direct OTEL API to:
207+
- Instrument domain-specific operations
208+
- Add business metrics as span attributes
209+
- Create logical groupings of related operations
210+
- Integrate with existing observability stack
211+
212+
### Production Observability
213+
- Export to multiple backends (e.g., Jaeger for dev, Datadog for prod)
214+
- Use `add_temporal_spans=False` for cleaner production traces
215+
- Add custom attributes for filtering/grouping in your observability tool
216+
217+
## Troubleshooting
218+
219+
### Spans not appearing in backend
220+
- Verify OTLP endpoint is accessible
221+
- Check that backend is configured to accept OTLP
222+
- Ensure workflow completes (spans only export on completion)
223+
224+
### Direct OTEL spans become root spans
225+
- Verify you're wrapping calls in `custom_span()`
226+
- Check sandbox passthrough is configured
227+
- Ensure you're within an existing trace context
228+
229+
### Duplicate spans across replays
230+
- This is expected behavior during development with workflow cache
231+
- Spans are only exported once per actual execution, not per replay
232+
233+
## Related Examples
234+
235+
- [grafana-tempo-openai-example](../../../../grafana-tempo-openai-example/) - End-to-end observability demo
236+
- [basic/](../basic/) - Simple agent examples without OTEL
237+
- [financial_research_agent](../financial_research_agent/) - Complex multi-agent example

openai_agents/otel_tracing/__init__.py

Whitespace-only changes.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env python3
2+
"""Client for basic OTEL tracing example.
3+
4+
This demonstrates the simplest OTEL integration - automatic instrumentation
5+
of agent/model/activity spans without any custom code.
6+
7+
The worker configuration handles all OTEL setup. This client just executes
8+
the workflow normally.
9+
"""
10+
11+
import asyncio
12+
import uuid
13+
from datetime import timedelta
14+
15+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
16+
from temporalio.client import Client
17+
from temporalio.contrib.openai_agents import ModelActivityParameters, OpenAIAgentsPlugin
18+
19+
from openai_agents.otel_tracing.workflows.otel_basic_workflow import OtelBasicWorkflow
20+
21+
22+
async def main():
23+
# Configure OTLP exporter (same as worker)
24+
exporter = OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True)
25+
26+
client = await Client.connect(
27+
"localhost:7233",
28+
plugins=[
29+
OpenAIAgentsPlugin(
30+
model_params=ModelActivityParameters(
31+
start_to_close_timeout=timedelta(seconds=60)
32+
),
33+
otel_exporters=[exporter],
34+
add_temporal_spans=False,
35+
),
36+
],
37+
)
38+
39+
question = "What's the weather like in Tokyo?"
40+
print(f"Question: {question}\n")
41+
42+
result = await client.execute_workflow(
43+
OtelBasicWorkflow.run,
44+
question,
45+
id=f"otel-basic-workflow-{uuid.uuid4()}",
46+
task_queue="otel-task-queue",
47+
)
48+
49+
print(f"Answer: {result}\n")
50+
print("✓ Workflow completed")
51+
print("\nView traces at:")
52+
print(" - Grafana Tempo: http://localhost:3000/explore")
53+
print(" - Jaeger: http://localhost:16686/")
54+
print("\nExpected spans in trace:")
55+
print(" - Workflow execution")
56+
print(" - Agent run (Weather Assistant)")
57+
print(" - Model invocation (activity)")
58+
print(" - Tool call (get_weather activity)")
59+
60+
61+
if __name__ == "__main__":
62+
asyncio.run(main())
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env python3
2+
"""Client for custom spans OTEL example.
3+
4+
This demonstrates using custom_span() to create logical groupings in traces
5+
while still benefiting from automatic instrumentation.
6+
"""
7+
8+
import asyncio
9+
import uuid
10+
from datetime import timedelta
11+
12+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
13+
from temporalio.client import Client
14+
from temporalio.contrib.openai_agents import ModelActivityParameters, OpenAIAgentsPlugin
15+
16+
from openai_agents.otel_tracing.workflows.otel_custom_spans_workflow import (
17+
OtelCustomSpansWorkflow,
18+
)
19+
20+
21+
async def main():
22+
# Configure OTLP exporter (same as worker)
23+
exporter = OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True)
24+
25+
client = await Client.connect(
26+
"localhost:7233",
27+
plugins=[
28+
OpenAIAgentsPlugin(
29+
model_params=ModelActivityParameters(
30+
start_to_close_timeout=timedelta(seconds=60)
31+
),
32+
otel_exporters=[exporter],
33+
add_temporal_spans=False,
34+
),
35+
],
36+
)
37+
38+
print("Checking weather for multiple cities...\n")
39+
40+
result = await client.execute_workflow(
41+
OtelCustomSpansWorkflow.run,
42+
id=f"otel-custom-spans-workflow-{uuid.uuid4()}",
43+
task_queue="otel-task-queue",
44+
)
45+
46+
print(f"Results:\n{result}\n")
47+
print("✓ Workflow completed")
48+
print("\nView traces at:")
49+
print(" - Grafana Tempo: http://localhost:3000/explore")
50+
print(" - Jaeger: http://localhost:16686/")
51+
print("\nExpected spans in trace:")
52+
print(" - Multi-city weather check (custom_span grouping)")
53+
print(" - Agent runs for Tokyo, Paris, New York (3 agents)")
54+
print(" - Model invocations (activities)")
55+
print(" - Tool calls (get_weather activities)")
56+
57+
58+
if __name__ == "__main__":
59+
asyncio.run(main())

0 commit comments

Comments
 (0)