agentreplay 0.1.2__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.
- agentreplay/__init__.py +81 -0
- agentreplay/auto_instrument/__init__.py +237 -0
- agentreplay/auto_instrument/openai.py +431 -0
- agentreplay/batching.py +270 -0
- agentreplay/bootstrap.py +202 -0
- agentreplay/circuit_breaker.py +300 -0
- agentreplay/client.py +1560 -0
- agentreplay/config.py +215 -0
- agentreplay/context.py +168 -0
- agentreplay/env_config.py +327 -0
- agentreplay/env_init.py +128 -0
- agentreplay/exceptions.py +92 -0
- agentreplay/genai.py +510 -0
- agentreplay/genai_conventions.py +502 -0
- agentreplay/install_pth.py +159 -0
- agentreplay/langchain_tracer.py +385 -0
- agentreplay/models.py +120 -0
- agentreplay/otel_bridge.py +281 -0
- agentreplay/patch.py +308 -0
- agentreplay/propagation.py +328 -0
- agentreplay/py.typed +3 -0
- agentreplay/retry.py +151 -0
- agentreplay/sampling.py +298 -0
- agentreplay/session.py +164 -0
- agentreplay/sitecustomize.py +73 -0
- agentreplay/span.py +270 -0
- agentreplay/unified.py +465 -0
- agentreplay-0.1.2.dist-info/METADATA +285 -0
- agentreplay-0.1.2.dist-info/RECORD +33 -0
- agentreplay-0.1.2.dist-info/WHEEL +5 -0
- agentreplay-0.1.2.dist-info/entry_points.txt +2 -0
- agentreplay-0.1.2.dist-info/licenses/LICENSE +190 -0
- agentreplay-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# Copyright 2025 Sushanth (https://github.com/sushanthpy)
|
|
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
|
+
|
|
15
|
+
"""Environment-based auto-configuration for Agentreplay.
|
|
16
|
+
|
|
17
|
+
This module provides automatic configuration from environment variables,
|
|
18
|
+
following OTEL conventions for zero-config deployments.
|
|
19
|
+
|
|
20
|
+
Supported environment variables:
|
|
21
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: Agentreplay server URL
|
|
22
|
+
OTEL_SERVICE_NAME: Service/agent name
|
|
23
|
+
OTEL_SERVICE_NAMESPACE: Project/namespace identifier
|
|
24
|
+
OTEL_TRACES_SAMPLER: Sampling strategy (always_on, always_off, traceidratio)
|
|
25
|
+
OTEL_TRACES_SAMPLER_ARG: Sampling rate (0.0-1.0 for traceidratio)
|
|
26
|
+
|
|
27
|
+
AGENTREPLAY_URL: Override for Agentreplay server URL
|
|
28
|
+
AGENTREPLAY_TENANT_ID: Tenant identifier
|
|
29
|
+
AGENTREPLAY_PROJECT_ID: Project identifier
|
|
30
|
+
AGENTREPLAY_AGENT_ID: Default agent identifier
|
|
31
|
+
AGENTREPLAY_AUTO_INSTRUMENT: Enable auto-instrumentation (true/false)
|
|
32
|
+
AGENTREPLAY_FRAMEWORKS: Comma-separated list of frameworks to instrument
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
# Set environment variables
|
|
36
|
+
export OTEL_SERVICE_NAME="my-agent"
|
|
37
|
+
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:8080"
|
|
38
|
+
export AGENTREPLAY_TENANT_ID="1"
|
|
39
|
+
export AGENTREPLAY_AUTO_INSTRUMENT="true"
|
|
40
|
+
|
|
41
|
+
# Python code - zero configuration needed!
|
|
42
|
+
from agentreplay import init_from_env
|
|
43
|
+
init_from_env()
|
|
44
|
+
|
|
45
|
+
# Now all your LLM calls are automatically traced
|
|
46
|
+
from openai import OpenAI
|
|
47
|
+
client = OpenAI()
|
|
48
|
+
response = client.chat.completions.create(...) # ✓ Traced!
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
import os
|
|
52
|
+
import logging
|
|
53
|
+
from typing import Optional, Dict, Any, List, Tuple
|
|
54
|
+
from agentreplay.client import AgentreplayClient
|
|
55
|
+
|
|
56
|
+
logger = logging.getLogger(__name__)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class EnvConfig:
|
|
60
|
+
"""Configuration loaded from environment variables."""
|
|
61
|
+
|
|
62
|
+
def __init__(self):
|
|
63
|
+
"""Load configuration from environment variables."""
|
|
64
|
+
# Server configuration
|
|
65
|
+
self.url = self._get_url()
|
|
66
|
+
|
|
67
|
+
# Identity configuration
|
|
68
|
+
self.service_name = os.getenv("OTEL_SERVICE_NAME", "agentreplay-agent")
|
|
69
|
+
self.tenant_id = int(os.getenv("AGENTREPLAY_TENANT_ID", "1"))
|
|
70
|
+
self.project_id = self._get_project_id()
|
|
71
|
+
self.agent_id = int(os.getenv("AGENTREPLAY_AGENT_ID", "1"))
|
|
72
|
+
|
|
73
|
+
# Sampling configuration
|
|
74
|
+
self.sampler, self.sampling_rate = self._get_sampling_config()
|
|
75
|
+
|
|
76
|
+
# Auto-instrumentation configuration
|
|
77
|
+
self.auto_instrument = os.getenv("AGENTREPLAY_AUTO_INSTRUMENT", "false").lower() in ("true", "1", "yes")
|
|
78
|
+
self.frameworks = self._get_frameworks()
|
|
79
|
+
|
|
80
|
+
# Validation
|
|
81
|
+
self.validate_on_init = os.getenv("AGENTREPLAY_VALIDATE", "true").lower() in ("true", "1", "yes")
|
|
82
|
+
|
|
83
|
+
# OTLP compatibility
|
|
84
|
+
self.enable_otlp_export = os.getenv("AGENTREPLAY_ENABLE_OTLP", "true").lower() in ("true", "1", "yes")
|
|
85
|
+
|
|
86
|
+
logger.info(f"EnvConfig loaded: service={self.service_name}, url={self.url}, tenant={self.tenant_id}")
|
|
87
|
+
|
|
88
|
+
def _get_url(self) -> str:
|
|
89
|
+
"""Get Agentreplay server URL from environment."""
|
|
90
|
+
# Priority: AGENTREPLAY_URL > OTEL_EXPORTER_OTLP_ENDPOINT > default
|
|
91
|
+
url = os.getenv("AGENTREPLAY_URL")
|
|
92
|
+
if url:
|
|
93
|
+
return url.rstrip("/")
|
|
94
|
+
|
|
95
|
+
otlp_endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
|
|
96
|
+
if otlp_endpoint:
|
|
97
|
+
# OTLP endpoint may include /v1/traces suffix, remove it
|
|
98
|
+
url = otlp_endpoint.rstrip("/")
|
|
99
|
+
if url.endswith("/v1/traces"):
|
|
100
|
+
url = url[:-10]
|
|
101
|
+
return url
|
|
102
|
+
|
|
103
|
+
return "http://localhost:8080"
|
|
104
|
+
|
|
105
|
+
def _get_project_id(self) -> int:
|
|
106
|
+
"""Get project ID from environment."""
|
|
107
|
+
# Try AGENTREPLAY_PROJECT_ID first
|
|
108
|
+
project_id_str = os.getenv("AGENTREPLAY_PROJECT_ID")
|
|
109
|
+
if project_id_str:
|
|
110
|
+
try:
|
|
111
|
+
return int(project_id_str)
|
|
112
|
+
except ValueError:
|
|
113
|
+
logger.warning(f"Invalid AGENTREPLAY_PROJECT_ID: {project_id_str}, using 0")
|
|
114
|
+
|
|
115
|
+
# Fallback to hashing OTEL_SERVICE_NAMESPACE
|
|
116
|
+
namespace = os.getenv("OTEL_SERVICE_NAMESPACE")
|
|
117
|
+
if namespace:
|
|
118
|
+
# Hash to 16-bit value
|
|
119
|
+
import hashlib
|
|
120
|
+
hash_val = int(hashlib.md5(namespace.encode()).hexdigest()[:4], 16)
|
|
121
|
+
return hash_val
|
|
122
|
+
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
def _get_sampling_config(self) -> Tuple[str, float]:
|
|
126
|
+
"""Get sampling configuration from environment.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Tuple of (sampler_name, sampling_rate)
|
|
130
|
+
"""
|
|
131
|
+
sampler = os.getenv("OTEL_TRACES_SAMPLER", "always_on").lower()
|
|
132
|
+
|
|
133
|
+
if sampler == "always_on":
|
|
134
|
+
return ("always_on", 1.0)
|
|
135
|
+
elif sampler == "always_off":
|
|
136
|
+
return ("always_off", 0.0)
|
|
137
|
+
elif sampler in ("traceidratio", "parentbased_traceidratio"):
|
|
138
|
+
# Get sampling rate from OTEL_TRACES_SAMPLER_ARG
|
|
139
|
+
arg = os.getenv("OTEL_TRACES_SAMPLER_ARG", "1.0")
|
|
140
|
+
try:
|
|
141
|
+
rate = float(arg)
|
|
142
|
+
rate = max(0.0, min(1.0, rate)) # Clamp to [0, 1]
|
|
143
|
+
return ("traceidratio", rate)
|
|
144
|
+
except ValueError:
|
|
145
|
+
logger.warning(f"Invalid OTEL_TRACES_SAMPLER_ARG: {arg}, using 1.0")
|
|
146
|
+
return ("traceidratio", 1.0)
|
|
147
|
+
else:
|
|
148
|
+
logger.warning(f"Unknown sampler: {sampler}, using always_on")
|
|
149
|
+
return ("always_on", 1.0)
|
|
150
|
+
|
|
151
|
+
def _get_frameworks(self) -> List[str]:
|
|
152
|
+
"""Get list of frameworks to auto-instrument.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
List of framework names, or empty list for all
|
|
156
|
+
"""
|
|
157
|
+
frameworks_str = os.getenv("AGENTREPLAY_FRAMEWORKS", "")
|
|
158
|
+
if not frameworks_str:
|
|
159
|
+
return [] # Empty = all frameworks
|
|
160
|
+
|
|
161
|
+
# Parse comma-separated list
|
|
162
|
+
frameworks = [fw.strip().lower() for fw in frameworks_str.split(",")]
|
|
163
|
+
return [fw for fw in frameworks if fw] # Filter empty strings
|
|
164
|
+
|
|
165
|
+
def validate_connection(self) -> bool:
|
|
166
|
+
"""Validate connection to Agentreplay server.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
True if server is reachable
|
|
170
|
+
"""
|
|
171
|
+
import httpx
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
client = httpx.Client(timeout=5.0)
|
|
175
|
+
# Try to hit health endpoint or root
|
|
176
|
+
for endpoint in ["/health", "/api/v1/health", "/"]:
|
|
177
|
+
try:
|
|
178
|
+
response = client.get(f"{self.url}{endpoint}")
|
|
179
|
+
if response.status_code < 500:
|
|
180
|
+
logger.info(f"✓ Agentreplay server reachable at {self.url}")
|
|
181
|
+
return True
|
|
182
|
+
except:
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
logger.warning(f"✗ Cannot reach Agentreplay server at {self.url}")
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.warning(f"✗ Failed to validate connection: {e}")
|
|
190
|
+
return False
|
|
191
|
+
finally:
|
|
192
|
+
try:
|
|
193
|
+
client.close()
|
|
194
|
+
except:
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
def create_client(self) -> AgentreplayClient:
|
|
198
|
+
"""Create a AgentreplayClient from this configuration.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Configured AgentreplayClient
|
|
202
|
+
"""
|
|
203
|
+
return AgentreplayClient(
|
|
204
|
+
url=self.url,
|
|
205
|
+
tenant_id=self.tenant_id,
|
|
206
|
+
project_id=self.project_id,
|
|
207
|
+
agent_id=self.agent_id,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def print_config(self) -> None:
|
|
211
|
+
"""Print configuration summary for debugging."""
|
|
212
|
+
print("Agentreplay Configuration:")
|
|
213
|
+
print(f" Service Name: {self.service_name}")
|
|
214
|
+
print(f" Server URL: {self.url}")
|
|
215
|
+
print(f" Tenant ID: {self.tenant_id}")
|
|
216
|
+
print(f" Project ID: {self.project_id}")
|
|
217
|
+
print(f" Agent ID: {self.agent_id}")
|
|
218
|
+
print(f" Sampler: {self.sampler}")
|
|
219
|
+
print(f" Sampling Rate: {self.sampling_rate}")
|
|
220
|
+
print(f" Auto-instrument: {self.auto_instrument}")
|
|
221
|
+
if self.frameworks:
|
|
222
|
+
print(f" Frameworks: {', '.join(self.frameworks)}")
|
|
223
|
+
else:
|
|
224
|
+
print(f" Frameworks: all")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def get_env_config() -> EnvConfig:
|
|
228
|
+
"""Get configuration from environment variables.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
EnvConfig instance
|
|
232
|
+
"""
|
|
233
|
+
return EnvConfig()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def init_from_env(
|
|
237
|
+
validate: bool = True,
|
|
238
|
+
auto_instrument: Optional[bool] = None,
|
|
239
|
+
verbose: bool = False,
|
|
240
|
+
) -> AgentreplayClient:
|
|
241
|
+
"""Initialize Agentreplay from environment variables.
|
|
242
|
+
|
|
243
|
+
This is the recommended way to set up Agentreplay for production deployments.
|
|
244
|
+
All configuration is read from environment variables following OTEL conventions.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
validate: If True, validate connection to server on initialization
|
|
248
|
+
auto_instrument: Override auto-instrumentation setting from env
|
|
249
|
+
verbose: If True, print configuration details
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Configured AgentreplayClient
|
|
253
|
+
|
|
254
|
+
Raises:
|
|
255
|
+
ConnectionError: If validate=True and server is unreachable
|
|
256
|
+
ValueError: If required environment variables are missing
|
|
257
|
+
|
|
258
|
+
Example:
|
|
259
|
+
>>> # Set environment variables first
|
|
260
|
+
>>> import os
|
|
261
|
+
>>> os.environ["OTEL_SERVICE_NAME"] = "my-agent"
|
|
262
|
+
>>> os.environ["AGENTREPLAY_TENANT_ID"] = "1"
|
|
263
|
+
>>>
|
|
264
|
+
>>> # Initialize with zero code configuration
|
|
265
|
+
>>> from agentreplay import init_from_env
|
|
266
|
+
>>> client = init_from_env()
|
|
267
|
+
>>>
|
|
268
|
+
>>> # Now use the client or just rely on auto-instrumentation
|
|
269
|
+
>>> with client.trace("operation"):
|
|
270
|
+
... pass
|
|
271
|
+
"""
|
|
272
|
+
config = get_env_config()
|
|
273
|
+
|
|
274
|
+
if verbose:
|
|
275
|
+
config.print_config()
|
|
276
|
+
|
|
277
|
+
# Validate connection if requested
|
|
278
|
+
if validate and config.validate_on_init:
|
|
279
|
+
if not config.validate_connection():
|
|
280
|
+
raise ConnectionError(
|
|
281
|
+
f"Cannot reach Agentreplay server at {config.url}\n"
|
|
282
|
+
f"Troubleshooting:\n"
|
|
283
|
+
f" 1. Check server is running: curl {config.url}/health\n"
|
|
284
|
+
f" 2. Verify firewall/network settings\n"
|
|
285
|
+
f" 3. Set AGENTREPLAY_URL or OTEL_EXPORTER_OTLP_ENDPOINT\n"
|
|
286
|
+
f" 4. Disable validation: init_from_env(validate=False)"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Set up auto-instrumentation if requested
|
|
290
|
+
should_auto_instrument = auto_instrument if auto_instrument is not None else config.auto_instrument
|
|
291
|
+
|
|
292
|
+
if should_auto_instrument:
|
|
293
|
+
try:
|
|
294
|
+
from agentreplay.auto_instrument import auto_instrument as do_auto_instrument
|
|
295
|
+
|
|
296
|
+
frameworks = config.frameworks if config.frameworks else None
|
|
297
|
+
|
|
298
|
+
do_auto_instrument(
|
|
299
|
+
service_name=config.service_name,
|
|
300
|
+
agentreplay_url=config.url,
|
|
301
|
+
tenant_id=config.tenant_id,
|
|
302
|
+
project_id=config.project_id,
|
|
303
|
+
frameworks=frameworks,
|
|
304
|
+
sample_rate=config.sampling_rate,
|
|
305
|
+
enable_otel_export=config.enable_otlp_export,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
logger.info("✓ Auto-instrumentation enabled")
|
|
309
|
+
|
|
310
|
+
except ImportError as e:
|
|
311
|
+
logger.warning(f"Auto-instrumentation failed: {e}")
|
|
312
|
+
except Exception as e:
|
|
313
|
+
logger.error(f"Error during auto-instrumentation: {e}")
|
|
314
|
+
|
|
315
|
+
# Create and return client
|
|
316
|
+
client = config.create_client()
|
|
317
|
+
|
|
318
|
+
logger.info("✓ Agentreplay initialized from environment")
|
|
319
|
+
|
|
320
|
+
return client
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
__all__ = [
|
|
324
|
+
"EnvConfig",
|
|
325
|
+
"get_env_config",
|
|
326
|
+
"init_from_env",
|
|
327
|
+
]
|
agentreplay/env_init.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Copyright 2025 Sushanth (https://github.com/sushanthpy)
|
|
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
|
+
|
|
15
|
+
"""
|
|
16
|
+
Agentreplay Environment-based Auto-Initialization
|
|
17
|
+
|
|
18
|
+
Simplified to use pure OpenTelemetry with OTLP export.
|
|
19
|
+
No custom span processors or framework-specific code.
|
|
20
|
+
|
|
21
|
+
Environment Variables:
|
|
22
|
+
AGENTREPLAY_ENABLED: Set to "1", "true", "yes" to enable
|
|
23
|
+
AGENTREPLAY_OTLP_ENDPOINT: OTLP gRPC endpoint (default: localhost:47117)
|
|
24
|
+
AGENTREPLAY_TENANT_ID: Tenant ID (default: 1)
|
|
25
|
+
AGENTREPLAY_PROJECT_ID: Project ID (default: 0)
|
|
26
|
+
AGENTREPLAY_SERVICE_NAME: Service name (default: "python-app")
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
export AGENTREPLAY_ENABLED=true
|
|
30
|
+
export AGENTREPLAY_PROJECT_ID=19358
|
|
31
|
+
python your_script.py # Automatically instrumented!
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import os
|
|
35
|
+
import logging
|
|
36
|
+
import atexit
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _parse_bool(value: str) -> bool:
|
|
42
|
+
"""Parse boolean from environment variable."""
|
|
43
|
+
return value.lower() in ("1", "true", "yes", "on", "enabled")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def init_from_env(force: bool = False) -> bool:
|
|
47
|
+
"""Initialize Agentreplay from environment variables.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
force: Force initialization even if already initialized
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
True if instrumentation was enabled, False otherwise
|
|
54
|
+
"""
|
|
55
|
+
# Check if already initialized
|
|
56
|
+
if hasattr(init_from_env, "_initialized") and not force:
|
|
57
|
+
return init_from_env._initialized
|
|
58
|
+
|
|
59
|
+
# Check if enabled
|
|
60
|
+
enabled = os.getenv("AGENTREPLAY_ENABLED", "").strip()
|
|
61
|
+
if not enabled or not _parse_bool(enabled):
|
|
62
|
+
logger.debug("Agentreplay disabled (AGENTREPLAY_ENABLED not set)")
|
|
63
|
+
init_from_env._initialized = False
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
# Get configuration
|
|
67
|
+
otlp_endpoint = os.getenv("AGENTREPLAY_OTLP_ENDPOINT", "localhost:47117")
|
|
68
|
+
tenant_id = int(os.getenv("AGENTREPLAY_TENANT_ID", "1"))
|
|
69
|
+
project_id = int(os.getenv("AGENTREPLAY_PROJECT_ID", "0"))
|
|
70
|
+
service_name = os.getenv("AGENTREPLAY_SERVICE_NAME", "python-app")
|
|
71
|
+
log_level = os.getenv("AGENTREPLAY_LOG_LEVEL", "INFO").upper()
|
|
72
|
+
|
|
73
|
+
# Set logging level
|
|
74
|
+
logging.basicConfig(
|
|
75
|
+
level=getattr(logging, log_level, logging.INFO),
|
|
76
|
+
format='%(asctime)s [%(name)s] %(levelname)s: %(message)s'
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
from agentreplay.auto_instrument import auto_instrument
|
|
81
|
+
|
|
82
|
+
logger.info(f"🚀 Initializing Agentreplay")
|
|
83
|
+
logger.info(f" OTLP Endpoint: {otlp_endpoint}")
|
|
84
|
+
logger.info(f" Tenant: {tenant_id}, Project: {project_id}")
|
|
85
|
+
logger.info(f" Service: {service_name}")
|
|
86
|
+
|
|
87
|
+
auto_instrument(
|
|
88
|
+
service_name=service_name,
|
|
89
|
+
otlp_endpoint=otlp_endpoint,
|
|
90
|
+
tenant_id=tenant_id,
|
|
91
|
+
project_id=project_id,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Register atexit handler to flush spans on program exit
|
|
95
|
+
def _flush_on_exit():
|
|
96
|
+
try:
|
|
97
|
+
from opentelemetry import trace
|
|
98
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
99
|
+
provider = trace.get_tracer_provider()
|
|
100
|
+
if isinstance(provider, TracerProvider):
|
|
101
|
+
logger.debug("Flushing spans on exit...")
|
|
102
|
+
provider.force_flush(timeout_millis=5000)
|
|
103
|
+
logger.debug("Spans flushed successfully")
|
|
104
|
+
except Exception as e:
|
|
105
|
+
logger.debug(f"Failed to flush spans on exit: {e}")
|
|
106
|
+
|
|
107
|
+
atexit.register(_flush_on_exit)
|
|
108
|
+
|
|
109
|
+
logger.info("✅ Agentreplay auto-instrumentation enabled")
|
|
110
|
+
init_from_env._initialized = True
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
except ImportError as e:
|
|
114
|
+
logger.error(f"❌ Failed to import: {e}")
|
|
115
|
+
init_from_env._initialized = False
|
|
116
|
+
return False
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logger.error(f"❌ Failed to initialize: {e}")
|
|
119
|
+
init_from_env._initialized = False
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# Auto-initialize on module import
|
|
124
|
+
_AUTO_INIT = os.getenv("AGENTREPLAY_AUTO_INIT", "1")
|
|
125
|
+
if _parse_bool(_AUTO_INIT):
|
|
126
|
+
init_from_env()
|
|
127
|
+
else:
|
|
128
|
+
logger.debug("Agentreplay auto-init on import disabled (AGENTREPLAY_AUTO_INIT=0)")
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Copyright 2025 Sushanth (https://github.com/sushanthpy)
|
|
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
|
+
|
|
15
|
+
"""Custom exceptions for Agentreplay client."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AgentreplayError(Exception):
|
|
19
|
+
"""Base exception for all Agentreplay client errors."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AuthenticationError(AgentreplayError):
|
|
24
|
+
"""Raised when authentication fails (401 Unauthorized)."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RateLimitError(AgentreplayError):
|
|
29
|
+
"""Raised when rate limited (429 Too Many Requests).
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
retry_after: Seconds to wait before retrying
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, retry_after: int):
|
|
36
|
+
"""Initialize rate limit error.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
retry_after: Seconds to wait before retrying
|
|
40
|
+
"""
|
|
41
|
+
self.retry_after = retry_after
|
|
42
|
+
super().__init__(f"Rate limited. Retry after {retry_after} seconds")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ServerError(AgentreplayError):
|
|
46
|
+
"""Raised on 5xx server errors."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, status_code: int, message: str):
|
|
49
|
+
"""Initialize server error.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
status_code: HTTP status code (500-599)
|
|
53
|
+
message: Error message from server
|
|
54
|
+
"""
|
|
55
|
+
self.status_code = status_code
|
|
56
|
+
super().__init__(f"Server error ({status_code}): {message}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ValidationError(AgentreplayError):
|
|
60
|
+
"""Raised on 400 Bad Request / validation errors."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, message: str):
|
|
63
|
+
"""Initialize validation error.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
message: Validation error details
|
|
67
|
+
"""
|
|
68
|
+
super().__init__(f"Validation error: {message}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class NotFoundError(AgentreplayError):
|
|
72
|
+
"""Raised on 404 Not Found errors."""
|
|
73
|
+
|
|
74
|
+
def __init__(self, resource: str):
|
|
75
|
+
"""Initialize not found error.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
resource: Resource that wasn't found
|
|
79
|
+
"""
|
|
80
|
+
super().__init__(f"Resource not found: {resource}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class NetworkError(AgentreplayError):
|
|
84
|
+
"""Raised on network/connection errors."""
|
|
85
|
+
|
|
86
|
+
def __init__(self, message: str):
|
|
87
|
+
"""Initialize network error.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
message: Network error details
|
|
91
|
+
"""
|
|
92
|
+
super().__init__(f"Network error: {message}")
|