LCOV - code coverage report
Current view: top level - lib/src/trace - tracer.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 97.4 % 154 150
Test Date: 2026-07-11 13:35:19 Functions: - 0 0

            Line data    Source code
       1              : // Copyright The OpenTelemetry Authors
       2              : // SPDX-License-Identifier: Apache-2.0
       3              : 
       4              : library;
       5              : 
       6              : import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart';
       7              : import 'package:meta/meta.dart';
       8              : 
       9              : import '../otel.dart';
      10              : import '../resource/resource.dart';
      11              : import 'sampling/sampler.dart';
      12              : import 'span.dart';
      13              : import 'span_exception_options.dart';
      14              : import 'tracer_provider.dart';
      15              : 
      16              : part 'tracer_create.dart';
      17              : 
      18              : /// SDK implementation of the APITracer interface.
      19              : ///
      20              : /// A Tracer is responsible for creating and managing spans. Each Tracer
      21              : /// is associated with a specific instrumentation scope and can create
      22              : /// spans that represent operations within that scope.
      23              : ///
      24              : /// This implementation delegates some functionality to the API Tracer
      25              : /// implementation while adding SDK-specific behaviors like sampling and
      26              : /// span processor notification.
      27              : ///
      28              : /// Note: Per [OTEP 0265](https://opentelemetry.io/docs/specs/semconv/general/events/),
      29              : /// span events are being deprecated and will be replaced by the Logging API in future versions.
      30              : ///
      31              : /// More information:
      32              : /// https://opentelemetry.io/docs/specs/otel/trace/sdk/
      33              : class Tracer implements APITracer {
      34              :   final TracerProvider _provider;
      35              :   final APITracer _delegate;
      36              :   final Sampler? _sampler;
      37              :   bool _enabled = true;
      38              : 
      39              :   /// Gets the sampler associated with this tracer.
      40              :   /// If no sampler was specified for this tracer, uses the provider's sampler.
      41          188 :   Sampler? get sampler => _sampler ?? _provider.sampler;
      42              : 
      43              :   /// The effective exception handling options used by [withSpan] /
      44              :   /// [withSpanAsync] when no per-call options are supplied.
      45              :   ///
      46              :   /// Falls back to the provider's [TracerProvider.spanExceptionOptions]
      47              :   /// (configured globally via `OTel.initialize(spanExceptionOptions: ...)`)
      48              :   /// and finally to a default [SpanExceptionOptions] that records the
      49              :   /// exception and sets the span status to error.
      50           10 :   SpanExceptionOptions get spanExceptionOptions =>
      51           20 :       _provider.spanExceptionOptions ?? SpanExceptionOptions.defaults;
      52              : 
      53              :   /// Private constructor for creating Tracer instances.
      54              :   ///
      55              :   /// @param provider The TracerProvider that created this Tracer
      56              :   /// @param delegate The API Tracer implementation to delegate to
      57              :   /// @param sampler Optional custom sampler for this Tracer
      58           49 :   Tracer._({
      59              :     required TracerProvider provider,
      60              :     required APITracer delegate,
      61              :     Sampler? sampler,
      62              :   })  : _provider = provider,
      63              :         _delegate = delegate,
      64              :         _sampler = sampler;
      65              : 
      66            1 :   @override
      67            2 :   String get name => _delegate.name;
      68              : 
      69            1 :   @override
      70            2 :   String? get schemaUrl => _delegate.schemaUrl;
      71              : 
      72            1 :   @override
      73            2 :   String? get version => _delegate.version;
      74              : 
      75            1 :   @override
      76            2 :   Attributes? get attributes => _delegate.attributes;
      77              : 
      78            1 :   @override
      79            2 :   set attributes(Attributes? attributes) => _delegate.attributes = attributes;
      80              : 
      81            2 :   @override
      82            2 :   bool get enabled => _enabled;
      83              : 
      84            5 :   @override
      85           10 :   APISpan? get currentSpan => _delegate.currentSpan;
      86              : 
      87              :   /// Sets whether this tracer is enabled.
      88              :   ///
      89              :   /// When disabled, the tracer will still create spans, but they may not be
      90              :   /// recorded or exported.
      91            2 :   set enabled(bool enable) => _enabled = enable;
      92              : 
      93              :   /// Gets the provider that created this tracer.
      94           84 :   TracerProvider get provider => _provider;
      95              : 
      96              :   /// Gets the resource associated with this tracer's provider.
      97          111 :   Resource? get resource => _provider.resource;
      98              : 
      99            0 :   @override
     100            0 :   TimeProvider get timeProvider => _delegate.timeProvider;
     101              : 
     102           10 :   @override
     103              :   T withSpan<T>(
     104              :     APISpan span,
     105              :     T Function() fn, {
     106              :     SpanExceptionOptions? exceptionOptions,
     107              :   }) {
     108              :     // Per-call options are merged field-by-field over the tracer/provider
     109              :     // default (set globally via OTel.initialize), so overriding a single flag
     110              :     // preserves the globally configured sanitizer.
     111           20 :     final options = spanExceptionOptions.mergeWith(exceptionOptions);
     112           10 :     if (OTelLog.isDebug()) {
     113           10 :       OTelLog.debug(
     114           40 :         'Tracer: withSpan called with span ${span.name}, spanId: ${span.spanContext.spanId}',
     115              :       );
     116              :     }
     117              :     // Activate the span in a new Zone via Context.runSync so the active span
     118              :     // propagates correctly across async boundaries inside fn. Wrap fn to
     119              :     // record exceptions on SDK spans.
     120              :     try {
     121           40 :       return Context.current.withSpan(span).runSync(() {
     122           10 :         if (OTelLog.isDebug()) {
     123           30 :           OTelLog.debug('Tracer: Context set with span ${span.name}');
     124              :         }
     125              :         try {
     126           10 :           final result = fn();
     127            9 :           if (OTelLog.isDebug()) {
     128            9 :             OTelLog.debug(
     129           18 :               'Tracer: Function completed in withSpan for ${span.name}',
     130              :             );
     131              :           }
     132              :           return result;
     133              :         } catch (e, stackTrace) {
     134            5 :           if (OTelLog.isError()) {
     135           15 :             OTelLog.error('Tracer: Exception in withSpan for ${span.name}: $e');
     136              :           }
     137              :           // SDK-specific exception recording only when the span is one
     138              :           // of ours. Foreign / no-op APISpans skip this branch — we
     139              :           // still activate them and rethrow.
     140            5 :           if (span is Span) {
     141            5 :             _handleSpanException(span, e, stackTrace, options);
     142              :           }
     143              :           rethrow;
     144              :         }
     145              :       });
     146              :     } finally {
     147           10 :       if (OTelLog.isDebug()) {
     148           30 :         OTelLog.debug('Tracer: withSpan completed for span ${span.name}');
     149           10 :         if (!span.isValid) {
     150            1 :           OTelLog.debug(
     151            2 :             'Tracer: Warning - span ${span.name} is invalid after withSpan operation',
     152              :           );
     153              :         }
     154              :       }
     155              :     }
     156              :   }
     157              : 
     158            7 :   @override
     159              :   Future<T> withSpanAsync<T>(
     160              :     APISpan span,
     161              :     Future<T> Function() fn, {
     162              :     SpanExceptionOptions? exceptionOptions,
     163              :   }) async {
     164              :     // Per-call options are merged field-by-field over the tracer/provider
     165              :     // default (set globally via OTel.initialize), so overriding a single flag
     166              :     // preserves the globally configured sanitizer.
     167           14 :     final options = spanExceptionOptions.mergeWith(exceptionOptions);
     168            7 :     if (OTelLog.isDebug()) {
     169            7 :       OTelLog.debug(
     170           28 :         'Tracer: withSpanAsync called with span ${span.name}, spanId: ${span.spanContext.spanId}',
     171              :       );
     172              :     }
     173              :     try {
     174           28 :       return await Context.current.withSpan(span).run(() async {
     175            7 :         if (OTelLog.isDebug()) {
     176            7 :           OTelLog.debug(
     177           14 :             'Tracer: Context set with span ${span.name} for async operation',
     178              :           );
     179              :         }
     180              :         try {
     181            7 :           return await fn();
     182              :         } catch (e, stackTrace) {
     183            3 :           if (OTelLog.isError()) {
     184            3 :             OTelLog.error(
     185            6 :               'Tracer: Exception in withSpanAsync for ${span.name}: $e',
     186              :             );
     187              :           }
     188              :           // SDK-specific exception recording only when the span is one
     189              :           // of ours. Foreign / no-op APISpans skip this branch — we
     190              :           // still activate them and rethrow.
     191            3 :           if (span is Span) {
     192            3 :             _handleSpanException(span, e, stackTrace, options);
     193              :           }
     194              :           rethrow;
     195              :         }
     196              :       });
     197              :     } finally {
     198            7 :       if (OTelLog.isDebug()) {
     199           21 :         OTelLog.debug('Tracer: withSpanAsync completed for span ${span.name}');
     200            7 :         if (!span.isValid) {
     201            1 :           OTelLog.debug(
     202            2 :             'Tracer: Warning - span ${span.name} is invalid after withSpanAsync operation',
     203              :           );
     204              :         }
     205              :       }
     206              :     }
     207              :   }
     208              : 
     209            2 :   @override
     210              :   Span createSpan({
     211              :     required String name,
     212              :     SpanContext? spanContext,
     213              :     APISpan? parentSpan,
     214              :     SpanKind kind = SpanKind.internal,
     215              :     Attributes? attributes,
     216              :     List<SpanLink>? links,
     217              :     List<SpanEvent>? spanEvents,
     218              :     DateTime? startTime,
     219              :     bool? isRecording = true,
     220              :     Context? context,
     221              :   }) {
     222            2 :     if (OTelLog.isDebug()) {
     223            4 :       OTelLog.debug('Tracer: Creating span with name: $name, kind: $kind');
     224              :     }
     225              : 
     226            4 :     final delegateSpan = _delegate.createSpan(
     227              :       name: name,
     228              :       spanContext: spanContext,
     229              :       parentSpan: parentSpan,
     230              :       kind: kind,
     231              :       attributes: attributes,
     232              :       links: links,
     233              :       startTime: startTime,
     234              :       spanEvents: spanEvents,
     235              :       isRecording: isRecording,
     236              :       context: context,
     237              :     );
     238              : 
     239            2 :     return SDKSpanCreate.create(delegateSpan: delegateSpan, sdkTracer: this);
     240              :   }
     241              : 
     242           47 :   @override
     243              :   Span startSpan(
     244              :     String name, {
     245              :     Context? context,
     246              :     SpanContext? spanContext,
     247              :     APISpan? parentSpan,
     248              :     SpanKind kind = SpanKind.internal,
     249              :     Attributes? attributes,
     250              :     List<SpanLink>? links,
     251              :     bool? isRecording = true,
     252              :   }) {
     253           47 :     if (OTelLog.isDebug()) {
     254           94 :       OTelLog.debug('Tracer: Starting span with name: $name, kind: $kind');
     255              :     }
     256              : 
     257              :     // Get parent context from either the passed context or parent span.
     258              :     // Use a content-based check rather than `effectiveContext != Context.root`
     259              :     // — Context.root can carry the propagated context inside an isolate
     260              :     // spawned via Context.runIsolate (the API treats the receiving isolate's
     261              :     // root as the propagated starting context), so an identity-style check
     262              :     // would incorrectly skip parent inheritance there.
     263              :     SpanContext? parentContext;
     264              :     var effectiveParentSpan = parentSpan;
     265           46 :     final effectiveContext = context ?? Context.current;
     266              : 
     267           47 :     if (effectiveContext.span != null) {
     268           11 :       effectiveParentSpan ??= effectiveContext.span;
     269              :     }
     270           47 :     parentContext = effectiveContext.spanContext;
     271              : 
     272              :     // If no parentContext from context but we have a parentSpan, use its context
     273              :     if (parentContext == null && effectiveParentSpan != null) {
     274            5 :       parentContext = effectiveParentSpan.spanContext;
     275              :     }
     276              : 
     277              :     // Determine the trace ID to use
     278              :     TraceId traceId;
     279            4 :     if (spanContext != null && spanContext.traceId.isValid) {
     280              :       // Use provided span context's trace ID if valid
     281            2 :       traceId = spanContext.traceId;
     282              : 
     283              :       // Validate it against parent if both exist and are valid
     284            1 :       if (parentContext != null && parentContext.isValid) {
     285            2 :         if (parentContext.traceId != traceId) {
     286            2 :           throw ArgumentError(
     287              :             'Cannot create span with different trace ID than parent. '
     288            1 :             'Parent trace ID: ${parentContext.traceId}, '
     289              :             'Provided trace ID: $traceId',
     290              :           );
     291              :         }
     292              :       }
     293           10 :     } else if (parentSpan != null && parentSpan.spanContext.isValid) {
     294              :       // An explicit parentSpan takes precedence over the context's span when
     295              :       // both are provided (the parent span ID and trace ID must come from the
     296              :       // same span — using context's traceId with parentSpan's spanId would
     297              :       // produce an invalid parent reference).
     298           10 :       traceId = parentSpan.spanContext.traceId;
     299           11 :     } else if (parentContext != null && parentContext.isValid) {
     300              :       // Inherit from parent if available
     301           11 :       traceId = parentContext.traceId;
     302              :     } else {
     303              :       // Generate new trace ID for root span
     304           47 :       traceId = OTel.traceId();
     305              :     }
     306              : 
     307              :     // Determine the parent span ID
     308              :     SpanId? parentSpanId;
     309              :     if (effectiveParentSpan != null &&
     310           30 :         effectiveParentSpan.spanContext.isValid) {
     311              :       // Use effective parent span's span ID
     312           30 :       parentSpanId = effectiveParentSpan.spanContext.spanId;
     313            2 :     } else if (parentContext != null && parentContext.isValid) {
     314              :       // Use parent context's span ID
     315            2 :       parentSpanId = parentContext.spanId;
     316              :     }
     317              : 
     318              :     // Inherit trace flags from parent — explicit parentSpan wins over context
     319              :     // for consistency with traceId resolution above.
     320              :     TraceFlags? traceFlags;
     321           10 :     if (parentSpan != null && parentSpan.spanContext.isValid) {
     322           10 :       traceFlags = parentSpan.spanContext.traceFlags;
     323           11 :     } else if (parentContext != null && parentContext.isValid) {
     324           11 :       traceFlags = parentContext.traceFlags;
     325              :     }
     326              : 
     327           47 :     if (OTelLog.isDebug()) {
     328              :       if (parentSpanId != null) {
     329           15 :         OTelLog.debug(
     330           15 :           'Creating child span: traceId=$traceId, parentSpanId=$parentSpanId',
     331              :         );
     332              :       } else {
     333           94 :         OTelLog.debug('Creating root span: traceId=$traceId');
     334              :       }
     335              :     }
     336              : 
     337              :     // Apply sampling decision if we have a sampler
     338              :     var shouldRecord = true;
     339           47 :     if (sampler != null) {
     340           94 :       final samplingResult = sampler!.shouldSample(
     341              :         parentContext: effectiveContext,
     342           47 :         traceId: traceId.toString(),
     343              :         name: name,
     344              :         spanKind: kind,
     345              :         attributes: attributes,
     346              :         links: links,
     347              :       );
     348              : 
     349              :       // Update the isRecording flag based on the sampling decision
     350           94 :       shouldRecord = samplingResult.decision != SamplingDecision.drop;
     351              : 
     352              :       // Update trace flags based on sampling decision
     353              :       if (traceFlags == null) {
     354           47 :         traceFlags = OTel.traceFlags(
     355              :           shouldRecord ? TraceFlags.SAMPLED_FLAG : TraceFlags.NONE_FLAG,
     356              :         );
     357           15 :       } else if (shouldRecord && !traceFlags.isSampled) {
     358              :         // Upgrade to sampled if necessary
     359            2 :         traceFlags = OTel.traceFlags(TraceFlags.SAMPLED_FLAG);
     360            0 :       } else if (!shouldRecord && traceFlags.isSampled) {
     361              :         // Downgrade to not sampled if necessary
     362            0 :         traceFlags = OTel.traceFlags(TraceFlags.NONE_FLAG);
     363              :       }
     364              : 
     365              :       // Add sampler attributes if provided
     366           47 :       if (samplingResult.attributes != null) {
     367              :         if (attributes == null) {
     368            1 :           attributes = samplingResult.attributes;
     369              :         } else {
     370            1 :           attributes = attributes.copyWithAttributes(
     371            1 :             samplingResult.attributes!,
     372              :           );
     373              :         }
     374              :       }
     375              : 
     376           47 :       if (OTelLog.isDebug()) {
     377           47 :         OTelLog.debug(
     378           94 :           'Sampling decision for span $name: ${samplingResult.decision}',
     379              :         );
     380              :       }
     381              :     }
     382              : 
     383              :     // Always create a new span context with a new span ID
     384              :     // For root spans, ensure we set an invalid parent span ID (zeros)
     385           47 :     final newSpanContext = OTel.spanContext(
     386              :       traceId: traceId,
     387           47 :       spanId: OTel.spanId(), // Always generate a new span ID
     388              :       parentSpanId: parentSpanId ??
     389           47 :           OTel.spanIdInvalid(), // Use invalid span ID for root spans
     390              :       traceFlags: traceFlags,
     391              :     );
     392              : 
     393              :     // Create the delegate span with our newly created span context
     394           94 :     final delegateSpan = _delegate.startSpan(
     395              :       name,
     396              :       context: effectiveContext,
     397              :       spanContext: newSpanContext,
     398              :       parentSpan: effectiveParentSpan,
     399              :       kind: kind,
     400              :       attributes: attributes,
     401              :       links: links,
     402              :       isRecording: isRecording ?? shouldRecord,
     403              :     );
     404              : 
     405              :     // Wrap it in our SDK span which will handle processing
     406           47 :     final sdkSpan = SDKSpanCreate.create(
     407              :       delegateSpan: delegateSpan,
     408              :       sdkTracer: this,
     409              :     );
     410              : 
     411              :     // Notify processors
     412          138 :     for (final processor in _provider.spanProcessors) {
     413           44 :       processor.onStart(sdkSpan, context);
     414              :     }
     415              : 
     416              :     return sdkSpan;
     417              :   }
     418              : 
     419              :   /// Like [startSpan] + [withSpan] but passes the started span to [fn]
     420              :   /// as an argument and ends the span when [fn] returns,
     421              :   /// so callers can attach attributes / events without going through
     422              :   /// `Context.current`. Routes through [withSpan] for activation;
     423              :   /// [withSpan] handles `recordException` / `setStatus(Error)` on throw,
     424              :   /// honoring [exceptionOptions].
     425            5 :   T startActiveSpan<T>({
     426              :     required String name,
     427              :     required T Function(APISpan span) fn,
     428              :     SpanKind kind = SpanKind.internal,
     429              :     Attributes? attributes,
     430              :     SpanExceptionOptions? exceptionOptions,
     431              :   }) {
     432            5 :     final span = startSpan(name, kind: kind, attributes: attributes);
     433              :     try {
     434           15 :       return withSpan(span, () => fn(span), exceptionOptions: exceptionOptions);
     435              :     } finally {
     436            5 :       span.end();
     437              :     }
     438              :   }
     439              : 
     440              :   /// Async variant of [startActiveSpan]. Routes through [withSpanAsync]
     441              :   /// for activation; [withSpanAsync] handles `recordException` /
     442              :   /// `setStatus(Error)` on throw, honoring [exceptionOptions].
     443            5 :   Future<T> startActiveSpanAsync<T>({
     444              :     required String name,
     445              :     required Future<T> Function(APISpan span) fn,
     446              :     SpanKind kind = SpanKind.internal,
     447              :     Attributes? attributes,
     448              :     SpanExceptionOptions? exceptionOptions,
     449              :   }) async {
     450            5 :     final span = startSpan(name, kind: kind, attributes: attributes);
     451              :     try {
     452            5 :       return await withSpanAsync(
     453              :         span,
     454           10 :         () => fn(span),
     455              :         exceptionOptions: exceptionOptions,
     456              :       );
     457              :     } finally {
     458            5 :       span.end();
     459              :     }
     460              :   }
     461              : 
     462              :   /// Applies [options] when [fn] throws inside [withSpan] / [withSpanAsync].
     463              :   ///
     464              :   /// Default behavior (no sanitizer): records the exception and sets the
     465              :   /// span status to [SpanStatusCode.Error], each gated by
     466              :   /// [SpanExceptionOptions.recordException] and
     467              :   /// [SpanExceptionOptions.setStatusOnException].
     468              :   ///
     469              :   /// When a [SpanExceptionOptions.exceptionSanitizer] is provided, it is
     470              :   /// invoked first and only its returned [SanitizedSpanException] values are
     471              :   /// recorded — the original exception's type, message, and stack trace are
     472              :   /// never recorded, so unsanitized data cannot leak. If the sanitizer
     473              :   /// throws, the span is marked with [SpanStatusCode.Error] using a generic
     474              :   /// description (when status updates are enabled) and the exception is not
     475              :   /// recorded.
     476              :   ///
     477              :   /// The caller always rethrows the original exception; this method never
     478              :   /// throws.
     479            5 :   void _handleSpanException(
     480              :     Span span,
     481              :     Object error,
     482              :     StackTrace stackTrace,
     483              :     SpanExceptionOptions options,
     484              :   ) {
     485            5 :     final sanitizer = options.exceptionSanitizer;
     486              :     if (sanitizer != null) {
     487              :       // Nothing to sanitize for if neither recording nor status is enabled.
     488            1 :       if (!options.recordException && !options.setStatusOnException) {
     489              :         return;
     490              :       }
     491              :       SanitizedSpanException sanitized;
     492              :       try {
     493            1 :         sanitized = sanitizer(error, stackTrace);
     494              :       } catch (sanitizerError) {
     495            1 :         if (OTelLog.isError()) {
     496            2 :           OTelLog.error(
     497              :             'Tracer: exceptionSanitizer threw while handling an exception '
     498            1 :             'on span ${span.name}: $sanitizerError',
     499              :           );
     500              :         }
     501              :         // The sanitizer failed, so we cannot safely record the original
     502              :         // (possibly sensitive) exception. Mark the span as failed with a
     503              :         // generic description instead.
     504            1 :         if (options.setStatusOnException) {
     505            1 :           span.setStatus(SpanStatusCode.Error, 'Exception sanitizer failed');
     506              :         }
     507              :         return;
     508              :       }
     509            1 :       if (options.recordException) {
     510              :         // Pass only the sanitized type/message/stacktrace. recordException
     511              :         // derives defaults from `error`, but the attribute overrides below
     512              :         // replace them, and the original stack trace is never forwarded.
     513            1 :         span.recordException(
     514              :           error,
     515            1 :           stackTrace: sanitized.stackTrace,
     516            2 :           attributes: OTel.attributesFromMap(<String, Object>{
     517            1 :             'exception.type': sanitized.type,
     518            1 :             'exception.message': sanitized.message,
     519              :           }),
     520              :         );
     521              :       }
     522            1 :       if (options.setStatusOnException) {
     523            1 :         span.setStatus(
     524              :           SpanStatusCode.Error,
     525            2 :           sanitized.statusDescription ?? sanitized.message,
     526              :         );
     527              :       }
     528              :       return;
     529              :     }
     530              : 
     531            5 :     if (options.recordException) {
     532            5 :       span.recordException(error, stackTrace: stackTrace);
     533              :     }
     534            5 :     if (options.setStatusOnException) {
     535           10 :       span.setStatus(SpanStatusCode.Error, error.toString());
     536              :     }
     537              :   }
     538              : }
        

Generated by: LCOV version 2.0-1