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.
- traccia/__init__.py +73 -0
- traccia/auto.py +736 -0
- traccia/auto_instrumentation.py +74 -0
- traccia/cli.py +349 -0
- traccia/config.py +693 -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 +20 -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 +178 -0
- traccia/instrumentation/requests.py +68 -0
- traccia/integrations/__init__.py +22 -0
- traccia/integrations/langchain/__init__.py +14 -0
- traccia/integrations/langchain/callback.py +418 -0
- traccia/integrations/langchain/utils.py +129 -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 +106 -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.5.dist-info}/METADATA +32 -15
- traccia-0.1.5.dist-info/RECORD +53 -0
- traccia-0.1.5.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.5.dist-info}/WHEEL +0 -0
- {traccia-0.1.2.dist-info → traccia-0.1.5.dist-info}/entry_points.txt +0 -0
- {traccia-0.1.2.dist-info → traccia-0.1.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""TracerProvider using OpenTelemetry SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from opentelemetry.sdk.trace import TracerProvider as OTelTracerProvider
|
|
9
|
+
from opentelemetry.sdk.trace import SpanProcessor as OTelSpanProcessor
|
|
10
|
+
from opentelemetry.sdk.resources import Resource as OTelResource
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SpanProcessor:
|
|
14
|
+
"""
|
|
15
|
+
Base span processor interface for Traccia enrichment processors.
|
|
16
|
+
|
|
17
|
+
Enrichment processors run BEFORE span.end() (span is mutable).
|
|
18
|
+
Export processors use OTel's SpanProcessor interface (run AFTER span.end()).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def on_end(self, span) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Called when a span ends.
|
|
24
|
+
|
|
25
|
+
Note: This is called BEFORE the OTel span ends, so the span is still mutable.
|
|
26
|
+
You can call span.set_attribute() here.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
span: Traccia Span instance (mutable)
|
|
30
|
+
"""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
def shutdown(self) -> None:
|
|
34
|
+
"""Shutdown the processor."""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
def force_flush(self, timeout: Optional[float] = None) -> None:
|
|
38
|
+
"""Force flush any pending spans."""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TracerProvider:
|
|
43
|
+
"""
|
|
44
|
+
TracerProvider using OpenTelemetry SDK.
|
|
45
|
+
|
|
46
|
+
Separates enrichment processors (Traccia) from export processors (OTel).
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, resource: Optional[Dict[str, str]] = None) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Initialize TracerProvider with OpenTelemetry.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
resource: Resource attributes dictionary (converted to OTel Resource)
|
|
55
|
+
"""
|
|
56
|
+
# Convert resource dict to OTel Resource
|
|
57
|
+
otel_resource = OTelResource.create(resource or {})
|
|
58
|
+
self._otel_provider = OTelTracerProvider(resource=otel_resource)
|
|
59
|
+
|
|
60
|
+
# Store resource as dict for backward compatibility
|
|
61
|
+
self.resource = resource or {}
|
|
62
|
+
|
|
63
|
+
# Separate enrichment vs export processors
|
|
64
|
+
self._enrichment_processors: List[SpanProcessor] = [] # Traccia processors
|
|
65
|
+
self._export_processors: List[OTelSpanProcessor] = [] # OTel processors
|
|
66
|
+
|
|
67
|
+
# Tracers cache
|
|
68
|
+
self._tracers: Dict[str, Any] = {}
|
|
69
|
+
self._lock = threading.Lock()
|
|
70
|
+
|
|
71
|
+
# Optional sampler used for head-based sampling at trace start
|
|
72
|
+
self.sampler: Optional[Any] = None
|
|
73
|
+
|
|
74
|
+
def get_tracer(self, name: str) -> "Tracer":
|
|
75
|
+
"""
|
|
76
|
+
Get a tracer by name.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
name: Instrumentation scope name
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Traccia Tracer instance (wraps OTel Tracer)
|
|
83
|
+
"""
|
|
84
|
+
with self._lock:
|
|
85
|
+
tracer = self._tracers.get(name)
|
|
86
|
+
if tracer is None:
|
|
87
|
+
from traccia.tracer.tracer import Tracer
|
|
88
|
+
tracer = Tracer(self, name)
|
|
89
|
+
self._tracers[name] = tracer
|
|
90
|
+
return tracer
|
|
91
|
+
|
|
92
|
+
def add_span_processor(self, processor: Any) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Add a span processor.
|
|
95
|
+
|
|
96
|
+
Separates enrichment processors (Traccia) from export processors (OTel).
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
processor: SpanProcessor instance (OTel-compatible or Traccia-compatible)
|
|
100
|
+
"""
|
|
101
|
+
# If processor is OTel-compatible, add to OTel provider
|
|
102
|
+
if isinstance(processor, OTelSpanProcessor):
|
|
103
|
+
self._otel_provider.add_span_processor(processor)
|
|
104
|
+
self._export_processors.append(processor)
|
|
105
|
+
else:
|
|
106
|
+
# Traccia enrichment processor
|
|
107
|
+
self._enrichment_processors.append(processor)
|
|
108
|
+
|
|
109
|
+
def set_sampler(self, sampler: Any) -> None:
|
|
110
|
+
"""
|
|
111
|
+
Set the sampler.
|
|
112
|
+
|
|
113
|
+
Note: OTel samplers are set at provider creation time.
|
|
114
|
+
This method stores the sampler for reference but doesn't
|
|
115
|
+
apply it dynamically. For dynamic sampling, recreate the provider.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
sampler: Sampler instance
|
|
119
|
+
"""
|
|
120
|
+
self.sampler = sampler
|
|
121
|
+
# Note: OTel doesn't support dynamic sampler changes
|
|
122
|
+
# The sampler would need to be set at provider creation
|
|
123
|
+
|
|
124
|
+
def get_sampler(self) -> Optional[Any]:
|
|
125
|
+
"""Get the current sampler."""
|
|
126
|
+
return self.sampler
|
|
127
|
+
|
|
128
|
+
def force_flush(self, timeout: Optional[float] = None) -> None:
|
|
129
|
+
"""Force flush all processors."""
|
|
130
|
+
# Flush OTel processors
|
|
131
|
+
self._otel_provider.force_flush(timeout_millis=int(timeout * 1000) if timeout else 30000)
|
|
132
|
+
|
|
133
|
+
# Flush Traccia enrichment processors
|
|
134
|
+
for processor in self._enrichment_processors:
|
|
135
|
+
try:
|
|
136
|
+
processor.force_flush(timeout=timeout)
|
|
137
|
+
except Exception:
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
def shutdown(self) -> None:
|
|
141
|
+
"""Shutdown the provider and all processors."""
|
|
142
|
+
# Shutdown OTel processors
|
|
143
|
+
self._otel_provider.shutdown()
|
|
144
|
+
|
|
145
|
+
# Shutdown Traccia enrichment processors
|
|
146
|
+
for processor in self._enrichment_processors:
|
|
147
|
+
try:
|
|
148
|
+
processor.shutdown()
|
|
149
|
+
except Exception:
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def _otel_tracer_provider(self) -> OTelTracerProvider:
|
|
154
|
+
"""Get the underlying OpenTelemetry TracerProvider."""
|
|
155
|
+
return self._otel_provider
|
traccia/tracer/span.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""Span implementation - minimal wrapper around OpenTelemetry Span."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
import traceback
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from opentelemetry.trace import Span as OTelSpan, Status, StatusCode
|
|
11
|
+
from opentelemetry.trace import set_span_in_context
|
|
12
|
+
from opentelemetry import context as context_api
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from traccia.tracer.tracer import Tracer
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SpanStatus(Enum):
|
|
19
|
+
UNSET = 0
|
|
20
|
+
OK = 1
|
|
21
|
+
ERROR = 2
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Span:
|
|
25
|
+
"""
|
|
26
|
+
Minimal wrapper around OpenTelemetry Span.
|
|
27
|
+
|
|
28
|
+
Provides Traccia API compatibility while using OTel span internally.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
otel_span: OTelSpan,
|
|
34
|
+
tracer: "Tracer",
|
|
35
|
+
parent_span_id: Optional[str] = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Initialize span wrapper.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
otel_span: OpenTelemetry Span instance
|
|
42
|
+
tracer: Traccia Tracer instance
|
|
43
|
+
parent_span_id: Parent span ID (hex string, for compatibility)
|
|
44
|
+
"""
|
|
45
|
+
self._otel_span = otel_span
|
|
46
|
+
self.tracer = tracer
|
|
47
|
+
self.parent_span_id = parent_span_id
|
|
48
|
+
self._ended = False
|
|
49
|
+
self._activation_token = None
|
|
50
|
+
|
|
51
|
+
# Store tracer reference on OTel span for context retrieval
|
|
52
|
+
otel_span._traccia_tracer = tracer
|
|
53
|
+
|
|
54
|
+
# Create Traccia-compatible properties
|
|
55
|
+
from traccia.tracer.span_context import SpanContext
|
|
56
|
+
from traccia.utils.helpers import format_trace_id, format_span_id
|
|
57
|
+
|
|
58
|
+
otel_context = otel_span.get_span_context()
|
|
59
|
+
self.context = SpanContext(
|
|
60
|
+
trace_id=format_trace_id(otel_context.trace_id),
|
|
61
|
+
span_id=format_span_id(otel_context.span_id),
|
|
62
|
+
trace_flags=1 if otel_context.trace_flags.sampled else 0,
|
|
63
|
+
trace_state=self._format_trace_state(otel_context.trace_state),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Enrich tracestate with runtime metadata
|
|
67
|
+
self._enrich_tracestate()
|
|
68
|
+
|
|
69
|
+
# Expose span properties for processor access
|
|
70
|
+
self.name = getattr(otel_span, 'name', 'unknown')
|
|
71
|
+
self.start_time_ns = time.time_ns()
|
|
72
|
+
self.end_time_ns: Optional[int] = None
|
|
73
|
+
|
|
74
|
+
# Status
|
|
75
|
+
self.status = SpanStatus.UNSET
|
|
76
|
+
self.status_description: Optional[str] = None
|
|
77
|
+
|
|
78
|
+
# Maintain attribute dict for easy access
|
|
79
|
+
# This mirrors OTel span attributes
|
|
80
|
+
self._attributes: Dict[str, Any] = {}
|
|
81
|
+
|
|
82
|
+
def _format_trace_state(self, trace_state) -> Optional[str]:
|
|
83
|
+
"""Format OTel TraceState to W3C string format."""
|
|
84
|
+
if not trace_state:
|
|
85
|
+
return None
|
|
86
|
+
items = []
|
|
87
|
+
for key, value in trace_state.items():
|
|
88
|
+
items.append(f"{key}={value}")
|
|
89
|
+
return ",".join(items) if items else None
|
|
90
|
+
|
|
91
|
+
def _enrich_tracestate(self) -> None:
|
|
92
|
+
"""Enrich tracestate with runtime metadata."""
|
|
93
|
+
try:
|
|
94
|
+
from traccia.context.propagators import format_tracestate, parse_tracestate
|
|
95
|
+
from traccia import runtime_config
|
|
96
|
+
|
|
97
|
+
base = parse_tracestate(self.context.trace_state or "")
|
|
98
|
+
if runtime_config.get_tenant_id():
|
|
99
|
+
base.setdefault("tenant", runtime_config.get_tenant_id())
|
|
100
|
+
if runtime_config.get_project_id():
|
|
101
|
+
base.setdefault("project", runtime_config.get_project_id())
|
|
102
|
+
if runtime_config.get_debug():
|
|
103
|
+
base.setdefault("dbg", "1")
|
|
104
|
+
|
|
105
|
+
ts = format_tracestate(base)
|
|
106
|
+
if ts:
|
|
107
|
+
from traccia.tracer.span_context import SpanContext
|
|
108
|
+
self.context = SpanContext(
|
|
109
|
+
trace_id=self.context.trace_id,
|
|
110
|
+
span_id=self.context.span_id,
|
|
111
|
+
trace_flags=self.context.trace_flags,
|
|
112
|
+
trace_state=ts,
|
|
113
|
+
)
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def attributes(self) -> Dict[str, Any]:
|
|
119
|
+
"""
|
|
120
|
+
Get span attributes (read/write).
|
|
121
|
+
|
|
122
|
+
This property provides direct access to attributes dict.
|
|
123
|
+
Changes are synced to OTel span via set_attribute().
|
|
124
|
+
"""
|
|
125
|
+
# Sync from OTel span if available (for processors that read directly)
|
|
126
|
+
if not self._ended and hasattr(self._otel_span, 'attributes'):
|
|
127
|
+
try:
|
|
128
|
+
otel_attrs = getattr(self._otel_span, 'attributes', {})
|
|
129
|
+
if otel_attrs:
|
|
130
|
+
# Merge OTel attributes into local dict
|
|
131
|
+
for k, v in otel_attrs.items():
|
|
132
|
+
if k not in self._attributes:
|
|
133
|
+
self._attributes[k] = v
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
return self._attributes
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def events(self) -> List[Dict[str, Any]]:
|
|
140
|
+
"""Get span events (read-only)."""
|
|
141
|
+
# OTel doesn't expose events on active span
|
|
142
|
+
# Return empty list for now
|
|
143
|
+
return []
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def duration_ns(self) -> Optional[int]:
|
|
147
|
+
"""Get span duration in nanoseconds."""
|
|
148
|
+
if self.end_time_ns is None:
|
|
149
|
+
return None
|
|
150
|
+
return self.end_time_ns - self.start_time_ns
|
|
151
|
+
|
|
152
|
+
def set_attribute(self, key: str, value: Any) -> None:
|
|
153
|
+
"""Set an attribute on the span."""
|
|
154
|
+
if self._ended:
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
# Store in local dict
|
|
158
|
+
self._attributes[key] = value
|
|
159
|
+
|
|
160
|
+
# Set on OTel span
|
|
161
|
+
try:
|
|
162
|
+
self._otel_span.set_attribute(key, value)
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
def add_event(
|
|
167
|
+
self,
|
|
168
|
+
name: str,
|
|
169
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
170
|
+
timestamp_ns: Optional[int] = None,
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Add an event to the span."""
|
|
173
|
+
if self._ended:
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
self._otel_span.add_event(
|
|
178
|
+
name=name,
|
|
179
|
+
attributes=attributes,
|
|
180
|
+
timestamp=timestamp_ns,
|
|
181
|
+
)
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
def record_exception(self, error: BaseException) -> None:
|
|
186
|
+
"""Record an exception event on the span."""
|
|
187
|
+
if self._ended:
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
self._otel_span.record_exception(error)
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
self.set_status(SpanStatus.ERROR, str(error))
|
|
196
|
+
|
|
197
|
+
def set_status(self, status: SpanStatus, description: Optional[str] = None) -> None:
|
|
198
|
+
"""Set the span status."""
|
|
199
|
+
if self._ended:
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
self.status = status
|
|
203
|
+
self.status_description = description
|
|
204
|
+
|
|
205
|
+
# Convert and set on OTel span
|
|
206
|
+
if status == SpanStatus.OK:
|
|
207
|
+
otel_status = Status(status_code=StatusCode.OK, description=description)
|
|
208
|
+
elif status == SpanStatus.ERROR:
|
|
209
|
+
otel_status = Status(status_code=StatusCode.ERROR, description=description)
|
|
210
|
+
else:
|
|
211
|
+
otel_status = Status(status_code=StatusCode.UNSET, description=description)
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
self._otel_span.set_status(otel_status)
|
|
215
|
+
except Exception:
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
def end(self) -> None:
|
|
219
|
+
"""
|
|
220
|
+
End the span.
|
|
221
|
+
|
|
222
|
+
Enrichment processors run BEFORE span.end() (span is still mutable).
|
|
223
|
+
Export processors run AFTER span.end() (OTel handles this automatically).
|
|
224
|
+
"""
|
|
225
|
+
if self._ended:
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
self.end_time_ns = time.time_ns()
|
|
229
|
+
if self.status == SpanStatus.UNSET:
|
|
230
|
+
self.status = SpanStatus.OK
|
|
231
|
+
# Set status on OTel span as well
|
|
232
|
+
try:
|
|
233
|
+
from opentelemetry.trace import Status, StatusCode
|
|
234
|
+
self._otel_span.set_status(Status(status_code=StatusCode.OK))
|
|
235
|
+
except Exception:
|
|
236
|
+
pass
|
|
237
|
+
|
|
238
|
+
# 1. Run enrichment processors (span is still mutable)
|
|
239
|
+
self.tracer._run_enrichment_processors(self)
|
|
240
|
+
|
|
241
|
+
# 2. End the OTel span (makes it immutable)
|
|
242
|
+
try:
|
|
243
|
+
self._otel_span.end(end_time=self.end_time_ns)
|
|
244
|
+
except Exception:
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
# 3. OTel export processors run automatically on ReadableSpan
|
|
248
|
+
|
|
249
|
+
self._ended = True
|
|
250
|
+
|
|
251
|
+
# Context manager support
|
|
252
|
+
def __enter__(self) -> "Span":
|
|
253
|
+
"""Enter context manager."""
|
|
254
|
+
ctx = set_span_in_context(self._otel_span)
|
|
255
|
+
self._activation_token = context_api.attach(ctx)
|
|
256
|
+
return self
|
|
257
|
+
|
|
258
|
+
def __exit__(self, exc_type, exc, tb) -> bool:
|
|
259
|
+
"""Exit context manager."""
|
|
260
|
+
try:
|
|
261
|
+
if exc:
|
|
262
|
+
self.record_exception(exc)
|
|
263
|
+
self.end()
|
|
264
|
+
finally:
|
|
265
|
+
if self._activation_token:
|
|
266
|
+
context_api.detach(self._activation_token)
|
|
267
|
+
self._activation_token = None
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
async def __aenter__(self) -> "Span":
|
|
271
|
+
"""Enter async context manager."""
|
|
272
|
+
ctx = set_span_in_context(self._otel_span)
|
|
273
|
+
self._activation_token = context_api.attach(ctx)
|
|
274
|
+
return self
|
|
275
|
+
|
|
276
|
+
async def __aexit__(self, exc_type, exc, tb) -> bool:
|
|
277
|
+
"""Exit async context manager."""
|
|
278
|
+
try:
|
|
279
|
+
if exc:
|
|
280
|
+
self.record_exception(exc)
|
|
281
|
+
self.end()
|
|
282
|
+
finally:
|
|
283
|
+
if self._activation_token:
|
|
284
|
+
context_api.detach(self._activation_token)
|
|
285
|
+
self._activation_token = None
|
|
286
|
+
return False
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Immutable trace metadata."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class SpanContext:
|
|
9
|
+
trace_id: str
|
|
10
|
+
span_id: str
|
|
11
|
+
trace_flags: int = 1 # 1 = sampled, 0 = not sampled
|
|
12
|
+
trace_state: Optional[str] = None
|
|
13
|
+
|
|
14
|
+
def is_valid(self) -> bool:
|
|
15
|
+
return bool(self.trace_id and self.span_id)
|
|
16
|
+
|
traccia/tracer/tracer.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Tracer using OpenTelemetry SDK with Traccia API compatibility."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from opentelemetry.trace import Tracer as OTelTracer
|
|
8
|
+
from opentelemetry.trace import set_span_in_context
|
|
9
|
+
from opentelemetry import context as context_api
|
|
10
|
+
|
|
11
|
+
from traccia import runtime_config
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Tracer:
|
|
15
|
+
"""
|
|
16
|
+
Tracer wrapper that uses OpenTelemetry Tracer internally.
|
|
17
|
+
|
|
18
|
+
Maintains Traccia API compatibility while using OTel underneath.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, provider: "TracerProvider", instrumentation_scope: str):
|
|
22
|
+
"""
|
|
23
|
+
Initialize tracer with OpenTelemetry Tracer.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
provider: Traccia TracerProvider instance
|
|
27
|
+
instrumentation_scope: Instrumentation scope name
|
|
28
|
+
"""
|
|
29
|
+
self._provider = provider
|
|
30
|
+
self.instrumentation_scope = instrumentation_scope
|
|
31
|
+
|
|
32
|
+
# Get OTel tracer from provider's OTel provider
|
|
33
|
+
self._otel_tracer: OTelTracer = provider._otel_provider.get_tracer(instrumentation_scope)
|
|
34
|
+
|
|
35
|
+
def start_span(
|
|
36
|
+
self,
|
|
37
|
+
name: str,
|
|
38
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
39
|
+
parent: Optional[Any] = None,
|
|
40
|
+
parent_context: Optional[Any] = None,
|
|
41
|
+
) -> "Span":
|
|
42
|
+
"""
|
|
43
|
+
Start a new span.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
name: Span name
|
|
47
|
+
attributes: Optional attributes dictionary
|
|
48
|
+
parent: Optional parent span
|
|
49
|
+
parent_context: Optional parent span context
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Traccia Span instance (wraps OTel Span)
|
|
53
|
+
"""
|
|
54
|
+
from opentelemetry.trace import get_current_span
|
|
55
|
+
|
|
56
|
+
# Determine parent context
|
|
57
|
+
otel_parent_context = None
|
|
58
|
+
parent_span_id = None
|
|
59
|
+
|
|
60
|
+
if parent:
|
|
61
|
+
# Extract parent span ID for Traccia compatibility
|
|
62
|
+
if hasattr(parent, 'context'):
|
|
63
|
+
parent_span_id = parent.context.span_id
|
|
64
|
+
|
|
65
|
+
# Get OTel span from parent
|
|
66
|
+
if hasattr(parent, '_otel_span'):
|
|
67
|
+
otel_parent_context = set_span_in_context(parent._otel_span)
|
|
68
|
+
elif hasattr(parent, 'get_span_context'):
|
|
69
|
+
# Direct OTel span
|
|
70
|
+
otel_parent_context = set_span_in_context(parent)
|
|
71
|
+
from traccia.utils.helpers import format_span_id
|
|
72
|
+
parent_span_id = format_span_id(parent.get_span_context().span_id)
|
|
73
|
+
|
|
74
|
+
elif parent_context:
|
|
75
|
+
# Convert Traccia SpanContext to OTel context
|
|
76
|
+
if hasattr(parent_context, 'trace_id'):
|
|
77
|
+
from traccia.utils.helpers import parse_trace_id, parse_span_id
|
|
78
|
+
from opentelemetry.trace import SpanContext as OTelSpanContext, TraceFlags, TraceState, NonRecordingSpan
|
|
79
|
+
|
|
80
|
+
trace_id = parse_trace_id(parent_context.trace_id)
|
|
81
|
+
span_id = parse_span_id(parent_context.span_id)
|
|
82
|
+
trace_flags = TraceFlags(parent_context.trace_flags)
|
|
83
|
+
|
|
84
|
+
# Parse trace_state
|
|
85
|
+
trace_state = None
|
|
86
|
+
if parent_context.trace_state:
|
|
87
|
+
from traccia.context.propagators import parse_tracestate
|
|
88
|
+
parsed = parse_tracestate(parent_context.trace_state)
|
|
89
|
+
if parsed:
|
|
90
|
+
items = [(k, v) for k, v in parsed.items()]
|
|
91
|
+
trace_state = TraceState(items)
|
|
92
|
+
|
|
93
|
+
otel_span_context = OTelSpanContext(
|
|
94
|
+
trace_id=trace_id,
|
|
95
|
+
span_id=span_id,
|
|
96
|
+
is_remote=False,
|
|
97
|
+
trace_flags=trace_flags,
|
|
98
|
+
trace_state=trace_state or TraceState(),
|
|
99
|
+
)
|
|
100
|
+
otel_parent_context = set_span_in_context(NonRecordingSpan(otel_span_context))
|
|
101
|
+
parent_span_id = parent_context.span_id
|
|
102
|
+
|
|
103
|
+
# If no parent specified, use current span
|
|
104
|
+
if otel_parent_context is None:
|
|
105
|
+
current_span = get_current_span()
|
|
106
|
+
if current_span and current_span.get_span_context().is_valid:
|
|
107
|
+
otel_parent_context = set_span_in_context(current_span)
|
|
108
|
+
from traccia.utils.helpers import format_span_id
|
|
109
|
+
parent_span_id = format_span_id(current_span.get_span_context().span_id)
|
|
110
|
+
|
|
111
|
+
# Handle sampling
|
|
112
|
+
sampler = getattr(self._provider, "sampler", None)
|
|
113
|
+
if sampler and otel_parent_context is None:
|
|
114
|
+
# New root trace - check sampler
|
|
115
|
+
try:
|
|
116
|
+
sampled = bool(sampler.should_sample().sampled)
|
|
117
|
+
if not sampled:
|
|
118
|
+
# Create a non-recording span for unsampled traces
|
|
119
|
+
from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags
|
|
120
|
+
from opentelemetry.trace.id_generator import RandomIdGenerator
|
|
121
|
+
|
|
122
|
+
id_generator = RandomIdGenerator()
|
|
123
|
+
trace_id = id_generator.generate_trace_id()
|
|
124
|
+
span_id = id_generator.generate_span_id()
|
|
125
|
+
|
|
126
|
+
unsampled_context = SpanContext(
|
|
127
|
+
trace_id=trace_id,
|
|
128
|
+
span_id=span_id,
|
|
129
|
+
is_remote=False,
|
|
130
|
+
trace_flags=TraceFlags(0), # Not sampled
|
|
131
|
+
)
|
|
132
|
+
unsampled_span = NonRecordingSpan(unsampled_context)
|
|
133
|
+
otel_parent_context = set_span_in_context(unsampled_span)
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
# Debug override: if enabled, force sampling for new traces
|
|
138
|
+
if otel_parent_context is None and runtime_config.get_debug():
|
|
139
|
+
pass # OTel will handle this
|
|
140
|
+
|
|
141
|
+
# Check for auto-trace conflict
|
|
142
|
+
self._check_auto_trace_conflict(name, otel_parent_context)
|
|
143
|
+
|
|
144
|
+
# Start OTel span
|
|
145
|
+
otel_span = self._otel_tracer.start_span(
|
|
146
|
+
name=name,
|
|
147
|
+
attributes=attributes,
|
|
148
|
+
context=otel_parent_context,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Wrap in Traccia Span
|
|
152
|
+
from traccia.tracer.span import Span
|
|
153
|
+
return Span(otel_span, self, parent_span_id)
|
|
154
|
+
|
|
155
|
+
def start_as_current_span(
|
|
156
|
+
self,
|
|
157
|
+
name: str,
|
|
158
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
159
|
+
parent: Optional[Any] = None,
|
|
160
|
+
parent_context: Optional[Any] = None,
|
|
161
|
+
) -> "Span":
|
|
162
|
+
"""
|
|
163
|
+
Start a span and set it as current (context manager).
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
name: Span name
|
|
167
|
+
attributes: Optional attributes dictionary
|
|
168
|
+
parent: Optional parent span
|
|
169
|
+
parent_context: Optional parent span context
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Traccia Span instance (wraps OTel Span)
|
|
173
|
+
"""
|
|
174
|
+
return self.start_span(
|
|
175
|
+
name=name,
|
|
176
|
+
attributes=attributes,
|
|
177
|
+
parent=parent,
|
|
178
|
+
parent_context=parent_context,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def get_current_span(self) -> Optional["Span"]:
|
|
182
|
+
"""Get the current span."""
|
|
183
|
+
from opentelemetry.trace import get_current_span
|
|
184
|
+
otel_span = get_current_span()
|
|
185
|
+
if otel_span and otel_span.get_span_context().is_valid:
|
|
186
|
+
from traccia.tracer.span import Span
|
|
187
|
+
# Check if we already have a Traccia wrapper
|
|
188
|
+
if hasattr(otel_span, '_traccia_tracer'):
|
|
189
|
+
# Try to return existing wrapper (best effort)
|
|
190
|
+
pass
|
|
191
|
+
return Span(otel_span, self)
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
def _check_auto_trace_conflict(self, span_name: str, parent_context: Optional[Any]) -> None:
|
|
195
|
+
"""
|
|
196
|
+
Check if user is creating a span with 'root' in name while auto-trace is active.
|
|
197
|
+
|
|
198
|
+
Logs an informational warning if a conflict is detected.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
span_name: Name of the span being created
|
|
202
|
+
parent_context: Parent context (if None, this might be a root span)
|
|
203
|
+
"""
|
|
204
|
+
# Import here to avoid circular dependency
|
|
205
|
+
from traccia import auto
|
|
206
|
+
|
|
207
|
+
# Only warn if auto-trace is active
|
|
208
|
+
if not auto._auto_trace_context:
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
# Only warn if span name is exactly "root" (case-insensitive) to avoid false positives
|
|
212
|
+
# This helps users who might be migrating from manual root span creation
|
|
213
|
+
if span_name.lower() != "root":
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
# Only warn if this would be a root span (no parent context)
|
|
217
|
+
# Note: If parent_context exists, this is a child span and that's expected
|
|
218
|
+
if parent_context is not None:
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
import logging
|
|
222
|
+
logger = logging.getLogger(__name__)
|
|
223
|
+
logger.debug(
|
|
224
|
+
f"Auto-started trace '{auto._auto_trace_name}' is active. "
|
|
225
|
+
f"Created span '{span_name}' will be a child of the auto-started trace. "
|
|
226
|
+
f"Use traccia.end_auto_trace() if you want a separate trace."
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def _run_enrichment_processors(self, span: "Span") -> None:
|
|
230
|
+
"""
|
|
231
|
+
Run enrichment processors before span ends.
|
|
232
|
+
|
|
233
|
+
Called by Span.end() before the OTel span is ended.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
span: Traccia Span instance (still mutable)
|
|
237
|
+
"""
|
|
238
|
+
for processor in self._provider._enrichment_processors:
|
|
239
|
+
try:
|
|
240
|
+
processor.on_end(span)
|
|
241
|
+
except Exception:
|
|
242
|
+
# Processors should not crash tracing
|
|
243
|
+
pass
|