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,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
+
@@ -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