22
33import dataclasses
44import json
5+ import threading
56import typing
67from collections .abc import AsyncIterator , Callable , Iterator , Sequence
78from contextlib import asynccontextmanager , contextmanager
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
58108def _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 ()
0 commit comments