traccia 0.1.2__py3-none-any.whl → 0.1.5__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 (55) hide show
  1. traccia/__init__.py +73 -0
  2. traccia/auto.py +736 -0
  3. traccia/auto_instrumentation.py +74 -0
  4. traccia/cli.py +349 -0
  5. traccia/config.py +693 -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 +20 -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 +178 -0
  22. traccia/instrumentation/requests.py +68 -0
  23. traccia/integrations/__init__.py +22 -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/pricing_config.py +58 -0
  28. traccia/processors/__init__.py +35 -0
  29. traccia/processors/agent_enricher.py +159 -0
  30. traccia/processors/batch_processor.py +140 -0
  31. traccia/processors/cost_engine.py +71 -0
  32. traccia/processors/cost_processor.py +70 -0
  33. traccia/processors/drop_policy.py +44 -0
  34. traccia/processors/logging_processor.py +31 -0
  35. traccia/processors/rate_limiter.py +223 -0
  36. traccia/processors/sampler.py +22 -0
  37. traccia/processors/token_counter.py +216 -0
  38. traccia/runtime_config.py +106 -0
  39. traccia/tracer/__init__.py +15 -0
  40. traccia/tracer/otel_adapter.py +577 -0
  41. traccia/tracer/otel_utils.py +24 -0
  42. traccia/tracer/provider.py +155 -0
  43. traccia/tracer/span.py +286 -0
  44. traccia/tracer/span_context.py +16 -0
  45. traccia/tracer/tracer.py +243 -0
  46. traccia/utils/__init__.py +19 -0
  47. traccia/utils/helpers.py +95 -0
  48. {traccia-0.1.2.dist-info → traccia-0.1.5.dist-info}/METADATA +32 -15
  49. traccia-0.1.5.dist-info/RECORD +53 -0
  50. traccia-0.1.5.dist-info/top_level.txt +1 -0
  51. traccia-0.1.2.dist-info/RECORD +0 -6
  52. traccia-0.1.2.dist-info/top_level.txt +0 -1
  53. {traccia-0.1.2.dist-info → traccia-0.1.5.dist-info}/WHEEL +0 -0
  54. {traccia-0.1.2.dist-info → traccia-0.1.5.dist-info}/entry_points.txt +0 -0
  55. {traccia-0.1.2.dist-info → traccia-0.1.5.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,33 @@
1
+ """Context utilities for the tracing SDK."""
2
+
3
+ from traccia.context.context import get_current_span, pop_span, push_span
4
+ from traccia.context.propagators import (
5
+ extract_trace_context,
6
+ extract_tracestate,
7
+ extract_traceparent,
8
+ format_traceparent,
9
+ format_tracestate,
10
+ inject_traceparent,
11
+ inject_tracestate,
12
+ parse_tracestate,
13
+ parse_traceparent,
14
+ inject,
15
+ extract,
16
+ )
17
+
18
+ __all__ = [
19
+ "get_current_span",
20
+ "push_span",
21
+ "pop_span",
22
+ "format_traceparent",
23
+ "inject_traceparent",
24
+ "parse_traceparent",
25
+ "extract_traceparent",
26
+ "format_tracestate",
27
+ "parse_tracestate",
28
+ "inject_tracestate",
29
+ "extract_tracestate",
30
+ "extract_trace_context",
31
+ "inject",
32
+ "extract",
33
+ ]
@@ -0,0 +1,67 @@
1
+ """Context helpers for managing the active span stack - using OpenTelemetry directly."""
2
+
3
+ from contextvars import Token
4
+ from typing import Optional, TYPE_CHECKING
5
+
6
+ from opentelemetry.trace import get_current_span as otel_get_current_span
7
+ from opentelemetry.trace import set_span_in_context
8
+ from opentelemetry import context as context_api
9
+
10
+ if TYPE_CHECKING:
11
+ from traccia.tracer.span import Span
12
+
13
+
14
+ def get_current_span() -> Optional["Span"]:
15
+ """
16
+ Return the currently active span, if any.
17
+
18
+ Uses OpenTelemetry's context API internally.
19
+ """
20
+ otel_span = otel_get_current_span()
21
+ if otel_span and otel_span.get_span_context().is_valid:
22
+ # Get tracer from span if available
23
+ try:
24
+ if hasattr(otel_span, '_traccia_tracer'):
25
+ tracer = otel_span._traccia_tracer
26
+ else:
27
+ # Fallback: create a tracer from global provider
28
+ from traccia import get_tracer_provider
29
+ provider = get_tracer_provider()
30
+ tracer = provider.get_tracer("context")
31
+
32
+ # Wrap in Traccia Span
33
+ from traccia.tracer.span import Span
34
+ return Span(otel_span, tracer)
35
+ except Exception:
36
+ # If wrapping fails, return None
37
+ return None
38
+ return None
39
+
40
+
41
+ def push_span(span: "Span") -> Token:
42
+ """
43
+ Push a span onto the context and set it as current.
44
+
45
+ Uses OpenTelemetry's context API internally.
46
+
47
+ Returns:
48
+ Token needed to restore the previous state
49
+ """
50
+ if hasattr(span, '_otel_span'):
51
+ otel_span = span._otel_span
52
+ else:
53
+ otel_span = span
54
+
55
+ ctx = set_span_in_context(otel_span)
56
+ token = context_api.attach(ctx)
57
+ return token
58
+
59
+
60
+ def pop_span(token: Token) -> None:
61
+ """
62
+ Restore the previous span context using the provided token.
63
+
64
+ Args:
65
+ token: Token returned by push_span()
66
+ """
67
+ context_api.detach(token)
@@ -0,0 +1,283 @@
1
+ """W3C trace context propagation using OpenTelemetry's standard propagators."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Dict, Optional, Any
6
+
7
+ from opentelemetry.propagate import inject as otel_inject, extract as otel_extract
8
+ from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
9
+ from opentelemetry.trace import get_current_span, set_span_in_context
10
+ from opentelemetry.trace import SpanContext as OTelSpanContext, TraceFlags, TraceState
11
+ from opentelemetry.trace import NonRecordingSpan
12
+ from opentelemetry import context as context_api
13
+
14
+ from traccia.tracer.span_context import SpanContext
15
+ from traccia.utils.helpers import format_trace_id, format_span_id, parse_trace_id, parse_span_id
16
+
17
+ # Use OTel's W3C Trace Context propagator
18
+ _propagator = TraceContextTextMapPropagator()
19
+
20
+
21
+ def format_traceparent(context: SpanContext) -> str:
22
+ """
23
+ Format traceparent header value (W3C Trace Context standard).
24
+
25
+ Uses OpenTelemetry's propagator internally.
26
+ """
27
+ # Convert Traccia SpanContext to OTel SpanContext
28
+ otel_context = _traccia_to_otel_context(context)
29
+
30
+ # Create a non-recording span with the context
31
+ span = NonRecordingSpan(otel_context)
32
+ ctx = set_span_in_context(span)
33
+
34
+ # Create a carrier dict and inject
35
+ carrier: Dict[str, str] = {}
36
+ _propagator.inject(carrier, context=ctx)
37
+
38
+ # Extract traceparent from carrier
39
+ return carrier.get("traceparent", "")
40
+
41
+
42
+ def format_tracestate(state: Dict[str, str]) -> str:
43
+ """
44
+ Format tracestate header value from a dict.
45
+
46
+ Formats according to W3C Trace Context standard.
47
+ """
48
+ if not state:
49
+ return ""
50
+
51
+ # Format manually (W3C format: key1=value1,key2=value2)
52
+ items = []
53
+ for k, v in state.items():
54
+ # Sanitize key and value
55
+ key = str(k).strip().lower()[:256]
56
+ value = str(v).strip().replace(",", "_").replace("=", "_")[:256]
57
+ if key and value:
58
+ items.append(f"{key}={value}")
59
+
60
+ return ",".join(items)
61
+
62
+
63
+ def parse_tracestate(header_value: str) -> Dict[str, str]:
64
+ """
65
+ Parse a tracestate header into a dict.
66
+
67
+ Parses W3C Trace Context tracestate format: key1=value1,key2=value2
68
+ """
69
+ if not header_value:
70
+ return {}
71
+
72
+ result = {}
73
+ for item in header_value.split(","):
74
+ item = item.strip()
75
+ if not item or "=" not in item:
76
+ continue
77
+ parts = item.split("=", 1)
78
+ if len(parts) == 2:
79
+ key = parts[0].strip().lower()
80
+ value = parts[1].strip()
81
+ if key and value:
82
+ result[key] = value
83
+
84
+ return result
85
+
86
+
87
+ def parse_traceparent(header_value: str) -> Optional[SpanContext]:
88
+ """
89
+ Parse a traceparent header into a SpanContext.
90
+
91
+ Uses OpenTelemetry's W3C Trace Context parser.
92
+ """
93
+ if not header_value:
94
+ return None
95
+
96
+ # Use OTel to parse traceparent
97
+ carrier = {"traceparent": header_value}
98
+ ctx = _propagator.extract(carrier)
99
+
100
+ # Get span context from OTel context
101
+ span = get_current_span(context=ctx)
102
+ if span:
103
+ otel_context = span.get_span_context()
104
+ if otel_context.is_valid:
105
+ return _otel_to_traccia_context(otel_context)
106
+
107
+ return None
108
+
109
+
110
+ def inject_traceparent(headers: Dict[str, str], context: SpanContext) -> None:
111
+ """
112
+ Inject traceparent header into headers dict.
113
+
114
+ Uses OpenTelemetry's inject() function.
115
+ """
116
+ # Convert Traccia context to OTel context
117
+ otel_context = _traccia_to_otel_context(context)
118
+
119
+ # Create OTel context with span
120
+ span = NonRecordingSpan(otel_context)
121
+ ctx = set_span_in_context(span)
122
+
123
+ # Inject using OTel
124
+ _propagator.inject(carrier=headers, context=ctx)
125
+
126
+
127
+ def inject_tracestate(headers: Dict[str, str], context: SpanContext) -> None:
128
+ """
129
+ Inject tracestate header if present on the context.
130
+
131
+ Uses OpenTelemetry's inject() function.
132
+ """
133
+ if not context.trace_state:
134
+ return
135
+
136
+ # Parse tracestate and create OTel TraceState
137
+ parsed = parse_tracestate(context.trace_state)
138
+ if parsed:
139
+ trace_state = TraceState([(k, v) for k, v in parsed.items()])
140
+
141
+ # Convert Traccia context to OTel context with tracestate
142
+ otel_context = _traccia_to_otel_context(context)
143
+ # Update trace_state
144
+ otel_context = OTelSpanContext(
145
+ trace_id=otel_context.trace_id,
146
+ span_id=otel_context.span_id,
147
+ is_remote=otel_context.is_remote,
148
+ trace_flags=otel_context.trace_flags,
149
+ trace_state=trace_state,
150
+ )
151
+
152
+ # Create span with context and inject
153
+ span = NonRecordingSpan(otel_context)
154
+ ctx = set_span_in_context(span)
155
+ _propagator.inject(carrier=headers, context=ctx)
156
+
157
+
158
+ def extract_traceparent(headers: Dict[str, str]) -> Optional[SpanContext]:
159
+ """
160
+ Extract traceparent header from headers and parse it.
161
+
162
+ Uses OpenTelemetry's extract() function.
163
+ """
164
+ # Use OTel to extract
165
+ ctx = _propagator.extract(carrier=headers)
166
+
167
+ # Get span from context
168
+ span = get_current_span(context=ctx)
169
+ if span:
170
+ otel_context = span.get_span_context()
171
+ if otel_context.is_valid:
172
+ return _otel_to_traccia_context(otel_context)
173
+
174
+ return None
175
+
176
+
177
+ def extract_tracestate(headers: Dict[str, str]) -> Optional[str]:
178
+ """
179
+ Extract tracestate header value (case-insensitive).
180
+
181
+ Returns the raw tracestate string.
182
+ """
183
+ # Case-insensitive lookup
184
+ for key, value in headers.items():
185
+ if key.lower() == "tracestate":
186
+ return value
187
+ return None
188
+
189
+
190
+ def extract_trace_context(headers: Dict[str, str]) -> Optional[SpanContext]:
191
+ """
192
+ Extract both traceparent and tracestate and return a combined SpanContext.
193
+
194
+ Uses OpenTelemetry's extract() function.
195
+ """
196
+ # Use OTel to extract both
197
+ ctx = _propagator.extract(carrier=headers)
198
+
199
+ # Get span from context
200
+ span = get_current_span(context=ctx)
201
+ if span:
202
+ otel_context = span.get_span_context()
203
+ if otel_context.is_valid:
204
+ # Extract tracestate separately
205
+ tracestate_str = extract_tracestate(headers)
206
+ return _otel_to_traccia_context(otel_context, tracestate_str)
207
+
208
+ return None
209
+
210
+
211
+ # Helper functions for context conversion
212
+
213
+ def _traccia_to_otel_context(traccia_context: SpanContext) -> OTelSpanContext:
214
+ """Convert Traccia SpanContext to OTel SpanContext."""
215
+ trace_id = parse_trace_id(traccia_context.trace_id)
216
+ span_id = parse_span_id(traccia_context.span_id)
217
+ trace_flags = TraceFlags(traccia_context.trace_flags)
218
+
219
+ # Parse trace_state
220
+ trace_state = TraceState()
221
+ if traccia_context.trace_state:
222
+ parsed = parse_tracestate(traccia_context.trace_state)
223
+ if parsed:
224
+ items = [(k, v) for k, v in parsed.items()]
225
+ trace_state = TraceState(items)
226
+
227
+ return OTelSpanContext(
228
+ trace_id=trace_id,
229
+ span_id=span_id,
230
+ is_remote=False,
231
+ trace_flags=trace_flags,
232
+ trace_state=trace_state,
233
+ )
234
+
235
+
236
+ def _otel_to_traccia_context(otel_context: OTelSpanContext, tracestate_str: Optional[str] = None) -> SpanContext:
237
+ """Convert OTel SpanContext to Traccia SpanContext."""
238
+ trace_id = format_trace_id(otel_context.trace_id)
239
+ span_id = format_span_id(otel_context.span_id)
240
+ trace_flags = 1 if otel_context.trace_flags.sampled else 0
241
+
242
+ # Format trace_state
243
+ trace_state = None
244
+ if tracestate_str:
245
+ trace_state = tracestate_str
246
+ elif otel_context.trace_state:
247
+ # Format OTel TraceState to string
248
+ items = []
249
+ for key, value in otel_context.trace_state.items():
250
+ items.append(f"{key}={value}")
251
+ if items:
252
+ trace_state = ",".join(items)
253
+
254
+ return SpanContext(
255
+ trace_id=trace_id,
256
+ span_id=span_id,
257
+ trace_flags=trace_flags,
258
+ trace_state=trace_state,
259
+ )
260
+
261
+
262
+ # Additional helper: inject/extract using OTel's standard API
263
+
264
+ def inject(carrier: Dict[str, str], context: Optional[Any] = None) -> None:
265
+ """
266
+ Inject trace context into carrier using OpenTelemetry's standard API.
267
+
268
+ This is a convenience function that uses OTel's inject() directly.
269
+ If context is not provided, uses current context.
270
+ """
271
+ if context is None:
272
+ otel_inject(carrier)
273
+ else:
274
+ otel_inject(carrier, context=context)
275
+
276
+
277
+ def extract(carrier: Dict[str, str]) -> Any:
278
+ """
279
+ Extract trace context from carrier using OpenTelemetry's standard API.
280
+
281
+ Returns OTel context that can be used with start_span().
282
+ """
283
+ return otel_extract(carrier)
traccia/errors.py ADDED
@@ -0,0 +1,48 @@
1
+ """Traccia SDK error hierarchy and exceptions."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class TracciaError(Exception):
7
+ """Base exception for all Traccia SDK errors."""
8
+
9
+ def __init__(self, message: str, details: dict = None):
10
+ super().__init__(message)
11
+ self.message = message
12
+ self.details = details or {}
13
+
14
+ def __str__(self) -> str:
15
+ if self.details:
16
+ details_str = ", ".join(f"{k}={v}" for k, v in self.details.items())
17
+ return f"{self.message} ({details_str})"
18
+ return self.message
19
+
20
+
21
+ class ConfigError(TracciaError):
22
+ """Raised when configuration is invalid or conflicting."""
23
+ pass
24
+
25
+
26
+ class ValidationError(TracciaError):
27
+ """Raised when validation fails."""
28
+ pass
29
+
30
+
31
+ class ExportError(TracciaError):
32
+ """Raised when span export fails."""
33
+ pass
34
+
35
+
36
+ class RateLimitError(TracciaError):
37
+ """Raised when rate limit is exceeded (in strict mode)."""
38
+ pass
39
+
40
+
41
+ class InitializationError(TracciaError):
42
+ """Raised when SDK initialization fails."""
43
+ pass
44
+
45
+
46
+ class InstrumentationError(TracciaError):
47
+ """Raised when instrumentation/patching fails."""
48
+ pass
@@ -0,0 +1,8 @@
1
+ """Exporters for delivering spans to backends."""
2
+
3
+ from traccia.exporter.http_exporter import HttpExporter
4
+ from traccia.exporter.console_exporter import ConsoleExporter
5
+ from traccia.exporter.file_exporter import FileExporter
6
+ from traccia.exporter.otlp_exporter import OTLPExporter
7
+
8
+ __all__ = ["HttpExporter", "ConsoleExporter", "FileExporter", "OTLPExporter"]
@@ -0,0 +1,31 @@
1
+ """Console exporter for developer visibility."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from typing import Iterable
7
+
8
+ from traccia.tracer.span import Span
9
+
10
+
11
+ class ConsoleExporter:
12
+ """Simple exporter that prints spans to stdout (or provided stream)."""
13
+
14
+ def __init__(self, stream=None) -> None:
15
+ self.stream = stream or sys.stdout
16
+
17
+ def export(self, spans: Iterable[Span]) -> bool:
18
+ for span in spans:
19
+ line = (
20
+ f"[span] name={span.name} trace_id={span.context.trace_id} "
21
+ f"span_id={span.context.span_id} status={span.status.name} "
22
+ f"duration_ns={span.duration_ns}"
23
+ )
24
+ if span.attributes:
25
+ line += f" attrs={span.attributes}"
26
+ print(line, file=self.stream)
27
+ return True
28
+
29
+ def shutdown(self) -> None:
30
+ return None
31
+
@@ -0,0 +1,178 @@
1
+ """File exporter for writing traces to a JSON file."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import threading
7
+ from typing import Any, Dict, Iterable, List
8
+
9
+ from traccia.tracer.span import Span
10
+ from traccia.tracer.span import SpanStatus
11
+ from traccia import runtime_config
12
+
13
+
14
+ class FileExporter:
15
+ """
16
+ Exporter that writes spans to a JSON file in JSONL format.
17
+
18
+ Each export() call writes one JSON object (containing resource and scopeSpans)
19
+ per line, following the same format as HttpExporter.
20
+ """
21
+
22
+ def __init__(self, file_path: str = "traces.jsonl", reset_on_start: bool = False) -> None:
23
+ """
24
+ Initialize the file exporter.
25
+
26
+ Args:
27
+ file_path: Path to the file where traces will be written (default: "traces.jsonl")
28
+ reset_on_start: If True, the file will be cleared on first export.
29
+ If False, traces will be appended to the file.
30
+ """
31
+ self.file_path = file_path
32
+ self.reset_on_start = reset_on_start
33
+ self._lock = threading.Lock()
34
+ self._first_export = True
35
+
36
+ def export(self, spans: Iterable[Span]) -> bool:
37
+ """
38
+ Export spans to the file in JSONL format.
39
+
40
+ Args:
41
+ spans: Iterable of Span objects to export
42
+
43
+ Returns:
44
+ True if export succeeded, False otherwise
45
+ """
46
+ spans_list = list(spans)
47
+ if not spans_list:
48
+ return True
49
+
50
+ try:
51
+ payload = self._serialize(spans_list)
52
+ # Convert bytes to string for JSONL format
53
+ json_str = payload.decode("utf-8")
54
+
55
+ with self._lock:
56
+ # Handle reset_on_start: clear file on first export if True
57
+ if self.reset_on_start and self._first_export:
58
+ mode = "w"
59
+ self._first_export = False
60
+ else:
61
+ mode = "a"
62
+ if self._first_export:
63
+ self._first_export = False
64
+
65
+ with open(self.file_path, mode, encoding="utf-8") as f:
66
+ f.write(json_str)
67
+ f.write("\n") # JSONL format: one JSON object per line
68
+
69
+ return True
70
+ except Exception:
71
+ # Silently fail on file write errors to avoid breaking the application
72
+ return False
73
+
74
+ def shutdown(self) -> None:
75
+ """Shutdown the exporter. No cleanup needed for file-based exporter."""
76
+ pass
77
+
78
+ def _serialize(self, spans: List[Span]) -> bytes:
79
+ """
80
+ Serialize spans to JSON bytes using the same format as HttpExporter.
81
+
82
+ This method replicates the serialization logic from HttpExporter._serialize()
83
+ to ensure consistent format across exporters.
84
+ """
85
+ trunc = runtime_config.get_attr_truncation_limit()
86
+
87
+ def _truncate_str(s: str) -> str:
88
+ if trunc is None or trunc <= 0:
89
+ return s
90
+ if len(s) <= trunc:
91
+ return s
92
+ return s[: max(0, trunc - 1)] + "…"
93
+
94
+ def _sanitize(value: Any, depth: int = 0) -> Any:
95
+ # Keep payload JSON-safe and bounded.
96
+ if value is None or isinstance(value, (bool, int, float)):
97
+ return value
98
+ if isinstance(value, str):
99
+ return _truncate_str(value)
100
+ if depth >= 6:
101
+ return _truncate_str(repr(value))
102
+ if isinstance(value, dict):
103
+ out: Dict[str, Any] = {}
104
+ for k, v in value.items():
105
+ try:
106
+ key = str(k)
107
+ except Exception:
108
+ key = "<unstringifiable>"
109
+ out[_truncate_str(key)] = _sanitize(v, depth + 1)
110
+ return out
111
+ if isinstance(value, (list, tuple, set)):
112
+ return [_sanitize(v, depth + 1) for v in list(value)[:100]]
113
+ # Fallback for unknown objects
114
+ return _truncate_str(repr(value))
115
+
116
+ def _status_code(status: SpanStatus) -> int:
117
+ # OTLP-style: 0=UNSET, 1=OK, 2=ERROR
118
+ if status == SpanStatus.OK:
119
+ return 1
120
+ if status == SpanStatus.ERROR:
121
+ return 2
122
+ return 0
123
+
124
+ def to_event(ev):
125
+ return {
126
+ "name": ev.get("name"),
127
+ "attributes": _sanitize(ev.get("attributes", {})),
128
+ "timestamp_ns": ev.get("timestamp_ns"),
129
+ }
130
+
131
+ resource_attrs: Dict[str, Any] = {}
132
+ try:
133
+ if spans and getattr(spans[0], "tracer", None) is not None:
134
+ provider = getattr(spans[0].tracer, "_provider", None)
135
+ if provider is not None:
136
+ resource_attrs.update(getattr(provider, "resource", {}) or {})
137
+ except Exception:
138
+ resource_attrs = resource_attrs
139
+
140
+ # Add optional session/user identifiers to resource for easier querying.
141
+ if runtime_config.get_session_id():
142
+ resource_attrs.setdefault("session.id", runtime_config.get_session_id())
143
+ if runtime_config.get_user_id():
144
+ resource_attrs.setdefault("user.id", runtime_config.get_user_id())
145
+ if runtime_config.get_tenant_id():
146
+ resource_attrs.setdefault("tenant.id", runtime_config.get_tenant_id())
147
+ if runtime_config.get_project_id():
148
+ resource_attrs.setdefault("project.id", runtime_config.get_project_id())
149
+ if runtime_config.get_debug():
150
+ resource_attrs.setdefault("trace.debug", True)
151
+
152
+ payload = {
153
+ "resource": {"attributes": _sanitize(resource_attrs)},
154
+ "scopeSpans": [
155
+ {
156
+ "scope": {"name": "agent-tracing-sdk", "version": "0.1.0"},
157
+ "spans": [
158
+ {
159
+ "traceId": span.context.trace_id,
160
+ "spanId": span.context.span_id,
161
+ "parentSpanId": span.parent_span_id,
162
+ "name": span.name,
163
+ "startTimeUnixNano": span.start_time_ns,
164
+ "endTimeUnixNano": span.end_time_ns,
165
+ "attributes": _sanitize(span.attributes),
166
+ "events": [to_event(e) for e in span.events],
167
+ "status": {
168
+ "code": _status_code(span.status),
169
+ "message": span.status_description or "",
170
+ },
171
+ }
172
+ for span in spans
173
+ ],
174
+ }
175
+ ]
176
+ }
177
+ # Ensure we never raise due to serialization edge cases.
178
+ return json.dumps(payload, ensure_ascii=False).encode("utf-8")