traccia 0.1.2__py3-none-any.whl → 0.1.6__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.
- traccia/__init__.py +73 -0
- traccia/auto.py +748 -0
- traccia/auto_instrumentation.py +74 -0
- traccia/cli.py +349 -0
- traccia/config.py +699 -0
- traccia/context/__init__.py +33 -0
- traccia/context/context.py +67 -0
- traccia/context/propagators.py +283 -0
- traccia/errors.py +48 -0
- traccia/exporter/__init__.py +8 -0
- traccia/exporter/console_exporter.py +31 -0
- traccia/exporter/file_exporter.py +178 -0
- traccia/exporter/http_exporter.py +214 -0
- traccia/exporter/otlp_exporter.py +190 -0
- traccia/instrumentation/__init__.py +26 -0
- traccia/instrumentation/anthropic.py +92 -0
- traccia/instrumentation/decorator.py +263 -0
- traccia/instrumentation/fastapi.py +38 -0
- traccia/instrumentation/http_client.py +21 -0
- traccia/instrumentation/http_server.py +25 -0
- traccia/instrumentation/openai.py +358 -0
- traccia/instrumentation/requests.py +68 -0
- traccia/integrations/__init__.py +39 -0
- traccia/integrations/langchain/__init__.py +14 -0
- traccia/integrations/langchain/callback.py +418 -0
- traccia/integrations/langchain/utils.py +129 -0
- traccia/integrations/openai_agents/__init__.py +73 -0
- traccia/integrations/openai_agents/processor.py +262 -0
- traccia/pricing_config.py +58 -0
- traccia/processors/__init__.py +35 -0
- traccia/processors/agent_enricher.py +159 -0
- traccia/processors/batch_processor.py +140 -0
- traccia/processors/cost_engine.py +71 -0
- traccia/processors/cost_processor.py +70 -0
- traccia/processors/drop_policy.py +44 -0
- traccia/processors/logging_processor.py +31 -0
- traccia/processors/rate_limiter.py +223 -0
- traccia/processors/sampler.py +22 -0
- traccia/processors/token_counter.py +216 -0
- traccia/runtime_config.py +127 -0
- traccia/tracer/__init__.py +15 -0
- traccia/tracer/otel_adapter.py +577 -0
- traccia/tracer/otel_utils.py +24 -0
- traccia/tracer/provider.py +155 -0
- traccia/tracer/span.py +286 -0
- traccia/tracer/span_context.py +16 -0
- traccia/tracer/tracer.py +243 -0
- traccia/utils/__init__.py +19 -0
- traccia/utils/helpers.py +95 -0
- {traccia-0.1.2.dist-info → traccia-0.1.6.dist-info}/METADATA +72 -15
- traccia-0.1.6.dist-info/RECORD +55 -0
- traccia-0.1.6.dist-info/top_level.txt +1 -0
- traccia-0.1.2.dist-info/RECORD +0 -6
- traccia-0.1.2.dist-info/top_level.txt +0 -1
- {traccia-0.1.2.dist-info → traccia-0.1.6.dist-info}/WHEEL +0 -0
- {traccia-0.1.2.dist-info → traccia-0.1.6.dist-info}/entry_points.txt +0 -0
- {traccia-0.1.2.dist-info → traccia-0.1.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""@observe decorator for instrumenting functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import inspect
|
|
7
|
+
import traceback
|
|
8
|
+
from typing import Any, Callable, Dict, Iterable, Optional
|
|
9
|
+
from traccia.tracer.span import SpanStatus
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _capture_args(bound_args: inspect.BoundArguments, skip: Iterable[str]) -> Dict[str, Any]:
|
|
13
|
+
"""Capture function arguments, converting complex types to OTel-compatible types."""
|
|
14
|
+
captured = {}
|
|
15
|
+
for name, value in bound_args.arguments.items():
|
|
16
|
+
if name in skip:
|
|
17
|
+
continue
|
|
18
|
+
# Skip 'self' - it's an object, not a valid OTel attribute
|
|
19
|
+
if name == "self":
|
|
20
|
+
continue
|
|
21
|
+
# Convert value to OTel-compatible type
|
|
22
|
+
captured[name] = _convert_to_otel_type(value)
|
|
23
|
+
return captured
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _convert_to_otel_type(value: Any) -> Any:
|
|
27
|
+
"""
|
|
28
|
+
Convert a value to an OpenTelemetry-compatible type.
|
|
29
|
+
|
|
30
|
+
OTel attributes must be: bool, str, bytes, int, float, or sequences of those.
|
|
31
|
+
"""
|
|
32
|
+
# Primitive types are fine
|
|
33
|
+
if isinstance(value, (bool, str, bytes, int, float)) or value is None:
|
|
34
|
+
return value
|
|
35
|
+
|
|
36
|
+
# For sequences, convert each element
|
|
37
|
+
if isinstance(value, (list, tuple)):
|
|
38
|
+
converted = []
|
|
39
|
+
for item in value:
|
|
40
|
+
if isinstance(item, (bool, str, bytes, int, float)) or item is None:
|
|
41
|
+
converted.append(item)
|
|
42
|
+
else:
|
|
43
|
+
# Convert complex types to string representation
|
|
44
|
+
converted.append(str(item)[:1000]) # Truncate long strings
|
|
45
|
+
return converted[:100] # Limit sequence length
|
|
46
|
+
|
|
47
|
+
# For dicts and other complex types, convert to JSON string
|
|
48
|
+
if isinstance(value, dict):
|
|
49
|
+
try:
|
|
50
|
+
import json
|
|
51
|
+
json_str = json.dumps(value, default=str)[:1000] # Truncate
|
|
52
|
+
return json_str
|
|
53
|
+
except Exception:
|
|
54
|
+
return str(value)[:1000]
|
|
55
|
+
|
|
56
|
+
# For other types, convert to string
|
|
57
|
+
return str(value)[:1000] # Truncate long strings
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _infer_type_from_attributes(attributes: Dict[str, Any]) -> Optional[str]:
|
|
61
|
+
"""
|
|
62
|
+
Infer span type from attributes.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
- "llm" if LLM-related attributes found
|
|
66
|
+
- "tool" if tool-related attributes found
|
|
67
|
+
- None otherwise (will use default "span")
|
|
68
|
+
"""
|
|
69
|
+
# Check for LLM indicators
|
|
70
|
+
if any(key in attributes for key in ["llm.model", "llm.vendor", "model"]):
|
|
71
|
+
return "llm"
|
|
72
|
+
|
|
73
|
+
# Check for tool indicators
|
|
74
|
+
if any(key in attributes for key in ["tool.name", "tool", "http.url"]):
|
|
75
|
+
return "tool"
|
|
76
|
+
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _extract_llm_attributes(span_attrs: Dict[str, Any], bound_args: inspect.BoundArguments) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Extract LLM-related attributes from function arguments.
|
|
83
|
+
|
|
84
|
+
Extracts common LLM parameters like model, temperature, max_tokens, messages.
|
|
85
|
+
Fails silently if extraction fails or attributes not found.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
span_attrs: Dictionary to add extracted attributes to
|
|
89
|
+
bound_args: Bound arguments from the function call
|
|
90
|
+
"""
|
|
91
|
+
try:
|
|
92
|
+
args_dict = dict(bound_args.arguments)
|
|
93
|
+
|
|
94
|
+
# Extract model
|
|
95
|
+
if "model" in args_dict and "llm.model" not in span_attrs:
|
|
96
|
+
span_attrs["llm.model"] = str(args_dict["model"])
|
|
97
|
+
|
|
98
|
+
# Extract temperature
|
|
99
|
+
if "temperature" in args_dict and "llm.temperature" not in span_attrs:
|
|
100
|
+
temp = args_dict["temperature"]
|
|
101
|
+
if isinstance(temp, (int, float)):
|
|
102
|
+
span_attrs["llm.temperature"] = temp
|
|
103
|
+
|
|
104
|
+
# Extract max_tokens
|
|
105
|
+
if "max_tokens" in args_dict and "llm.max_tokens" not in span_attrs:
|
|
106
|
+
max_tok = args_dict["max_tokens"]
|
|
107
|
+
if isinstance(max_tok, int):
|
|
108
|
+
span_attrs["llm.max_tokens"] = max_tok
|
|
109
|
+
|
|
110
|
+
# Extract messages/prompt
|
|
111
|
+
if "messages" in args_dict and "llm.prompt" not in span_attrs:
|
|
112
|
+
messages = args_dict["messages"]
|
|
113
|
+
if isinstance(messages, (list, str)):
|
|
114
|
+
# Convert messages to string representation
|
|
115
|
+
prompt_str = _convert_to_otel_type(messages)
|
|
116
|
+
span_attrs["llm.prompt"] = prompt_str
|
|
117
|
+
elif "prompt" in args_dict and "llm.prompt" not in span_attrs:
|
|
118
|
+
prompt = args_dict["prompt"]
|
|
119
|
+
if isinstance(prompt, str):
|
|
120
|
+
span_attrs["llm.prompt"] = prompt[:1000]
|
|
121
|
+
|
|
122
|
+
except Exception:
|
|
123
|
+
# Fail silently - don't interrupt span creation if extraction fails
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def observe(
|
|
128
|
+
name: Optional[str] = None,
|
|
129
|
+
*,
|
|
130
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
131
|
+
tags: Optional[Iterable[str]] = None,
|
|
132
|
+
as_type: str = "span",
|
|
133
|
+
skip_args: Optional[Iterable[str]] = None,
|
|
134
|
+
skip_result: bool = False,
|
|
135
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
136
|
+
"""
|
|
137
|
+
Decorate a function to create a span around its execution.
|
|
138
|
+
|
|
139
|
+
- Supports sync and async functions.
|
|
140
|
+
- Captures errors and records exception events.
|
|
141
|
+
- Optionally captures arguments/results (skip controls).
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
145
|
+
span_name = name or func.__name__
|
|
146
|
+
arg_names = func.__code__.co_varnames
|
|
147
|
+
skip_args_set = set(skip_args or [])
|
|
148
|
+
tags_list = [str(tag) for tag in tags] if tags is not None else []
|
|
149
|
+
|
|
150
|
+
is_coro = inspect.iscoroutinefunction(func)
|
|
151
|
+
|
|
152
|
+
@functools.wraps(func)
|
|
153
|
+
def sync_wrapper(*args, **kwargs):
|
|
154
|
+
tracer = _get_tracer(func.__module__ or "default")
|
|
155
|
+
bound = inspect.signature(func).bind_partial(*args, **kwargs)
|
|
156
|
+
bound.apply_defaults()
|
|
157
|
+
|
|
158
|
+
span_attrs = dict(attributes or {})
|
|
159
|
+
if tags_list:
|
|
160
|
+
span_attrs["span.tags"] = tags_list
|
|
161
|
+
|
|
162
|
+
# Capture function arguments first
|
|
163
|
+
span_attrs.update(_capture_args(bound, skip_args_set))
|
|
164
|
+
|
|
165
|
+
# Infer type from attributes if not explicitly set (or if set to default "span")
|
|
166
|
+
inferred_type = as_type
|
|
167
|
+
if as_type == "span":
|
|
168
|
+
# Try to infer from attributes
|
|
169
|
+
detected_type = _infer_type_from_attributes(span_attrs)
|
|
170
|
+
if detected_type:
|
|
171
|
+
inferred_type = detected_type
|
|
172
|
+
|
|
173
|
+
# Set span type
|
|
174
|
+
span_attrs["span.type"] = inferred_type
|
|
175
|
+
|
|
176
|
+
# Extract LLM attributes if this is an LLM call
|
|
177
|
+
if inferred_type == "llm":
|
|
178
|
+
_extract_llm_attributes(span_attrs, bound)
|
|
179
|
+
|
|
180
|
+
with tracer.start_as_current_span(span_name, attributes=span_attrs) as span:
|
|
181
|
+
try:
|
|
182
|
+
result = func(*args, **kwargs)
|
|
183
|
+
if not skip_result:
|
|
184
|
+
# Convert result to OTel-compatible type
|
|
185
|
+
otel_result = _convert_to_otel_type(result)
|
|
186
|
+
span.set_attribute("result", otel_result)
|
|
187
|
+
return result
|
|
188
|
+
except Exception as exc:
|
|
189
|
+
# Record detailed error information
|
|
190
|
+
span.record_exception(exc)
|
|
191
|
+
span.set_status(SpanStatus.ERROR, str(exc))
|
|
192
|
+
|
|
193
|
+
# Add error attributes
|
|
194
|
+
span.set_attribute("error.type", type(exc).__name__)
|
|
195
|
+
span.set_attribute("error.message", str(exc))
|
|
196
|
+
|
|
197
|
+
# Add truncated stack trace
|
|
198
|
+
tb = traceback.format_exc()
|
|
199
|
+
span.set_attribute("error.stack_trace", tb[:2000]) # Truncate to 2000 chars
|
|
200
|
+
|
|
201
|
+
raise
|
|
202
|
+
|
|
203
|
+
@functools.wraps(func)
|
|
204
|
+
async def async_wrapper(*args, **kwargs):
|
|
205
|
+
tracer = _get_tracer(func.__module__ or "default")
|
|
206
|
+
bound = inspect.signature(func).bind_partial(*args, **kwargs)
|
|
207
|
+
bound.apply_defaults()
|
|
208
|
+
|
|
209
|
+
span_attrs = dict(attributes or {})
|
|
210
|
+
if tags_list:
|
|
211
|
+
span_attrs["span.tags"] = tags_list
|
|
212
|
+
|
|
213
|
+
# Capture function arguments first
|
|
214
|
+
span_attrs.update(_capture_args(bound, skip_args_set))
|
|
215
|
+
|
|
216
|
+
# Infer type from attributes if not explicitly set (or if set to default "span")
|
|
217
|
+
inferred_type = as_type
|
|
218
|
+
if as_type == "span":
|
|
219
|
+
# Try to infer from attributes
|
|
220
|
+
detected_type = _infer_type_from_attributes(span_attrs)
|
|
221
|
+
if detected_type:
|
|
222
|
+
inferred_type = detected_type
|
|
223
|
+
|
|
224
|
+
# Set span type
|
|
225
|
+
span_attrs["span.type"] = inferred_type
|
|
226
|
+
|
|
227
|
+
# Extract LLM attributes if this is an LLM call
|
|
228
|
+
if inferred_type == "llm":
|
|
229
|
+
_extract_llm_attributes(span_attrs, bound)
|
|
230
|
+
|
|
231
|
+
async with tracer.start_as_current_span(span_name, attributes=span_attrs) as span:
|
|
232
|
+
try:
|
|
233
|
+
result = await func(*args, **kwargs)
|
|
234
|
+
if not skip_result:
|
|
235
|
+
# Convert result to OTel-compatible type
|
|
236
|
+
otel_result = _convert_to_otel_type(result)
|
|
237
|
+
span.set_attribute("result", otel_result)
|
|
238
|
+
return result
|
|
239
|
+
except Exception as exc:
|
|
240
|
+
# Record detailed error information
|
|
241
|
+
span.record_exception(exc)
|
|
242
|
+
span.set_status(SpanStatus.ERROR, str(exc))
|
|
243
|
+
|
|
244
|
+
# Add error attributes
|
|
245
|
+
span.set_attribute("error.type", type(exc).__name__)
|
|
246
|
+
span.set_attribute("error.message", str(exc))
|
|
247
|
+
|
|
248
|
+
# Add truncated stack trace
|
|
249
|
+
tb = traceback.format_exc()
|
|
250
|
+
span.set_attribute("error.stack_trace", tb[:2000]) # Truncate to 2000 chars
|
|
251
|
+
|
|
252
|
+
raise
|
|
253
|
+
|
|
254
|
+
return async_wrapper if is_coro else sync_wrapper
|
|
255
|
+
|
|
256
|
+
return decorator
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _get_tracer(name: str):
|
|
260
|
+
import traccia
|
|
261
|
+
|
|
262
|
+
return traccia.get_tracer(name)
|
|
263
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI middleware helpers for tracing HTTP requests with the SDK.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any, Awaitable, Callable
|
|
8
|
+
|
|
9
|
+
from traccia.instrumentation import start_server_span
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def install_http_middleware(app: Any, *, tracer_name: str = "agents-fastapi") -> None:
|
|
13
|
+
"""
|
|
14
|
+
Attach an HTTP middleware that wraps each FastAPI request in a server span.
|
|
15
|
+
|
|
16
|
+
- Propagates incoming context from headers
|
|
17
|
+
- Records method/path and response status code
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@app.middleware("http")
|
|
21
|
+
async def tracing_middleware(request, call_next: Callable[[Any], Awaitable[Any]]): # type: ignore
|
|
22
|
+
# Lazy import to avoid circular import when traccia initializes.
|
|
23
|
+
from traccia import get_tracer
|
|
24
|
+
tracer = get_tracer(tracer_name)
|
|
25
|
+
headers = dict(request.headers)
|
|
26
|
+
attrs = {
|
|
27
|
+
"http.method": request.method,
|
|
28
|
+
"http.target": request.url.path,
|
|
29
|
+
}
|
|
30
|
+
async with start_server_span(tracer, "http.request", headers, attributes=attrs) as span:
|
|
31
|
+
response = await call_next(request)
|
|
32
|
+
try:
|
|
33
|
+
span.set_attribute("http.status_code", response.status_code)
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
return response
|
|
37
|
+
|
|
38
|
+
return None
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""HTTP client helpers for context propagation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Dict
|
|
6
|
+
|
|
7
|
+
from traccia.context import inject_traceparent, inject_tracestate, get_current_span
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def inject_headers(headers: Dict[str, str]) -> Dict[str, str]:
|
|
11
|
+
"""
|
|
12
|
+
Inject traceparent/tracestate into the provided headers dict if a current span exists.
|
|
13
|
+
|
|
14
|
+
Returns the same headers mapping for convenience.
|
|
15
|
+
"""
|
|
16
|
+
span = get_current_span()
|
|
17
|
+
if span:
|
|
18
|
+
inject_traceparent(headers, span.context)
|
|
19
|
+
inject_tracestate(headers, span.context)
|
|
20
|
+
return headers
|
|
21
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""HTTP server helpers for extracting context and creating server spans."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Optional
|
|
6
|
+
|
|
7
|
+
from traccia.context import extract_trace_context
|
|
8
|
+
from traccia.tracer.tracer import Tracer
|
|
9
|
+
from traccia.tracer.span_context import SpanContext
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def extract_parent_context(headers: Dict[str, str]) -> Optional[SpanContext]:
|
|
13
|
+
"""Parse traceparent/tracestate from headers and return SpanContext if valid."""
|
|
14
|
+
return extract_trace_context(headers)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def start_server_span(tracer: Tracer, name: str, headers: Dict[str, str], attributes=None):
|
|
18
|
+
"""
|
|
19
|
+
Convenience helper to start a server span with extracted parent context.
|
|
20
|
+
|
|
21
|
+
Returns the span context manager (caller should use 'with' or 'async with').
|
|
22
|
+
"""
|
|
23
|
+
parent_ctx = extract_parent_context(headers)
|
|
24
|
+
return tracer.start_as_current_span(name, attributes=attributes, parent_context=parent_ctx)
|
|
25
|
+
|