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.
Files changed (57) hide show
  1. traccia/__init__.py +73 -0
  2. traccia/auto.py +748 -0
  3. traccia/auto_instrumentation.py +74 -0
  4. traccia/cli.py +349 -0
  5. traccia/config.py +699 -0
  6. traccia/context/__init__.py +33 -0
  7. traccia/context/context.py +67 -0
  8. traccia/context/propagators.py +283 -0
  9. traccia/errors.py +48 -0
  10. traccia/exporter/__init__.py +8 -0
  11. traccia/exporter/console_exporter.py +31 -0
  12. traccia/exporter/file_exporter.py +178 -0
  13. traccia/exporter/http_exporter.py +214 -0
  14. traccia/exporter/otlp_exporter.py +190 -0
  15. traccia/instrumentation/__init__.py +26 -0
  16. traccia/instrumentation/anthropic.py +92 -0
  17. traccia/instrumentation/decorator.py +263 -0
  18. traccia/instrumentation/fastapi.py +38 -0
  19. traccia/instrumentation/http_client.py +21 -0
  20. traccia/instrumentation/http_server.py +25 -0
  21. traccia/instrumentation/openai.py +358 -0
  22. traccia/instrumentation/requests.py +68 -0
  23. traccia/integrations/__init__.py +39 -0
  24. traccia/integrations/langchain/__init__.py +14 -0
  25. traccia/integrations/langchain/callback.py +418 -0
  26. traccia/integrations/langchain/utils.py +129 -0
  27. traccia/integrations/openai_agents/__init__.py +73 -0
  28. traccia/integrations/openai_agents/processor.py +262 -0
  29. traccia/pricing_config.py +58 -0
  30. traccia/processors/__init__.py +35 -0
  31. traccia/processors/agent_enricher.py +159 -0
  32. traccia/processors/batch_processor.py +140 -0
  33. traccia/processors/cost_engine.py +71 -0
  34. traccia/processors/cost_processor.py +70 -0
  35. traccia/processors/drop_policy.py +44 -0
  36. traccia/processors/logging_processor.py +31 -0
  37. traccia/processors/rate_limiter.py +223 -0
  38. traccia/processors/sampler.py +22 -0
  39. traccia/processors/token_counter.py +216 -0
  40. traccia/runtime_config.py +127 -0
  41. traccia/tracer/__init__.py +15 -0
  42. traccia/tracer/otel_adapter.py +577 -0
  43. traccia/tracer/otel_utils.py +24 -0
  44. traccia/tracer/provider.py +155 -0
  45. traccia/tracer/span.py +286 -0
  46. traccia/tracer/span_context.py +16 -0
  47. traccia/tracer/tracer.py +243 -0
  48. traccia/utils/__init__.py +19 -0
  49. traccia/utils/helpers.py +95 -0
  50. {traccia-0.1.2.dist-info → traccia-0.1.6.dist-info}/METADATA +72 -15
  51. traccia-0.1.6.dist-info/RECORD +55 -0
  52. traccia-0.1.6.dist-info/top_level.txt +1 -0
  53. traccia-0.1.2.dist-info/RECORD +0 -6
  54. traccia-0.1.2.dist-info/top_level.txt +0 -1
  55. {traccia-0.1.2.dist-info → traccia-0.1.6.dist-info}/WHEEL +0 -0
  56. {traccia-0.1.2.dist-info → traccia-0.1.6.dist-info}/entry_points.txt +0 -0
  57. {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
+