Line data Source code
1 : // Licensed under the Apache License, Version 2.0
2 : // Copyright 2025, Michael Bushe, All rights reserved.
3 :
4 : library;
5 :
6 : import 'package:dartastic_opentelemetry/src/trace/sampling/sampler.dart';
7 : import 'package:dartastic_opentelemetry/src/trace/tracer_provider.dart';
8 : import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart';
9 :
10 : import '../otel.dart';
11 : import '../resource/resource.dart';
12 : import 'span.dart';
13 :
14 : part 'tracer_create.dart';
15 :
16 : /// SDK implementation of the APITracer interface.
17 : ///
18 : /// A Tracer is responsible for creating and managing spans. Each Tracer
19 : /// is associated with a specific instrumentation scope and can create
20 : /// spans that represent operations within that scope.
21 : ///
22 : /// This implementation delegates some functionality to the API Tracer
23 : /// implementation while adding SDK-specific behaviors like sampling and
24 : /// span processor notification.
25 : ///
26 : /// Note: Per [OTEP 0265](https://opentelemetry.io/docs/specs/semconv/general/events/),
27 : /// span events are being deprecated and will be replaced by the Logging API in future versions.
28 : ///
29 : /// More information:
30 : /// https://opentelemetry.io/docs/specs/otel/trace/sdk/
31 : class Tracer implements APITracer {
32 : final TracerProvider _provider;
33 : final APITracer _delegate;
34 : final Sampler? _sampler;
35 : bool _enabled = true;
36 :
37 : /// Gets the sampler associated with this tracer.
38 : /// If no sampler was specified for this tracer, uses the provider's sampler.
39 108 : Sampler? get sampler => _sampler ?? _provider.sampler;
40 :
41 : /// Private constructor for creating Tracer instances.
42 : ///
43 : /// @param provider The TracerProvider that created this Tracer
44 : /// @param delegate The API Tracer implementation to delegate to
45 : /// @param sampler Optional custom sampler for this Tracer
46 29 : Tracer._({
47 : required TracerProvider provider,
48 : required APITracer delegate,
49 : Sampler? sampler,
50 : }) : _provider = provider,
51 : _delegate = delegate,
52 : _sampler = sampler;
53 :
54 1 : @override
55 2 : String get name => _delegate.name;
56 :
57 0 : @override
58 0 : String? get schemaUrl => _delegate.schemaUrl;
59 :
60 0 : @override
61 0 : String? get version => _delegate.version;
62 :
63 0 : @override
64 0 : Attributes? get attributes => _delegate.attributes;
65 :
66 0 : @override
67 0 : set attributes(Attributes? attributes) => _delegate.attributes = attributes;
68 :
69 1 : @override
70 1 : bool get enabled => _enabled;
71 :
72 3 : @override
73 6 : APISpan? get currentSpan => _delegate.currentSpan;
74 :
75 : /// Sets whether this tracer is enabled.
76 : ///
77 : /// When disabled, the tracer will still create spans, but they may not be
78 : /// recorded or exported.
79 0 : set enabled(bool enable) => _enabled = enable;
80 :
81 : /// Gets the provider that created this tracer.
82 44 : TracerProvider get provider => _provider;
83 :
84 : /// Gets the resource associated with this tracer's provider.
85 57 : Resource? get resource => _provider.resource;
86 :
87 3 : @override
88 : T withSpan<T>(APISpan span, T Function() fn) {
89 3 : if (OTelLog.isDebug()) {
90 3 : OTelLog.debug(
91 12 : 'Tracer: withSpan called with span ${span.name}, spanId: ${span.spanContext.spanId}');
92 : }
93 3 : final originalContext = Context.current;
94 : try {
95 6 : Context.current = originalContext.setCurrentSpan(span);
96 3 : if (OTelLog.isDebug()) {
97 9 : OTelLog.debug('Tracer: Context set with span ${span.name}');
98 : }
99 3 : final result = fn();
100 3 : if (OTelLog.isDebug()) {
101 3 : OTelLog.debug(
102 6 : 'Tracer: Function completed in withSpan for ${span.name}');
103 : }
104 : return result;
105 : } catch (e, stackTrace) {
106 0 : if (OTelLog.isError()) {
107 0 : OTelLog.error('Tracer: Exception in withSpan for ${span.name}: $e');
108 : }
109 0 : if (span is Span) {
110 0 : span.recordException(e, stackTrace: stackTrace);
111 0 : span.setStatus(SpanStatusCode.Error, e.toString());
112 : }
113 : rethrow;
114 : } finally {
115 3 : Context.current = originalContext;
116 3 : if (OTelLog.isDebug()) {
117 9 : OTelLog.debug('Tracer: withSpan completed for span ${span.name}');
118 : }
119 : // Check span validity but don't automatically end it
120 3 : if (!span.isValid) {
121 0 : if (OTelLog.isDebug()) {
122 0 : OTelLog.debug(
123 0 : 'Tracer: Warning - span ${span.name} is invalid after withSpan operation');
124 : }
125 : }
126 : }
127 : }
128 :
129 3 : @override
130 : Future<T> withSpanAsync<T>(APISpan span, Future<T> Function() fn) async {
131 3 : if (OTelLog.isDebug()) {
132 3 : OTelLog.debug(
133 12 : 'Tracer: withSpanAsync called with span ${span.name}, spanId: ${span.spanContext.spanId}');
134 : }
135 3 : final originalContext = Context.current;
136 : try {
137 6 : Context.current = originalContext.setCurrentSpan(span);
138 3 : if (OTelLog.isDebug()) {
139 3 : OTelLog.debug(
140 6 : 'Tracer: Context set with span ${span.name} for async operation');
141 : }
142 3 : return await fn();
143 : } catch (e, stackTrace) {
144 0 : if (OTelLog.isError()) {
145 0 : OTelLog.error(
146 0 : 'Tracer: Exception in withSpanAsync for ${span.name}: $e');
147 : }
148 0 : if (span is Span) {
149 0 : span.recordException(e, stackTrace: stackTrace);
150 0 : span.setStatus(SpanStatusCode.Error, e.toString());
151 : }
152 : rethrow;
153 : } finally {
154 3 : Context.current = originalContext;
155 3 : if (OTelLog.isDebug()) {
156 9 : OTelLog.debug('Tracer: withSpanAsync completed for span ${span.name}');
157 : }
158 : // Check span validity but don't automatically end it
159 3 : if (!span.isValid) {
160 0 : if (OTelLog.isDebug()) {
161 0 : OTelLog.debug(
162 0 : 'Tracer: Warning - span ${span.name} is invalid after withSpanAsync operation');
163 : }
164 : }
165 : }
166 : }
167 :
168 : /// Starts a span with the given context instead of the current context
169 : /// Sets the current span in the given context
170 0 : APISpan startSpanWithContext({
171 : required String name,
172 : required Context context,
173 : SpanKind kind = SpanKind.internal,
174 : Attributes? attributes,
175 : }) {
176 0 : final span = startSpan(
177 : name,
178 : context: context,
179 : kind: kind,
180 : attributes: attributes,
181 : );
182 :
183 : // Set the span in the context we were given
184 0 : final updatedContext = context.setCurrentSpan(span);
185 :
186 : // Also update the current global context if needed
187 0 : if (Context.current == context) {
188 0 : Context.current = updatedContext;
189 : }
190 :
191 : return span;
192 : }
193 :
194 1 : @override
195 : Span createSpan({
196 : required String name,
197 : SpanContext? spanContext,
198 : APISpan? parentSpan,
199 : SpanKind kind = SpanKind.internal,
200 : Attributes? attributes,
201 : List<SpanLink>? links,
202 : List<SpanEvent>? spanEvents,
203 : DateTime? startTime,
204 : bool? isRecording = true,
205 : Context? context,
206 : }) {
207 1 : if (OTelLog.isDebug()) {
208 2 : OTelLog.debug('Tracer: Creating span with name: $name, kind: $kind');
209 : }
210 :
211 2 : final APISpan delegateSpan = _delegate.createSpan(
212 : name: name,
213 : spanContext: spanContext,
214 : parentSpan: parentSpan,
215 : kind: kind,
216 : attributes: attributes,
217 : links: links,
218 : startTime: startTime,
219 : spanEvents: spanEvents,
220 : isRecording: isRecording,
221 : context: context,
222 : );
223 :
224 1 : return SDKSpanCreate.create(
225 : delegateSpan: delegateSpan,
226 : sdkTracer: this,
227 : );
228 : }
229 :
230 27 : @override
231 : Span startSpan(
232 : String name, {
233 : Context? context,
234 : SpanContext? spanContext,
235 : APISpan? parentSpan,
236 : SpanKind kind = SpanKind.internal,
237 : Attributes? attributes,
238 : List<SpanLink>? links,
239 : bool? isRecording = true,
240 : }) {
241 27 : if (OTelLog.isDebug()) {
242 54 : OTelLog.debug('Tracer: Starting span with name: $name, kind: $kind');
243 : }
244 :
245 : // Get parent context from either the passed context or parent span
246 : SpanContext? parentContext;
247 : APISpan? effectiveParentSpan = parentSpan;
248 27 : final effectiveContext = context ?? Context.current;
249 :
250 54 : if (effectiveContext != Context.root) {
251 : // If an explicit context was provided, check for a span
252 20 : if (effectiveContext.span != null) {
253 : // Use the span from the context as parent (if no explicit parent span)
254 19 : effectiveParentSpan ??= effectiveContext.span;
255 : }
256 : // Always check for span context in the context
257 20 : parentContext = effectiveContext.spanContext;
258 : }
259 :
260 : // If no parentContext from context but we have a parentSpan, use its context
261 : if (parentContext == null && effectiveParentSpan != null) {
262 0 : parentContext = effectiveParentSpan.spanContext;
263 : }
264 :
265 : // Determine the trace ID to use
266 : TraceId traceId;
267 4 : if (spanContext != null && spanContext.traceId.isValid) {
268 : // Use provided span context's trace ID if valid
269 2 : traceId = spanContext.traceId;
270 :
271 : // Validate it against parent if both exist and are valid
272 1 : if (parentContext != null && parentContext.isValid) {
273 2 : if (parentContext.traceId != traceId) {
274 2 : throw ArgumentError(
275 : 'Cannot create span with different trace ID than parent. '
276 1 : 'Parent trace ID: ${parentContext.traceId}, '
277 : 'Provided trace ID: $traceId');
278 : }
279 : }
280 20 : } else if (parentContext != null && parentContext.isValid) {
281 : // Inherit from parent if available
282 20 : traceId = parentContext.traceId;
283 : } else {
284 : // Generate new trace ID for root span
285 27 : traceId = OTel.traceId();
286 : }
287 :
288 : // Determine the parent span ID
289 : SpanId? parentSpanId;
290 : if (effectiveParentSpan != null &&
291 40 : effectiveParentSpan.spanContext.isValid) {
292 : // Use effective parent span's span ID
293 40 : parentSpanId = effectiveParentSpan.spanContext.spanId;
294 2 : } else if (parentContext != null && parentContext.isValid) {
295 : // Use parent context's span ID
296 2 : parentSpanId = parentContext.spanId;
297 : }
298 :
299 : // Inherit trace flags from parent if available
300 : TraceFlags? traceFlags;
301 20 : if (parentContext != null && parentContext.isValid) {
302 20 : traceFlags = parentContext.traceFlags;
303 : }
304 :
305 27 : if (OTelLog.isDebug()) {
306 : if (parentSpanId != null) {
307 20 : OTelLog.debug(
308 20 : 'Creating child span: traceId=$traceId, parentSpanId=$parentSpanId');
309 : } else {
310 54 : OTelLog.debug('Creating root span: traceId=$traceId');
311 : }
312 : }
313 :
314 : // Apply sampling decision if we have a sampler
315 : bool shouldRecord = true;
316 27 : if (sampler != null) {
317 54 : final samplingResult = sampler!.shouldSample(
318 : parentContext: effectiveContext,
319 27 : traceId: traceId.toString(),
320 : name: name,
321 : spanKind: kind,
322 : attributes: attributes,
323 : links: links,
324 : );
325 :
326 : // Update the isRecording flag based on the sampling decision
327 54 : shouldRecord = samplingResult.decision != SamplingDecision.drop;
328 :
329 : // Update trace flags based on sampling decision
330 : if (traceFlags == null) {
331 27 : traceFlags = OTel.traceFlags(
332 : shouldRecord ? TraceFlags.SAMPLED_FLAG : TraceFlags.NONE_FLAG);
333 20 : } else if (shouldRecord && !traceFlags.isSampled) {
334 : // Upgrade to sampled if necessary
335 2 : traceFlags = OTel.traceFlags(TraceFlags.SAMPLED_FLAG);
336 2 : } else if (!shouldRecord && traceFlags.isSampled) {
337 : // Downgrade to not sampled if necessary
338 2 : traceFlags = OTel.traceFlags(TraceFlags.NONE_FLAG);
339 : }
340 :
341 : // Add sampler attributes if provided
342 27 : if (samplingResult.attributes != null) {
343 : if (attributes == null) {
344 0 : attributes = samplingResult.attributes;
345 : } else {
346 : attributes =
347 0 : attributes.copyWithAttributes(samplingResult.attributes!);
348 : }
349 : }
350 :
351 27 : if (OTelLog.isDebug()) {
352 27 : OTelLog.debug(
353 54 : 'Sampling decision for span $name: ${samplingResult.decision}');
354 : }
355 : }
356 :
357 : // Always create a new span context with a new span ID
358 : // For root spans, ensure we set an invalid parent span ID (zeros)
359 27 : final newSpanContext = OTel.spanContext(
360 : traceId: traceId,
361 27 : spanId: OTel.spanId(), // Always generate a new span ID
362 : parentSpanId: parentSpanId ??
363 27 : OTel.spanIdInvalid(), // Use invalid span ID for root spans
364 : traceFlags: traceFlags,
365 : );
366 :
367 : // Create the delegate span with our newly created span context
368 54 : final APISpan delegateSpan = _delegate.startSpan(name,
369 : context: effectiveContext,
370 : spanContext: newSpanContext,
371 : parentSpan: effectiveParentSpan,
372 : kind: kind,
373 : attributes: attributes,
374 : links: links,
375 : isRecording: isRecording ?? shouldRecord);
376 :
377 : // Wrap it in our SDK span which will handle processing
378 27 : final sdkSpan = SDKSpanCreate.create(
379 : delegateSpan: delegateSpan,
380 : sdkTracer: this,
381 : );
382 :
383 : // Notify processors
384 78 : for (final processor in _provider.spanProcessors) {
385 24 : processor.onStart(sdkSpan, context);
386 : }
387 :
388 : return sdkSpan;
389 : }
390 :
391 : /// Convenience method that starts a span and runs function fn
392 : /// on error, records the exception and sets status to SpanStatusCode.Error
393 : /// ends the span, always
394 4 : T recordSpan<T>({
395 : required String name,
396 : required T Function() fn,
397 : SpanKind kind = SpanKind.internal,
398 : Attributes? attributes,
399 : }) {
400 4 : final span = startSpan(name, kind: kind, attributes: attributes);
401 : try {
402 4 : return fn();
403 : } catch (e, stackTrace) {
404 2 : span.recordException(e, stackTrace: stackTrace);
405 4 : span.setStatus(SpanStatusCode.Error, e.toString());
406 : rethrow;
407 : } finally {
408 4 : span.end();
409 : }
410 : }
411 :
412 : /// Convenience method thatstarts a span and runs function fn and
413 : /// awaits its future
414 : /// on error, records the exception and sets status to SpanStatusCode.Error
415 : /// ends the span, always
416 2 : Future<T> recordSpanAsync<T>({
417 : required String name,
418 : required Future<T> Function() fn,
419 : SpanKind kind = SpanKind.internal,
420 : Attributes? attributes,
421 : }) async {
422 2 : final span = startSpan(name, kind: kind, attributes: attributes);
423 : try {
424 2 : return await fn();
425 : } catch (e, stackTrace) {
426 0 : span.recordException(e, stackTrace: stackTrace);
427 0 : span.setStatus(SpanStatusCode.Error, e.toString());
428 : rethrow;
429 : } finally {
430 2 : span.end();
431 : }
432 : }
433 :
434 : /// TODO - needs better doc. Is recordSpan superfluous?
435 2 : T startActiveSpan<T>({
436 : required String name,
437 : required T Function(APISpan span) fn,
438 : SpanKind kind = SpanKind.internal,
439 : Attributes? attributes,
440 : }) {
441 2 : final span = startSpan(name, kind: kind, attributes: attributes);
442 : try {
443 : // Use our own withSpan to ensure proper context propagation
444 6 : return withSpan(span, () => fn(span));
445 : } finally {
446 2 : span.end();
447 : }
448 : }
449 :
450 : /// Same as startActiveSpan but awaits the future
451 2 : Future<T> startActiveSpanAsync<T>({
452 : required String name,
453 : required Future<T> Function(APISpan span) fn,
454 : SpanKind kind = SpanKind.internal,
455 : Attributes? attributes,
456 : }) async {
457 2 : final span = startSpan(name, kind: kind, attributes: attributes);
458 : try {
459 : // Use our own withSpanAsync to ensure proper context propagation
460 6 : return await withSpanAsync(span, () => fn(span));
461 : } finally {
462 2 : span.end();
463 : }
464 : }
465 : }
|