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
agentreplay/sampling.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
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
|
+
"""Sampling strategies for trace collection.
|
|
16
|
+
|
|
17
|
+
This module provides OTEL-compatible sampling strategies to control
|
|
18
|
+
trace data volume while maintaining statistical validity.
|
|
19
|
+
|
|
20
|
+
Supported samplers:
|
|
21
|
+
- AlwaysOnSampler: Sample every trace (100%)
|
|
22
|
+
- AlwaysOffSampler: Sample no traces (0%)
|
|
23
|
+
- TraceIdRatioBasedSampler: Sample based on trace ID hash
|
|
24
|
+
- ParentBasedSampler: Respect parent span's sampling decision
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
>>> from agentreplay.sampling import TraceIdRatioBasedSampler
|
|
28
|
+
>>>
|
|
29
|
+
>>> # Sample 10% of traces
|
|
30
|
+
>>> sampler = TraceIdRatioBasedSampler(0.1)
|
|
31
|
+
>>>
|
|
32
|
+
>>> # Check if trace should be sampled
|
|
33
|
+
>>> if sampler.should_sample(trace_id=0x123abc):
|
|
34
|
+
... # Record trace
|
|
35
|
+
... pass
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
import logging
|
|
39
|
+
from typing import Optional
|
|
40
|
+
from abc import ABC, abstractmethod
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Sampler(ABC):
|
|
46
|
+
"""Abstract base class for sampling strategies."""
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def should_sample(
|
|
50
|
+
self,
|
|
51
|
+
trace_id: int,
|
|
52
|
+
parent_sampled: Optional[bool] = None,
|
|
53
|
+
) -> bool:
|
|
54
|
+
"""Determine if a trace should be sampled.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
trace_id: 128-bit trace identifier
|
|
58
|
+
parent_sampled: Whether parent span was sampled (if known)
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
True if trace should be sampled
|
|
62
|
+
"""
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def get_description(self) -> str:
|
|
67
|
+
"""Get human-readable description of sampler.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Description string
|
|
71
|
+
"""
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class AlwaysOnSampler(Sampler):
|
|
76
|
+
"""Sample all traces (100%).
|
|
77
|
+
|
|
78
|
+
Use this for development or low-volume production workloads.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def should_sample(
|
|
82
|
+
self,
|
|
83
|
+
trace_id: int,
|
|
84
|
+
parent_sampled: Optional[bool] = None,
|
|
85
|
+
) -> bool:
|
|
86
|
+
"""Always returns True."""
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
def get_description(self) -> str:
|
|
90
|
+
"""Get description."""
|
|
91
|
+
return "AlwaysOnSampler"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class AlwaysOffSampler(Sampler):
|
|
95
|
+
"""Sample no traces (0%).
|
|
96
|
+
|
|
97
|
+
Use this to completely disable tracing.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def should_sample(
|
|
101
|
+
self,
|
|
102
|
+
trace_id: int,
|
|
103
|
+
parent_sampled: Optional[bool] = None,
|
|
104
|
+
) -> bool:
|
|
105
|
+
"""Always returns False."""
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
def get_description(self) -> str:
|
|
109
|
+
"""Get description."""
|
|
110
|
+
return "AlwaysOffSampler"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class TraceIdRatioBasedSampler(Sampler):
|
|
114
|
+
"""Sample traces based on trace ID hash.
|
|
115
|
+
|
|
116
|
+
Uses deterministic sampling: traces with the same ID always get
|
|
117
|
+
the same sampling decision. This ensures consistent sampling across
|
|
118
|
+
distributed services.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
rate: Sampling rate between 0.0 and 1.0
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
>>> # Sample 10% of traces
|
|
125
|
+
>>> sampler = TraceIdRatioBasedSampler(0.1)
|
|
126
|
+
>>>
|
|
127
|
+
>>> # Same trace ID always gets same decision
|
|
128
|
+
>>> assert sampler.should_sample(0x123) == sampler.should_sample(0x123)
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def __init__(self, rate: float):
|
|
132
|
+
"""Initialize ratio-based sampler.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
rate: Sampling rate (0.0 = none, 1.0 = all)
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
ValueError: If rate is not in [0.0, 1.0]
|
|
139
|
+
"""
|
|
140
|
+
if not 0.0 <= rate <= 1.0:
|
|
141
|
+
raise ValueError(f"Sampling rate must be in [0.0, 1.0], got {rate}")
|
|
142
|
+
|
|
143
|
+
self.rate = rate
|
|
144
|
+
# Calculate threshold for comparison
|
|
145
|
+
# Use upper 64 bits of trace_id for sampling decision
|
|
146
|
+
self.threshold = int(rate * (2**64 - 1))
|
|
147
|
+
|
|
148
|
+
logger.info(f"TraceIdRatioBasedSampler initialized: rate={rate:.2%}")
|
|
149
|
+
|
|
150
|
+
def should_sample(
|
|
151
|
+
self,
|
|
152
|
+
trace_id: int,
|
|
153
|
+
parent_sampled: Optional[bool] = None,
|
|
154
|
+
) -> bool:
|
|
155
|
+
"""Determine if trace should be sampled based on trace ID.
|
|
156
|
+
|
|
157
|
+
Uses the upper 64 bits of the 128-bit trace ID for sampling decision.
|
|
158
|
+
This ensures uniform distribution and deterministic decisions.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
trace_id: 128-bit trace identifier
|
|
162
|
+
parent_sampled: Ignored (not used for ratio-based sampling)
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
True if trace should be sampled
|
|
166
|
+
"""
|
|
167
|
+
if self.rate == 1.0:
|
|
168
|
+
return True
|
|
169
|
+
if self.rate == 0.0:
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
# Extract upper 64 bits of trace_id
|
|
173
|
+
upper_64 = (trace_id >> 64) & ((1 << 64) - 1)
|
|
174
|
+
|
|
175
|
+
# Compare with threshold
|
|
176
|
+
return upper_64 < self.threshold
|
|
177
|
+
|
|
178
|
+
def get_description(self) -> str:
|
|
179
|
+
"""Get description."""
|
|
180
|
+
return f"TraceIdRatioBasedSampler(rate={self.rate:.2%})"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class ParentBasedSampler(Sampler):
|
|
184
|
+
"""Sample based on parent span's sampling decision.
|
|
185
|
+
|
|
186
|
+
If parent span was sampled, sample this span too. This ensures
|
|
187
|
+
complete traces are captured (no partial traces with missing spans).
|
|
188
|
+
|
|
189
|
+
Falls back to root_sampler for root spans (no parent).
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
root_sampler: Sampler to use for root spans (no parent)
|
|
193
|
+
|
|
194
|
+
Example:
|
|
195
|
+
>>> # Use 10% sampling for root spans, but always sample if parent was sampled
|
|
196
|
+
>>> root_sampler = TraceIdRatioBasedSampler(0.1)
|
|
197
|
+
>>> sampler = ParentBasedSampler(root_sampler)
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
def __init__(self, root_sampler: Sampler):
|
|
201
|
+
"""Initialize parent-based sampler.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
root_sampler: Sampler for root spans
|
|
205
|
+
"""
|
|
206
|
+
self.root_sampler = root_sampler
|
|
207
|
+
logger.info(f"ParentBasedSampler initialized with root: {root_sampler.get_description()}")
|
|
208
|
+
|
|
209
|
+
def should_sample(
|
|
210
|
+
self,
|
|
211
|
+
trace_id: int,
|
|
212
|
+
parent_sampled: Optional[bool] = None,
|
|
213
|
+
) -> bool:
|
|
214
|
+
"""Determine if trace should be sampled.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
trace_id: 128-bit trace identifier
|
|
218
|
+
parent_sampled: Whether parent span was sampled
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
True if trace should be sampled
|
|
222
|
+
"""
|
|
223
|
+
# If parent sampling decision is known, use it
|
|
224
|
+
if parent_sampled is not None:
|
|
225
|
+
return parent_sampled
|
|
226
|
+
|
|
227
|
+
# No parent (root span), use root sampler
|
|
228
|
+
return self.root_sampler.should_sample(trace_id, parent_sampled=None)
|
|
229
|
+
|
|
230
|
+
def get_description(self) -> str:
|
|
231
|
+
"""Get description."""
|
|
232
|
+
return f"ParentBasedSampler(root={self.root_sampler.get_description()})"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def create_sampler_from_config(sampler_name: str, sampler_arg: str = "1.0") -> Sampler:
|
|
236
|
+
"""Create sampler from OTEL environment variable values.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
sampler_name: OTEL_TRACES_SAMPLER value
|
|
240
|
+
sampler_arg: OTEL_TRACES_SAMPLER_ARG value
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Configured Sampler instance
|
|
244
|
+
|
|
245
|
+
Example:
|
|
246
|
+
>>> import os
|
|
247
|
+
>>> sampler = create_sampler_from_config(
|
|
248
|
+
... os.getenv("OTEL_TRACES_SAMPLER", "always_on"),
|
|
249
|
+
... os.getenv("OTEL_TRACES_SAMPLER_ARG", "1.0")
|
|
250
|
+
... )
|
|
251
|
+
"""
|
|
252
|
+
sampler_name = sampler_name.lower().strip()
|
|
253
|
+
|
|
254
|
+
if sampler_name == "always_on":
|
|
255
|
+
return AlwaysOnSampler()
|
|
256
|
+
|
|
257
|
+
elif sampler_name == "always_off":
|
|
258
|
+
return AlwaysOffSampler()
|
|
259
|
+
|
|
260
|
+
elif sampler_name == "traceidratio":
|
|
261
|
+
try:
|
|
262
|
+
rate = float(sampler_arg)
|
|
263
|
+
rate = max(0.0, min(1.0, rate)) # Clamp to [0, 1]
|
|
264
|
+
return TraceIdRatioBasedSampler(rate)
|
|
265
|
+
except ValueError:
|
|
266
|
+
logger.warning(f"Invalid sampler_arg: {sampler_arg}, using 1.0")
|
|
267
|
+
return TraceIdRatioBasedSampler(1.0)
|
|
268
|
+
|
|
269
|
+
elif sampler_name == "parentbased_always_on":
|
|
270
|
+
return ParentBasedSampler(AlwaysOnSampler())
|
|
271
|
+
|
|
272
|
+
elif sampler_name == "parentbased_always_off":
|
|
273
|
+
return ParentBasedSampler(AlwaysOffSampler())
|
|
274
|
+
|
|
275
|
+
elif sampler_name == "parentbased_traceidratio":
|
|
276
|
+
try:
|
|
277
|
+
rate = float(sampler_arg)
|
|
278
|
+
rate = max(0.0, min(1.0, rate))
|
|
279
|
+
root_sampler = TraceIdRatioBasedSampler(rate)
|
|
280
|
+
return ParentBasedSampler(root_sampler)
|
|
281
|
+
except ValueError:
|
|
282
|
+
logger.warning(f"Invalid sampler_arg: {sampler_arg}, using 1.0")
|
|
283
|
+
root_sampler = TraceIdRatioBasedSampler(1.0)
|
|
284
|
+
return ParentBasedSampler(root_sampler)
|
|
285
|
+
|
|
286
|
+
else:
|
|
287
|
+
logger.warning(f"Unknown sampler: {sampler_name}, using AlwaysOnSampler")
|
|
288
|
+
return AlwaysOnSampler()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
__all__ = [
|
|
292
|
+
"Sampler",
|
|
293
|
+
"AlwaysOnSampler",
|
|
294
|
+
"AlwaysOffSampler",
|
|
295
|
+
"TraceIdRatioBasedSampler",
|
|
296
|
+
"ParentBasedSampler",
|
|
297
|
+
"create_sampler_from_config",
|
|
298
|
+
]
|
agentreplay/session.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
"""Session management for conversational applications."""
|
|
16
|
+
|
|
17
|
+
import time
|
|
18
|
+
from typing import Optional
|
|
19
|
+
from agentreplay.client import AgentreplayClient
|
|
20
|
+
from agentreplay.models import SpanType
|
|
21
|
+
from agentreplay.span import Span
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Session:
|
|
25
|
+
"""Session manager for chatbot and conversational applications.
|
|
26
|
+
|
|
27
|
+
Automatically tracks session_id and message counts, simplifying
|
|
28
|
+
session lifecycle management for multi-turn conversations.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
client: Agentreplay client instance
|
|
32
|
+
session_id: Optional session identifier (auto-generated if not provided)
|
|
33
|
+
agent_id: Optional agent identifier (uses client default if not provided)
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
session_id: Session identifier
|
|
37
|
+
message_count: Number of messages/traces in this session
|
|
38
|
+
start_time: Session start timestamp (microseconds)
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
>>> client = AgentreplayClient(url="http://localhost:8080", tenant_id=1)
|
|
42
|
+
>>> session = Session(client)
|
|
43
|
+
>>>
|
|
44
|
+
>>> # Track conversation turns
|
|
45
|
+
>>> with session.trace(SpanType.LLM) as turn1:
|
|
46
|
+
... turn1.set_attribute("prompt", "Hello")
|
|
47
|
+
... turn1.set_token_count(50)
|
|
48
|
+
...
|
|
49
|
+
>>> with session.trace(SpanType.LLM) as turn2:
|
|
50
|
+
... turn2.set_attribute("prompt", "How are you?")
|
|
51
|
+
... turn2.set_token_count(60)
|
|
52
|
+
...
|
|
53
|
+
>>> print(f"Session {session.session_id} had {session.message_count} turns")
|
|
54
|
+
>>> session.end()
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
client: AgentreplayClient,
|
|
60
|
+
session_id: Optional[int] = None,
|
|
61
|
+
agent_id: Optional[int] = None,
|
|
62
|
+
):
|
|
63
|
+
"""Initialize session manager.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
client: Agentreplay client
|
|
67
|
+
session_id: Session identifier (auto-generated if None)
|
|
68
|
+
agent_id: Agent identifier (uses client default if None)
|
|
69
|
+
"""
|
|
70
|
+
self.client = client
|
|
71
|
+
self.session_id = session_id if session_id is not None else int(time.time() * 1000)
|
|
72
|
+
self.agent_id = agent_id if agent_id is not None else client.agent_id
|
|
73
|
+
self.message_count = 0
|
|
74
|
+
self.start_time = int(time.time() * 1_000_000)
|
|
75
|
+
self._end_time: Optional[int] = None # Track when session ended
|
|
76
|
+
self._ended = False
|
|
77
|
+
|
|
78
|
+
def __enter__(self) -> "Session":
|
|
79
|
+
"""Context manager entry."""
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
83
|
+
"""Context manager exit - end session."""
|
|
84
|
+
self.end()
|
|
85
|
+
|
|
86
|
+
def trace(
|
|
87
|
+
self,
|
|
88
|
+
span_type: SpanType = SpanType.ROOT,
|
|
89
|
+
**metadata,
|
|
90
|
+
) -> Span:
|
|
91
|
+
"""Create a trace within this session.
|
|
92
|
+
|
|
93
|
+
Automatically sets session_id and tracks message count.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
span_type: Type of span (default: ROOT)
|
|
97
|
+
**metadata: Additional metadata to attach to span
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Span context manager
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
>>> session = Session(client)
|
|
104
|
+
>>> with session.trace(SpanType.LLM, model="gpt-4") as span:
|
|
105
|
+
... span.set_token_count(100)
|
|
106
|
+
"""
|
|
107
|
+
if self._ended:
|
|
108
|
+
raise RuntimeError("Cannot create trace in ended session")
|
|
109
|
+
|
|
110
|
+
self.message_count += 1
|
|
111
|
+
|
|
112
|
+
# Add session metadata
|
|
113
|
+
full_metadata = {
|
|
114
|
+
"message_num": self.message_count,
|
|
115
|
+
"session_duration_us": int(time.time() * 1_000_000) - self.start_time,
|
|
116
|
+
**metadata,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return self.client.trace(
|
|
120
|
+
span_type=span_type,
|
|
121
|
+
agent_id=self.agent_id,
|
|
122
|
+
session_id=self.session_id,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def end(self) -> None:
|
|
126
|
+
"""Mark session as ended.
|
|
127
|
+
|
|
128
|
+
Optionally send session summary metrics to backend.
|
|
129
|
+
"""
|
|
130
|
+
if self._ended:
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
self._end_time = int(time.time() * 1_000_000)
|
|
134
|
+
self._ended = True
|
|
135
|
+
|
|
136
|
+
# Compute session statistics
|
|
137
|
+
duration_us = self._end_time - self.start_time
|
|
138
|
+
|
|
139
|
+
# Could send session summary span
|
|
140
|
+
# with self.client.trace(
|
|
141
|
+
# span_type=SpanType.ROOT,
|
|
142
|
+
# agent_id=self.agent_id,
|
|
143
|
+
# session_id=self.session_id
|
|
144
|
+
# ) as summary:
|
|
145
|
+
# summary.set_attribute("session_ended", True)
|
|
146
|
+
# summary.set_attribute("total_messages", self.message_count)
|
|
147
|
+
# summary.set_attribute("duration_us", duration_us)
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def is_active(self) -> bool:
|
|
151
|
+
"""Check if session is still active."""
|
|
152
|
+
return not self._ended
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def duration_seconds(self) -> float:
|
|
156
|
+
"""Get session duration in seconds.
|
|
157
|
+
|
|
158
|
+
Returns accurate duration even after session has ended.
|
|
159
|
+
"""
|
|
160
|
+
if self._ended and self._end_time is not None:
|
|
161
|
+
# Return actual session duration
|
|
162
|
+
return (self._end_time - self.start_time) / 1_000_000
|
|
163
|
+
# Session still active, return current duration
|
|
164
|
+
return (int(time.time() * 1_000_000) - self.start_time) / 1_000_000
|
|
@@ -0,0 +1,73 @@
|
|
|
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 automatic instrumentation via sitecustomize.
|
|
17
|
+
|
|
18
|
+
This file is automatically loaded by Python if it's in the site-packages directory.
|
|
19
|
+
It enables zero-code instrumentation - just set env vars and run!
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
export AGENTREPLAY_ENABLED=true
|
|
23
|
+
export AGENTREPLAY_URL=http://localhost:47100
|
|
24
|
+
python my_app.py # Automatically instrumented!
|
|
25
|
+
|
|
26
|
+
Environment Variables:
|
|
27
|
+
AGENTREPLAY_ENABLED: Set to 'true' to enable auto-instrumentation
|
|
28
|
+
AGENTREPLAY_URL: Agentreplay server URL (default: http://localhost:47100)
|
|
29
|
+
AGENTREPLAY_TENANT_ID: Tenant ID (default: 1)
|
|
30
|
+
AGENTREPLAY_PROJECT_ID: Project ID (default: 0)
|
|
31
|
+
AGENTREPLAY_DEBUG: Set to 'true' for verbose logging
|
|
32
|
+
OTEL_SERVICE_NAME: Service name for traces
|
|
33
|
+
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: Capture message content
|
|
34
|
+
|
|
35
|
+
Note:
|
|
36
|
+
This module is loaded very early in the Python startup process.
|
|
37
|
+
It must handle all errors gracefully to avoid breaking user applications.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
import os
|
|
41
|
+
import sys
|
|
42
|
+
|
|
43
|
+
# Only auto-instrument if explicitly enabled
|
|
44
|
+
if os.getenv('AGENTREPLAY_ENABLED', '').lower() == 'true':
|
|
45
|
+
try:
|
|
46
|
+
# Import and initialize BEFORE any user code runs
|
|
47
|
+
from agentreplay.bootstrap import init_otel_instrumentation
|
|
48
|
+
|
|
49
|
+
init_otel_instrumentation(
|
|
50
|
+
service_name=os.getenv('OTEL_SERVICE_NAME', os.path.basename(sys.argv[0])),
|
|
51
|
+
agentreplay_url=os.getenv('AGENTREPLAY_URL', 'http://localhost:47100'),
|
|
52
|
+
tenant_id=int(os.getenv('AGENTREPLAY_TENANT_ID', '1')),
|
|
53
|
+
project_id=int(os.getenv('AGENTREPLAY_PROJECT_ID', '0')),
|
|
54
|
+
capture_content=os.getenv('OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT', 'false').lower() == 'true'
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Silent by default, verbose if DEBUG enabled
|
|
58
|
+
if os.getenv('AGENTREPLAY_DEBUG', '').lower() == 'true':
|
|
59
|
+
print("[Agentreplay] ✓ Auto-instrumentation enabled", file=sys.stderr)
|
|
60
|
+
print(f"[Agentreplay] Service: {os.getenv('OTEL_SERVICE_NAME', os.path.basename(sys.argv[0]))}", file=sys.stderr)
|
|
61
|
+
print(f"[Agentreplay] URL: {os.getenv('AGENTREPLAY_URL', 'http://localhost:47100')}", file=sys.stderr)
|
|
62
|
+
print(f"[Agentreplay] Project: {os.getenv('AGENTREPLAY_PROJECT_ID', '0')}", file=sys.stderr)
|
|
63
|
+
|
|
64
|
+
except ImportError as e:
|
|
65
|
+
if os.getenv('AGENTREPLAY_DEBUG', '').lower() == 'true':
|
|
66
|
+
print(f"[Agentreplay] ✗ Failed to auto-instrument: {e}", file=sys.stderr)
|
|
67
|
+
print("[Agentreplay] Install: pip install opentelemetry-api opentelemetry-sdk", file=sys.stderr)
|
|
68
|
+
|
|
69
|
+
except Exception as e:
|
|
70
|
+
if os.getenv('AGENTREPLAY_DEBUG', '').lower() == 'true':
|
|
71
|
+
print(f"[Agentreplay] ✗ Auto-instrumentation error: {e}", file=sys.stderr)
|
|
72
|
+
import traceback
|
|
73
|
+
traceback.print_exc(file=sys.stderr)
|