mseep-agentops 0.4.18__py3-none-any.whl → 0.4.22__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentops/__init__.py +0 -0
- agentops/client/api/base.py +28 -30
- agentops/client/api/versions/v3.py +29 -25
- agentops/client/api/versions/v4.py +87 -46
- agentops/client/client.py +98 -29
- agentops/client/http/README.md +87 -0
- agentops/client/http/http_client.py +126 -172
- agentops/config.py +8 -2
- agentops/instrumentation/OpenTelemetry.md +133 -0
- agentops/instrumentation/README.md +167 -0
- agentops/instrumentation/__init__.py +13 -1
- agentops/instrumentation/agentic/ag2/__init__.py +18 -0
- agentops/instrumentation/agentic/ag2/instrumentor.py +922 -0
- agentops/instrumentation/agentic/agno/__init__.py +19 -0
- agentops/instrumentation/agentic/agno/attributes/__init__.py +20 -0
- agentops/instrumentation/agentic/agno/attributes/agent.py +250 -0
- agentops/instrumentation/agentic/agno/attributes/metrics.py +214 -0
- agentops/instrumentation/agentic/agno/attributes/storage.py +158 -0
- agentops/instrumentation/agentic/agno/attributes/team.py +195 -0
- agentops/instrumentation/agentic/agno/attributes/tool.py +210 -0
- agentops/instrumentation/agentic/agno/attributes/workflow.py +254 -0
- agentops/instrumentation/agentic/agno/instrumentor.py +1313 -0
- agentops/instrumentation/agentic/crewai/LICENSE +201 -0
- agentops/instrumentation/agentic/crewai/NOTICE.md +10 -0
- agentops/instrumentation/agentic/crewai/__init__.py +6 -0
- agentops/instrumentation/agentic/crewai/crewai_span_attributes.py +335 -0
- agentops/instrumentation/agentic/crewai/instrumentation.py +535 -0
- agentops/instrumentation/agentic/crewai/version.py +1 -0
- agentops/instrumentation/agentic/google_adk/__init__.py +19 -0
- agentops/instrumentation/agentic/google_adk/instrumentor.py +68 -0
- agentops/instrumentation/agentic/google_adk/patch.py +767 -0
- agentops/instrumentation/agentic/haystack/__init__.py +1 -0
- agentops/instrumentation/agentic/haystack/instrumentor.py +186 -0
- agentops/instrumentation/agentic/langgraph/__init__.py +3 -0
- agentops/instrumentation/agentic/langgraph/attributes.py +54 -0
- agentops/instrumentation/agentic/langgraph/instrumentation.py +598 -0
- agentops/instrumentation/agentic/langgraph/version.py +1 -0
- agentops/instrumentation/agentic/openai_agents/README.md +156 -0
- agentops/instrumentation/agentic/openai_agents/SPANS.md +145 -0
- agentops/instrumentation/agentic/openai_agents/TRACING_API.md +144 -0
- agentops/instrumentation/agentic/openai_agents/__init__.py +30 -0
- agentops/instrumentation/agentic/openai_agents/attributes/common.py +549 -0
- agentops/instrumentation/agentic/openai_agents/attributes/completion.py +172 -0
- agentops/instrumentation/agentic/openai_agents/attributes/model.py +58 -0
- agentops/instrumentation/agentic/openai_agents/attributes/tokens.py +275 -0
- agentops/instrumentation/agentic/openai_agents/exporter.py +469 -0
- agentops/instrumentation/agentic/openai_agents/instrumentor.py +107 -0
- agentops/instrumentation/agentic/openai_agents/processor.py +58 -0
- agentops/instrumentation/agentic/smolagents/README.md +88 -0
- agentops/instrumentation/agentic/smolagents/__init__.py +12 -0
- agentops/instrumentation/agentic/smolagents/attributes/agent.py +354 -0
- agentops/instrumentation/agentic/smolagents/attributes/model.py +205 -0
- agentops/instrumentation/agentic/smolagents/instrumentor.py +286 -0
- agentops/instrumentation/agentic/smolagents/stream_wrapper.py +258 -0
- agentops/instrumentation/agentic/xpander/__init__.py +15 -0
- agentops/instrumentation/agentic/xpander/context.py +112 -0
- agentops/instrumentation/agentic/xpander/instrumentor.py +877 -0
- agentops/instrumentation/agentic/xpander/trace_probe.py +86 -0
- agentops/instrumentation/agentic/xpander/version.py +3 -0
- agentops/instrumentation/common/README.md +65 -0
- agentops/instrumentation/common/attributes.py +1 -2
- agentops/instrumentation/providers/anthropic/__init__.py +24 -0
- agentops/instrumentation/providers/anthropic/attributes/__init__.py +23 -0
- agentops/instrumentation/providers/anthropic/attributes/common.py +64 -0
- agentops/instrumentation/providers/anthropic/attributes/message.py +541 -0
- agentops/instrumentation/providers/anthropic/attributes/tools.py +231 -0
- agentops/instrumentation/providers/anthropic/event_handler_wrapper.py +90 -0
- agentops/instrumentation/providers/anthropic/instrumentor.py +146 -0
- agentops/instrumentation/providers/anthropic/stream_wrapper.py +436 -0
- agentops/instrumentation/providers/google_genai/README.md +33 -0
- agentops/instrumentation/providers/google_genai/__init__.py +24 -0
- agentops/instrumentation/providers/google_genai/attributes/__init__.py +25 -0
- agentops/instrumentation/providers/google_genai/attributes/chat.py +125 -0
- agentops/instrumentation/providers/google_genai/attributes/common.py +88 -0
- agentops/instrumentation/providers/google_genai/attributes/model.py +284 -0
- agentops/instrumentation/providers/google_genai/instrumentor.py +170 -0
- agentops/instrumentation/providers/google_genai/stream_wrapper.py +238 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/__init__.py +28 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/attributes/__init__.py +27 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/attributes/attributes.py +277 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/attributes/common.py +104 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/instrumentor.py +162 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/stream_wrapper.py +302 -0
- agentops/instrumentation/providers/mem0/__init__.py +45 -0
- agentops/instrumentation/providers/mem0/common.py +377 -0
- agentops/instrumentation/providers/mem0/instrumentor.py +270 -0
- agentops/instrumentation/providers/mem0/memory.py +430 -0
- agentops/instrumentation/providers/openai/__init__.py +21 -0
- agentops/instrumentation/providers/openai/attributes/__init__.py +7 -0
- agentops/instrumentation/providers/openai/attributes/common.py +55 -0
- agentops/instrumentation/providers/openai/attributes/response.py +607 -0
- agentops/instrumentation/providers/openai/config.py +36 -0
- agentops/instrumentation/providers/openai/instrumentor.py +312 -0
- agentops/instrumentation/providers/openai/stream_wrapper.py +941 -0
- agentops/instrumentation/providers/openai/utils.py +44 -0
- agentops/instrumentation/providers/openai/v0.py +176 -0
- agentops/instrumentation/providers/openai/v0_wrappers.py +483 -0
- agentops/instrumentation/providers/openai/wrappers/__init__.py +30 -0
- agentops/instrumentation/providers/openai/wrappers/assistant.py +277 -0
- agentops/instrumentation/providers/openai/wrappers/chat.py +259 -0
- agentops/instrumentation/providers/openai/wrappers/completion.py +109 -0
- agentops/instrumentation/providers/openai/wrappers/embeddings.py +94 -0
- agentops/instrumentation/providers/openai/wrappers/image_gen.py +75 -0
- agentops/instrumentation/providers/openai/wrappers/responses.py +191 -0
- agentops/instrumentation/providers/openai/wrappers/shared.py +81 -0
- agentops/instrumentation/utilities/concurrent_futures/__init__.py +10 -0
- agentops/instrumentation/utilities/concurrent_futures/instrumentation.py +206 -0
- agentops/integration/callbacks/dspy/__init__.py +11 -0
- agentops/integration/callbacks/dspy/callback.py +471 -0
- agentops/integration/callbacks/langchain/README.md +59 -0
- agentops/integration/callbacks/langchain/__init__.py +15 -0
- agentops/integration/callbacks/langchain/callback.py +791 -0
- agentops/integration/callbacks/langchain/utils.py +54 -0
- agentops/legacy/crewai.md +121 -0
- agentops/logging/instrument_logging.py +4 -0
- agentops/sdk/README.md +220 -0
- agentops/sdk/core.py +75 -32
- agentops/sdk/descriptors/classproperty.py +28 -0
- agentops/sdk/exporters.py +152 -33
- agentops/semconv/README.md +125 -0
- agentops/semconv/span_kinds.py +0 -2
- agentops/validation.py +102 -63
- {mseep_agentops-0.4.18.dist-info → mseep_agentops-0.4.22.dist-info}/METADATA +30 -40
- mseep_agentops-0.4.22.dist-info/RECORD +178 -0
- {mseep_agentops-0.4.18.dist-info → mseep_agentops-0.4.22.dist-info}/WHEEL +1 -2
- mseep_agentops-0.4.18.dist-info/RECORD +0 -94
- mseep_agentops-0.4.18.dist-info/top_level.txt +0 -2
- tests/conftest.py +0 -10
- tests/unit/client/__init__.py +0 -1
- tests/unit/client/test_http_adapter.py +0 -221
- tests/unit/client/test_http_client.py +0 -206
- tests/unit/conftest.py +0 -54
- tests/unit/sdk/__init__.py +0 -1
- tests/unit/sdk/instrumentation_tester.py +0 -207
- tests/unit/sdk/test_attributes.py +0 -392
- tests/unit/sdk/test_concurrent_instrumentation.py +0 -468
- tests/unit/sdk/test_decorators.py +0 -763
- tests/unit/sdk/test_exporters.py +0 -241
- tests/unit/sdk/test_factory.py +0 -1188
- tests/unit/sdk/test_internal_span_processor.py +0 -397
- tests/unit/sdk/test_resource_attributes.py +0 -35
- tests/unit/test_config.py +0 -82
- tests/unit/test_context_manager.py +0 -777
- tests/unit/test_events.py +0 -27
- tests/unit/test_host_env.py +0 -54
- tests/unit/test_init_py.py +0 -501
- tests/unit/test_serialization.py +0 -433
- tests/unit/test_session.py +0 -676
- tests/unit/test_user_agent.py +0 -34
- tests/unit/test_validation.py +0 -405
- {tests → agentops/instrumentation/agentic/openai_agents/attributes}/__init__.py +0 -0
- /tests/unit/__init__.py → /agentops/instrumentation/providers/openai/attributes/tools.py +0 -0
- {mseep_agentops-0.4.18.dist-info → mseep_agentops-0.4.22.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,469 @@
|
|
1
|
+
"""OpenAI Agents SDK Instrumentation Exporter for AgentOps
|
2
|
+
|
3
|
+
This module handles the conversion of Agents SDK spans to OpenTelemetry spans.
|
4
|
+
It manages the complete span lifecycle, attribute application, and proper span hierarchy.
|
5
|
+
|
6
|
+
See the README.md in this directory for complete documentation on:
|
7
|
+
- Span lifecycle management approach
|
8
|
+
- Serialization rules for attributes
|
9
|
+
- Structured attribute handling
|
10
|
+
- Semantic conventions usage
|
11
|
+
|
12
|
+
IMPORTANT FOR TESTING:
|
13
|
+
- Tests should verify attribute existence using MessageAttributes constants
|
14
|
+
- Do not check for the presence of SpanAttributes.LLM_COMPLETIONS
|
15
|
+
- Verify individual content/tool attributes instead of root attributes
|
16
|
+
"""
|
17
|
+
|
18
|
+
import json
|
19
|
+
from typing import Any, Dict, Optional
|
20
|
+
|
21
|
+
from opentelemetry import trace, context as context_api
|
22
|
+
from opentelemetry.trace import get_tracer, SpanKind, Status, StatusCode, NonRecordingSpan
|
23
|
+
from opentelemetry import trace as trace_api
|
24
|
+
from opentelemetry.sdk.trace import Span
|
25
|
+
|
26
|
+
from agentops.logging import logger
|
27
|
+
from agentops.semconv import (
|
28
|
+
CoreAttributes,
|
29
|
+
)
|
30
|
+
|
31
|
+
from agentops.instrumentation.common.attributes import (
|
32
|
+
get_base_trace_attributes,
|
33
|
+
get_base_span_attributes,
|
34
|
+
)
|
35
|
+
|
36
|
+
from agentops.instrumentation.agentic.openai_agents import LIBRARY_NAME, LIBRARY_VERSION
|
37
|
+
from agentops.instrumentation.agentic.openai_agents.attributes.common import (
|
38
|
+
get_span_attributes,
|
39
|
+
)
|
40
|
+
|
41
|
+
|
42
|
+
def log_otel_trace_id(span_type):
|
43
|
+
"""Log the OpenTelemetry trace ID for debugging and correlation purposes.
|
44
|
+
|
45
|
+
The hexadecimal OTel trace ID is essential for querying the backend database
|
46
|
+
and correlating local debugging logs with server-side trace data. This ID
|
47
|
+
is different from the Agents SDK trace_id and is the primary key used in
|
48
|
+
observability systems and the AgentOps dashboard.
|
49
|
+
|
50
|
+
This function retrieves the current OpenTelemetry trace ID directly from the
|
51
|
+
active span context and formats it as a 32-character hex string.
|
52
|
+
|
53
|
+
Args:
|
54
|
+
span_type: The type of span being exported for logging context
|
55
|
+
|
56
|
+
Returns:
|
57
|
+
str or None: The OpenTelemetry trace ID as a hex string, or None if unavailable
|
58
|
+
"""
|
59
|
+
current_span = trace.get_current_span()
|
60
|
+
if hasattr(current_span, "get_span_context"):
|
61
|
+
ctx = current_span.get_span_context()
|
62
|
+
if hasattr(ctx, "trace_id") and ctx.trace_id:
|
63
|
+
# Convert trace_id to 32-character hex string as shown in the API
|
64
|
+
otel_trace_id = f"{ctx.trace_id:032x}" if isinstance(ctx.trace_id, int) else str(ctx.trace_id)
|
65
|
+
logger.debug(f"[SPAN] Export | Type: {span_type} | TRACE ID: {otel_trace_id}")
|
66
|
+
return otel_trace_id
|
67
|
+
|
68
|
+
logger.debug(f"[SPAN] Export | Type: {span_type} | NO TRACE ID AVAILABLE")
|
69
|
+
return None
|
70
|
+
|
71
|
+
|
72
|
+
def get_span_kind(span: Any) -> SpanKind:
|
73
|
+
"""Determine the appropriate span kind based on span type."""
|
74
|
+
span_data = span.span_data
|
75
|
+
span_type = span_data.__class__.__name__
|
76
|
+
|
77
|
+
if span_type == "AgentSpanData":
|
78
|
+
return SpanKind.CONSUMER
|
79
|
+
elif span_type in ["FunctionSpanData", "GenerationSpanData", "ResponseSpanData"]:
|
80
|
+
return SpanKind.CLIENT
|
81
|
+
else:
|
82
|
+
return SpanKind.INTERNAL
|
83
|
+
|
84
|
+
|
85
|
+
def get_span_name(span: Any) -> str:
|
86
|
+
"""Get the name of the span based on its type and attributes."""
|
87
|
+
span_data = span.span_data
|
88
|
+
span_type = span_data.__class__.__name__
|
89
|
+
|
90
|
+
if hasattr(span_data, "name") and span_data.name:
|
91
|
+
return span_data.name
|
92
|
+
else:
|
93
|
+
return span_type.replace("SpanData", "").lower() # fallback
|
94
|
+
|
95
|
+
|
96
|
+
def _get_span_lookup_key(trace_id: str, span_id: str) -> str:
|
97
|
+
"""Generate a unique lookup key for spans based on trace and span IDs.
|
98
|
+
|
99
|
+
This key is used to track spans in the exporter and allows for efficient
|
100
|
+
lookups and management of spans during their lifecycle.
|
101
|
+
|
102
|
+
Args:
|
103
|
+
trace_id: The trace ID for the current span
|
104
|
+
span_id: The span ID for the current span
|
105
|
+
|
106
|
+
Returns:
|
107
|
+
str: A unique lookup key for the span
|
108
|
+
"""
|
109
|
+
return f"span:{trace_id}:{span_id}"
|
110
|
+
|
111
|
+
|
112
|
+
class OpenAIAgentsExporter:
|
113
|
+
"""Exporter for Agents SDK traces and spans that forwards them to OpenTelemetry.
|
114
|
+
|
115
|
+
This exporter is responsible for:
|
116
|
+
1. Creating and configuring spans
|
117
|
+
2. Setting span attributes based on data from the processor
|
118
|
+
3. Managing the span lifecycle
|
119
|
+
4. Using semantic conventions for attribute naming
|
120
|
+
5. Interacting with the OpenTelemetry API
|
121
|
+
6. Tracking spans to allow updating them when tasks complete
|
122
|
+
"""
|
123
|
+
|
124
|
+
def __init__(self, tracer_provider=None):
|
125
|
+
self.tracer_provider = tracer_provider
|
126
|
+
# Dictionary to track active spans by their SDK span ID
|
127
|
+
# Allows us to reference spans later during task completion
|
128
|
+
self._active_spans = {}
|
129
|
+
# Dictionary to track spans by trace/span ID for faster lookups
|
130
|
+
self._span_map = {}
|
131
|
+
|
132
|
+
def export_trace(self, trace: Any) -> None:
|
133
|
+
"""
|
134
|
+
Handle exporting the trace.
|
135
|
+
"""
|
136
|
+
tracer = get_tracer(LIBRARY_NAME, LIBRARY_VERSION, self.tracer_provider)
|
137
|
+
trace_id = getattr(trace, "trace_id", "unknown")
|
138
|
+
|
139
|
+
if not hasattr(trace, "trace_id"):
|
140
|
+
logger.debug("Cannot export trace: missing trace_id")
|
141
|
+
return
|
142
|
+
|
143
|
+
# Determine if this is a trace end event using status field
|
144
|
+
# We use the status field to determine if this is an end event
|
145
|
+
is_end_event = hasattr(trace, "status") and trace.status == StatusCode.OK.name
|
146
|
+
trace_lookup_key = _get_span_lookup_key(trace_id, trace_id)
|
147
|
+
attributes = get_base_trace_attributes(trace)
|
148
|
+
|
149
|
+
# For end events, check if we already have the span
|
150
|
+
if is_end_event and trace_lookup_key in self._span_map:
|
151
|
+
existing_span = self._span_map[trace_lookup_key]
|
152
|
+
|
153
|
+
span_is_ended = False
|
154
|
+
if isinstance(existing_span, Span) and hasattr(existing_span, "_end_time"):
|
155
|
+
span_is_ended = existing_span._end_time is not None
|
156
|
+
|
157
|
+
if not span_is_ended:
|
158
|
+
# Update with core attributes
|
159
|
+
for key, value in attributes.items():
|
160
|
+
existing_span.set_attribute(key, value)
|
161
|
+
|
162
|
+
# Handle error if present
|
163
|
+
if hasattr(trace, "error") and trace.error:
|
164
|
+
self._handle_span_error(trace, existing_span)
|
165
|
+
# Set status to OK if no error
|
166
|
+
else:
|
167
|
+
existing_span.set_status(Status(StatusCode.OK))
|
168
|
+
|
169
|
+
existing_span.end()
|
170
|
+
|
171
|
+
# Clean up our tracking resources
|
172
|
+
self._active_spans.pop(trace_id, None)
|
173
|
+
self._span_map.pop(trace_lookup_key, None)
|
174
|
+
return
|
175
|
+
|
176
|
+
# Create span directly instead of using context manager
|
177
|
+
span = tracer.start_span(name=trace.name, kind=SpanKind.INTERNAL, attributes=attributes)
|
178
|
+
|
179
|
+
# Add any additional trace attributes
|
180
|
+
if hasattr(trace, "group_id") and trace.group_id:
|
181
|
+
span.set_attribute(CoreAttributes.GROUP_ID, trace.group_id)
|
182
|
+
|
183
|
+
if hasattr(trace, "metadata") and trace.metadata:
|
184
|
+
for key, value in trace.metadata.items():
|
185
|
+
if isinstance(value, (str, int, float, bool)):
|
186
|
+
span.set_attribute(f"trace.metadata.{key}", value)
|
187
|
+
|
188
|
+
# Record error if present
|
189
|
+
if hasattr(trace, "error") and trace.error:
|
190
|
+
self._handle_span_error(trace, span)
|
191
|
+
|
192
|
+
# For start events, store the span for later reference
|
193
|
+
if not is_end_event:
|
194
|
+
self._span_map[trace_lookup_key] = span
|
195
|
+
self._active_spans[trace_id] = {
|
196
|
+
"span": span,
|
197
|
+
"span_type": "TraceSpan",
|
198
|
+
"trace_id": trace_id,
|
199
|
+
"parent_id": None,
|
200
|
+
}
|
201
|
+
else:
|
202
|
+
span.end()
|
203
|
+
|
204
|
+
def _get_parent_context(self, trace_id: str, span_id: str, parent_id: Optional[str] = None) -> Any:
|
205
|
+
"""Find the parent span context for proper span nesting.
|
206
|
+
|
207
|
+
This method checks:
|
208
|
+
1. First for an explicit parent ID in our span tracking dictionary
|
209
|
+
2. Then checks if the trace span is the parent
|
210
|
+
3. Falls back to the current active span context if no parent is found
|
211
|
+
|
212
|
+
Args:
|
213
|
+
trace_id: The trace ID for the current span
|
214
|
+
span_id: The span ID for the current span
|
215
|
+
parent_id: Optional parent span ID to look up
|
216
|
+
|
217
|
+
Returns:
|
218
|
+
The OpenTelemetry span context to use as parent
|
219
|
+
"""
|
220
|
+
parent_span_ctx = None
|
221
|
+
|
222
|
+
if parent_id:
|
223
|
+
# Try to find the parent span in our tracking dictionary
|
224
|
+
parent_lookup_key = f"span:{trace_id}:{parent_id}"
|
225
|
+
if parent_lookup_key in self._span_map:
|
226
|
+
parent_span = self._span_map[parent_lookup_key]
|
227
|
+
# Get the context from the parent span if it exists
|
228
|
+
if hasattr(parent_span, "get_span_context"):
|
229
|
+
parent_span_ctx = parent_span.get_span_context()
|
230
|
+
|
231
|
+
# If parent not found by span ID, check if trace span should be the parent
|
232
|
+
if not parent_span_ctx and parent_id is None:
|
233
|
+
# Try using the trace span as parent
|
234
|
+
trace_lookup_key = _get_span_lookup_key(trace_id, trace_id)
|
235
|
+
|
236
|
+
if trace_lookup_key in self._span_map:
|
237
|
+
trace_span = self._span_map[trace_lookup_key]
|
238
|
+
if hasattr(trace_span, "get_span_context"):
|
239
|
+
parent_span_ctx = trace_span.get_span_context()
|
240
|
+
|
241
|
+
# If we couldn't find the parent by ID, use the current span context as parent
|
242
|
+
if not parent_span_ctx:
|
243
|
+
# Get the current span context from the context API
|
244
|
+
ctx = context_api.get_current()
|
245
|
+
parent_span_ctx = trace_api.get_current_span(ctx).get_span_context()
|
246
|
+
|
247
|
+
return parent_span_ctx
|
248
|
+
|
249
|
+
def _create_span_with_parent(
|
250
|
+
self, name: str, kind: SpanKind, attributes: Dict[str, Any], parent_ctx: Any, end_immediately: bool = False
|
251
|
+
) -> Any:
|
252
|
+
"""Create a span with the specified parent context.
|
253
|
+
|
254
|
+
This centralizes span creation with proper parent nesting.
|
255
|
+
|
256
|
+
Args:
|
257
|
+
name: The name for the new span
|
258
|
+
kind: The span kind (CLIENT, SERVER, etc.)
|
259
|
+
attributes: The attributes to set on the span
|
260
|
+
parent_ctx: The parent context to use for nesting
|
261
|
+
end_immediately: Whether to end the span immediately
|
262
|
+
|
263
|
+
Returns:
|
264
|
+
The newly created span
|
265
|
+
"""
|
266
|
+
# Get tracer from provider
|
267
|
+
tracer = get_tracer(LIBRARY_NAME, LIBRARY_VERSION, self.tracer_provider)
|
268
|
+
|
269
|
+
# Create span with context so we get proper nesting
|
270
|
+
with trace_api.use_span(NonRecordingSpan(parent_ctx), end_on_exit=False):
|
271
|
+
span = tracer.start_span(name=name, kind=kind, attributes=attributes)
|
272
|
+
|
273
|
+
# Optionally end the span immediately
|
274
|
+
if end_immediately:
|
275
|
+
span.end()
|
276
|
+
|
277
|
+
return span
|
278
|
+
|
279
|
+
def export_span(self, span: Any) -> None:
|
280
|
+
"""Export a span to OpenTelemetry, creating or updating as needed.
|
281
|
+
|
282
|
+
This method decides whether to create a new span or update an existing one
|
283
|
+
based on whether this is a start or end event for a given span ID.
|
284
|
+
|
285
|
+
For start events:
|
286
|
+
- Create a new span and store it for later updates
|
287
|
+
- Leave status as UNSET (in progress)
|
288
|
+
- Do not end the span
|
289
|
+
- Properly set parent span reference for nesting
|
290
|
+
|
291
|
+
For end events:
|
292
|
+
- Look for an existing span to update
|
293
|
+
- If found and not ended, update with final data and end it
|
294
|
+
- If not found or already ended, create a new complete span with all data
|
295
|
+
- End the span with proper status
|
296
|
+
"""
|
297
|
+
if not hasattr(span, "span_data"):
|
298
|
+
return
|
299
|
+
|
300
|
+
span_data = span.span_data
|
301
|
+
span_type = span_data.__class__.__name__
|
302
|
+
span_id = getattr(span, "span_id", "unknown")
|
303
|
+
trace_id = getattr(span, "trace_id", "unknown")
|
304
|
+
parent_id = getattr(span, "parent_id", None)
|
305
|
+
|
306
|
+
# Check if this is a span end event
|
307
|
+
is_end_event = hasattr(span, "status") and span.status == StatusCode.OK.name
|
308
|
+
|
309
|
+
# Unique lookup key for this span
|
310
|
+
span_lookup_key = _get_span_lookup_key(trace_id, span_id)
|
311
|
+
|
312
|
+
attributes = get_base_span_attributes(span)
|
313
|
+
span_attributes = get_span_attributes(span_data)
|
314
|
+
attributes.update(span_attributes)
|
315
|
+
|
316
|
+
if is_end_event:
|
317
|
+
# Update all attributes for end events
|
318
|
+
attributes.update(span_attributes)
|
319
|
+
|
320
|
+
# Log the trace ID for debugging and correlation with AgentOps API
|
321
|
+
log_otel_trace_id(span_type)
|
322
|
+
|
323
|
+
# For start events, create a new span and store it (don't end it)
|
324
|
+
if not is_end_event:
|
325
|
+
# Process the span based on its type
|
326
|
+
# TODO span_name should come from the attributes module
|
327
|
+
span_name = get_span_name(span)
|
328
|
+
span_kind = get_span_kind(span)
|
329
|
+
|
330
|
+
# Get parent context for proper nesting
|
331
|
+
parent_span_ctx = self._get_parent_context(trace_id, span_id, parent_id)
|
332
|
+
|
333
|
+
# Create the span with proper parent context
|
334
|
+
otel_span = self._create_span_with_parent(
|
335
|
+
name=span_name, kind=span_kind, attributes=attributes, parent_ctx=parent_span_ctx
|
336
|
+
)
|
337
|
+
|
338
|
+
# Store the span for later reference
|
339
|
+
if not isinstance(otel_span, NonRecordingSpan):
|
340
|
+
self._span_map[span_lookup_key] = otel_span
|
341
|
+
self._active_spans[span_id] = {
|
342
|
+
"span": otel_span,
|
343
|
+
"span_type": span_type,
|
344
|
+
"trace_id": trace_id,
|
345
|
+
"parent_id": parent_id,
|
346
|
+
}
|
347
|
+
|
348
|
+
# Handle any error information
|
349
|
+
self._handle_span_error(span, otel_span)
|
350
|
+
# DO NOT end the span for start events - we want to keep it open for updates
|
351
|
+
return
|
352
|
+
|
353
|
+
# For end events, check if we already have the span
|
354
|
+
if span_lookup_key in self._span_map:
|
355
|
+
existing_span = self._span_map[span_lookup_key]
|
356
|
+
|
357
|
+
span_is_ended = False
|
358
|
+
if isinstance(existing_span, Span) and hasattr(existing_span, "_end_time"):
|
359
|
+
span_is_ended = existing_span._end_time is not None
|
360
|
+
|
361
|
+
if not span_is_ended:
|
362
|
+
# Update with final attributes
|
363
|
+
for key, value in attributes.items():
|
364
|
+
existing_span.set_attribute(key, value)
|
365
|
+
|
366
|
+
existing_span.set_status(
|
367
|
+
Status(StatusCode.OK if getattr(span, "status", "OK") == "OK" else StatusCode.ERROR)
|
368
|
+
)
|
369
|
+
self._handle_span_error(span, existing_span)
|
370
|
+
existing_span.end()
|
371
|
+
# Span already ended, create a new one (should be rare if logic is correct)
|
372
|
+
else:
|
373
|
+
logger.warning(
|
374
|
+
f"[Exporter] SDK span_id: {span_id} (END event) - Attempting to end an ALREADY ENDED span: {span_lookup_key}. Creating a new one instead."
|
375
|
+
)
|
376
|
+
self.create_span(span, span_type, attributes, is_already_ended=True)
|
377
|
+
# No existing span found for end event, create a new one
|
378
|
+
else:
|
379
|
+
logger.warning(
|
380
|
+
f"[Exporter] SDK span_id: {span_id} (END event) - No active span found for end event: {span_lookup_key}. Creating a new one."
|
381
|
+
)
|
382
|
+
self.create_span(span, span_type, attributes, is_already_ended=True)
|
383
|
+
|
384
|
+
self._active_spans.pop(span_id, None)
|
385
|
+
self._span_map.pop(span_lookup_key, None)
|
386
|
+
|
387
|
+
def create_span(
|
388
|
+
self, span: Any, span_type: str, attributes: Dict[str, Any], is_already_ended: bool = False
|
389
|
+
) -> None:
|
390
|
+
"""Create a new span with the provided data and end it immediately.
|
391
|
+
|
392
|
+
This method creates a span using the appropriate parent context, applies
|
393
|
+
all attributes, and ends it immediately since it's for spans that are
|
394
|
+
already in an ended state.
|
395
|
+
|
396
|
+
Args:
|
397
|
+
span: The span data from the Agents SDK
|
398
|
+
span_type: The type of span being created
|
399
|
+
attributes: The attributes to set on the span
|
400
|
+
"""
|
401
|
+
parent_ctx = None
|
402
|
+
if hasattr(span, "parent_id") and span.parent_id:
|
403
|
+
parent_ctx = self._get_parent_context(
|
404
|
+
getattr(span, "trace_id", "unknown"), getattr(span, "id", "unknown"), span.parent_id
|
405
|
+
)
|
406
|
+
|
407
|
+
name = get_span_name(span)
|
408
|
+
kind = get_span_kind(span)
|
409
|
+
|
410
|
+
self._create_span_with_parent(
|
411
|
+
name=name, kind=kind, attributes=attributes, parent_ctx=parent_ctx, end_immediately=True
|
412
|
+
)
|
413
|
+
|
414
|
+
def _handle_span_error(self, span: Any, otel_span: Any) -> None:
|
415
|
+
"""Handle error information from spans."""
|
416
|
+
if hasattr(span, "error") and span.error:
|
417
|
+
# Set status to error
|
418
|
+
status = Status(StatusCode.ERROR)
|
419
|
+
otel_span.set_status(status)
|
420
|
+
|
421
|
+
# Determine error message - handle various error formats
|
422
|
+
error_message = "Unknown error"
|
423
|
+
error_data = {}
|
424
|
+
error_type = "AgentError"
|
425
|
+
|
426
|
+
# Handle different error formats
|
427
|
+
if isinstance(span.error, dict):
|
428
|
+
error_message = span.error.get("message", span.error.get("error", "Unknown error"))
|
429
|
+
error_data = span.error.get("data", {})
|
430
|
+
# Extract error type if available
|
431
|
+
if "type" in span.error:
|
432
|
+
error_type = span.error["type"]
|
433
|
+
elif "code" in span.error:
|
434
|
+
error_type = span.error["code"]
|
435
|
+
elif isinstance(span.error, str):
|
436
|
+
error_message = span.error
|
437
|
+
elif hasattr(span.error, "message"):
|
438
|
+
error_message = span.error.message
|
439
|
+
# Use type() for more reliable class name access
|
440
|
+
error_type = type(span.error).__name__
|
441
|
+
elif hasattr(span.error, "__str__"):
|
442
|
+
# Fallback to string representation
|
443
|
+
error_message = str(span.error)
|
444
|
+
|
445
|
+
# Record the exception with proper error data
|
446
|
+
try:
|
447
|
+
exception = Exception(error_message)
|
448
|
+
error_data_json = json.dumps(error_data) if error_data else "{}"
|
449
|
+
otel_span.record_exception(
|
450
|
+
exception=exception,
|
451
|
+
attributes={"error.data": error_data_json},
|
452
|
+
)
|
453
|
+
except Exception as e:
|
454
|
+
# If JSON serialization fails, use simpler approach
|
455
|
+
logger.warning(f"Error serializing error data: {e}")
|
456
|
+
otel_span.record_exception(Exception(error_message))
|
457
|
+
|
458
|
+
# Set error attributes
|
459
|
+
otel_span.set_attribute(CoreAttributes.ERROR_TYPE, error_type)
|
460
|
+
otel_span.set_attribute(CoreAttributes.ERROR_MESSAGE, error_message)
|
461
|
+
|
462
|
+
def cleanup(self):
|
463
|
+
"""Clean up any outstanding spans during shutdown.
|
464
|
+
|
465
|
+
This ensures we don't leak span resources when the exporter is shutdown.
|
466
|
+
"""
|
467
|
+
# Clear all tracking dictionaries
|
468
|
+
self._active_spans.clear()
|
469
|
+
self._span_map.clear()
|
@@ -0,0 +1,107 @@
|
|
1
|
+
"""OpenAI Agents SDK Instrumentation for AgentOps
|
2
|
+
|
3
|
+
This module provides instrumentation for the OpenAI Agents SDK, leveraging its built-in
|
4
|
+
tracing API for observability. It captures detailed information about agent execution,
|
5
|
+
tool usage, LLM requests, and token metrics.
|
6
|
+
|
7
|
+
The implementation uses a clean separation between exporters and processors. The exporter
|
8
|
+
translates Agent spans into OpenTelemetry spans with appropriate semantic conventions.
|
9
|
+
|
10
|
+
The processor implements the tracing interface, collects metrics, and manages timing data.
|
11
|
+
|
12
|
+
We use the built-in add_trace_processor hook for all functionality. Streaming support
|
13
|
+
would require monkey-patching the run method of `Runner`, but doesn't really get us
|
14
|
+
more data than we already have, since the `Response` object is always passed to us
|
15
|
+
from the `agents.tracing` module.
|
16
|
+
|
17
|
+
TODO Calls to the OpenAI API are not available in this tracing context, so we may
|
18
|
+
need to monkey-patch the `openai` from here to get that data. While we do have
|
19
|
+
separate instrumentation for the OpenAI API, in order to get it to nest with the
|
20
|
+
spans we create here, it's probably easier (or even required) that we incorporate
|
21
|
+
that here as well.
|
22
|
+
"""
|
23
|
+
|
24
|
+
from typing import Collection
|
25
|
+
|
26
|
+
from opentelemetry import trace
|
27
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore
|
28
|
+
from agentops.instrumentation.agentic.openai_agents import LIBRARY_VERSION
|
29
|
+
|
30
|
+
from agentops.logging import logger
|
31
|
+
from agentops.instrumentation.agentic.openai_agents.processor import OpenAIAgentsProcessor
|
32
|
+
from agentops.instrumentation.agentic.openai_agents.exporter import OpenAIAgentsExporter
|
33
|
+
|
34
|
+
|
35
|
+
class OpenAIAgentsInstrumentor(BaseInstrumentor):
|
36
|
+
"""An instrumentor for OpenAI Agents SDK that uses the built-in tracing API."""
|
37
|
+
|
38
|
+
_processor = None
|
39
|
+
_exporter = None
|
40
|
+
_default_processor = None
|
41
|
+
|
42
|
+
def __init__(self):
|
43
|
+
super().__init__()
|
44
|
+
self._tracer = None
|
45
|
+
self._is_instrumented_instance_flag = False
|
46
|
+
|
47
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
48
|
+
"""Return packages required for instrumentation."""
|
49
|
+
return ["openai-agents >= 0.0.1"]
|
50
|
+
|
51
|
+
def _instrument(self, **kwargs):
|
52
|
+
"""Instrument the OpenAI Agents SDK."""
|
53
|
+
if self._is_instrumented_instance_flag:
|
54
|
+
logger.debug("OpenAI Agents SDK already instrumented. Skipping.")
|
55
|
+
return
|
56
|
+
|
57
|
+
tracer_provider = kwargs.get("tracer_provider")
|
58
|
+
if self._tracer is None:
|
59
|
+
logger.debug("OpenAI Agents SDK tracer is None, creating new tracer.")
|
60
|
+
self._tracer = trace.get_tracer("agentops.instrumentation.openai_agents", LIBRARY_VERSION)
|
61
|
+
|
62
|
+
try:
|
63
|
+
self._exporter = OpenAIAgentsExporter(tracer_provider=tracer_provider)
|
64
|
+
self._processor = OpenAIAgentsProcessor(
|
65
|
+
exporter=self._exporter,
|
66
|
+
)
|
67
|
+
|
68
|
+
# Replace the default processor with our processor
|
69
|
+
from agents import set_trace_processors
|
70
|
+
from agents.tracing.processors import default_processor
|
71
|
+
|
72
|
+
if getattr(self, "_default_processor", None) is None:
|
73
|
+
self._default_processor = default_processor()
|
74
|
+
|
75
|
+
# Store reference to default processor for later restoration
|
76
|
+
set_trace_processors([self._processor])
|
77
|
+
self._is_instrumented_instance_flag = True
|
78
|
+
|
79
|
+
except Exception as e:
|
80
|
+
logger.warning(f"Failed to instrument OpenAI Agents SDK: {e}", exc_info=True)
|
81
|
+
|
82
|
+
def _uninstrument(self, **kwargs):
|
83
|
+
"""Remove instrumentation from OpenAI Agents SDK."""
|
84
|
+
if not self._is_instrumented_instance_flag:
|
85
|
+
logger.debug("OpenAI Agents SDK not currently instrumented. Skipping uninstrument.")
|
86
|
+
return
|
87
|
+
try:
|
88
|
+
# Clean up any active spans in the exporter
|
89
|
+
if hasattr(self, "_exporter") and self._exporter:
|
90
|
+
if hasattr(self._exporter, "cleanup"):
|
91
|
+
self._exporter.cleanup()
|
92
|
+
|
93
|
+
# Put back the default processor
|
94
|
+
from agents import set_trace_processors
|
95
|
+
|
96
|
+
if hasattr(self, "_default_processor") and self._default_processor:
|
97
|
+
set_trace_processors([self._default_processor])
|
98
|
+
self._default_processor = None
|
99
|
+
else:
|
100
|
+
logger.warning("OpenAI Agents SDK has no default processor to restore.")
|
101
|
+
self._processor = None
|
102
|
+
self._exporter = None
|
103
|
+
|
104
|
+
self._is_instrumented_instance_flag = False
|
105
|
+
logger.info("Successfully removed OpenAI Agents SDK instrumentation")
|
106
|
+
except Exception as e:
|
107
|
+
logger.warning(f"Failed to uninstrument OpenAI Agents SDK: {e}", exc_info=True)
|
@@ -0,0 +1,58 @@
|
|
1
|
+
from typing import Any
|
2
|
+
from opentelemetry.trace import StatusCode
|
3
|
+
from agentops.logging import logger
|
4
|
+
|
5
|
+
|
6
|
+
class OpenAIAgentsProcessor:
|
7
|
+
"""Processor for OpenAI Agents SDK traces.
|
8
|
+
|
9
|
+
This processor implements the TracingProcessor interface from the Agents SDK
|
10
|
+
and converts trace events to OpenTelemetry spans and metrics.
|
11
|
+
|
12
|
+
The processor does NOT directly create OpenTelemetry spans.
|
13
|
+
It delegates span creation to the OpenAIAgentsExporter.
|
14
|
+
"""
|
15
|
+
|
16
|
+
def __init__(self, exporter=None):
|
17
|
+
self.exporter = exporter
|
18
|
+
|
19
|
+
def on_trace_start(self, sdk_trace: Any) -> None:
|
20
|
+
"""Called when a trace starts in the Agents SDK."""
|
21
|
+
|
22
|
+
logger.debug(f"[agentops.instrumentation.openai_agents] Trace started: {sdk_trace}")
|
23
|
+
self.exporter.export_trace(sdk_trace)
|
24
|
+
|
25
|
+
def on_trace_end(self, sdk_trace: Any) -> None:
|
26
|
+
"""Called when a trace ends in the Agents SDK."""
|
27
|
+
|
28
|
+
# Mark this as an end event
|
29
|
+
# This is used by the exporter to determine whether to create or update a trace
|
30
|
+
sdk_trace.status = StatusCode.OK.name
|
31
|
+
|
32
|
+
logger.debug(f"[agentops.instrumentation.openai_agents] Trace ended: {sdk_trace}")
|
33
|
+
self.exporter.export_trace(sdk_trace)
|
34
|
+
|
35
|
+
def on_span_start(self, span: Any) -> None:
|
36
|
+
"""Called when a span starts in the Agents SDK."""
|
37
|
+
|
38
|
+
logger.debug(f"[agentops.instrumentation.openai_agents] Span started: {span}")
|
39
|
+
self.exporter.export_span(span)
|
40
|
+
|
41
|
+
def on_span_end(self, span: Any) -> None:
|
42
|
+
"""Called when a span ends in the Agents SDK."""
|
43
|
+
|
44
|
+
# Mark this as an end event
|
45
|
+
# This is used by the exporter to determine whether to create or update a span
|
46
|
+
span.status = StatusCode.OK.name
|
47
|
+
|
48
|
+
logger.debug(f"[agentops.instrumentation.openai_agents] Span ended: {span}")
|
49
|
+
self.exporter.export_span(span)
|
50
|
+
|
51
|
+
def shutdown(self) -> None:
|
52
|
+
"""Called when the application stops."""
|
53
|
+
pass
|
54
|
+
|
55
|
+
def force_flush(self) -> None:
|
56
|
+
"""Forces an immediate flush of all queued spans/traces."""
|
57
|
+
# We don't queue spans so this is a no-op
|
58
|
+
pass
|