1- # Copyright The OpenTelemetry Authors
2- #
3- # Licensed under the Apache License, Version 2.0 (the "License");
4- # you may not use this file except in compliance with the License.
5- # You may obtain a copy of the License at
6- #
7- # http://www.apache.org/licenses/LICENSE-2.0
8- #
9- # Unless required by applicable law or agreed to in writing, software
10- # distributed under the License is distributed on an "AS IS" BASIS,
11- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12- # See the License for the specific language governing permissions and
13- # limitations under the License.
14-
151"""OpenAI Agents instrumentation for OpenTelemetry."""
162
173from __future__ import annotations
184
5+ import importlib
6+ import logging
197import os
20- from typing import Collection , Protocol
21-
22- from agents import tracing
23- from agents .tracing .processor_interface import TracingProcessor
8+ from typing import Any , Collection
249
10+ from opentelemetry ._events import get_event_logger
2511from opentelemetry .instrumentation .instrumentor import BaseInstrumentor
2612from opentelemetry .semconv ._incubating .attributes import (
2713 gen_ai_attributes as GenAI ,
2814)
2915from opentelemetry .semconv .schemas import Schemas
3016from opentelemetry .trace import get_tracer
3117
18+ from .constants import (
19+ GenAIEvaluationAttributes ,
20+ GenAIOperationName ,
21+ GenAIOutputType ,
22+ GenAIProvider ,
23+ GenAIToolType ,
24+ )
25+ from .genai_semantic_processor import (
26+ ContentCaptureMode ,
27+ GenAISemanticProcessor ,
28+ )
3229from .package import _instruments
33- from .span_processor import _OpenAIAgentsSpanProcessor
34- from .version import __version__ # noqa: F401
3530
31+ __all__ = [
32+ "OpenAIAgentsInstrumentor" ,
33+ "GenAIProvider" ,
34+ "GenAIOperationName" ,
35+ "GenAIToolType" ,
36+ "GenAIOutputType" ,
37+ "GenAIEvaluationAttributes" ,
38+ ]
3639
37- class _ProcessorHolder (Protocol ):
38- _processors : Collection [TracingProcessor ]
40+ logger = logging .getLogger (__name__ )
3941
42+ _CONTENT_CAPTURE_ENV = "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"
43+ _SYSTEM_OVERRIDE_ENV = "OTEL_INSTRUMENTATION_OPENAI_AGENTS_SYSTEM"
44+ _CAPTURE_CONTENT_ENV = "OTEL_INSTRUMENTATION_OPENAI_AGENTS_CAPTURE_CONTENT"
45+ _CAPTURE_METRICS_ENV = "OTEL_INSTRUMENTATION_OPENAI_AGENTS_CAPTURE_METRICS"
4046
41- class _TraceProviderLike (Protocol ):
42- _multi_processor : _ProcessorHolder
4347
48+ def _load_tracing_module (): # pragma: no cover - exercised via tests
49+ return importlib .import_module ("agents.tracing" )
4450
45- __all__ = ["OpenAIAgentsInstrumentor" ]
4651
52+ def _get_registered_processors (provider ) -> list :
53+ multi = getattr (provider , "_multi_processor" , None )
54+ processors = getattr (multi , "_processors" , ())
55+ return list (processors )
4756
48- def _resolve_system (_ : str | None ) -> str :
49- # OpenAI spans must report provider name "openai" per semantic conventions.
50- return GenAI .GenAiSystemValues .OPENAI .value
5157
58+ def _resolve_system (value : str | None ) -> str :
59+ if not value :
60+ return GenAI .GenAiSystemValues .OPENAI .value
5261
53- def _get_registered_processors (
54- provider : _TraceProviderLike ,
55- ) -> list [TracingProcessor ]:
56- """Return tracing processors registered on the OpenAI Agents trace provider.
62+ normalized = value .strip ().lower ()
63+ for member in GenAI .GenAiSystemValues :
64+ if normalized == member .value :
65+ return member .value
66+ if normalized == member .name .lower ():
67+ return member .value
68+ return value
5769
58- The provider exposes a private `_multi_processor` attribute with a `_processors`
59- collection that stores the currently registered processors in execution order.
60- """
61- multi = getattr (provider , "_multi_processor" , None )
62- processors = getattr (multi , "_processors" , ())
63- return list (processors )
70+
71+ def _resolve_content_mode (value : Any ) -> ContentCaptureMode :
72+ if isinstance (value , ContentCaptureMode ):
73+ return value
74+ if isinstance (value , bool ):
75+ return (
76+ ContentCaptureMode .SPAN_AND_EVENT
77+ if value
78+ else ContentCaptureMode .NO_CONTENT
79+ )
80+
81+ if value is None :
82+ return ContentCaptureMode .SPAN_AND_EVENT
83+
84+ text = str (value ).strip ().lower ()
85+ if not text :
86+ return ContentCaptureMode .SPAN_AND_EVENT
87+
88+ mapping = {
89+ "span_only" : ContentCaptureMode .SPAN_ONLY ,
90+ "span-only" : ContentCaptureMode .SPAN_ONLY ,
91+ "span" : ContentCaptureMode .SPAN_ONLY ,
92+ "event_only" : ContentCaptureMode .EVENT_ONLY ,
93+ "event-only" : ContentCaptureMode .EVENT_ONLY ,
94+ "event" : ContentCaptureMode .EVENT_ONLY ,
95+ "span_and_event" : ContentCaptureMode .SPAN_AND_EVENT ,
96+ "span-and-event" : ContentCaptureMode .SPAN_AND_EVENT ,
97+ "span_and_events" : ContentCaptureMode .SPAN_AND_EVENT ,
98+ "all" : ContentCaptureMode .SPAN_AND_EVENT ,
99+ "true" : ContentCaptureMode .SPAN_AND_EVENT ,
100+ "1" : ContentCaptureMode .SPAN_AND_EVENT ,
101+ "yes" : ContentCaptureMode .SPAN_AND_EVENT ,
102+ "no_content" : ContentCaptureMode .NO_CONTENT ,
103+ "false" : ContentCaptureMode .NO_CONTENT ,
104+ "0" : ContentCaptureMode .NO_CONTENT ,
105+ "no" : ContentCaptureMode .NO_CONTENT ,
106+ "none" : ContentCaptureMode .NO_CONTENT ,
107+ }
108+
109+ return mapping .get (text , ContentCaptureMode .SPAN_AND_EVENT )
110+
111+
112+ def _resolve_bool (value : Any , default : bool ) -> bool :
113+ if value is None :
114+ return default
115+ if isinstance (value , bool ):
116+ return value
117+ text = str (value ).strip ().lower ()
118+ if text in {"true" , "1" , "yes" , "on" }:
119+ return True
120+ if text in {"false" , "0" , "no" , "off" }:
121+ return False
122+ return default
64123
65124
66125class OpenAIAgentsInstrumentor (BaseInstrumentor ):
67- """Instrumentation that bridges OpenAI Agents tracing to OpenTelemetry spans ."""
126+ """Instrumentation that bridges OpenAI Agents tracing to OpenTelemetry."""
68127
69128 def __init__ (self ) -> None :
70129 super ().__init__ ()
71- self ._processor : _OpenAIAgentsSpanProcessor | None = None
130+ self ._processor : GenAISemanticProcessor | None = None
72131
73132 def _instrument (self , ** kwargs ) -> None :
74133 if self ._processor is not None :
@@ -82,17 +141,48 @@ def _instrument(self, **kwargs) -> None:
82141 schema_url = Schemas .V1_28_0 .value ,
83142 )
84143
85- system = _resolve_system (kwargs .get ("system" ))
86- agent_name_override = kwargs .get ("agent_name" ) or os .getenv (
87- "OTEL_GENAI_AGENT_NAME"
144+ event_logger_provider = kwargs .get ("event_logger_provider" )
145+ event_logger = get_event_logger (
146+ __name__ ,
147+ "" ,
148+ schema_url = Schemas .V1_28_0 .value ,
149+ event_logger_provider = event_logger_provider ,
150+ )
151+
152+ system_override = kwargs .get ("system" ) or os .getenv (
153+ _SYSTEM_OVERRIDE_ENV
88154 )
155+ system = _resolve_system (system_override )
156+
157+ content_override = kwargs .get ("capture_message_content" )
158+ if content_override is None :
159+ content_override = os .getenv (_CONTENT_CAPTURE_ENV ) or os .getenv (
160+ _CAPTURE_CONTENT_ENV
161+ )
162+ content_mode = _resolve_content_mode (content_override )
163+
164+ metrics_override = kwargs .get ("capture_metrics" )
165+ if metrics_override is None :
166+ metrics_override = os .getenv (_CAPTURE_METRICS_ENV )
167+ metrics_enabled = _resolve_bool (metrics_override , default = True )
89168
90- processor = _OpenAIAgentsSpanProcessor (
169+ processor = GenAISemanticProcessor (
91170 tracer = tracer ,
92- system = system ,
93- agent_name_override = agent_name_override ,
171+ event_logger = event_logger ,
172+ system_name = system ,
173+ include_sensitive_data = content_mode
174+ != ContentCaptureMode .NO_CONTENT ,
175+ content_mode = content_mode ,
176+ metrics_enabled = metrics_enabled ,
177+ agent_name = kwargs .get ("agent_name" ),
178+ agent_id = kwargs .get ("agent_id" ),
179+ agent_description = kwargs .get ("agent_description" ),
180+ base_url = kwargs .get ("base_url" ),
181+ server_address = kwargs .get ("server_address" ),
182+ server_port = kwargs .get ("server_port" ),
94183 )
95184
185+ tracing = _load_tracing_module ()
96186 provider = tracing .get_trace_provider ()
97187 existing = _get_registered_processors (provider )
98188 provider .set_processors ([* existing , processor ])
@@ -102,13 +192,16 @@ def _uninstrument(self, **kwargs) -> None:
102192 if self ._processor is None :
103193 return
104194
195+ tracing = _load_tracing_module ()
105196 provider = tracing .get_trace_provider ()
106197 current = _get_registered_processors (provider )
107198 filtered = [proc for proc in current if proc is not self ._processor ]
108199 provider .set_processors (filtered )
109200
110- self ._processor .shutdown ()
111- self ._processor = None
201+ try :
202+ self ._processor .shutdown ()
203+ finally :
204+ self ._processor = None
112205
113206 def instrumentation_dependencies (self ) -> Collection [str ]:
114207 return _instruments
0 commit comments