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.
@@ -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
+ ]
@@ -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}")