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/logger.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This file exports `logger` object for unified logging across the Paid SDK.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
import dotenv
|
|
9
|
+
|
|
10
|
+
# Configure logging
|
|
11
|
+
_ = dotenv.load_dotenv()
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
# Set default log level to ERROR, allow override via PAID_LOG_LEVEL environment variable
|
|
15
|
+
log_level_name = os.environ.get("PAID_LOG_LEVEL")
|
|
16
|
+
if log_level_name is not None:
|
|
17
|
+
log_level = getattr(logging, log_level_name.upper(), logging.ERROR)
|
|
18
|
+
else:
|
|
19
|
+
log_level = logging.ERROR
|
|
20
|
+
|
|
21
|
+
logger.setLevel(log_level)
|
paid/tracing/__init__.py
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# Tracing module for OpenTelemetry integration
|
|
2
2
|
from .autoinstrumentation import paid_autoinstrument
|
|
3
|
-
from .
|
|
4
|
-
|
|
3
|
+
from .context_manager import paid_tracing
|
|
4
|
+
from .distributed_tracing import (
|
|
5
5
|
generate_tracing_token,
|
|
6
|
-
paid_tracing,
|
|
7
6
|
set_tracing_token,
|
|
8
7
|
unset_tracing_token,
|
|
9
8
|
)
|
|
9
|
+
from .signal import signal
|
|
10
10
|
|
|
11
11
|
__all__ = [
|
|
12
|
-
"generate_and_set_tracing_token",
|
|
13
12
|
"generate_tracing_token",
|
|
14
13
|
"paid_autoinstrument",
|
|
15
14
|
"paid_tracing",
|
|
16
15
|
"set_tracing_token",
|
|
17
16
|
"unset_tracing_token",
|
|
17
|
+
"signal",
|
|
18
18
|
]
|
|
@@ -8,7 +8,10 @@ sending traces to the Paid collector endpoint.
|
|
|
8
8
|
from typing import List, Optional
|
|
9
9
|
|
|
10
10
|
from . import tracing
|
|
11
|
-
from .tracing import
|
|
11
|
+
from .tracing import initialize_tracing_
|
|
12
|
+
from opentelemetry.trace import NoOpTracerProvider
|
|
13
|
+
|
|
14
|
+
from paid.logger import logger
|
|
12
15
|
|
|
13
16
|
# Safe imports for instrumentation libraries
|
|
14
17
|
try:
|
|
@@ -89,9 +92,9 @@ def paid_autoinstrument(libraries: Optional[List[str]] = None) -> None:
|
|
|
89
92
|
>>> anthropic_client.messages.create(...)
|
|
90
93
|
"""
|
|
91
94
|
# Initialize tracing if not already initialized
|
|
92
|
-
if
|
|
95
|
+
if isinstance(tracing.paid_tracer_provider, NoOpTracerProvider):
|
|
93
96
|
logger.info("Tracing not initialized, initializing automatically")
|
|
94
|
-
|
|
97
|
+
initialize_tracing_()
|
|
95
98
|
|
|
96
99
|
# Default to all supported libraries if none specified
|
|
97
100
|
if libraries is None:
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextvars
|
|
3
|
+
import functools
|
|
4
|
+
from typing import Any, Callable, Dict, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from . import distributed_tracing, tracing
|
|
7
|
+
from .tracing import get_paid_tracer, get_token, initialize_tracing_, trace_async_, trace_sync_
|
|
8
|
+
from opentelemetry import trace
|
|
9
|
+
from opentelemetry.context import Context
|
|
10
|
+
from opentelemetry.trace import NonRecordingSpan, Span, SpanContext, Status, StatusCode, TraceFlags
|
|
11
|
+
|
|
12
|
+
from paid.logger import logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class paid_tracing:
|
|
16
|
+
"""
|
|
17
|
+
Decorator and context manager for tracing with Paid.
|
|
18
|
+
|
|
19
|
+
This class can be used both as a decorator and as a context manager (with/async with),
|
|
20
|
+
providing flexible tracing capabilities for both functions and code blocks.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
external_customer_id : str
|
|
25
|
+
The external customer ID to associate with the trace.
|
|
26
|
+
external_agent_id : Optional[str], optional
|
|
27
|
+
The external agent ID to associate with the trace, by default None.
|
|
28
|
+
tracing_token : Optional[int], optional
|
|
29
|
+
Optional tracing token for distributed tracing, by default None.
|
|
30
|
+
store_prompt : bool, optional
|
|
31
|
+
Whether to store prompt contents in span attributes, by default False.
|
|
32
|
+
collector_endpoint: Optional[str], optional
|
|
33
|
+
OTEL collector HTTP endpoint, by default "https://collector.agentpaid.io:4318/v1/traces".
|
|
34
|
+
metadata : Optional[Dict[str, Any]], optional
|
|
35
|
+
Optional metadata to attach to the trace, by default None.
|
|
36
|
+
|
|
37
|
+
Examples
|
|
38
|
+
--------
|
|
39
|
+
As a decorator (sync):
|
|
40
|
+
>>> @paid_tracing(external_customer_id="customer123", external_agent_id="agent456")
|
|
41
|
+
... def my_function(arg1, arg2):
|
|
42
|
+
... return arg1 + arg2
|
|
43
|
+
|
|
44
|
+
As a decorator (async):
|
|
45
|
+
>>> @paid_tracing(external_customer_id="customer123")
|
|
46
|
+
... async def my_async_function(arg1, arg2):
|
|
47
|
+
... return arg1 + arg2
|
|
48
|
+
|
|
49
|
+
As a context manager (sync):
|
|
50
|
+
>>> with paid_tracing(external_customer_id="customer123", external_agent_id="agent456"):
|
|
51
|
+
... result = expensive_computation()
|
|
52
|
+
|
|
53
|
+
As a context manager (async):
|
|
54
|
+
>>> async with paid_tracing(external_customer_id="customer123"):
|
|
55
|
+
... result = await async_operation()
|
|
56
|
+
|
|
57
|
+
Notes
|
|
58
|
+
-----
|
|
59
|
+
If tracing is not already initialized, the decorator will automatically
|
|
60
|
+
initialize it using the PAID_API_KEY environment variable.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
external_customer_id: str,
|
|
66
|
+
*,
|
|
67
|
+
external_agent_id: Optional[str] = None,
|
|
68
|
+
tracing_token: Optional[int] = None,
|
|
69
|
+
store_prompt: bool = False,
|
|
70
|
+
collector_endpoint: Optional[str] = tracing.DEFAULT_COLLECTOR_ENDPOINT,
|
|
71
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
72
|
+
):
|
|
73
|
+
self.external_customer_id = external_customer_id
|
|
74
|
+
self.external_agent_id = external_agent_id
|
|
75
|
+
self.tracing_token = tracing_token
|
|
76
|
+
self.store_prompt = store_prompt
|
|
77
|
+
self.collector_endpoint = collector_endpoint
|
|
78
|
+
self.metadata = metadata
|
|
79
|
+
self.span: Optional[Span] = None
|
|
80
|
+
self.span_ctx: Optional[Any] = None # Context manager for the span
|
|
81
|
+
self.reset_tokens: Optional[
|
|
82
|
+
Tuple[
|
|
83
|
+
contextvars.Token[Optional[str]], # external_customer_id
|
|
84
|
+
contextvars.Token[Optional[str]], # external_agent_id
|
|
85
|
+
contextvars.Token[Optional[bool]], # store_prompt
|
|
86
|
+
contextvars.Token[Optional[Dict[str, Any]]], # metadata
|
|
87
|
+
]
|
|
88
|
+
] = None
|
|
89
|
+
|
|
90
|
+
if not get_token():
|
|
91
|
+
initialize_tracing_(None, self.collector_endpoint)
|
|
92
|
+
|
|
93
|
+
def _setup_context(self) -> Optional[Context]:
|
|
94
|
+
"""Set up context variables and return OTEL context if needed."""
|
|
95
|
+
|
|
96
|
+
# Set context variables
|
|
97
|
+
reset_customer_id_ctx_token = tracing.paid_external_customer_id_var.set(self.external_customer_id)
|
|
98
|
+
reset_agent_id_ctx_token = tracing.paid_external_agent_id_var.set(self.external_agent_id)
|
|
99
|
+
reset_store_prompt_ctx_token = tracing.paid_store_prompt_var.set(self.store_prompt)
|
|
100
|
+
reset_user_metadata_ctx_token = tracing.paid_user_metadata_var.set(self.metadata)
|
|
101
|
+
|
|
102
|
+
# Store reset tokens for cleanup
|
|
103
|
+
self.reset_tokens = (
|
|
104
|
+
reset_customer_id_ctx_token,
|
|
105
|
+
reset_agent_id_ctx_token,
|
|
106
|
+
reset_store_prompt_ctx_token,
|
|
107
|
+
reset_user_metadata_ctx_token,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Handle distributed tracing token
|
|
111
|
+
override_trace_id = self.tracing_token
|
|
112
|
+
if not override_trace_id:
|
|
113
|
+
override_trace_id = tracing.paid_trace_id_var.get()
|
|
114
|
+
|
|
115
|
+
ctx: Optional[Context] = None
|
|
116
|
+
if override_trace_id is not None:
|
|
117
|
+
span_context = SpanContext(
|
|
118
|
+
trace_id=override_trace_id,
|
|
119
|
+
span_id=distributed_tracing.otel_id_generator.generate_span_id(),
|
|
120
|
+
is_remote=True,
|
|
121
|
+
trace_flags=TraceFlags(TraceFlags.SAMPLED),
|
|
122
|
+
)
|
|
123
|
+
ctx = trace.set_span_in_context(NonRecordingSpan(span_context))
|
|
124
|
+
|
|
125
|
+
return ctx
|
|
126
|
+
|
|
127
|
+
def _cleanup_context(self):
|
|
128
|
+
"""Reset all context variables."""
|
|
129
|
+
if self.reset_tokens:
|
|
130
|
+
(
|
|
131
|
+
reset_customer_id_ctx_token,
|
|
132
|
+
reset_agent_id_ctx_token,
|
|
133
|
+
reset_store_prompt_ctx_token,
|
|
134
|
+
reset_user_metadata_ctx_token,
|
|
135
|
+
) = self.reset_tokens
|
|
136
|
+
tracing.paid_external_customer_id_var.reset(reset_customer_id_ctx_token)
|
|
137
|
+
tracing.paid_external_agent_id_var.reset(reset_agent_id_ctx_token)
|
|
138
|
+
tracing.paid_store_prompt_var.reset(reset_store_prompt_ctx_token)
|
|
139
|
+
tracing.paid_user_metadata_var.reset(reset_user_metadata_ctx_token)
|
|
140
|
+
self.reset_tokens = None
|
|
141
|
+
|
|
142
|
+
# Context manager methods for sync
|
|
143
|
+
def __enter__(self):
|
|
144
|
+
return self._enter_ctx()
|
|
145
|
+
|
|
146
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
147
|
+
return self._exit_ctx(exc_type, exc_val, exc_tb)
|
|
148
|
+
|
|
149
|
+
# Context manager methods for async
|
|
150
|
+
async def __aenter__(self):
|
|
151
|
+
return self._enter_ctx()
|
|
152
|
+
|
|
153
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
154
|
+
return self._exit_ctx(exc_type, exc_val, exc_tb)
|
|
155
|
+
|
|
156
|
+
def _enter_ctx(self):
|
|
157
|
+
ctx = self._setup_context()
|
|
158
|
+
tracer = get_paid_tracer()
|
|
159
|
+
logger.info(f"Creating span for external_customer_id: {self.external_customer_id}")
|
|
160
|
+
self.span_ctx = tracer.start_as_current_span("parent_span", context=ctx)
|
|
161
|
+
self.span = self.span_ctx.__enter__()
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
def _exit_ctx(self, exc_type, exc_val, exc_tb):
|
|
165
|
+
"""Exit synchronous context."""
|
|
166
|
+
try:
|
|
167
|
+
if self.span and self.span_ctx:
|
|
168
|
+
if exc_type is not None:
|
|
169
|
+
self.span.set_status(Status(StatusCode.ERROR, str(exc_val)))
|
|
170
|
+
else:
|
|
171
|
+
self.span.set_status(Status(StatusCode.OK))
|
|
172
|
+
logger.info("Context block executed successfully")
|
|
173
|
+
|
|
174
|
+
self.span_ctx.__exit__(exc_type, exc_val, exc_tb)
|
|
175
|
+
self.span_ctx = None
|
|
176
|
+
self.span = None
|
|
177
|
+
|
|
178
|
+
finally:
|
|
179
|
+
self._cleanup_context()
|
|
180
|
+
|
|
181
|
+
return False # Don't suppress exceptions
|
|
182
|
+
|
|
183
|
+
# Decorator functionality
|
|
184
|
+
def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
|
|
185
|
+
"""Use as a decorator."""
|
|
186
|
+
if asyncio.iscoroutinefunction(func):
|
|
187
|
+
|
|
188
|
+
@functools.wraps(func)
|
|
189
|
+
async def async_wrapper(*args, **kwargs):
|
|
190
|
+
# Auto-initialize tracing if not done
|
|
191
|
+
if get_token() is None:
|
|
192
|
+
try:
|
|
193
|
+
initialize_tracing_(None, self.collector_endpoint)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
logger.error(f"Failed to auto-initialize tracing: {e}")
|
|
196
|
+
# Fall back to executing function without tracing
|
|
197
|
+
return await func(*args, **kwargs)
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
return await trace_async_(
|
|
201
|
+
external_customer_id=self.external_customer_id,
|
|
202
|
+
fn=func,
|
|
203
|
+
external_agent_id=self.external_agent_id,
|
|
204
|
+
tracing_token=self.tracing_token,
|
|
205
|
+
store_prompt=self.store_prompt,
|
|
206
|
+
metadata=self.metadata,
|
|
207
|
+
args=args,
|
|
208
|
+
kwargs=kwargs,
|
|
209
|
+
)
|
|
210
|
+
except Exception as e:
|
|
211
|
+
logger.error(f"Failed to trace async function {func.__name__}: {e}")
|
|
212
|
+
raise e
|
|
213
|
+
|
|
214
|
+
return async_wrapper
|
|
215
|
+
else:
|
|
216
|
+
|
|
217
|
+
@functools.wraps(func)
|
|
218
|
+
def sync_wrapper(*args, **kwargs):
|
|
219
|
+
# Auto-initialize tracing if not done
|
|
220
|
+
if get_token() is None:
|
|
221
|
+
try:
|
|
222
|
+
initialize_tracing_(None, self.collector_endpoint)
|
|
223
|
+
except Exception as e:
|
|
224
|
+
logger.error(f"Failed to auto-initialize tracing: {e}")
|
|
225
|
+
# Fall back to executing function without tracing
|
|
226
|
+
return func(*args, **kwargs)
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
return trace_sync_(
|
|
230
|
+
external_customer_id=self.external_customer_id,
|
|
231
|
+
fn=func,
|
|
232
|
+
external_agent_id=self.external_agent_id,
|
|
233
|
+
tracing_token=self.tracing_token,
|
|
234
|
+
store_prompt=self.store_prompt,
|
|
235
|
+
metadata=self.metadata,
|
|
236
|
+
args=args,
|
|
237
|
+
kwargs=kwargs,
|
|
238
|
+
)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.error(f"Failed to trace sync function {func.__name__}: {e}")
|
|
241
|
+
raise e
|
|
242
|
+
|
|
243
|
+
return sync_wrapper
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
3
|
+
from . import tracing
|
|
4
|
+
from opentelemetry.sdk.trace.id_generator import RandomIdGenerator
|
|
5
|
+
|
|
6
|
+
otel_id_generator = RandomIdGenerator()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def generate_tracing_token() -> int:
|
|
10
|
+
"""
|
|
11
|
+
Generate a unique tracing token without setting it in the context.
|
|
12
|
+
|
|
13
|
+
Use this when you want to generate a trace ID to store or pass to another
|
|
14
|
+
process/service without immediately associating it with the current tracing context.
|
|
15
|
+
The token can later be used with set_tracing_token() to link traces across
|
|
16
|
+
different execution contexts.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
int: A unique OpenTelemetry trace ID.
|
|
20
|
+
|
|
21
|
+
Notes:
|
|
22
|
+
- This function only generates and returns the token; it does NOT set it in the context.
|
|
23
|
+
- Use this when you need to store the token separately before setting it.
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
Generate token to store for later use:
|
|
27
|
+
|
|
28
|
+
from paid.tracing import generate_tracing_token, set_tracing_token
|
|
29
|
+
|
|
30
|
+
# Process 1: Generate and store
|
|
31
|
+
token = generate_tracing_token()
|
|
32
|
+
save_to_database("task_123", token)
|
|
33
|
+
|
|
34
|
+
# Process 2: Retrieve and use
|
|
35
|
+
token = load_from_database("task_123")
|
|
36
|
+
set_tracing_token(token)
|
|
37
|
+
|
|
38
|
+
@paid_tracing(external_customer_id="cust_123", external_agent_id="agent_456")
|
|
39
|
+
def process_task():
|
|
40
|
+
# This trace is now linked to the same token
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
See Also:
|
|
44
|
+
set_tracing_token: Set a previously generated token.
|
|
45
|
+
"""
|
|
46
|
+
return otel_id_generator.generate_trace_id()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def set_tracing_token(token: int):
|
|
50
|
+
"""
|
|
51
|
+
Deprecated: Pass tracing_token directly to @paid_tracing() decorator instead.
|
|
52
|
+
|
|
53
|
+
This function is deprecated and will be removed in a future version.
|
|
54
|
+
Use the tracing_token parameter in @paid_tracing() to link traces across processes.
|
|
55
|
+
|
|
56
|
+
Instead of:
|
|
57
|
+
token = load_from_storage("workflow_123")
|
|
58
|
+
set_tracing_token(token)
|
|
59
|
+
@paid_tracing(external_customer_id="cust_123", external_agent_id="agent_456")
|
|
60
|
+
def process_workflow():
|
|
61
|
+
...
|
|
62
|
+
unset_tracing_token()
|
|
63
|
+
|
|
64
|
+
Use:
|
|
65
|
+
token = load_from_storage("workflow_123")
|
|
66
|
+
|
|
67
|
+
@paid_tracing(
|
|
68
|
+
external_customer_id="cust_123",
|
|
69
|
+
external_agent_id="agent_456",
|
|
70
|
+
tracing_token=token
|
|
71
|
+
)
|
|
72
|
+
def process_workflow():
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
Parameters:
|
|
76
|
+
token (int): A tracing token (for backward compatibility only).
|
|
77
|
+
|
|
78
|
+
Old behavior (for reference):
|
|
79
|
+
This function set a token in the context, so all subsequent @paid_tracing() calls
|
|
80
|
+
would use it automatically until unset_tracing_token() was called.
|
|
81
|
+
"""
|
|
82
|
+
warnings.warn(
|
|
83
|
+
"set_tracing_token() is deprecated and will be removed in a future version. "
|
|
84
|
+
"Pass tracing_token directly to @paid_tracing(tracing_token=...) decorator instead.",
|
|
85
|
+
DeprecationWarning,
|
|
86
|
+
stacklevel=2,
|
|
87
|
+
)
|
|
88
|
+
_ = tracing.paid_trace_id_var.set(token)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def unset_tracing_token():
|
|
92
|
+
"""
|
|
93
|
+
Deprecated: No longer needed. Use tracing_token parameter in @paid_tracing() instead.
|
|
94
|
+
|
|
95
|
+
This function is deprecated and will be removed in a future version.
|
|
96
|
+
Since tracing_token is now passed directly to @paid_tracing(), there's no need
|
|
97
|
+
to manually set/unset tokens in the context.
|
|
98
|
+
|
|
99
|
+
Old behavior (for reference):
|
|
100
|
+
This function unset a token previously set by set_tracing_token(), allowing subsequent @paid_tracing() calls
|
|
101
|
+
to have independent traces.
|
|
102
|
+
|
|
103
|
+
Migration:
|
|
104
|
+
If you were using set_tracing_token() + unset_tracing_token() pattern,
|
|
105
|
+
simply pass the token directly to @paid_tracing(tracing_token=...) instead.
|
|
106
|
+
"""
|
|
107
|
+
warnings.warn(
|
|
108
|
+
"unset_tracing_token() is deprecated and will be removed in a future version. "
|
|
109
|
+
"Use tracing_token parameter in @paid_tracing(tracing_token=...) decorator instead.",
|
|
110
|
+
DeprecationWarning,
|
|
111
|
+
stacklevel=2,
|
|
112
|
+
)
|
|
113
|
+
_ = tracing.paid_trace_id_var.set(None)
|
paid/tracing/signal.py
CHANGED
|
@@ -1,44 +1,69 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import typing
|
|
3
3
|
|
|
4
|
-
from .tracing import get_paid_tracer
|
|
5
|
-
from opentelemetry import trace
|
|
4
|
+
from .tracing import get_paid_tracer
|
|
6
5
|
from opentelemetry.trace import Status, StatusCode
|
|
7
6
|
|
|
7
|
+
from paid.logger import logger
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
9
|
+
|
|
10
|
+
def signal(event_name: str, enable_cost_tracing: bool = False, data: typing.Optional[dict[str, typing.Any]] = None):
|
|
11
|
+
"""
|
|
12
|
+
Emit a signal within a tracing context.
|
|
13
|
+
|
|
14
|
+
This function must be called within an active @paid_tracing() context (decorator or context manager).
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
event_name : str
|
|
19
|
+
The name of the signal (e.g., "user_signup", "payment_processed", "task_completed").
|
|
20
|
+
enable_cost_tracing : bool, optional
|
|
21
|
+
If True, associates this signal with cost/usage traces from the same tracing context.
|
|
22
|
+
Should only be called once per tracing context to avoid multiple signals referring to the same costs.
|
|
23
|
+
Default is False.
|
|
24
|
+
data : dict[str, Any], optional
|
|
25
|
+
Additional context data to attach to the signal. Will be JSON-serialized and stored
|
|
26
|
+
as a span attribute. Example: {"user_id": "123", "amount": 99.99}.
|
|
27
|
+
|
|
28
|
+
Notes
|
|
29
|
+
-----
|
|
30
|
+
- Signal must be called within a @paid_tracing() context; calling outside will log an error and return.
|
|
31
|
+
- Use enable_cost_tracing=True when you want to mark the point where costs were incurred
|
|
32
|
+
and link that signal to cost/usage data from the same trace.
|
|
33
|
+
|
|
34
|
+
Examples
|
|
35
|
+
--------
|
|
36
|
+
Basic signal within a tracing context:
|
|
37
|
+
|
|
38
|
+
from paid.tracing import paid_tracing, signal
|
|
39
|
+
|
|
40
|
+
@paid_tracing(external_customer_id="cust_123", external_agent_id="agent_456")
|
|
41
|
+
def process_order(order_id):
|
|
42
|
+
# ... do work ...
|
|
43
|
+
signal("order_processed", data={"order_id": order_id})
|
|
44
|
+
|
|
45
|
+
Signal with cost tracking:
|
|
46
|
+
|
|
47
|
+
@paid_tracing(external_customer_id="cust_123", external_agent_id="agent_456")
|
|
48
|
+
def call_ai_api():
|
|
49
|
+
# ... call AI provider ...
|
|
50
|
+
signal("ai_api_call_complete", enable_cost_tracing=True)
|
|
51
|
+
|
|
52
|
+
Using context manager:
|
|
53
|
+
|
|
54
|
+
with paid_tracing(external_customer_id="cust_123", external_agent_id="agent_456"):
|
|
55
|
+
# ... do work ...
|
|
56
|
+
signal("milestone_reached", data={"step": "validation_complete"})
|
|
57
|
+
"""
|
|
30
58
|
|
|
31
59
|
tracer = get_paid_tracer()
|
|
32
60
|
with tracer.start_as_current_span("signal") as span:
|
|
33
|
-
attributes:
|
|
34
|
-
"external_customer_id": external_customer_id,
|
|
35
|
-
"external_agent_id": external_agent_id,
|
|
61
|
+
attributes: dict[str, typing.Union[str, bool, int, float]] = {
|
|
36
62
|
"event_name": event_name,
|
|
37
63
|
}
|
|
38
64
|
|
|
39
65
|
if enable_cost_tracing:
|
|
40
66
|
# let the app know to associate this signal with cost traces
|
|
41
|
-
attributes["enable_cost_tracing"] = True
|
|
42
67
|
if data is None:
|
|
43
68
|
data = {"paid": {"enable_cost_tracing": True}}
|
|
44
69
|
else:
|
|
@@ -46,7 +71,12 @@ def _signal(event_name: str, enable_cost_tracing: bool, data: typing.Optional[ty
|
|
|
46
71
|
|
|
47
72
|
# optional data (ex. manual cost tracking)
|
|
48
73
|
if data:
|
|
49
|
-
|
|
74
|
+
try:
|
|
75
|
+
attributes["data"] = json.dumps(data)
|
|
76
|
+
except (TypeError, ValueError) as e:
|
|
77
|
+
logger.error(f"Failed to serialize data into JSON for signal [{event_name}]: {e}")
|
|
78
|
+
if enable_cost_tracing:
|
|
79
|
+
attributes["data"] = json.dumps({"paid": {"enable_cost_tracing": True}})
|
|
50
80
|
|
|
51
81
|
span.set_attributes(attributes)
|
|
52
82
|
# Mark span as successful
|