mseep-agentops 0.4.18__py3-none-any.whl → 0.4.23__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.23.dist-info}/METADATA +30 -40
- mseep_agentops-0.4.23.dist-info/RECORD +178 -0
- {mseep_agentops-0.4.18.dist-info → mseep_agentops-0.4.23.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.23.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,5 @@
|
|
1
1
|
from typing import Dict, Optional
|
2
|
+
import threading
|
2
3
|
|
3
4
|
import requests
|
4
5
|
|
@@ -6,150 +7,118 @@ from agentops.client.http.http_adapter import BaseHTTPAdapter
|
|
6
7
|
from agentops.logging import logger
|
7
8
|
from agentops.helpers.version import get_agentops_version
|
8
9
|
|
10
|
+
# Import aiohttp for async requests
|
11
|
+
try:
|
12
|
+
import aiohttp
|
13
|
+
|
14
|
+
AIOHTTP_AVAILABLE = True
|
15
|
+
except ImportError:
|
16
|
+
AIOHTTP_AVAILABLE = False
|
17
|
+
# Don't log warning here, only when actually trying to use async functionality
|
18
|
+
|
9
19
|
|
10
20
|
class HttpClient:
|
11
|
-
"""
|
21
|
+
"""HTTP client with async-first design and optional sync fallback for log uploads"""
|
12
22
|
|
13
23
|
_session: Optional[requests.Session] = None
|
24
|
+
_async_session: Optional[aiohttp.ClientSession] = None
|
14
25
|
_project_id: Optional[str] = None
|
26
|
+
_session_lock = threading.Lock()
|
15
27
|
|
16
28
|
@classmethod
|
17
29
|
def get_project_id(cls) -> Optional[str]:
|
18
30
|
"""Get the stored project ID"""
|
19
31
|
return cls._project_id
|
20
32
|
|
33
|
+
@classmethod
|
34
|
+
def set_project_id(cls, project_id: str) -> None:
|
35
|
+
"""Set the project ID"""
|
36
|
+
cls._project_id = project_id
|
37
|
+
|
21
38
|
@classmethod
|
22
39
|
def get_session(cls) -> requests.Session:
|
23
|
-
"""
|
40
|
+
"""
|
41
|
+
Get or create the global session with optimized connection pooling.
|
42
|
+
|
43
|
+
Note: This method is deprecated. Use async_request() instead.
|
44
|
+
Only kept for log upload module compatibility.
|
45
|
+
"""
|
24
46
|
if cls._session is None:
|
25
|
-
cls.
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
47
|
+
with cls._session_lock:
|
48
|
+
if cls._session is None: # Double-check locking
|
49
|
+
cls._session = requests.Session()
|
50
|
+
|
51
|
+
# Configure connection pooling
|
52
|
+
adapter = BaseHTTPAdapter()
|
53
|
+
|
54
|
+
# Mount adapter for both HTTP and HTTPS
|
55
|
+
cls._session.mount("http://", adapter)
|
56
|
+
cls._session.mount("https://", adapter)
|
57
|
+
|
58
|
+
# Set default headers
|
59
|
+
cls._session.headers.update(
|
60
|
+
{
|
61
|
+
"Connection": "keep-alive",
|
62
|
+
"Keep-Alive": "timeout=10, max=1000",
|
63
|
+
"Content-Type": "application/json",
|
64
|
+
"User-Agent": f"agentops-python/{get_agentops_version() or 'unknown'}",
|
65
|
+
}
|
66
|
+
)
|
67
|
+
logger.debug(f"Agentops version: agentops-python/{get_agentops_version() or 'unknown'}")
|
44
68
|
return cls._session
|
45
69
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
# except Exception:
|
87
|
-
# if response.text:
|
88
|
-
# error_msg = response.text
|
89
|
-
#
|
90
|
-
# logger.error(f"Authentication failed: {error_msg}")
|
91
|
-
# raise AgentOpsApiJwtExpiredException(f"Authentication failed: {error_msg}")
|
92
|
-
#
|
93
|
-
# if response.status_code >= 500:
|
94
|
-
# logger.error(f"Server error during authentication: {response.status_code}")
|
95
|
-
# raise ApiServerException(f"Server error during authentication: {response.status_code}")
|
96
|
-
#
|
97
|
-
# if response.status_code != 200:
|
98
|
-
# logger.error(f"Unexpected status code during authentication: {response.status_code}")
|
99
|
-
# raise AgentOpsApiJwtExpiredException(f"Failed to fetch token: {response.status_code}")
|
100
|
-
#
|
101
|
-
# token_data = response.json()
|
102
|
-
# if "token" not in token_data:
|
103
|
-
# logger.error("Token not found in response")
|
104
|
-
# raise AgentOpsApiJwtExpiredException("Token not found in response")
|
105
|
-
#
|
106
|
-
# # Store project_id if present in the response
|
107
|
-
# if "project_id" in token_data:
|
108
|
-
# HttpClient._project_id = token_data["project_id"]
|
109
|
-
# logger.debug(f"Project ID stored: {HttpClient._project_id} (will be set as {ResourceAttributes.PROJECT_ID})")
|
110
|
-
#
|
111
|
-
# return token_data["token"]
|
112
|
-
# except requests.RequestException as e:
|
113
|
-
# logger.error(f"Network error during authentication: {e}")
|
114
|
-
# raise AgentOpsApiJwtExpiredException(f"Network error during authentication: {e}")
|
115
|
-
#
|
116
|
-
# token_fetcher = default_token_fetcher
|
117
|
-
#
|
118
|
-
# # Create a new session
|
119
|
-
# session = requests.Session()
|
120
|
-
#
|
121
|
-
# # Create an authenticated adapter
|
122
|
-
# adapter = AuthenticatedHttpAdapter(
|
123
|
-
# auth_manager=auth_manager,
|
124
|
-
# api_key=api_key,
|
125
|
-
# token_fetcher=token_fetcher
|
126
|
-
# )
|
127
|
-
#
|
128
|
-
# # Mount the adapter for both HTTP and HTTPS
|
129
|
-
# session.mount("http://", adapter)
|
130
|
-
# session.mount("https://", adapter)
|
131
|
-
#
|
132
|
-
# # Set default headers
|
133
|
-
# session.headers.update({
|
134
|
-
# "Connection": "keep-alive",
|
135
|
-
# "Keep-Alive": "timeout=10, max=1000",
|
136
|
-
# "Content-Type": "application/json",
|
137
|
-
# })
|
138
|
-
#
|
139
|
-
# return session
|
70
|
+
@classmethod
|
71
|
+
async def get_async_session(cls) -> Optional[aiohttp.ClientSession]:
|
72
|
+
"""Get or create the global async session with optimized connection pooling"""
|
73
|
+
if not AIOHTTP_AVAILABLE:
|
74
|
+
logger.warning("aiohttp not available, cannot create async session")
|
75
|
+
return None
|
76
|
+
|
77
|
+
# Always create a new session if the current one is None or closed
|
78
|
+
if cls._async_session is None or cls._async_session.closed:
|
79
|
+
# Close the old session if it exists but is closed
|
80
|
+
if cls._async_session is not None and cls._async_session.closed:
|
81
|
+
cls._async_session = None
|
82
|
+
|
83
|
+
# Create connector with connection pooling
|
84
|
+
connector = aiohttp.TCPConnector(
|
85
|
+
limit=100, # Total connection pool size
|
86
|
+
limit_per_host=30, # Per-host connection limit
|
87
|
+
ttl_dns_cache=300, # DNS cache TTL
|
88
|
+
use_dns_cache=True,
|
89
|
+
enable_cleanup_closed=True,
|
90
|
+
)
|
91
|
+
|
92
|
+
# Create session with default headers
|
93
|
+
headers = {
|
94
|
+
"Content-Type": "application/json",
|
95
|
+
"User-Agent": f"agentops-python/{get_agentops_version() or 'unknown'}",
|
96
|
+
}
|
97
|
+
|
98
|
+
cls._async_session = aiohttp.ClientSession(
|
99
|
+
connector=connector, headers=headers, timeout=aiohttp.ClientTimeout(total=30)
|
100
|
+
)
|
101
|
+
|
102
|
+
return cls._async_session
|
103
|
+
|
104
|
+
@classmethod
|
105
|
+
async def close_async_session(cls):
|
106
|
+
"""Close the async session"""
|
107
|
+
if cls._async_session and not cls._async_session.closed:
|
108
|
+
await cls._async_session.close()
|
109
|
+
cls._async_session = None
|
140
110
|
|
141
111
|
@classmethod
|
142
|
-
def
|
112
|
+
async def async_request(
|
143
113
|
cls,
|
144
114
|
method: str,
|
145
115
|
url: str,
|
146
116
|
data: Optional[Dict] = None,
|
147
117
|
headers: Optional[Dict] = None,
|
148
118
|
timeout: int = 30,
|
149
|
-
|
150
|
-
) -> requests.Response:
|
119
|
+
) -> Optional[Dict]:
|
151
120
|
"""
|
152
|
-
Make
|
121
|
+
Make an async HTTP request and return JSON response
|
153
122
|
|
154
123
|
Args:
|
155
124
|
method: HTTP method (e.g., 'get', 'post', 'put', 'delete')
|
@@ -157,59 +126,44 @@ class HttpClient:
|
|
157
126
|
data: Request payload (for POST, PUT methods)
|
158
127
|
headers: Request headers
|
159
128
|
timeout: Request timeout in seconds
|
160
|
-
max_redirects: Maximum number of redirects to follow (default: 5)
|
161
129
|
|
162
130
|
Returns:
|
163
|
-
|
164
|
-
|
165
|
-
Raises:
|
166
|
-
requests.RequestException: If the request fails
|
167
|
-
ValueError: If the redirect limit is exceeded or an unsupported HTTP method is used
|
131
|
+
JSON response as dictionary, or None if request failed
|
168
132
|
"""
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
if
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
#
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
if
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
logger.debug(f"Following redirect ({redirect_count}/{max_redirects}) to: {url}")
|
207
|
-
|
208
|
-
# Continue the loop to make the next request
|
209
|
-
continue
|
210
|
-
|
211
|
-
# Not a redirect, return the response
|
212
|
-
return response
|
213
|
-
|
214
|
-
# This should never be reached due to the max_redirects check above
|
215
|
-
return response
|
133
|
+
if not AIOHTTP_AVAILABLE:
|
134
|
+
logger.warning("aiohttp not available, cannot make async request")
|
135
|
+
return None
|
136
|
+
|
137
|
+
try:
|
138
|
+
session = await cls.get_async_session()
|
139
|
+
if not session:
|
140
|
+
return None
|
141
|
+
|
142
|
+
logger.debug(f"Making async {method} request to {url}")
|
143
|
+
|
144
|
+
# Prepare request parameters
|
145
|
+
kwargs = {"timeout": aiohttp.ClientTimeout(total=timeout), "headers": headers or {}}
|
146
|
+
|
147
|
+
if data and method.lower() in ["post", "put", "patch"]:
|
148
|
+
kwargs["json"] = data
|
149
|
+
|
150
|
+
# Make the request
|
151
|
+
async with session.request(method.upper(), url, **kwargs) as response:
|
152
|
+
logger.debug(f"Async request response status: {response.status}")
|
153
|
+
|
154
|
+
# Check if response is successful
|
155
|
+
if response.status >= 400:
|
156
|
+
return None
|
157
|
+
|
158
|
+
# Parse JSON response
|
159
|
+
try:
|
160
|
+
response_data = await response.json()
|
161
|
+
logger.debug(
|
162
|
+
f"Async request successful, response keys: {list(response_data.keys()) if response_data else 'None'}"
|
163
|
+
)
|
164
|
+
return response_data
|
165
|
+
except Exception:
|
166
|
+
return None
|
167
|
+
|
168
|
+
except Exception:
|
169
|
+
return None
|
agentops/config.py
CHANGED
@@ -9,7 +9,6 @@ from uuid import UUID
|
|
9
9
|
from opentelemetry.sdk.trace import SpanProcessor
|
10
10
|
from opentelemetry.sdk.trace.export import SpanExporter
|
11
11
|
|
12
|
-
from agentops.exceptions import InvalidApiKeyException
|
13
12
|
from agentops.helpers.env import get_env_bool, get_env_int, get_env_list
|
14
13
|
from agentops.helpers.serialization import AgentOpsJSONEncoder
|
15
14
|
|
@@ -166,7 +165,14 @@ class Config:
|
|
166
165
|
try:
|
167
166
|
UUID(api_key)
|
168
167
|
except ValueError:
|
169
|
-
|
168
|
+
# Log warning but don't throw exception - let async auth handle it
|
169
|
+
from agentops.logging import logger
|
170
|
+
|
171
|
+
logger.warning(
|
172
|
+
f"API key format appears invalid: {api_key[:8]}... "
|
173
|
+
f"Authentication may fail. Find your API key at {self.endpoint}/settings/projects"
|
174
|
+
)
|
175
|
+
# Continue with the invalid key - async auth will handle the failure gracefully
|
170
176
|
|
171
177
|
if endpoint is not None:
|
172
178
|
self.endpoint = endpoint
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# OpenTelemetry Implementation Notes
|
2
|
+
|
3
|
+
This document outlines best practices and implementation details for OpenTelemetry in AgentOps instrumentations.
|
4
|
+
|
5
|
+
## Key Concepts
|
6
|
+
|
7
|
+
### Context Propagation
|
8
|
+
|
9
|
+
OpenTelemetry relies on proper context propagation to maintain parent-child relationships between spans. This is essential for:
|
10
|
+
|
11
|
+
- Creating accurate trace waterfalls in visualizations
|
12
|
+
- Ensuring all spans from the same logical operation share a trace ID
|
13
|
+
- Allowing proper querying and filtering of related operations
|
14
|
+
|
15
|
+
### Core Patterns
|
16
|
+
|
17
|
+
When implementing instrumentations that need to maintain context across different execution contexts:
|
18
|
+
|
19
|
+
1. **Store span contexts in dictionaries:**
|
20
|
+
```python
|
21
|
+
# Use weakref dictionaries to avoid memory leaks
|
22
|
+
self._span_contexts = weakref.WeakKeyDictionary()
|
23
|
+
self._trace_root_contexts = weakref.WeakKeyDictionary()
|
24
|
+
```
|
25
|
+
|
26
|
+
2. **Create spans with explicit parent contexts:**
|
27
|
+
```python
|
28
|
+
parent_context = self._get_parent_context(trace_obj)
|
29
|
+
with trace.start_as_current_span(
|
30
|
+
name=span_name,
|
31
|
+
context=parent_context,
|
32
|
+
kind=trace.SpanKind.CLIENT,
|
33
|
+
attributes=attributes,
|
34
|
+
) as span:
|
35
|
+
# Span operations here
|
36
|
+
# Store the span's context for future reference
|
37
|
+
context = trace.set_span_in_context(span)
|
38
|
+
self._span_contexts[span_obj] = context
|
39
|
+
```
|
40
|
+
|
41
|
+
3. **Implement helper methods to retrieve appropriate parent contexts:**
|
42
|
+
```python
|
43
|
+
def _get_parent_context(self, trace_obj):
|
44
|
+
# Try to get the trace's root context if it exists
|
45
|
+
if trace_obj in self._trace_root_contexts:
|
46
|
+
return self._trace_root_contexts[trace_obj]
|
47
|
+
|
48
|
+
# Otherwise, use the current context
|
49
|
+
return context_api.context.get_current()
|
50
|
+
```
|
51
|
+
|
52
|
+
4. **Debug trace continuity:**
|
53
|
+
```python
|
54
|
+
current_span = trace.get_current_span()
|
55
|
+
span_context = current_span.get_span_context()
|
56
|
+
trace_id = format_trace_id(span_context.trace_id)
|
57
|
+
logging.debug(f"Current span trace ID: {trace_id}")
|
58
|
+
```
|
59
|
+
|
60
|
+
## Common Pitfalls
|
61
|
+
|
62
|
+
1. **Naming conflicts:** Avoid using `trace` as a parameter name when you're also importing the OpenTelemetry `trace` module
|
63
|
+
```python
|
64
|
+
# Bad
|
65
|
+
def on_trace_start(self, trace):
|
66
|
+
# This will cause conflicts with the imported trace module
|
67
|
+
|
68
|
+
# Good
|
69
|
+
def on_trace_start(self, trace_obj):
|
70
|
+
# No conflicts with OpenTelemetry's trace module
|
71
|
+
```
|
72
|
+
|
73
|
+
2. **Missing parent contexts:** Always explicitly provide parent contexts when available, don't rely on current context alone
|
74
|
+
|
75
|
+
3. **Memory leaks:** Use `weakref.WeakKeyDictionary()` for storing spans to allow garbage collection
|
76
|
+
|
77
|
+
4. **Lost context:** When calling async or callback functions, be sure to preserve and pass the context
|
78
|
+
|
79
|
+
## Testing Context Propagation
|
80
|
+
|
81
|
+
To verify proper context propagation:
|
82
|
+
|
83
|
+
1. Enable debug logging for trace IDs
|
84
|
+
2. Run a simple end-to-end test that generates multiple spans
|
85
|
+
3. Verify all spans share the same trace ID
|
86
|
+
4. Check that parent-child relationships are correctly established
|
87
|
+
|
88
|
+
```python
|
89
|
+
# Example debug logging
|
90
|
+
logging.debug(f"Span {span.name} has trace ID: {format_trace_id(span.get_span_context().trace_id)}")
|
91
|
+
```
|
92
|
+
|
93
|
+
## Timestamp Handling in OpenTelemetry
|
94
|
+
|
95
|
+
When working with OpenTelemetry spans and timestamps:
|
96
|
+
|
97
|
+
1. **Automatic Timestamp Tracking:** OpenTelemetry automatically tracks timestamps for spans. When a span is created with `tracer.start_span()` or `tracer.start_as_current_span()`, the start time is captured automatically. When `span.end()` is called, the end time is recorded.
|
98
|
+
|
99
|
+
2. **No Manual Timestamp Setting Required:** The standard instrumentation pattern does not require manually setting timestamp attributes on spans. Instead, OpenTelemetry handles this internally through the SpanProcessor and Exporter classes.
|
100
|
+
|
101
|
+
3. **Timestamp Representation:** In the OpenTelemetry data model, timestamps are stored as nanoseconds since the Unix epoch (January 1, 1970).
|
102
|
+
|
103
|
+
4. **Serialization Responsibility:** The serialization of timestamps from OTel spans to output formats like JSON is handled by the Exporter components. If timestamps aren't appearing correctly in output APIs, the issue is likely in the API exporter, not in the span creation code.
|
104
|
+
|
105
|
+
5. **Debugging Timestamps:** To debug timestamp issues, verify that spans are properly starting and ending, rather than manually setting timestamp attributes:
|
106
|
+
|
107
|
+
```python
|
108
|
+
# Good pattern - timestamps handled by OpenTelemetry automatically
|
109
|
+
with tracer.start_as_current_span("my_operation") as span:
|
110
|
+
# Do work
|
111
|
+
pass # span.end() is called automatically
|
112
|
+
```
|
113
|
+
|
114
|
+
Note: If timestamps are missing in API output (e.g., empty "start_time" fields), focus on fixes in the exporter and serialization layer, not by manually tracking timestamps in instrumentation code.
|
115
|
+
|
116
|
+
## Attributes in OpenTelemetry
|
117
|
+
|
118
|
+
When working with span attributes in OpenTelemetry:
|
119
|
+
|
120
|
+
1. **Root Attributes Node:** The root `attributes` object in the API output JSON should always be empty. This is by design. All attribute data should be stored in the `span_attributes` object.
|
121
|
+
|
122
|
+
2. **Span Attributes:** The `span_attributes` object is where all user-defined and semantic attribute data should be stored. This allows for a structured, hierarchical representation of attributes.
|
123
|
+
|
124
|
+
3. **Structure Difference:** While the root `attributes` appears as an empty object in the API output, this is normal and expected. Do not attempt to populate this object directly or duplicate data from `span_attributes` into it.
|
125
|
+
|
126
|
+
4. **Setting Attributes:** Always set span attributes using the semantic conventions defined in the `agentops/semconv` module:
|
127
|
+
|
128
|
+
```python
|
129
|
+
from agentops.semconv import agent
|
130
|
+
|
131
|
+
# Good pattern - using semantic conventions
|
132
|
+
span.set_attribute(agent.AGENT_NAME, "My Agent")
|
133
|
+
```
|
@@ -0,0 +1,167 @@
|
|
1
|
+
# AgentOps Instrumentation
|
2
|
+
|
3
|
+
This package provides OpenTelemetry instrumentation for various LLM providers and related services.
|
4
|
+
|
5
|
+
## Available Instrumentors
|
6
|
+
|
7
|
+
- **OpenAI** (`v0.27.0+` and `v1.0.0+`)
|
8
|
+
- **Anthropic** (`v0.7.0+`)
|
9
|
+
- **Google GenAI** (`v0.1.0+`)
|
10
|
+
- **IBM WatsonX AI** (`v0.1.0+`)
|
11
|
+
- **CrewAI** (`v0.56.0+`)
|
12
|
+
- **AG2/AutoGen** (`v0.3.2+`)
|
13
|
+
- **Google ADK** (`v0.1.0+`)
|
14
|
+
- **Agno** (`v0.0.1+`)
|
15
|
+
- **Mem0** (`v0.1.0+`)
|
16
|
+
- **smolagents** (`v0.1.0+`)
|
17
|
+
|
18
|
+
## Common Module Usage
|
19
|
+
|
20
|
+
The `agentops.instrumentation.common` module provides shared utilities for creating instrumentations:
|
21
|
+
|
22
|
+
### Base Instrumentor
|
23
|
+
|
24
|
+
Use `CommonInstrumentor` for creating new instrumentations:
|
25
|
+
|
26
|
+
```python
|
27
|
+
from agentops.instrumentation.common import CommonInstrumentor, InstrumentorConfig, WrapConfig
|
28
|
+
|
29
|
+
class MyInstrumentor(CommonInstrumentor):
|
30
|
+
def __init__(self):
|
31
|
+
config = InstrumentorConfig(
|
32
|
+
library_name="my-library",
|
33
|
+
library_version="1.0.0",
|
34
|
+
wrapped_methods=[
|
35
|
+
WrapConfig(
|
36
|
+
trace_name="my.method",
|
37
|
+
package="my_library.module",
|
38
|
+
class_name="MyClass",
|
39
|
+
method_name="my_method",
|
40
|
+
handler=my_attribute_handler
|
41
|
+
)
|
42
|
+
],
|
43
|
+
dependencies=["my-library >= 1.0.0"]
|
44
|
+
)
|
45
|
+
super().__init__(config)
|
46
|
+
```
|
47
|
+
|
48
|
+
### Attribute Handlers
|
49
|
+
|
50
|
+
Create attribute handlers to extract data from method calls:
|
51
|
+
|
52
|
+
```python
|
53
|
+
from agentops.instrumentation.common import AttributeMap
|
54
|
+
|
55
|
+
def my_attribute_handler(args=None, kwargs=None, return_value=None) -> AttributeMap:
|
56
|
+
attributes = {}
|
57
|
+
|
58
|
+
if kwargs and "model" in kwargs:
|
59
|
+
attributes["llm.request.model"] = kwargs["model"]
|
60
|
+
|
61
|
+
if return_value and hasattr(return_value, "usage"):
|
62
|
+
attributes["llm.usage.total_tokens"] = return_value.usage.total_tokens
|
63
|
+
|
64
|
+
return attributes
|
65
|
+
```
|
66
|
+
|
67
|
+
### Span Management
|
68
|
+
|
69
|
+
Use the span management utilities for consistent span creation:
|
70
|
+
|
71
|
+
```python
|
72
|
+
from agentops.instrumentation.common import create_span, SpanAttributeManager
|
73
|
+
|
74
|
+
# Create an attribute manager
|
75
|
+
attr_manager = SpanAttributeManager(service_name="my-service")
|
76
|
+
|
77
|
+
# Use the create_span context manager
|
78
|
+
with create_span(
|
79
|
+
tracer,
|
80
|
+
"my.operation",
|
81
|
+
attributes={"my.attribute": "value"},
|
82
|
+
attribute_manager=attr_manager
|
83
|
+
) as span:
|
84
|
+
# Your operation code here
|
85
|
+
pass
|
86
|
+
```
|
87
|
+
|
88
|
+
### Token Counting
|
89
|
+
|
90
|
+
Use the token counting utilities for consistent token usage extraction:
|
91
|
+
|
92
|
+
```python
|
93
|
+
from agentops.instrumentation.common import TokenUsageExtractor, set_token_usage_attributes
|
94
|
+
|
95
|
+
# Extract token usage from a response
|
96
|
+
usage = TokenUsageExtractor.extract_from_response(response)
|
97
|
+
|
98
|
+
# Set token usage attributes on a span
|
99
|
+
set_token_usage_attributes(span, response)
|
100
|
+
```
|
101
|
+
|
102
|
+
### Streaming Support
|
103
|
+
|
104
|
+
Use streaming utilities for handling streaming responses:
|
105
|
+
|
106
|
+
```python
|
107
|
+
from agentops.instrumentation.common import create_stream_wrapper_factory, StreamingResponseHandler
|
108
|
+
|
109
|
+
# Create a stream wrapper factory
|
110
|
+
wrapper = create_stream_wrapper_factory(
|
111
|
+
tracer,
|
112
|
+
"my.stream",
|
113
|
+
extract_chunk_content=StreamingResponseHandler.extract_generic_chunk_content,
|
114
|
+
initial_attributes={"stream.type": "text"}
|
115
|
+
)
|
116
|
+
|
117
|
+
# Apply to streaming methods
|
118
|
+
wrap_function_wrapper("my_module", "stream_method", wrapper)
|
119
|
+
```
|
120
|
+
|
121
|
+
### Metrics
|
122
|
+
|
123
|
+
Use standard metrics for consistency across instrumentations:
|
124
|
+
|
125
|
+
```python
|
126
|
+
from agentops.instrumentation.common import StandardMetrics, MetricsRecorder
|
127
|
+
|
128
|
+
# Create standard metrics
|
129
|
+
metrics = StandardMetrics.create_standard_metrics(meter)
|
130
|
+
|
131
|
+
# Use the metrics recorder
|
132
|
+
recorder = MetricsRecorder(metrics)
|
133
|
+
recorder.record_token_usage(prompt_tokens=100, completion_tokens=50)
|
134
|
+
recorder.record_duration(1.5)
|
135
|
+
```
|
136
|
+
|
137
|
+
## Creating a New Instrumentor
|
138
|
+
|
139
|
+
1. Create a new directory under `agentops/instrumentation/` for your provider
|
140
|
+
2. Create an `__init__.py` file with version information
|
141
|
+
3. Create an `instrumentor.py` file extending `CommonInstrumentor`
|
142
|
+
4. Create attribute handlers in an `attributes/` subdirectory
|
143
|
+
5. Add your instrumentor to the main `__init__.py` configuration
|
144
|
+
|
145
|
+
Example structure:
|
146
|
+
```
|
147
|
+
agentops/instrumentation/
|
148
|
+
├── my_provider/
|
149
|
+
│ ├── __init__.py
|
150
|
+
│ ├── instrumentor.py
|
151
|
+
│ └── attributes/
|
152
|
+
│ ├── __init__.py
|
153
|
+
│ └── handlers.py
|
154
|
+
```
|
155
|
+
|
156
|
+
## Best Practices
|
157
|
+
|
158
|
+
1. **Use Common Utilities**: Leverage the common module for consistency
|
159
|
+
2. **Follow Semantic Conventions**: Use attributes from `agentops.semconv`
|
160
|
+
3. **Handle Errors Gracefully**: Wrap operations in try-except blocks
|
161
|
+
4. **Support Async**: Provide both sync and async method wrapping
|
162
|
+
5. **Document Attributes**: Comment on what attributes are captured
|
163
|
+
6. **Test Thoroughly**: Write unit tests for your instrumentor
|
164
|
+
|
165
|
+
## Examples
|
166
|
+
|
167
|
+
See the `examples/` directory for usage examples of each instrumentor.
|