Skip to content

Commit f105e90

Browse files
committed
Fix OpenInference trace patch lifecycle
1 parent 2eea030 commit f105e90

2 files changed

Lines changed: 80 additions & 16 deletions

File tree

temporalio/contrib/openai_agents/_temporal_openai_agents.py

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import dataclasses
44
import json
5+
import threading
56
import typing
67
from collections.abc import AsyncIterator, Callable, Iterator, Sequence
78
from contextlib import asynccontextmanager, contextmanager
@@ -54,6 +55,55 @@
5455
)
5556

5657

58+
_otel_trace_start_patch_lock = threading.RLock()
59+
_otel_trace_start_patch_ref_count = 0
60+
_otel_trace_start_original: Callable[..., typing.Any] | None = None
61+
62+
63+
def _install_otel_trace_start_patch() -> None:
64+
"""Make an OpenInference root span current while tracing is configured."""
65+
global _otel_trace_start_original
66+
global _otel_trace_start_patch_ref_count
67+
68+
from openinference.instrumentation.openai_agents._processor import (
69+
OpenInferenceTracingProcessor,
70+
)
71+
from opentelemetry.context import attach
72+
from opentelemetry.trace import set_span_in_context
73+
74+
with _otel_trace_start_patch_lock:
75+
if _otel_trace_start_patch_ref_count == 0:
76+
_otel_trace_start_original = OpenInferenceTracingProcessor.on_trace_start
77+
78+
def on_trace_start(self: typing.Any, trace: Trace) -> None: # type: ignore[reportUnusedFunction]
79+
_otel_trace_start_original(self, trace) # type: ignore[operator]
80+
attach(set_span_in_context(self._root_spans[trace.trace_id]))
81+
82+
setattr(OpenInferenceTracingProcessor, "on_trace_start", on_trace_start)
83+
_otel_trace_start_patch_ref_count += 1
84+
85+
86+
def _uninstall_otel_trace_start_patch() -> None:
87+
"""Restore OpenInference after the final tracing context exits."""
88+
global _otel_trace_start_original
89+
global _otel_trace_start_patch_ref_count
90+
91+
from openinference.instrumentation.openai_agents._processor import (
92+
OpenInferenceTracingProcessor,
93+
)
94+
95+
with _otel_trace_start_patch_lock:
96+
_otel_trace_start_patch_ref_count -= 1
97+
if _otel_trace_start_patch_ref_count == 0:
98+
if _otel_trace_start_original is not None:
99+
setattr(
100+
OpenInferenceTracingProcessor,
101+
"on_trace_start",
102+
_otel_trace_start_original,
103+
)
104+
_otel_trace_start_original = None
105+
106+
57107
@contextmanager
58108
def _set_open_ai_agent_temporal_overrides(
59109
model_params: ModelActivityParameters,
@@ -377,28 +427,15 @@ def tracing_context(self) -> Iterator[None]:
377427
"""
378428
# Set up OTEL instrumentation if exporters are provided
379429
otel_instrumentor = None
430+
otel_trace_start_patch_installed = False
380431
if self._use_otel_instrumentation and not self._instrumented:
381432
from openinference.instrumentation.openai_agents import (
382433
OpenAIAgentsInstrumentor,
383434
)
384-
from openinference.instrumentation.openai_agents._processor import (
385-
OpenInferenceTracingProcessor,
386-
)
387435
from opentelemetry import trace
388-
from opentelemetry.context import attach
389-
from opentelemetry.trace import set_span_in_context
390-
391-
# Unfortunate monkey patching is needed to ensure the trace is set in context so we can propagate it.
392-
original_on_trace_start = OpenInferenceTracingProcessor.on_trace_start
393-
394-
def on_trace_start(self, trace: Trace) -> None: # type: ignore[reportMissingParameterType]
395-
original_on_trace_start(self, trace)
396-
otel_span = self._root_spans[trace.trace_id]
397-
attach(set_span_in_context(otel_span))
398-
399-
OpenInferenceTracingProcessor.on_trace_start = on_trace_start # type:ignore[method-assign]
400436

401-
# Set up instrumentor
437+
_install_otel_trace_start_patch()
438+
otel_trace_start_patch_installed = True
402439
otel_instrumentor = OpenAIAgentsInstrumentor()
403440
otel_instrumentor.instrument(tracer_provider=trace.get_tracer_provider())
404441
self._instrumented = True
@@ -408,3 +445,5 @@ def on_trace_start(self, trace: Trace) -> None: # type: ignore[reportMissingPar
408445
# Clean up OTEL instrumentation
409446
if otel_instrumentor is not None:
410447
otel_instrumentor.uninstrument()
448+
if otel_trace_start_patch_installed:
449+
_uninstall_otel_trace_start_patch()

tests/contrib/openai_agents/test_openai_tracing.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from temporalio import activity, workflow
1313
from temporalio.client import Client
14+
from temporalio.contrib.openai_agents import _temporal_openai_agents
1415
from temporalio.contrib.openai_agents.testing import (
1516
AgentEnvironment,
1617
)
@@ -50,6 +51,30 @@ def force_flush(self) -> None:
5051
pass
5152

5253

54+
def test_otel_trace_start_patch_does_not_nest() -> None:
55+
from openinference.instrumentation.openai_agents._processor import (
56+
OpenInferenceTracingProcessor,
57+
)
58+
59+
original = OpenInferenceTracingProcessor.on_trace_start
60+
_temporal_openai_agents._install_otel_trace_start_patch()
61+
try:
62+
installed_patch = OpenInferenceTracingProcessor.on_trace_start
63+
assert installed_patch is not original
64+
65+
_temporal_openai_agents._install_otel_trace_start_patch()
66+
try:
67+
assert OpenInferenceTracingProcessor.on_trace_start is installed_patch
68+
finally:
69+
_temporal_openai_agents._uninstall_otel_trace_start_patch()
70+
71+
assert OpenInferenceTracingProcessor.on_trace_start is installed_patch
72+
finally:
73+
_temporal_openai_agents._uninstall_otel_trace_start_patch()
74+
75+
assert OpenInferenceTracingProcessor.on_trace_start is original
76+
77+
5378
async def test_tracing(client: Client):
5479
async with AgentEnvironment(model=research_mock_model()) as env:
5580
client = env.applied_on_client(client)

0 commit comments

Comments
 (0)