Skip to content

Commit 033eb5d

Browse files
committed
Make arize_tracing a standalone sample on temporalio-openai-agents 1.1.0
temporalio-openai-agents 1.1.0 carries the span-parenting fix for running workflows and activities in one worker, but it requires openai 3, and the root lock stays on openai 2 for the LiteLLM samples (litellm caps openai<3). Give the sample its own uv project, like openai_agents/mcp_v2, and add it to the standalone CI matrix. With the fix in place, drop the --role split, the workflow-only plugin switch, the detach-context log filter, and the known-issue notes; update the trace diagrams and the screenshot from a single-process run.
1 parent d2183b6 commit 033eb5d

26 files changed

Lines changed: 2497 additions & 249 deletions

‎.github/CODEOWNERS‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
/litellm_activity/ @temporalio/sdk @temporalio/ai-sdk
2626
/openai_agents/ @temporalio/sdk @temporalio/ai-sdk
2727
/strands_plugin/ @temporalio/sdk @temporalio/ai-sdk
28-
/tests/arize_tracing/ @temporalio/sdk @temporalio/ai-sdk
2928
/tests/deepagents_plugin/ @temporalio/sdk @temporalio/ai-sdk
3029
/tests/google_adk_agents/ @temporalio/sdk @temporalio/ai-sdk
3130
/tests/google_genai/ @temporalio/sdk @temporalio/ai-sdk

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ jobs:
3333
3434
projects_json="$({
3535
for project in \
36+
arize_tracing \
3637
bedrock_agentcore/strands_agent \
3738
lambda_worker \
3839
openai_agents/mcp_v2

‎arize_tracing/README.md‎

Lines changed: 41 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -38,23 +38,29 @@ Contents:
3838

3939
## Run it
4040

41+
The sample has its own uv project because the repository's root lock still
42+
pins `openai` 2.x for the LiteLLM samples, while `temporalio-openai-agents`
43+
1.1.0 needs `openai` 3. Run everything from this directory:
44+
4145
```bash
46+
cd arize_tracing
47+
4248
# 1. Start Phoenix (UI, REST API, and OTLP collector on port 6006)
43-
docker compose -f arize_tracing/phoenix/docker-compose.yml up -d
49+
docker compose -f phoenix/docker-compose.yml up -d
4450
curl -sf http://localhost:6006/healthz && echo ok
4551
# ...or without Docker: uvx --from "arize-phoenix==20.16.0" phoenix serve
4652

47-
# 2. Install dependencies and set environment (repo root)
48-
uv sync --group arize-tracing
49-
cp arize_tracing/.env.example arize_tracing/.env # edit the LLM settings
50-
set -a; source arize_tracing/.env; set +a
53+
# 2. Install dependencies and set environment
54+
uv sync
55+
cp .env.example .env # edit the LLM settings
56+
set -a; source .env; set +a
5157

5258
# 3. Run the sample (two terminals, same environment)
53-
uv run python -m arize_tracing.ticket_triage.worker
54-
uv run python -m arize_tracing.ticket_triage.starter
59+
uv run python -m ticket_triage.worker
60+
uv run python -m ticket_triage.starter
5561

5662
# 4. Verify the trace through the Phoenix API (uses the printed trace ID)
57-
uv run python -m arize_tracing.verify_trace --trace-id <printed trace id>
63+
uv run verify_trace.py --trace-id <printed trace id>
5864
```
5965

