paid-python 0.0.5a40__py3-none-any.whl → 0.1.0__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.
- paid/client.py +339 -233
- paid/logger.py +21 -0
- paid/tracing/__init__.py +4 -4
- paid/tracing/autoinstrumentation.py +6 -3
- paid/tracing/context_manager.py +243 -0
- paid/tracing/distributed_tracing.py +113 -0
- paid/tracing/signal.py +58 -28
- paid/tracing/tracing.py +103 -439
- paid/tracing/wrappers/anthropic/anthropicWrapper.py +11 -72
- paid/tracing/wrappers/bedrock/bedrockWrapper.py +3 -32
- paid/tracing/wrappers/gemini/geminiWrapper.py +10 -46
- paid/tracing/wrappers/langchain/paidLangChainCallback.py +3 -38
- paid/tracing/wrappers/llamaindex/llamaIndexWrapper.py +4 -38
- paid/tracing/wrappers/mistral/mistralWrapper.py +7 -118
- paid/tracing/wrappers/openai/openAiWrapper.py +56 -323
- paid/tracing/wrappers/openai_agents/openaiAgentsHook.py +8 -76
- {paid_python-0.0.5a40.dist-info → paid_python-0.1.0.dist-info}/METADATA +39 -192
- {paid_python-0.0.5a40.dist-info → paid_python-0.1.0.dist-info}/RECORD +20 -17
- {paid_python-0.0.5a40.dist-info → paid_python-0.1.0.dist-info}/LICENSE +0 -0
- {paid_python-0.0.5a40.dist-info → paid_python-0.1.0.dist-info}/WHEEL +0 -0
paid/tracing/tracing.py
CHANGED
|
@@ -2,37 +2,26 @@
|
|
|
2
2
|
import asyncio
|
|
3
3
|
import atexit
|
|
4
4
|
import contextvars
|
|
5
|
-
import functools
|
|
6
|
-
import logging
|
|
7
5
|
import os
|
|
8
6
|
import signal
|
|
9
7
|
from typing import Any, Awaitable, Callable, Dict, Optional, Tuple, TypeVar, Union
|
|
10
8
|
|
|
11
9
|
import dotenv
|
|
10
|
+
from . import distributed_tracing
|
|
12
11
|
from opentelemetry import trace
|
|
13
12
|
from opentelemetry.context import Context
|
|
14
13
|
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
15
14
|
from opentelemetry.sdk.resources import Resource
|
|
16
15
|
from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor, TracerProvider
|
|
17
16
|
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
|
18
|
-
from opentelemetry.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
dotenv.load_dotenv()
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
else:
|
|
27
|
-
log_level = logging.ERROR # Default to show errors
|
|
28
|
-
logger = logging.getLogger(__name__)
|
|
29
|
-
logger.setLevel(log_level)
|
|
30
|
-
if not logger.hasHandlers():
|
|
31
|
-
handler = logging.StreamHandler()
|
|
32
|
-
handler.setLevel(log_level)
|
|
33
|
-
formatter = logging.Formatter("%(levelname)s:%(name)s:%(message)s")
|
|
34
|
-
handler.setFormatter(formatter)
|
|
35
|
-
logger.addHandler(handler)
|
|
17
|
+
from opentelemetry.trace import NonRecordingSpan, NoOpTracerProvider, SpanContext, Status, StatusCode, TraceFlags
|
|
18
|
+
|
|
19
|
+
from paid.logger import logger
|
|
20
|
+
|
|
21
|
+
_ = dotenv.load_dotenv()
|
|
22
|
+
DEFAULT_COLLECTOR_ENDPOINT = (
|
|
23
|
+
os.environ.get("PAID_OTEL_COLLECTOR_ENDPOINT") or "https://collector.agentpaid.io:4318/v1/traces"
|
|
24
|
+
)
|
|
36
25
|
|
|
37
26
|
# Context variables for passing data to nested spans (e.g., in openAiWrapper)
|
|
38
27
|
paid_external_customer_id_var: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
|
|
@@ -41,10 +30,8 @@ paid_external_customer_id_var: contextvars.ContextVar[Optional[str]] = contextva
|
|
|
41
30
|
paid_external_agent_id_var: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
|
|
42
31
|
"paid_external_agent_id", default=None
|
|
43
32
|
)
|
|
44
|
-
# api_key storage
|
|
45
|
-
paid_token_var: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar("paid_token", default=None)
|
|
46
33
|
# trace id storage (generated from token)
|
|
47
|
-
|
|
34
|
+
paid_trace_id_var: contextvars.ContextVar[Optional[int]] = contextvars.ContextVar("paid_trace_id", default=None)
|
|
48
35
|
# flag to enable storing prompt contents
|
|
49
36
|
paid_store_prompt_var: contextvars.ContextVar[Optional[bool]] = contextvars.ContextVar(
|
|
50
37
|
"paid_store_prompt", default=False
|
|
@@ -56,25 +43,36 @@ paid_user_metadata_var: contextvars.ContextVar[Optional[Dict[str, Any]]] = conte
|
|
|
56
43
|
|
|
57
44
|
T = TypeVar("T")
|
|
58
45
|
|
|
59
|
-
|
|
46
|
+
|
|
47
|
+
class _TokenStore:
|
|
48
|
+
"""Private token storage to enforce access through getter/setter."""
|
|
49
|
+
|
|
50
|
+
__token: Optional[str] = None
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def get(cls) -> Optional[str]:
|
|
54
|
+
"""Get the stored API token."""
|
|
55
|
+
return cls.__token
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def set(cls, token: str) -> None:
|
|
59
|
+
"""Set the API token."""
|
|
60
|
+
cls.__token = token
|
|
60
61
|
|
|
61
62
|
|
|
62
63
|
def get_token() -> Optional[str]:
|
|
63
64
|
"""Get the stored API token."""
|
|
64
|
-
|
|
65
|
-
return _token
|
|
65
|
+
return _TokenStore.get()
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
def set_token(token: str) -> None:
|
|
69
69
|
"""Set the API token."""
|
|
70
|
-
|
|
71
|
-
_token = token
|
|
70
|
+
_TokenStore.set(token)
|
|
72
71
|
|
|
73
72
|
|
|
74
|
-
otel_id_generator = RandomIdGenerator()
|
|
75
|
-
|
|
76
73
|
# Isolated tracer provider for Paid - separate from any user OTEL setup
|
|
77
|
-
|
|
74
|
+
# Initialized at module load with defaults, never None (uses no-op provider if not initialized or API key isn't available)
|
|
75
|
+
paid_tracer_provider: Union[TracerProvider, NoOpTracerProvider] = NoOpTracerProvider()
|
|
78
76
|
|
|
79
77
|
|
|
80
78
|
class PaidSpanProcessor(SpanProcessor):
|
|
@@ -146,7 +144,6 @@ class PaidSpanProcessor(SpanProcessor):
|
|
|
146
144
|
for k, v in original_attributes.items()
|
|
147
145
|
if not any(k.startswith(prefix) for prefix in self.PROMPT_ATTRIBUTES_PREFIXES)
|
|
148
146
|
}
|
|
149
|
-
# Temporarily replace attributes for export
|
|
150
147
|
# This works because the exporter reads attributes during serialization
|
|
151
148
|
object.__setattr__(span, "_attributes", filtered_attrs)
|
|
152
149
|
|
|
@@ -159,9 +156,7 @@ class PaidSpanProcessor(SpanProcessor):
|
|
|
159
156
|
return True
|
|
160
157
|
|
|
161
158
|
|
|
162
|
-
def
|
|
163
|
-
api_key: Optional[str] = None, collector_endpoint: Optional[str] = "https://collector.agentpaid.io:4318/v1/traces"
|
|
164
|
-
):
|
|
159
|
+
def initialize_tracing_(api_key: Optional[str] = None, collector_endpoint: Optional[str] = DEFAULT_COLLECTOR_ENDPOINT):
|
|
165
160
|
"""
|
|
166
161
|
Initialize OpenTelemetry with OTLP exporter for Paid backend.
|
|
167
162
|
|
|
@@ -171,20 +166,21 @@ def _initialize_tracing(
|
|
|
171
166
|
"""
|
|
172
167
|
global paid_tracer_provider
|
|
173
168
|
|
|
169
|
+
if not collector_endpoint:
|
|
170
|
+
collector_endpoint = DEFAULT_COLLECTOR_ENDPOINT
|
|
171
|
+
|
|
174
172
|
try:
|
|
175
|
-
if
|
|
176
|
-
|
|
173
|
+
if get_token() is not None:
|
|
174
|
+
logger.warning("Tracing is already initialized - skipping re-initialization")
|
|
175
|
+
return
|
|
177
176
|
|
|
178
177
|
# Get API key from parameter or environment
|
|
179
178
|
if api_key is None:
|
|
180
|
-
import dotenv
|
|
181
|
-
|
|
182
|
-
dotenv.load_dotenv()
|
|
183
179
|
api_key = os.environ.get("PAID_API_KEY")
|
|
184
180
|
if api_key is None:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
181
|
+
logger.error("API key must be provided via PAID_API_KEY environment variable")
|
|
182
|
+
# don't throw - tracing should not break the app
|
|
183
|
+
return
|
|
188
184
|
|
|
189
185
|
set_token(api_key)
|
|
190
186
|
|
|
@@ -210,7 +206,9 @@ def _initialize_tracing(
|
|
|
210
206
|
# Terminate gracefully and don't lose traces
|
|
211
207
|
def flush_traces():
|
|
212
208
|
try:
|
|
213
|
-
if paid_tracer_provider
|
|
209
|
+
if not isinstance(paid_tracer_provider, NoOpTracerProvider) and not paid_tracer_provider.force_flush(
|
|
210
|
+
10000
|
|
211
|
+
):
|
|
214
212
|
logger.error("OTEL force flush : timeout reached")
|
|
215
213
|
except Exception as e:
|
|
216
214
|
logger.error(f"Error flushing traces: {e}")
|
|
@@ -237,9 +235,9 @@ def _initialize_tracing(
|
|
|
237
235
|
signal.signal(sig, create_chained_signal_handler(sig))
|
|
238
236
|
|
|
239
237
|
logger.info("Paid tracing initialized successfully - collector at %s", collector_endpoint)
|
|
240
|
-
except Exception:
|
|
241
|
-
logger.
|
|
242
|
-
|
|
238
|
+
except Exception as e:
|
|
239
|
+
logger.error(f"Failed to initialize Paid tracing: {e}")
|
|
240
|
+
# don't throw - tracing should not break the app
|
|
243
241
|
|
|
244
242
|
|
|
245
243
|
def get_paid_tracer() -> trace.Tracer:
|
|
@@ -251,13 +249,15 @@ def get_paid_tracer() -> trace.Tracer:
|
|
|
251
249
|
|
|
252
250
|
Raises:
|
|
253
251
|
RuntimeError: If the tracer provider is not initialized.
|
|
252
|
+
|
|
253
|
+
Notes:
|
|
254
|
+
Tracing is automatically initialized when using @paid_tracing decorator or context manager.
|
|
254
255
|
"""
|
|
255
|
-
|
|
256
|
-
raise RuntimeError("Paid tracer provider is not initialized. Call Paid.initialize_tracing() first.")
|
|
256
|
+
global paid_tracer_provider
|
|
257
257
|
return paid_tracer_provider.get_tracer("paid.python")
|
|
258
258
|
|
|
259
259
|
|
|
260
|
-
def
|
|
260
|
+
def trace_sync_(
|
|
261
261
|
external_customer_id: str,
|
|
262
262
|
fn: Callable[..., T],
|
|
263
263
|
external_agent_id: Optional[str] = None,
|
|
@@ -267,30 +267,46 @@ def _trace_sync(
|
|
|
267
267
|
args: Optional[Tuple] = None,
|
|
268
268
|
kwargs: Optional[Dict] = None,
|
|
269
269
|
) -> T:
|
|
270
|
+
"""
|
|
271
|
+
Internal function for synchronous tracing. Use @paid_tracing decorator instead.
|
|
272
|
+
|
|
273
|
+
This is a low-level internal function. Users should use the @paid_tracing decorator
|
|
274
|
+
or context manager for a more Pythonic interface.
|
|
275
|
+
|
|
276
|
+
Parameters:
|
|
277
|
+
external_customer_id: The external customer ID to associate with the trace.
|
|
278
|
+
fn: The function to execute and trace.
|
|
279
|
+
external_agent_id: Optional external agent ID.
|
|
280
|
+
tracing_token: Optional token for distributed tracing.
|
|
281
|
+
store_prompt: Whether to store prompt/completion contents.
|
|
282
|
+
metadata: Optional metadata to attach to the trace.
|
|
283
|
+
args: Positional arguments for the function.
|
|
284
|
+
kwargs: Keyword arguments for the function.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
The result of executing fn(*args, **kwargs).
|
|
288
|
+
|
|
289
|
+
Raises:
|
|
290
|
+
Only when user callback raises.
|
|
291
|
+
"""
|
|
270
292
|
args = args or ()
|
|
271
293
|
kwargs = kwargs or {}
|
|
272
|
-
token = get_token()
|
|
273
|
-
if not token:
|
|
274
|
-
raise RuntimeError(
|
|
275
|
-
"No token found - tracing is not initialized and will not be captured. Call Paid.initialize_tracing() first."
|
|
276
|
-
)
|
|
277
294
|
|
|
278
295
|
# Set context variables for access by nested spans
|
|
279
|
-
|
|
296
|
+
reset_customer_id_ctx_token = paid_external_customer_id_var.set(external_customer_id)
|
|
280
297
|
reset_agent_id_ctx_token = paid_external_agent_id_var.set(external_agent_id)
|
|
281
|
-
reset_token_ctx_token = paid_token_var.set(token)
|
|
282
298
|
reset_store_prompt_ctx_token = paid_store_prompt_var.set(store_prompt)
|
|
283
299
|
reset_user_metadata_ctx_token = paid_user_metadata_var.set(metadata)
|
|
284
300
|
|
|
285
301
|
# If user set trace context manually
|
|
286
302
|
override_trace_id = tracing_token
|
|
287
303
|
if not override_trace_id:
|
|
288
|
-
override_trace_id =
|
|
304
|
+
override_trace_id = paid_trace_id_var.get()
|
|
289
305
|
ctx: Optional[Context] = None
|
|
290
306
|
if override_trace_id is not None:
|
|
291
307
|
span_context = SpanContext(
|
|
292
308
|
trace_id=override_trace_id,
|
|
293
|
-
span_id=otel_id_generator.generate_span_id(),
|
|
309
|
+
span_id=distributed_tracing.otel_id_generator.generate_span_id(),
|
|
294
310
|
is_remote=True,
|
|
295
311
|
trace_flags=TraceFlags(TraceFlags.SAMPLED),
|
|
296
312
|
)
|
|
@@ -312,14 +328,13 @@ def _trace_sync(
|
|
|
312
328
|
span.set_status(Status(StatusCode.ERROR, str(error)))
|
|
313
329
|
raise
|
|
314
330
|
finally:
|
|
315
|
-
paid_external_customer_id_var.reset(
|
|
331
|
+
paid_external_customer_id_var.reset(reset_customer_id_ctx_token)
|
|
316
332
|
paid_external_agent_id_var.reset(reset_agent_id_ctx_token)
|
|
317
|
-
paid_token_var.reset(reset_token_ctx_token)
|
|
318
333
|
paid_store_prompt_var.reset(reset_store_prompt_ctx_token)
|
|
319
334
|
paid_user_metadata_var.reset(reset_user_metadata_ctx_token)
|
|
320
335
|
|
|
321
336
|
|
|
322
|
-
async def
|
|
337
|
+
async def trace_async_(
|
|
323
338
|
external_customer_id: str,
|
|
324
339
|
fn: Callable[..., Union[T, Awaitable[T]]],
|
|
325
340
|
external_agent_id: Optional[str] = None,
|
|
@@ -329,30 +344,46 @@ async def _trace_async(
|
|
|
329
344
|
args: Optional[Tuple] = None,
|
|
330
345
|
kwargs: Optional[Dict] = None,
|
|
331
346
|
) -> Union[T, Awaitable[T]]:
|
|
347
|
+
"""
|
|
348
|
+
Internal function for asynchronous tracing. Use @paid_tracing decorator instead.
|
|
349
|
+
|
|
350
|
+
This is a low-level internal function. Users should use the @paid_tracing decorator
|
|
351
|
+
or context manager for a more Pythonic interface.
|
|
352
|
+
|
|
353
|
+
Parameters:
|
|
354
|
+
external_customer_id: The external customer ID to associate with the trace.
|
|
355
|
+
fn: The async function to execute and trace.
|
|
356
|
+
external_agent_id: Optional external agent ID.
|
|
357
|
+
tracing_token: Optional token for distributed tracing.
|
|
358
|
+
store_prompt: Whether to store prompt/completion contents.
|
|
359
|
+
metadata: Optional metadata to attach to the trace.
|
|
360
|
+
args: Positional arguments for the function.
|
|
361
|
+
kwargs: Keyword arguments for the function.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
The result of executing fn(*args, **kwargs).
|
|
365
|
+
|
|
366
|
+
Raises:
|
|
367
|
+
Only when user callback raises.
|
|
368
|
+
"""
|
|
332
369
|
args = args or ()
|
|
333
370
|
kwargs = kwargs or {}
|
|
334
|
-
token = get_token()
|
|
335
|
-
if not token:
|
|
336
|
-
raise RuntimeError(
|
|
337
|
-
"No token found - tracing is not initialized and will not be captured. Call Paid.initialize_tracing() first."
|
|
338
|
-
)
|
|
339
371
|
|
|
340
372
|
# Set context variables for access by nested spans
|
|
341
|
-
|
|
373
|
+
reset_customer_id_ctx_token = paid_external_customer_id_var.set(external_customer_id)
|
|
342
374
|
reset_agent_id_ctx_token = paid_external_agent_id_var.set(external_agent_id)
|
|
343
|
-
reset_token_ctx_token = paid_token_var.set(token)
|
|
344
375
|
reset_store_prompt_ctx_token = paid_store_prompt_var.set(store_prompt)
|
|
345
376
|
reset_user_metadata_ctx_token = paid_user_metadata_var.set(metadata)
|
|
346
377
|
|
|
347
378
|
# If user set trace context manually
|
|
348
379
|
override_trace_id = tracing_token
|
|
349
380
|
if not override_trace_id:
|
|
350
|
-
override_trace_id =
|
|
381
|
+
override_trace_id = paid_trace_id_var.get()
|
|
351
382
|
ctx: Optional[Context] = None
|
|
352
383
|
if override_trace_id is not None:
|
|
353
384
|
span_context = SpanContext(
|
|
354
385
|
trace_id=override_trace_id,
|
|
355
|
-
span_id=otel_id_generator.generate_span_id(),
|
|
386
|
+
span_id=distributed_tracing.otel_id_generator.generate_span_id(),
|
|
356
387
|
is_remote=True,
|
|
357
388
|
trace_flags=TraceFlags(TraceFlags.SAMPLED),
|
|
358
389
|
)
|
|
@@ -377,374 +408,7 @@ async def _trace_async(
|
|
|
377
408
|
span.set_status(Status(StatusCode.ERROR, str(error)))
|
|
378
409
|
raise
|
|
379
410
|
finally:
|
|
380
|
-
paid_external_customer_id_var.reset(
|
|
411
|
+
paid_external_customer_id_var.reset(reset_customer_id_ctx_token)
|
|
381
412
|
paid_external_agent_id_var.reset(reset_agent_id_ctx_token)
|
|
382
|
-
paid_token_var.reset(reset_token_ctx_token)
|
|
383
413
|
paid_store_prompt_var.reset(reset_store_prompt_ctx_token)
|
|
384
414
|
paid_user_metadata_var.reset(reset_user_metadata_ctx_token)
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
def generate_tracing_token() -> int:
|
|
388
|
-
"""
|
|
389
|
-
This will generate and return a tracing token but it will not set it
|
|
390
|
-
for the tracing context. Needed when you only want to store or send a tracing token
|
|
391
|
-
somewhere else.
|
|
392
|
-
"""
|
|
393
|
-
return otel_id_generator.generate_trace_id()
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
def generate_and_set_tracing_token() -> int:
|
|
397
|
-
"""
|
|
398
|
-
*Advanced feature*
|
|
399
|
-
In cases when you can't share the same Paid.trace() or @paid_tracing() context with
|
|
400
|
-
code that you want to track together (complex concurrency logic,
|
|
401
|
-
or disjoint workflows, or work is separated between processes),
|
|
402
|
-
then you can manually generate a tracing token with generate_and_set_tracing_token()
|
|
403
|
-
and share it with the other parts of your application or service using set_tracing_token().
|
|
404
|
-
|
|
405
|
-
This function returns tracing token and attaches it to all consequent
|
|
406
|
-
Paid.trace() or @paid_tracing() tracing contexts. So all the costs and signals that share this
|
|
407
|
-
tracing context are associated with each other.
|
|
408
|
-
|
|
409
|
-
To stop associating the traces one can either call
|
|
410
|
-
generate_and_set_tracing_token() once again or call unset_tracing_token().
|
|
411
|
-
The former is suitable if you still want to trace but in a fresh
|
|
412
|
-
context, and the latter will go back to unique traces per Paid.trace() or @paid_tracing().
|
|
413
|
-
|
|
414
|
-
Returns:
|
|
415
|
-
int: The tracing token (OpenTelemetry trace ID)
|
|
416
|
-
|
|
417
|
-
Example:
|
|
418
|
-
>>> from paid.tracing import generate_and_set_tracing_token, set_tracing_token, unset_tracing_token
|
|
419
|
-
>>> # Process 1: Generate token
|
|
420
|
-
>>> token = generate_and_set_tracing_token()
|
|
421
|
-
>>> save_to_redis("workflow_123", token)
|
|
422
|
-
>>>
|
|
423
|
-
>>> # Process 2: Use token
|
|
424
|
-
>>> token = load_from_redis("workflow_123")
|
|
425
|
-
>>> set_tracing_token(token)
|
|
426
|
-
>>> # ... do traced work ...
|
|
427
|
-
>>> unset_tracing_token()
|
|
428
|
-
"""
|
|
429
|
-
random_trace_id = otel_id_generator.generate_trace_id()
|
|
430
|
-
_ = paid_trace_id.set(random_trace_id)
|
|
431
|
-
return random_trace_id
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
def set_tracing_token(token: int):
|
|
435
|
-
"""
|
|
436
|
-
*Advanced feature*
|
|
437
|
-
In cases when you can't share the same Paid.trace() or @paid_tracing() context with
|
|
438
|
-
code that you want to track together (complex concurrency logic,
|
|
439
|
-
or disjoint workflows, or work is separated between processes),
|
|
440
|
-
then you can manually generate a tracing token with generate_and_set_tracing_token()
|
|
441
|
-
and share it with the other parts of your application or service using set_tracing_token().
|
|
442
|
-
|
|
443
|
-
Sets tracing token. Provided token should come from generate_and_set_tracing_token().
|
|
444
|
-
Once set, the consequent traces will be related to each other.
|
|
445
|
-
|
|
446
|
-
Args:
|
|
447
|
-
token (int): A tracing token from generate_and_set_tracing_token()
|
|
448
|
-
|
|
449
|
-
Example:
|
|
450
|
-
>>> from paid.tracing import set_tracing_token, unset_tracing_token, paid_tracing
|
|
451
|
-
>>> # Retrieve token from storage
|
|
452
|
-
>>> token = get_from_redis("workflow_123")
|
|
453
|
-
>>> set_tracing_token(token)
|
|
454
|
-
>>>
|
|
455
|
-
>>> @paid_tracing("customer_123", "agent_123")
|
|
456
|
-
>>> def process_workflow():
|
|
457
|
-
... # This trace will be linked to the token
|
|
458
|
-
... pass
|
|
459
|
-
>>>
|
|
460
|
-
>>> process_workflow()
|
|
461
|
-
>>> unset_tracing_token()
|
|
462
|
-
"""
|
|
463
|
-
_ = paid_trace_id.set(token)
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
def unset_tracing_token():
|
|
467
|
-
"""
|
|
468
|
-
Unsets the token previously set by generate_and_set_tracing_token()
|
|
469
|
-
or by set_tracing_token(token). Does nothing if the token was never set.
|
|
470
|
-
When tracing token is unset, traces are unique for a single Paid.trace() or @paid_tracing() context.
|
|
471
|
-
|
|
472
|
-
Example:
|
|
473
|
-
>>> from paid.tracing import set_tracing_token, unset_tracing_token
|
|
474
|
-
>>> set_tracing_token(stored_token)
|
|
475
|
-
>>> try:
|
|
476
|
-
... process_workflow()
|
|
477
|
-
... finally:
|
|
478
|
-
... unset_tracing_token() # Always clean up
|
|
479
|
-
"""
|
|
480
|
-
_ = paid_trace_id.set(None)
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
class paid_tracing:
|
|
484
|
-
"""
|
|
485
|
-
Decorator and context manager for tracing with Paid.
|
|
486
|
-
|
|
487
|
-
This class can be used both as a decorator and as a context manager (with/async with),
|
|
488
|
-
providing flexible tracing capabilities for both functions and code blocks.
|
|
489
|
-
|
|
490
|
-
Parameters
|
|
491
|
-
----------
|
|
492
|
-
external_customer_id : str
|
|
493
|
-
The external customer ID to associate with the trace.
|
|
494
|
-
external_agent_id : Optional[str], optional
|
|
495
|
-
The external agent ID to associate with the trace, by default None.
|
|
496
|
-
tracing_token : Optional[int], optional
|
|
497
|
-
Optional tracing token for distributed tracing, by default None.
|
|
498
|
-
store_prompt : bool, optional
|
|
499
|
-
Whether to store prompt contents in span attributes, by default False.
|
|
500
|
-
collector_endpoint: Optional[str], optional
|
|
501
|
-
OTEL collector HTTP endpoint, by default "https://collector.agentpaid.io:4318/v1/traces".
|
|
502
|
-
metadata : Optional[Dict[str, Any]], optional
|
|
503
|
-
Optional metadata to attach to the trace, by default None.
|
|
504
|
-
|
|
505
|
-
Examples
|
|
506
|
-
--------
|
|
507
|
-
As a decorator (sync):
|
|
508
|
-
>>> @paid_tracing(external_customer_id="customer123", external_agent_id="agent456")
|
|
509
|
-
... def my_function(arg1, arg2):
|
|
510
|
-
... return arg1 + arg2
|
|
511
|
-
|
|
512
|
-
As a decorator (async):
|
|
513
|
-
>>> @paid_tracing(external_customer_id="customer123")
|
|
514
|
-
... async def my_async_function(arg1, arg2):
|
|
515
|
-
... return arg1 + arg2
|
|
516
|
-
|
|
517
|
-
As a context manager (sync):
|
|
518
|
-
>>> with paid_tracing(external_customer_id="customer123", external_agent_id="agent456"):
|
|
519
|
-
... result = expensive_computation()
|
|
520
|
-
|
|
521
|
-
As a context manager (async):
|
|
522
|
-
>>> async with paid_tracing(external_customer_id="customer123"):
|
|
523
|
-
... result = await async_operation()
|
|
524
|
-
|
|
525
|
-
Notes
|
|
526
|
-
-----
|
|
527
|
-
If tracing is not already initialized, the decorator will automatically
|
|
528
|
-
initialize it using the PAID_API_KEY environment variable.
|
|
529
|
-
"""
|
|
530
|
-
|
|
531
|
-
def __init__(
|
|
532
|
-
self,
|
|
533
|
-
external_customer_id: str,
|
|
534
|
-
*,
|
|
535
|
-
external_agent_id: Optional[str] = None,
|
|
536
|
-
tracing_token: Optional[int] = None,
|
|
537
|
-
store_prompt: bool = False,
|
|
538
|
-
collector_endpoint: Optional[str] = "https://collector.agentpaid.io:4318/v1/traces",
|
|
539
|
-
metadata: Optional[Dict[str, Any]] = None,
|
|
540
|
-
):
|
|
541
|
-
self.external_customer_id = external_customer_id
|
|
542
|
-
self.external_agent_id = external_agent_id
|
|
543
|
-
self.tracing_token = tracing_token
|
|
544
|
-
self.store_prompt = store_prompt
|
|
545
|
-
self.collector_endpoint = collector_endpoint
|
|
546
|
-
self.metadata = metadata
|
|
547
|
-
self._span: Any = None
|
|
548
|
-
self._reset_tokens: Optional[
|
|
549
|
-
Tuple[
|
|
550
|
-
contextvars.Token[Optional[str]],
|
|
551
|
-
contextvars.Token[Optional[str]],
|
|
552
|
-
contextvars.Token[Optional[str]],
|
|
553
|
-
contextvars.Token[Optional[bool]],
|
|
554
|
-
contextvars.Token[Optional[Dict[str, Any]]],
|
|
555
|
-
]
|
|
556
|
-
] = None
|
|
557
|
-
|
|
558
|
-
def _setup_context(self) -> Optional[Context]:
|
|
559
|
-
"""Set up context variables and return OTEL context if needed."""
|
|
560
|
-
token = get_token()
|
|
561
|
-
if not token:
|
|
562
|
-
raise RuntimeError("No token found - tracing is not initialized. Call Paid.initialize_tracing() first.")
|
|
563
|
-
|
|
564
|
-
# Set context variables
|
|
565
|
-
reset_id_ctx_token = paid_external_customer_id_var.set(self.external_customer_id)
|
|
566
|
-
reset_agent_id_ctx_token = paid_external_agent_id_var.set(self.external_agent_id)
|
|
567
|
-
reset_token_ctx_token = paid_token_var.set(token)
|
|
568
|
-
reset_store_prompt_ctx_token = paid_store_prompt_var.set(self.store_prompt)
|
|
569
|
-
reset_user_metadata_ctx_token = paid_user_metadata_var.set(self.metadata)
|
|
570
|
-
|
|
571
|
-
# Store reset tokens for cleanup
|
|
572
|
-
self._reset_tokens = (
|
|
573
|
-
reset_id_ctx_token,
|
|
574
|
-
reset_agent_id_ctx_token,
|
|
575
|
-
reset_token_ctx_token,
|
|
576
|
-
reset_store_prompt_ctx_token,
|
|
577
|
-
reset_user_metadata_ctx_token,
|
|
578
|
-
)
|
|
579
|
-
|
|
580
|
-
# Handle distributed tracing token
|
|
581
|
-
override_trace_id = self.tracing_token
|
|
582
|
-
if not override_trace_id:
|
|
583
|
-
override_trace_id = paid_trace_id.get()
|
|
584
|
-
|
|
585
|
-
ctx: Optional[Context] = None
|
|
586
|
-
if override_trace_id is not None:
|
|
587
|
-
span_context = SpanContext(
|
|
588
|
-
trace_id=override_trace_id,
|
|
589
|
-
span_id=otel_id_generator.generate_span_id(),
|
|
590
|
-
is_remote=True,
|
|
591
|
-
trace_flags=TraceFlags(TraceFlags.SAMPLED),
|
|
592
|
-
)
|
|
593
|
-
ctx = trace.set_span_in_context(NonRecordingSpan(span_context))
|
|
594
|
-
|
|
595
|
-
return ctx
|
|
596
|
-
|
|
597
|
-
def _cleanup_context(self):
|
|
598
|
-
"""Reset all context variables."""
|
|
599
|
-
if self._reset_tokens:
|
|
600
|
-
(
|
|
601
|
-
reset_id_ctx_token,
|
|
602
|
-
reset_agent_id_ctx_token,
|
|
603
|
-
reset_token_ctx_token,
|
|
604
|
-
reset_store_prompt_ctx_token,
|
|
605
|
-
reset_user_metadata_ctx_token,
|
|
606
|
-
) = self._reset_tokens
|
|
607
|
-
paid_external_customer_id_var.reset(reset_id_ctx_token)
|
|
608
|
-
paid_external_agent_id_var.reset(reset_agent_id_ctx_token)
|
|
609
|
-
paid_token_var.reset(reset_token_ctx_token)
|
|
610
|
-
paid_store_prompt_var.reset(reset_store_prompt_ctx_token)
|
|
611
|
-
paid_user_metadata_var.reset(reset_user_metadata_ctx_token)
|
|
612
|
-
self._reset_tokens = None
|
|
613
|
-
|
|
614
|
-
# Context manager methods for sync
|
|
615
|
-
def __enter__(self):
|
|
616
|
-
"""Enter synchronous context."""
|
|
617
|
-
ctx = self._setup_context()
|
|
618
|
-
|
|
619
|
-
tracer = get_paid_tracer()
|
|
620
|
-
logger.info(f"Creating span for external_customer_id: {self.external_customer_id}")
|
|
621
|
-
self._span = tracer.start_as_current_span("parent_span", context=ctx)
|
|
622
|
-
span = self._span.__enter__()
|
|
623
|
-
|
|
624
|
-
span.set_attribute("external_customer_id", self.external_customer_id)
|
|
625
|
-
if self.external_agent_id:
|
|
626
|
-
span.set_attribute("external_agent_id", self.external_agent_id)
|
|
627
|
-
|
|
628
|
-
return self
|
|
629
|
-
|
|
630
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
631
|
-
"""Exit synchronous context."""
|
|
632
|
-
try:
|
|
633
|
-
if self._span:
|
|
634
|
-
if exc_type is not None:
|
|
635
|
-
# Get the actual span object to set status
|
|
636
|
-
span_obj = trace.get_current_span()
|
|
637
|
-
if span_obj:
|
|
638
|
-
span_obj.set_status(Status(StatusCode.ERROR, str(exc_val)))
|
|
639
|
-
else:
|
|
640
|
-
span_obj = trace.get_current_span()
|
|
641
|
-
if span_obj:
|
|
642
|
-
span_obj.set_status(Status(StatusCode.OK))
|
|
643
|
-
logger.info("Context block executed successfully")
|
|
644
|
-
|
|
645
|
-
self._span.__exit__(exc_type, exc_val, exc_tb)
|
|
646
|
-
self._span = None
|
|
647
|
-
finally:
|
|
648
|
-
self._cleanup_context()
|
|
649
|
-
|
|
650
|
-
return False # Don't suppress exceptions
|
|
651
|
-
|
|
652
|
-
# Context manager methods for async
|
|
653
|
-
async def __aenter__(self):
|
|
654
|
-
"""Enter asynchronous context."""
|
|
655
|
-
ctx = self._setup_context()
|
|
656
|
-
|
|
657
|
-
tracer = get_paid_tracer()
|
|
658
|
-
logger.info(f"Creating span for external_customer_id: {self.external_customer_id}")
|
|
659
|
-
self._span = tracer.start_as_current_span("parent_span", context=ctx)
|
|
660
|
-
span = self._span.__enter__()
|
|
661
|
-
|
|
662
|
-
span.set_attribute("external_customer_id", self.external_customer_id)
|
|
663
|
-
if self.external_agent_id:
|
|
664
|
-
span.set_attribute("external_agent_id", self.external_agent_id)
|
|
665
|
-
|
|
666
|
-
return self
|
|
667
|
-
|
|
668
|
-
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
669
|
-
"""Exit asynchronous context."""
|
|
670
|
-
try:
|
|
671
|
-
if self._span:
|
|
672
|
-
if exc_type is not None:
|
|
673
|
-
# Get the actual span object to set status
|
|
674
|
-
span_obj = trace.get_current_span()
|
|
675
|
-
if span_obj:
|
|
676
|
-
span_obj.set_status(Status(StatusCode.ERROR, str(exc_val)))
|
|
677
|
-
else:
|
|
678
|
-
span_obj = trace.get_current_span()
|
|
679
|
-
if span_obj:
|
|
680
|
-
span_obj.set_status(Status(StatusCode.OK))
|
|
681
|
-
logger.info("Async context block executed successfully")
|
|
682
|
-
|
|
683
|
-
self._span.__exit__(exc_type, exc_val, exc_tb)
|
|
684
|
-
self._span = None
|
|
685
|
-
finally:
|
|
686
|
-
self._cleanup_context()
|
|
687
|
-
|
|
688
|
-
return False # Don't suppress exceptions
|
|
689
|
-
|
|
690
|
-
# Decorator functionality
|
|
691
|
-
def __call__(self, func: Callable) -> Callable:
|
|
692
|
-
"""Use as a decorator."""
|
|
693
|
-
if asyncio.iscoroutinefunction(func):
|
|
694
|
-
|
|
695
|
-
@functools.wraps(func)
|
|
696
|
-
async def async_wrapper(*args, **kwargs):
|
|
697
|
-
# Auto-initialize tracing if not done
|
|
698
|
-
if get_token() is None:
|
|
699
|
-
try:
|
|
700
|
-
_initialize_tracing(None, self.collector_endpoint)
|
|
701
|
-
except Exception as e:
|
|
702
|
-
logger.error(f"Failed to auto-initialize tracing: {e}")
|
|
703
|
-
# Fall back to executing function without tracing
|
|
704
|
-
return await func(*args, **kwargs)
|
|
705
|
-
|
|
706
|
-
try:
|
|
707
|
-
return await _trace_async(
|
|
708
|
-
external_customer_id=self.external_customer_id,
|
|
709
|
-
fn=func,
|
|
710
|
-
external_agent_id=self.external_agent_id,
|
|
711
|
-
tracing_token=self.tracing_token,
|
|
712
|
-
store_prompt=self.store_prompt,
|
|
713
|
-
metadata=self.metadata,
|
|
714
|
-
args=args,
|
|
715
|
-
kwargs=kwargs,
|
|
716
|
-
)
|
|
717
|
-
except Exception as e:
|
|
718
|
-
logger.error(f"Failed to trace async function {func.__name__}: {e}")
|
|
719
|
-
raise e
|
|
720
|
-
|
|
721
|
-
return async_wrapper
|
|
722
|
-
else:
|
|
723
|
-
|
|
724
|
-
@functools.wraps(func)
|
|
725
|
-
def sync_wrapper(*args, **kwargs):
|
|
726
|
-
# Auto-initialize tracing if not done
|
|
727
|
-
if get_token() is None:
|
|
728
|
-
try:
|
|
729
|
-
_initialize_tracing(None, self.collector_endpoint)
|
|
730
|
-
except Exception as e:
|
|
731
|
-
logger.error(f"Failed to auto-initialize tracing: {e}")
|
|
732
|
-
# Fall back to executing function without tracing
|
|
733
|
-
return func(*args, **kwargs)
|
|
734
|
-
|
|
735
|
-
try:
|
|
736
|
-
return _trace_sync(
|
|
737
|
-
external_customer_id=self.external_customer_id,
|
|
738
|
-
fn=func,
|
|
739
|
-
external_agent_id=self.external_agent_id,
|
|
740
|
-
tracing_token=self.tracing_token,
|
|
741
|
-
store_prompt=self.store_prompt,
|
|
742
|
-
metadata=self.metadata,
|
|
743
|
-
args=args,
|
|
744
|
-
kwargs=kwargs,
|
|
745
|
-
)
|
|
746
|
-
except Exception as e:
|
|
747
|
-
logger.error(f"Failed to trace sync function {func.__name__}: {e}")
|
|
748
|
-
raise e
|
|
749
|
-
|
|
750
|
-
return sync_wrapper
|