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,577 @@
|
|
|
1
|
+
"""Adapter layer wrapping OpenTelemetry components to match Traccia API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
import traceback
|
|
7
|
+
from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from opentelemetry import trace as otel_trace_api
|
|
10
|
+
from opentelemetry.sdk.trace import TracerProvider as OTelTracerProvider
|
|
11
|
+
from opentelemetry.sdk.trace import ReadableSpan
|
|
12
|
+
from opentelemetry.sdk.resources import Resource as OTelResource
|
|
13
|
+
from opentelemetry.trace import Status as OTelStatus, StatusCode as OTelStatusCode
|
|
14
|
+
from opentelemetry.trace import SpanContext as OTelSpanContext
|
|
15
|
+
from opentelemetry.trace import Span as OTelSpan, NonRecordingSpan
|
|
16
|
+
|
|
17
|
+
from traccia.tracer.otel_utils import (
|
|
18
|
+
otel_trace_id_to_traccia,
|
|
19
|
+
otel_span_id_to_traccia,
|
|
20
|
+
traccia_id_to_otel_trace_id,
|
|
21
|
+
traccia_id_to_otel_span_id,
|
|
22
|
+
otel_timestamp_to_ns,
|
|
23
|
+
)
|
|
24
|
+
from traccia.tracer.span_context import SpanContext as TracciaSpanContext
|
|
25
|
+
from traccia.tracer.span import SpanStatus as TracciaSpanStatus
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from traccia.tracer.tracer import Tracer as TracciaTracer
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TracciaSpanContextAdapter:
|
|
32
|
+
"""Adapter wrapping OpenTelemetry SpanContext to match Traccia SpanContext API."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, otel_context: OTelSpanContext):
|
|
35
|
+
"""
|
|
36
|
+
Initialize adapter with OpenTelemetry SpanContext.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
otel_context: OpenTelemetry SpanContext instance
|
|
40
|
+
"""
|
|
41
|
+
self._otel_context = otel_context
|
|
42
|
+
# Convert int IDs to hex strings for Traccia compatibility
|
|
43
|
+
self.trace_id = otel_trace_id_to_traccia(otel_context.trace_id)
|
|
44
|
+
self.span_id = otel_span_id_to_traccia(otel_context.span_id)
|
|
45
|
+
self.trace_flags = otel_context.trace_flags.sampled if hasattr(otel_context.trace_flags, 'sampled') else (1 if otel_context.is_valid else 0)
|
|
46
|
+
# Convert TraceState to string
|
|
47
|
+
trace_state = otel_context.trace_state
|
|
48
|
+
if trace_state:
|
|
49
|
+
# Convert OTel TraceState to W3C format string
|
|
50
|
+
items = []
|
|
51
|
+
for key, value in trace_state.items():
|
|
52
|
+
items.append(f"{key}={value}")
|
|
53
|
+
self.trace_state = ",".join(items) if items else None
|
|
54
|
+
else:
|
|
55
|
+
self.trace_state = None
|
|
56
|
+
|
|
57
|
+
def is_valid(self) -> bool:
|
|
58
|
+
"""Check if the span context is valid."""
|
|
59
|
+
return self._otel_context.is_valid
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def from_traccia(traccia_context: TracciaSpanContext) -> OTelSpanContext:
|
|
63
|
+
"""
|
|
64
|
+
Convert Traccia SpanContext to OpenTelemetry SpanContext.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
traccia_context: Traccia SpanContext instance
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
OpenTelemetry SpanContext
|
|
71
|
+
"""
|
|
72
|
+
from opentelemetry.trace import TraceFlags, TraceState
|
|
73
|
+
|
|
74
|
+
trace_id = traccia_id_to_otel_trace_id(traccia_context.trace_id)
|
|
75
|
+
span_id = traccia_id_to_otel_span_id(traccia_context.span_id)
|
|
76
|
+
trace_flags = TraceFlags(traccia_context.trace_flags)
|
|
77
|
+
|
|
78
|
+
# Parse trace_state string to OTel TraceState
|
|
79
|
+
trace_state = None
|
|
80
|
+
if traccia_context.trace_state:
|
|
81
|
+
# Parse W3C format: key1=value1,key2=value2
|
|
82
|
+
items = []
|
|
83
|
+
for pair in traccia_context.trace_state.split(','):
|
|
84
|
+
if '=' in pair:
|
|
85
|
+
key, value = pair.split('=', 1)
|
|
86
|
+
items.append((key.strip(), value.strip()))
|
|
87
|
+
if items:
|
|
88
|
+
trace_state = TraceState(items)
|
|
89
|
+
|
|
90
|
+
return OTelSpanContext(
|
|
91
|
+
trace_id=trace_id,
|
|
92
|
+
span_id=span_id,
|
|
93
|
+
is_remote=False,
|
|
94
|
+
trace_flags=trace_flags,
|
|
95
|
+
trace_state=trace_state or TraceState(),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TracciaSpanAdapter:
|
|
100
|
+
"""Adapter wrapping OpenTelemetry Span to match Traccia Span API."""
|
|
101
|
+
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
otel_span: OTelSpan,
|
|
105
|
+
tracer: "TracciaTracerAdapter",
|
|
106
|
+
parent_span_id: Optional[str] = None,
|
|
107
|
+
):
|
|
108
|
+
"""
|
|
109
|
+
Initialize adapter with OpenTelemetry Span.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
otel_span: OpenTelemetry Span instance
|
|
113
|
+
tracer: Traccia TracerAdapter that created this span
|
|
114
|
+
parent_span_id: Parent span ID in hex string format
|
|
115
|
+
"""
|
|
116
|
+
self._otel_span = otel_span
|
|
117
|
+
self.tracer = tracer
|
|
118
|
+
self.parent_span_id = parent_span_id
|
|
119
|
+
|
|
120
|
+
# Get span context
|
|
121
|
+
otel_context = otel_span.get_span_context()
|
|
122
|
+
self.context = TracciaSpanContextAdapter(otel_context)
|
|
123
|
+
|
|
124
|
+
# Get name
|
|
125
|
+
if isinstance(otel_span, ReadableSpan):
|
|
126
|
+
self.name = otel_span.name
|
|
127
|
+
else:
|
|
128
|
+
# For non-readable spans, we need to track name separately
|
|
129
|
+
self.name = getattr(otel_span, '_traccia_name', 'unknown')
|
|
130
|
+
|
|
131
|
+
# Initialize attributes dict - will be kept in sync with OTel span
|
|
132
|
+
self.attributes: Dict[str, Any] = {}
|
|
133
|
+
if isinstance(otel_span, ReadableSpan):
|
|
134
|
+
if otel_span.attributes:
|
|
135
|
+
self.attributes = dict(otel_span.attributes)
|
|
136
|
+
|
|
137
|
+
# Initialize events list - will be kept in sync
|
|
138
|
+
self.events: List[Dict[str, Any]] = []
|
|
139
|
+
if isinstance(otel_span, ReadableSpan):
|
|
140
|
+
if otel_span.events:
|
|
141
|
+
for event in otel_span.events:
|
|
142
|
+
self.events.append({
|
|
143
|
+
"name": event.name,
|
|
144
|
+
"attributes": dict(event.attributes) if event.attributes else {},
|
|
145
|
+
"timestamp_ns": event.timestamp,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
# Convert status
|
|
149
|
+
if isinstance(otel_span, ReadableSpan):
|
|
150
|
+
self.status = self._convert_status(otel_span.status)
|
|
151
|
+
self.status_description = otel_span.status.description if otel_span.status else None
|
|
152
|
+
else:
|
|
153
|
+
self.status = TracciaSpanStatus.UNSET
|
|
154
|
+
self.status_description = None
|
|
155
|
+
|
|
156
|
+
# Get timestamps
|
|
157
|
+
if isinstance(otel_span, ReadableSpan):
|
|
158
|
+
self.start_time_ns = otel_span.start_time if otel_span.start_time else time.time_ns()
|
|
159
|
+
self.end_time_ns = otel_span.end_time if otel_span.end_time else None
|
|
160
|
+
else:
|
|
161
|
+
self.start_time_ns = time.time_ns()
|
|
162
|
+
self.end_time_ns = None
|
|
163
|
+
|
|
164
|
+
self._activation_tokens: Optional[Tuple] = None
|
|
165
|
+
self._ended = False
|
|
166
|
+
|
|
167
|
+
# Apply tracestate enrichment (matching Traccia behavior)
|
|
168
|
+
self._enrich_tracestate()
|
|
169
|
+
|
|
170
|
+
def _enrich_tracestate(self) -> None:
|
|
171
|
+
"""Enrich tracestate with runtime metadata (tenant, project, debug)."""
|
|
172
|
+
try:
|
|
173
|
+
from traccia.context.propagators import format_tracestate, parse_tracestate
|
|
174
|
+
from traccia import runtime_config
|
|
175
|
+
|
|
176
|
+
base = parse_tracestate(self.context.trace_state or "")
|
|
177
|
+
if runtime_config.get_tenant_id():
|
|
178
|
+
base.setdefault("tenant", runtime_config.get_tenant_id())
|
|
179
|
+
if runtime_config.get_project_id():
|
|
180
|
+
base.setdefault("project", runtime_config.get_project_id())
|
|
181
|
+
if runtime_config.get_debug():
|
|
182
|
+
base.setdefault("dbg", "1")
|
|
183
|
+
|
|
184
|
+
ts = format_tracestate(base)
|
|
185
|
+
if ts:
|
|
186
|
+
# Update the context
|
|
187
|
+
self.context.trace_state = ts
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def _convert_status(otel_status: OTelStatus) -> TracciaSpanStatus:
|
|
193
|
+
"""Convert OpenTelemetry Status to Traccia SpanStatus."""
|
|
194
|
+
if not otel_status:
|
|
195
|
+
return TracciaSpanStatus.UNSET
|
|
196
|
+
|
|
197
|
+
if otel_status.status_code == OTelStatusCode.OK:
|
|
198
|
+
return TracciaSpanStatus.OK
|
|
199
|
+
elif otel_status.status_code == OTelStatusCode.ERROR:
|
|
200
|
+
return TracciaSpanStatus.ERROR
|
|
201
|
+
else:
|
|
202
|
+
return TracciaSpanStatus.UNSET
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def _convert_status_to_otel(traccia_status: TracciaSpanStatus, description: Optional[str] = None) -> OTelStatus:
|
|
206
|
+
"""Convert Traccia SpanStatus to OpenTelemetry Status."""
|
|
207
|
+
if traccia_status == TracciaSpanStatus.OK:
|
|
208
|
+
return OTelStatus(status_code=OTelStatusCode.OK, description=description)
|
|
209
|
+
elif traccia_status == TracciaSpanStatus.ERROR:
|
|
210
|
+
return OTelStatus(status_code=OTelStatusCode.ERROR, description=description)
|
|
211
|
+
else:
|
|
212
|
+
return OTelStatus(status_code=OTelStatusCode.UNSET, description=description)
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def duration_ns(self) -> Optional[int]:
|
|
216
|
+
"""Get span duration in nanoseconds."""
|
|
217
|
+
if self.end_time_ns is None:
|
|
218
|
+
return None
|
|
219
|
+
return self.end_time_ns - self.start_time_ns
|
|
220
|
+
|
|
221
|
+
def set_attribute(self, key: str, value: Any) -> None:
|
|
222
|
+
"""Set an attribute on the span."""
|
|
223
|
+
# Only set on OTel span if it's not ended
|
|
224
|
+
# OTel spans don't allow setting attributes after end, but Traccia does
|
|
225
|
+
# We'll allow it for Traccia compatibility but only update our local dict
|
|
226
|
+
try:
|
|
227
|
+
if not self._ended:
|
|
228
|
+
self._otel_span.set_attribute(key, value)
|
|
229
|
+
except Exception:
|
|
230
|
+
# Span may be ended, just update local dict
|
|
231
|
+
pass
|
|
232
|
+
self.attributes[key] = value
|
|
233
|
+
|
|
234
|
+
def add_event(
|
|
235
|
+
self,
|
|
236
|
+
name: str,
|
|
237
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
238
|
+
timestamp_ns: Optional[int] = None,
|
|
239
|
+
) -> None:
|
|
240
|
+
"""Add an event to the span."""
|
|
241
|
+
event_dict = {
|
|
242
|
+
"name": name,
|
|
243
|
+
"attributes": dict(attributes) if attributes else {},
|
|
244
|
+
"timestamp_ns": timestamp_ns or time.time_ns(),
|
|
245
|
+
}
|
|
246
|
+
self.events.append(event_dict)
|
|
247
|
+
|
|
248
|
+
# Add to OTel span
|
|
249
|
+
self._otel_span.add_event(
|
|
250
|
+
name=name,
|
|
251
|
+
attributes=attributes,
|
|
252
|
+
timestamp=timestamp_ns,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def record_exception(self, error: BaseException) -> None:
|
|
256
|
+
"""Record an exception event on the span."""
|
|
257
|
+
# Add to Traccia events list
|
|
258
|
+
self.add_event(
|
|
259
|
+
"exception",
|
|
260
|
+
{
|
|
261
|
+
"exception.type": error.__class__.__name__,
|
|
262
|
+
"exception.message": str(error),
|
|
263
|
+
"exception.stacktrace": "".join(
|
|
264
|
+
traceback.format_exception(error.__class__, error, error.__traceback__)
|
|
265
|
+
),
|
|
266
|
+
},
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Record on OTel span
|
|
270
|
+
self._otel_span.record_exception(error)
|
|
271
|
+
|
|
272
|
+
# Set error status
|
|
273
|
+
self.set_status(TracciaSpanStatus.ERROR, str(error))
|
|
274
|
+
|
|
275
|
+
def set_status(self, status: TracciaSpanStatus, description: Optional[str] = None) -> None:
|
|
276
|
+
"""Set the span status."""
|
|
277
|
+
self.status = status
|
|
278
|
+
self.status_description = description
|
|
279
|
+
|
|
280
|
+
# Convert and set on OTel span
|
|
281
|
+
otel_status = self._convert_status_to_otel(status, description)
|
|
282
|
+
self._otel_span.set_status(otel_status)
|
|
283
|
+
|
|
284
|
+
def end(self) -> None:
|
|
285
|
+
"""End the span."""
|
|
286
|
+
if self._ended:
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
self.end_time_ns = time.time_ns()
|
|
290
|
+
if self.status == TracciaSpanStatus.UNSET:
|
|
291
|
+
self.status = TracciaSpanStatus.OK
|
|
292
|
+
|
|
293
|
+
self._ended = True
|
|
294
|
+
|
|
295
|
+
# End OTel span
|
|
296
|
+
self._otel_span.end(end_time=self.end_time_ns)
|
|
297
|
+
|
|
298
|
+
# Notify tracer's provider
|
|
299
|
+
self.tracer._on_span_end(self)
|
|
300
|
+
|
|
301
|
+
def __enter__(self) -> "TracciaSpanAdapter":
|
|
302
|
+
"""Enter context manager."""
|
|
303
|
+
self._activation_tokens = self.tracer._activate_span(self)
|
|
304
|
+
return self
|
|
305
|
+
|
|
306
|
+
def __exit__(self, exc_type, exc, tb) -> bool:
|
|
307
|
+
"""Exit context manager."""
|
|
308
|
+
try:
|
|
309
|
+
if exc:
|
|
310
|
+
self.record_exception(exc)
|
|
311
|
+
self.end()
|
|
312
|
+
finally:
|
|
313
|
+
if self._activation_tokens:
|
|
314
|
+
self.tracer._deactivate_span(self._activation_tokens)
|
|
315
|
+
self._activation_tokens = None
|
|
316
|
+
return False
|
|
317
|
+
|
|
318
|
+
async def __aenter__(self) -> "TracciaSpanAdapter":
|
|
319
|
+
"""Enter async context manager."""
|
|
320
|
+
self._activation_tokens = self.tracer._activate_span(self)
|
|
321
|
+
return self
|
|
322
|
+
|
|
323
|
+
async def __aexit__(self, exc_type, exc, tb) -> bool:
|
|
324
|
+
"""Exit async context manager."""
|
|
325
|
+
try:
|
|
326
|
+
if exc:
|
|
327
|
+
self.record_exception(exc)
|
|
328
|
+
self.end()
|
|
329
|
+
finally:
|
|
330
|
+
if self._activation_tokens:
|
|
331
|
+
self.tracer._deactivate_span(self._activation_tokens)
|
|
332
|
+
self._activation_tokens = None
|
|
333
|
+
return False
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class TracciaTracerAdapter:
|
|
337
|
+
"""Adapter wrapping OpenTelemetry Tracer to match Traccia Tracer API."""
|
|
338
|
+
|
|
339
|
+
def __init__(self, otel_tracer: otel_trace_api.Tracer, provider: "TracciaTracerProviderAdapter", instrumentation_scope: str):
|
|
340
|
+
"""
|
|
341
|
+
Initialize adapter with OpenTelemetry Tracer.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
otel_tracer: OpenTelemetry Tracer instance
|
|
345
|
+
provider: Traccia TracerProviderAdapter that created this tracer
|
|
346
|
+
instrumentation_scope: Instrumentation scope name
|
|
347
|
+
"""
|
|
348
|
+
self._otel_tracer = otel_tracer
|
|
349
|
+
self._provider = provider
|
|
350
|
+
self.instrumentation_scope = instrumentation_scope
|
|
351
|
+
|
|
352
|
+
def start_span(
|
|
353
|
+
self,
|
|
354
|
+
name: str,
|
|
355
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
356
|
+
parent: Optional["TracciaSpanAdapter"] = None,
|
|
357
|
+
parent_context: Optional[TracciaSpanContext] = None,
|
|
358
|
+
) -> TracciaSpanAdapter:
|
|
359
|
+
"""
|
|
360
|
+
Start a new span.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
name: Span name
|
|
364
|
+
attributes: Optional attributes dictionary
|
|
365
|
+
parent: Optional parent Traccia span
|
|
366
|
+
parent_context: Optional parent Traccia span context
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
TracciaSpanAdapter wrapping the new OTel span
|
|
370
|
+
"""
|
|
371
|
+
from traccia.context import context as span_context
|
|
372
|
+
|
|
373
|
+
# Determine parent
|
|
374
|
+
parent_span = parent or span_context.get_current_span()
|
|
375
|
+
|
|
376
|
+
# Convert parent context if provided
|
|
377
|
+
otel_parent_context = None
|
|
378
|
+
parent_span_id = None
|
|
379
|
+
|
|
380
|
+
if parent_span:
|
|
381
|
+
# Use parent span's context
|
|
382
|
+
parent_span_id = parent_span.context.span_id
|
|
383
|
+
if hasattr(parent_span, '_otel_span'):
|
|
384
|
+
otel_parent_context = otel_trace_api.set_span_in_context(parent_span._otel_span)
|
|
385
|
+
else:
|
|
386
|
+
# Parent is native Traccia span, convert its context
|
|
387
|
+
otel_parent_ctx = TracciaSpanContextAdapter.from_traccia(parent_span.context)
|
|
388
|
+
otel_parent_context = otel_trace_api.set_span_in_context(
|
|
389
|
+
NonRecordingSpan(otel_parent_ctx)
|
|
390
|
+
)
|
|
391
|
+
elif parent_context and parent_context.is_valid():
|
|
392
|
+
# Convert provided parent context
|
|
393
|
+
parent_span_id = parent_context.span_id
|
|
394
|
+
otel_parent_ctx = TracciaSpanContextAdapter.from_traccia(parent_context)
|
|
395
|
+
otel_parent_context = otel_trace_api.set_span_in_context(
|
|
396
|
+
NonRecordingSpan(otel_parent_ctx)
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# Start OTel span
|
|
400
|
+
otel_span = self._otel_tracer.start_span(
|
|
401
|
+
name=name,
|
|
402
|
+
attributes=attributes,
|
|
403
|
+
context=otel_parent_context,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
# Store name for non-readable spans
|
|
407
|
+
if not isinstance(otel_span, ReadableSpan):
|
|
408
|
+
otel_span._traccia_name = name
|
|
409
|
+
|
|
410
|
+
# Wrap in adapter
|
|
411
|
+
return TracciaSpanAdapter(otel_span, self, parent_span_id)
|
|
412
|
+
|
|
413
|
+
def start_as_current_span(
|
|
414
|
+
self,
|
|
415
|
+
name: str,
|
|
416
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
417
|
+
parent: Optional["TracciaSpanAdapter"] = None,
|
|
418
|
+
parent_context: Optional[TracciaSpanContext] = None,
|
|
419
|
+
) -> TracciaSpanAdapter:
|
|
420
|
+
"""
|
|
421
|
+
Start a span and set it as current.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
name: Span name
|
|
425
|
+
attributes: Optional attributes dictionary
|
|
426
|
+
parent: Optional parent Traccia span
|
|
427
|
+
parent_context: Optional parent Traccia span context
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
TracciaSpanAdapter wrapping the new OTel span
|
|
431
|
+
"""
|
|
432
|
+
return self.start_span(
|
|
433
|
+
name=name,
|
|
434
|
+
attributes=attributes,
|
|
435
|
+
parent=parent,
|
|
436
|
+
parent_context=parent_context,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
def get_current_span(self) -> Optional["TracciaSpanAdapter"]:
|
|
440
|
+
"""Get the current span."""
|
|
441
|
+
from traccia.context import context as span_context
|
|
442
|
+
return span_context.get_current_span()
|
|
443
|
+
|
|
444
|
+
def _activate_span(self, span: "TracciaSpanAdapter"):
|
|
445
|
+
"""Activate a span (set as current)."""
|
|
446
|
+
from traccia.context import context as span_context
|
|
447
|
+
return span_context.push_span(span)
|
|
448
|
+
|
|
449
|
+
def _deactivate_span(self, tokens) -> None:
|
|
450
|
+
"""Deactivate a span (restore previous)."""
|
|
451
|
+
from traccia.context import context as span_context
|
|
452
|
+
span_context.pop_span(tokens)
|
|
453
|
+
|
|
454
|
+
def _on_span_end(self, span: "TracciaSpanAdapter") -> None:
|
|
455
|
+
"""Called when a span ends."""
|
|
456
|
+
self._provider._notify_span_end(span)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class TracciaTracerProviderAdapter:
|
|
460
|
+
"""Adapter wrapping OpenTelemetry TracerProvider to match Traccia TracerProvider API."""
|
|
461
|
+
|
|
462
|
+
def __init__(self, resource: Optional[Dict[str, str]] = None):
|
|
463
|
+
"""
|
|
464
|
+
Initialize adapter with OpenTelemetry TracerProvider.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
resource: Optional resource attributes dictionary
|
|
468
|
+
"""
|
|
469
|
+
# Convert resource dict to OTel Resource
|
|
470
|
+
otel_resource = OTelResource.create(resource or {})
|
|
471
|
+
self._otel_provider = OTelTracerProvider(resource=otel_resource)
|
|
472
|
+
|
|
473
|
+
# Store resource as dict for Traccia compatibility
|
|
474
|
+
self.resource = resource or {}
|
|
475
|
+
|
|
476
|
+
# Tracers cache
|
|
477
|
+
self._tracers: Dict[str, TracciaTracerAdapter] = {}
|
|
478
|
+
self._span_processors: List[Any] = []
|
|
479
|
+
self.sampler: Optional[Any] = None
|
|
480
|
+
|
|
481
|
+
def get_tracer(self, name: str) -> TracciaTracerAdapter:
|
|
482
|
+
"""
|
|
483
|
+
Get a tracer by name.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
name: Instrumentation scope name
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
TracciaTracerAdapter wrapping OTel Tracer
|
|
490
|
+
"""
|
|
491
|
+
if name in self._tracers:
|
|
492
|
+
return self._tracers[name]
|
|
493
|
+
|
|
494
|
+
# Get OTel tracer
|
|
495
|
+
otel_tracer = self._otel_provider.get_tracer(name)
|
|
496
|
+
|
|
497
|
+
# Wrap in adapter
|
|
498
|
+
tracer = TracciaTracerAdapter(otel_tracer, self, name)
|
|
499
|
+
self._tracers[name] = tracer
|
|
500
|
+
return tracer
|
|
501
|
+
|
|
502
|
+
def add_span_processor(self, processor: Any) -> None:
|
|
503
|
+
"""
|
|
504
|
+
Add a span processor.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
processor: Traccia SpanProcessor instance
|
|
508
|
+
"""
|
|
509
|
+
self._span_processors.append(processor)
|
|
510
|
+
|
|
511
|
+
# If processor has OTel compatibility, add to OTel provider
|
|
512
|
+
# For now, we'll handle Traccia processors via _notify_span_end
|
|
513
|
+
|
|
514
|
+
def set_sampler(self, sampler: Any) -> None:
|
|
515
|
+
"""
|
|
516
|
+
Set the sampler.
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
sampler: Traccia Sampler instance
|
|
520
|
+
"""
|
|
521
|
+
self.sampler = sampler
|
|
522
|
+
# Note: OTel sampler is set at provider creation time
|
|
523
|
+
# For dynamic sampler changes, we'd need to recreate the provider
|
|
524
|
+
|
|
525
|
+
def get_sampler(self) -> Optional[Any]:
|
|
526
|
+
"""Get the current sampler."""
|
|
527
|
+
return self.sampler
|
|
528
|
+
|
|
529
|
+
def _notify_span_end(self, span: TracciaSpanAdapter) -> None:
|
|
530
|
+
"""
|
|
531
|
+
Notify processors that a span has ended.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
span: The span that ended
|
|
535
|
+
"""
|
|
536
|
+
for processor in list(self._span_processors):
|
|
537
|
+
try:
|
|
538
|
+
processor.on_end(span)
|
|
539
|
+
except Exception:
|
|
540
|
+
# Processors should not crash tracing
|
|
541
|
+
continue
|
|
542
|
+
|
|
543
|
+
def force_flush(self, timeout: Optional[float] = None) -> None:
|
|
544
|
+
"""Force flush all processors."""
|
|
545
|
+
# Flush OTel provider
|
|
546
|
+
self._otel_provider.force_flush(timeout_millis=int(timeout * 1000) if timeout else 30000)
|
|
547
|
+
|
|
548
|
+
# Flush Traccia processors
|
|
549
|
+
for processor in list(self._span_processors):
|
|
550
|
+
try:
|
|
551
|
+
processor.force_flush(timeout=timeout)
|
|
552
|
+
except Exception:
|
|
553
|
+
continue
|
|
554
|
+
|
|
555
|
+
def shutdown(self) -> None:
|
|
556
|
+
"""Shutdown the provider and all processors."""
|
|
557
|
+
# Shutdown OTel provider
|
|
558
|
+
self._otel_provider.shutdown()
|
|
559
|
+
|
|
560
|
+
# Shutdown Traccia processors
|
|
561
|
+
for processor in list(self._span_processors):
|
|
562
|
+
try:
|
|
563
|
+
processor.shutdown()
|
|
564
|
+
except Exception:
|
|
565
|
+
continue
|
|
566
|
+
|
|
567
|
+
@staticmethod
|
|
568
|
+
def generate_trace_id() -> str:
|
|
569
|
+
"""Generate a new trace ID in Traccia format (hex string)."""
|
|
570
|
+
import secrets
|
|
571
|
+
return secrets.token_hex(16)
|
|
572
|
+
|
|
573
|
+
@staticmethod
|
|
574
|
+
def generate_span_id() -> str:
|
|
575
|
+
"""Generate a new span ID in Traccia format (hex string)."""
|
|
576
|
+
import secrets
|
|
577
|
+
return secrets.token_hex(8)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""DEPRECATED: Use traccia.utils.helpers instead.
|
|
2
|
+
|
|
3
|
+
This file is kept for backward compatibility with old adapter code.
|
|
4
|
+
All new code should use traccia.utils.helpers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
# Re-export from utils.helpers for backward compatibility
|
|
10
|
+
from traccia.utils.helpers import (
|
|
11
|
+
format_trace_id as otel_trace_id_to_traccia,
|
|
12
|
+
format_span_id as otel_span_id_to_traccia,
|
|
13
|
+
parse_trace_id as traccia_id_to_otel_trace_id,
|
|
14
|
+
parse_span_id as traccia_id_to_otel_span_id,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Timestamp functions (passthrough)
|
|
18
|
+
def otel_timestamp_to_ns(otel_time: int) -> int:
|
|
19
|
+
"""Convert OpenTelemetry timestamp to nanoseconds (passthrough)."""
|
|
20
|
+
return otel_time
|
|
21
|
+
|
|
22
|
+
def ns_to_otel_timestamp(ns: int) -> int:
|
|
23
|
+
"""Convert nanoseconds to OpenTelemetry timestamp (passthrough)."""
|
|
24
|
+
return ns
|