6066
The starter prints a direct link to the trace in Phoenix (project
@@ -86,11 +92,9 @@ session.
8692
### With the OpenAI Agents SDK
8793

8894
```bash
89-
# Two worker processes (see "Known issue" below), then the starter
90-
uv run python -m arize_tracing.ticket_triage_agents.worker --role workflows
91-
uv run python -m arize_tracing.ticket_triage_agents.worker --role activities
92-
uv run python -m arize_tracing.ticket_triage_agents.starter
93-
uv run python -m arize_tracing.verify_trace --trace-id <printed trace id> --scenario agents
95+
uv run python -m ticket_triage_agents.worker
96+
uv run python -m ticket_triage_agents.starter
97+
uv run verify_trace.py --trace-id <printed trace id> --scenario agents
9498
```
9599

96100
Here the Agents SDK trace itself becomes the root, and the plugin's
@@ -102,40 +106,17 @@ Ticket triage agents AGENT (root; session, user, in
102106
│ └─ temporal:executeWorkflow CHAIN
103107
│ ├─ Agent workflow → Triage agent AGENT
104108
│ │ ├─ turn CHAIN
105-
│ │ │ └─ temporal:startActivity CHAIN (model call)
106-
│ │ │ ├─ temporal:executeActivity → response LLM
107-
│ │ │ └─ lookup_account TOOL (a Temporal activity as an agent tool)
108-
│ │ │ └─ temporal:startActivity → temporal:executeActivity
109+
│ │ │ ├─ temporal:startActivity → temporal:executeActivity → response LLM (model call)
110+
│ │ │ └─ lookup_account TOOL (a Temporal activity as an agent tool)
111+
│ │ │ └─ temporal:startActivity → temporal:executeActivity
109112
│ │ └─ turn → temporal:startActivity → temporal:executeActivity → response LLM
110113
│ └─ Agent workflow → Reply agent AGENT
111114
│ └─ turn → temporal:startActivity → temporal:executeActivity → response LLM
112115
└─ temporal:updateWorkflow CHAIN
113116
```
114117

115-
(The tool span nests under the preceding model call's `temporal:startActivity`
116-
span rather than directly under the turn because `temporalio-openai-agents`
117-
1.0.0 leaves that span current in the Agents SDK scope after it finishes
118-
([temporalio/sdk-python#1855](https://github.com/temporalio/sdk-python/issues/1855),
119-
fixed upstream in
120-
[temporalio/ai-integrations#26](https://github.com/temporalio/ai-integrations/pull/26)
121-
but not yet released).)
122-
123118
![Ticket triage agents trace in Phoenix](phoenix-ticket-triage-agents.png)
124119

125-
**Known issue.** With `use_otel_instrumentation=True`, `temporalio-openai-agents`
126-
1.0.0 (like `temporalio.contrib.openai_agents`) exports the
127-
`temporal:startActivity` spans with a parent that is not in the trace when a
128-
single worker process runs both the workflow and its activities, so the
129-
model-call and tool subtrees appear detached from the agent turns in Arize
130-
([temporalio/sdk-python#1852](https://github.com/temporalio/sdk-python/issues/1852)).
131-
The fix is merged upstream in
132-
[temporalio/ai-integrations#26](https://github.com/temporalio/ai-integrations/pull/26)
133-
but not yet in a release. `verify_trace.py` reports the problem as spans
134-
referencing a missing parent. Running the workflow and the activities in
135-
separate worker processes, as above, avoids it; `worker.py` without `--role`
136-
runs both in one process, so keep the split until a release with the fix is
137-
out.
138-
139120
## How replay, retries, and restarts show up
140121

141122
Durable execution means workflow code re-executes (replays) on worker
@@ -159,37 +140,37 @@ Reproduce each case with the flags below; every run must still verify cleanly:
159140
```bash
160141
# Replay stress: disable the workflow cache so EVERY workflow task replays
161142
# the workflow from the start of history. The trace must be identical.
162-
uv run python -m arize_tracing.ticket_triage.worker --replay-stress
163-
uv run python -m arize_tracing.ticket_triage.starter
164-
uv run python -m arize_tracing.verify_trace --trace-id <printed trace id>
143+
uv run python -m ticket_triage.worker --replay-stress
144+
uv run python -m ticket_triage.starter
145+
uv run verify_trace.py --trace-id <printed trace id>
165146

166147
# Worker restart mid-workflow: the starter waits 20s before sending the
167148
# approval. Give the triage activities a few seconds to finish, then kill the
168149
# worker while the workflow durably awaits approval; start a new worker and
169150
# watch the workflow (and its trace) complete cleanly.
170-
uv run python -m arize_tracing.ticket_triage.starter --pause-before-approval 20
151+
uv run python -m ticket_triage.starter --pause-before-approval 20
171152
# ... after ~5s, ctrl+c the worker, then start it again in another terminal
172-
uv run python -m arize_tracing.verify_trace --trace-id <printed trace id>
153+
uv run verify_trace.py --trace-id <printed trace id>
173154

174155
# Activity retry: classify_ticket fails on its first attempt. Arize shows two
175156
# RunActivity:classify_ticket spans, the first with an error status.
176-
uv run python -m arize_tracing.ticket_triage.worker --fail-first-attempt
177-
uv run python -m arize_tracing.ticket_triage.starter
178-
uv run python -m arize_tracing.verify_trace --trace-id <id> --expect-attempts classify_ticket=2
157+
uv run python -m ticket_triage.worker --fail-first-attempt
158+
uv run python -m ticket_triage.starter
159+
uv run verify_trace.py --trace-id <id> --expect-attempts classify_ticket=2
179160

180161
# Worker crash mid-activity: classify_ticket heartbeats for 30s first. Kill
181162
# the worker hard (kill -9) during that time and start a new one. The first
182163
# attempt's span was never ended, so it is absent; the retry appears with
183164
# attempt 2 after the heartbeat timeout.
184-
uv run python -m arize_tracing.ticket_triage.worker --slow-classify 30
185-
uv run python -m arize_tracing.ticket_triage.starter
186-
uv run python -m arize_tracing.verify_trace --trace-id <id> --expect-attempt classify_ticket=2
165+
uv run python -m ticket_triage.worker --slow-classify 30
166+
uv run python -m ticket_triage.starter
167+
uv run verify_trace.py --trace-id <id> --expect-attempt classify_ticket=2
187168

188169
# Reset: rerun a finished workflow from its first workflow task. The reset
189170
# reapplies the original approval update, and the new run shares the trace as
190171
# a second RunWorkflow span with its own run ID.
191172
temporal workflow reset --workflow-id <workflow id> --type FirstWorkflowTask --reason demo
192-
uv run python -m arize_tracing.verify_trace --workflow-id <workflow id> --expect-runs 2
173+
uv run verify_trace.py --workflow-id <workflow id> --expect-runs 2
193174
```
194175

195176
![Activity retry attempts in Phoenix](phoenix-retry-attempts.png)
@@ -241,27 +222,25 @@ and have not been exercised against an AX space here.
241222
- `OTEL_SDK_DISABLED=true` turns off export without code changes.
242223
- Ingestion is asynchronous; `verify_trace.py` polls until the trace is stable.
243224
- The OpenAI Agents SDK bridge (`use_otel_instrumentation=True`) ships in the
244-
standalone `temporalio-openai-agents` package and is Public Preview. Run the
245-
starter, the workflow worker, and the activity worker as separate processes,
246-
as the sample does (see the known issue above), and see
247-
`quiet_otel_context_detach_errors()` in `telemetry.py` for a log-noise issue
248-
in 1.0.0 (fixed upstream in temporalio/ai-integrations#26).
225+
standalone `temporalio-openai-agents` package and is Public Preview. Use
226+
1.1.0 or later: earlier versions mis-parent the `temporal:startActivity`
227+
spans when one worker runs both the workflow and its activities
228+
([temporalio/sdk-python#1852](https://github.com/temporalio/sdk-python/issues/1852)).
249229

250230
## Tests
251231

252-
`tests/arize_tracing/` runs without Arize, Docker, or an LLM: mocked activities
232+
`tests/` runs without Arize, Docker, or an LLM: mocked activities
253233
(and the SDK's `TestModel` for the agents scenario), an in-memory span
254234
exporter, a worker with the workflow cache disabled, whole-tree span
255235
assertions, enrichment and retry-attempt assertions, and a `Replayer` pass
256236
asserting that replaying the finished workflow's history emits zero new spans.
257237

258238
```bash
259-
uv run --group arize-tracing pytest tests/arize_tracing -v
239+
uv run poe test # or: uv run pytest -v
240+
uv run poe test --workflow-environment time-skipping
260241
```
261242

262243
## Using this outside samples-python
263244

264-
The sample is self-contained: copy the `arize_tracing/` directory, change the
265-
absolute imports (`arize_tracing.ticket_triage.activities` →
266-
`ticket_triage.activities` or similar), and install the dependencies listed
267-
under `arize-tracing` in this repo's `pyproject.toml`.
245+
The sample is self-contained: copy this directory, keep its `pyproject.toml`
246+
(or merge its dependencies into yours), and run the same commands.
-56.3 KB
Loading

‎arize_tracing/pyproject.toml‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
[project]
2+
name = "temporalio-samples-arize-tracing"
3+
version = "0.1.0"
4+
requires-python = ">=3.10"
5+
dependencies = [
6+
"temporalio[opentelemetry]>=1.33.0,<2",
7+
# 1.1.0 fixes span parenting when one worker runs both workflows and
8+
# activities; its openinference extra brings the Agents SDK instrumentation.
9+
"temporalio-openai-agents[openinference]>=1.1.0",
10+
"openai>=3,<4",
11+
# Phoenix and Arize AX ingest OTLP/HTTP; this sample never uses gRPC.
12+
"opentelemetry-exporter-otlp-proto-http>=1.30.0,<2",
13+
"openinference-semantic-conventions>=0.1.30",
14+
"openinference-instrumentation-openai>=0.1.52",
15+
]
16+
17+
[dependency-groups]
18+
dev = [
19+
"ruff>=0.5.0,<0.6",
20+
"mypy>=1.4.1,<2",
21+
"poethepoet>=0.36.0",
22+
"pytest>=7.1.2,<8",
23+
"pytest-asyncio>=0.18.3,<0.19",
24+
]
25+
26+
[tool.poe.tasks]
27+
format = [
28+
{ cmd = "uv run ruff check --select I --fix" },
29+
{ cmd = "uv run ruff format" },
30+
]
31+
lint = [
32+
{ cmd = "uv run ruff check --select I" },
33+
{ cmd = "uv run ruff format --check" },
34+
{ ref = "lint-types" },
35+
]
36+
lint-types = "uv run --all-groups mypy --check-untyped-defs --namespace-packages --explicit-package-bases ."
37+
test = "uv run pytest"
38+
39+
[tool.ruff]
40+
target-version = "py310"
41+
42+
[tool.mypy]
43+
ignore_missing_imports = true
44+
45+
[tool.pytest.ini_options]
46+
asyncio_mode = "auto"
47+
pythonpath = ["."]
48+
testpaths = ["tests"]

‎arize_tracing/telemetry.py‎

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -196,26 +196,6 @@ def setup_tracing(service_name: str) -> None:
196196
# @@@SNIPEND
197197

198198

199-
class _IgnoreContextDetachErrors(logging.Filter):
200-
def filter(self, record: logging.LogRecord) -> bool:
201-
return not record.getMessage().startswith("Failed to detach context")
202-
203-
204-
def quiet_otel_context_detach_errors() -> None:
205-
"""Silence OpenTelemetry's "Failed to detach context" errors.
206-
207-
The OpenInference OpenAI Agents processor attaches an OpenTelemetry
208-
context when an Agents SDK span starts and detaches it when the span ends.
209-
Temporal runs each workflow task in its own ``contextvars`` context, so for
210-
agent spans that outlive a workflow task the detach happens in a different
211-
context and OpenTelemetry logs an error with a traceback. Tracing is
212-
unaffected (the spans are still correct and exported once), so processes
213-
that use ``OpenAIAgentsPlugin(use_otel_instrumentation=True)`` call this
214-
to keep their logs readable.
215-
"""
216-
logging.getLogger("opentelemetry.context").addFilter(_IgnoreContextDetachErrors())
217-
218-
219199
def force_flush() -> None:
220200
"""Flush any buffered spans to Arize immediately."""
221201
# The replay-safe provider implements force_flush but the base

‎arize_tracing/tests/conftest.py‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Pytest fixtures for the arize_tracing sample tests.
2+
3+
The sample is its own uv project, so these fixtures replace the ones the
4+
repository root provides to the other samples: a Temporal test environment
5+
(``--workflow-environment local|time-skipping|<address>``) and a reset of the
6+
global OpenTelemetry tracer provider so each test can install its own.
7+
"""
8+
9+
import asyncio
10+
from typing import AsyncGenerator, Iterator
11+
12+
import opentelemetry.trace
13+
import pytest
14+
import pytest_asyncio
15+
from opentelemetry.util._once import Once
16+
from temporalio.client import Client
17+
from temporalio.testing import WorkflowEnvironment
18+
19+
20+
def pytest_addoption(parser):
21+
parser.addoption(
22+
"--workflow-environment",
23+
default="local",
24+
help="Which workflow environment to use ('local', 'time-skipping', or target to existing server)",
25+
)
26+
27+
28+
@pytest.fixture(scope="session")
29+
def event_loop():
30+
# Session-scoped async fixtures need a session-scoped loop with this
31+
# version of pytest-asyncio.
32+
loop = asyncio.get_event_loop_policy().new_event_loop()
33+
yield loop
34+
loop.close()
35+
36+
37+
@pytest_asyncio.fixture(scope="session")
38+
async def env(request) -> AsyncGenerator[WorkflowEnvironment, None]:
39+
env_type = request.config.getoption("--workflow-environment")
40+
if env_type == "local":
41+
env = await WorkflowEnvironment.start_local()
42+
elif env_type == "time-skipping":
43+
env = await WorkflowEnvironment.start_time_skipping()
44+
else:
45+
env = WorkflowEnvironment.from_client(await Client.connect(env_type))
46+
yield env
47+
await env.shutdown()
48+
49+
50+
@pytest_asyncio.fixture
51+
async def client(env: WorkflowEnvironment) -> Client:
52+
return env.client
53+
54+
55+
@pytest.fixture
56+
def reset_otel_tracer_provider() -> Iterator[None]:
57+
"""Reset global OpenTelemetry tracer provider state around a test.
58+
59+
OpenTelemetry only allows the global tracer provider to be set once per
60+
process; tests that install their own provider need this reset.
61+
"""
62+
opentelemetry.trace._TRACER_PROVIDER_SET_ONCE = Once()
63+
opentelemetry.trace._TRACER_PROVIDER = None
64+
yield
65+
opentelemetry.trace._TRACER_PROVIDER_SET_ONCE = Once()
66+
opentelemetry.trace._TRACER_PROVIDER = None
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,20 @@
2525
from temporalio.exceptions import ApplicationError
2626
from temporalio.worker import Replayer, Worker
2727

28-
from arize_tracing.telemetry import (
28+
from telemetry import (
2929
ACTIVITY_ATTEMPT_ATTRIBUTE,
3030
TEMPORAL_SCOPE_PREFIX,
3131
OpenInferenceEnrichmentProcessor,
3232
)
33-
from arize_tracing.ticket_triage.activities import (
33+
from tests.helpers import dump_spans
34+
from ticket_triage.activities import (
3435
AccountInfo,
3536
ApprovalDecision,
3637
Classification,
3738
DraftReplyInput,
3839
Ticket,
3940
)
40-
from arize_tracing.ticket_triage.workflows import TicketTriageWorkflow
41-
from tests.arize_tracing.helpers import dump_spans
41+
from ticket_triage.workflows import TicketTriageWorkflow
4242

4343
TICKET = Ticket(
4444
ticket_id="T-1",

0 commit comments

Comments
 (0)