paid-python 0.0.5a40__py3-none-any.whl → 0.1.1__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/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.sdk.trace.id_generator import RandomIdGenerator
19
- from opentelemetry.trace import NonRecordingSpan, SpanContext, Status, StatusCode, TraceFlags
20
-
21
- # Configure logging
22
- dotenv.load_dotenv()
23
- log_level_name = os.environ.get("PAID_LOG_LEVEL")
24
- if log_level_name is not None:
25
- log_level = getattr(logging, log_level_name.upper())
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
- paid_trace_id: contextvars.ContextVar[Optional[int]] = contextvars.ContextVar("paid_trace_id", default=None)
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
- _token: Optional[str] = None
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
- global _token
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
- global _token
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
- paid_tracer_provider: Optional[TracerProvider] = None
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 _initialize_tracing(
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 _token is not None:
176
- raise RuntimeError("Tracing is already initialized.")
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
- raise ValueError(
186
- "API key must be provided either as parameter or via PAID_API_KEY environment variable"
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 is not None and not paid_tracer_provider.force_flush(10000):
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.exception("Failed to initialize Paid tracing")
242
- raise
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,14 +249,16 @@ 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
- if paid_tracer_provider is None:
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 _trace_sync(
261
- external_customer_id: str,
260
+ def trace_sync_(
261
+ external_customer_id: Optional[str],
262
262
  fn: Callable[..., T],
263
263
  external_agent_id: Optional[str] = None,
264
264
  tracing_token: Optional[int] = 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
- reset_id_ctx_token = paid_external_customer_id_var.set(external_customer_id)
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 = paid_trace_id.get()
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
  )
@@ -300,9 +316,6 @@ def _trace_sync(
300
316
  tracer = get_paid_tracer()
301
317
  logger.info(f"Creating span for external_customer_id: {external_customer_id}")
302
318
  with tracer.start_as_current_span("parent_span", context=ctx) as span:
303
- span.set_attribute("external_customer_id", external_customer_id)
304
- if external_agent_id:
305
- span.set_attribute("external_agent_id", external_agent_id)
306
319
  try:
307
320
  result = fn(*args, **kwargs)
308
321
  span.set_status(Status(StatusCode.OK))
@@ -312,15 +325,14 @@ def _trace_sync(
312
325
  span.set_status(Status(StatusCode.ERROR, str(error)))
313
326
  raise
314
327
  finally:
315
- paid_external_customer_id_var.reset(reset_id_ctx_token)
328
+ paid_external_customer_id_var.reset(reset_customer_id_ctx_token)
316
329
  paid_external_agent_id_var.reset(reset_agent_id_ctx_token)
317
- paid_token_var.reset(reset_token_ctx_token)
318
330
  paid_store_prompt_var.reset(reset_store_prompt_ctx_token)
319
331
  paid_user_metadata_var.reset(reset_user_metadata_ctx_token)
320
332
 
321
333
 
322
- async def _trace_async(
323
- external_customer_id: str,
334
+ async def trace_async_(
335
+ external_customer_id: Optional[str],
324
336
  fn: Callable[..., Union[T, Awaitable[T]]],
325
337
  external_agent_id: Optional[str] = None,
326
338
  tracing_token: Optional[int] = None,
@@ -329,30 +341,46 @@ async def _trace_async(
329
341
  args: Optional[Tuple] = None,
330
342
  kwargs: Optional[Dict] = None,
331
343
  ) -> Union[T, Awaitable[T]]:
344
+ """
345
+ Internal function for asynchronous tracing. Use @paid_tracing decorator instead.
346
+
347
+ This is a low-level internal function. Users should use the @paid_tracing decorator
348
+ or context manager for a more Pythonic interface.
349
+
350
+ Parameters:
351
+ external_customer_id: The external customer ID to associate with the trace.
352
+ fn: The async function to execute and trace.
353
+ external_agent_id: Optional external agent ID.
354
+ tracing_token: Optional token for distributed tracing.
355
+ store_prompt: Whether to store prompt/completion contents.
356
+ metadata: Optional metadata to attach to the trace.
357
+ args: Positional arguments for the function.
358
+ kwargs: Keyword arguments for the function.
359
+
360
+ Returns:
361
+ The result of executing fn(*args, **kwargs).
362
+
363
+ Raises:
364
+ Only when user callback raises.
365
+ """
332
366
  args = args or ()
333
367
  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
368
 
340
369
  # Set context variables for access by nested spans
341
- reset_id_ctx_token = paid_external_customer_id_var.set(external_customer_id)
370
+ reset_customer_id_ctx_token = paid_external_customer_id_var.set(external_customer_id)
342
371
  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
372
  reset_store_prompt_ctx_token = paid_store_prompt_var.set(store_prompt)
345
373
  reset_user_metadata_ctx_token = paid_user_metadata_var.set(metadata)
346
374
 
347
375
  # If user set trace context manually
348
376
  override_trace_id = tracing_token
349
377
  if not override_trace_id:
350
- override_trace_id = paid_trace_id.get()
378
+ override_trace_id = paid_trace_id_var.get()
351
379
  ctx: Optional[Context] = None
352
380
  if override_trace_id is not None:
353
381
  span_context = SpanContext(
354
382
  trace_id=override_trace_id,
355
- span_id=otel_id_generator.generate_span_id(),
383
+ span_id=distributed_tracing.otel_id_generator.generate_span_id(),
356
384
  is_remote=True,
357
385
  trace_flags=TraceFlags(TraceFlags.SAMPLED),
358
386
  )
@@ -362,9 +390,6 @@ async def _trace_async(
362
390
  tracer = get_paid_tracer()
363
391
  logger.info(f"Creating span for external_customer_id: {external_customer_id}")
364
392
  with tracer.start_as_current_span("parent_span", context=ctx) as span:
365
- span.set_attribute("external_customer_id", external_customer_id)
366
- if external_agent_id:
367
- span.set_attribute("external_agent_id", external_agent_id)
368
393
  try:
369
394
  if asyncio.iscoroutinefunction(fn):
370
395
  result = await fn(*args, **kwargs)
@@ -377,374 +402,7 @@ async def _trace_async(
377
402
  span.set_status(Status(StatusCode.ERROR, str(error)))
378
403
  raise
379
404
  finally:
380
- paid_external_customer_id_var.reset(reset_id_ctx_token)
405
+ paid_external_customer_id_var.reset(reset_customer_id_ctx_token)
381
406
  paid_external_agent_id_var.reset(reset_agent_id_ctx_token)
382
- paid_token_var.reset(reset_token_ctx_token)
383
407
  paid_store_prompt_var.reset(reset_store_prompt_ctx_token)
384
408
  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