LCOV - code coverage report
Current view: top level - lib/src/trace - tracer.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 92.9 % 169 157
Test Date: 2026-08-27 23:42:02 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: Event Vision](https://github.com/open-telemetry/opentelemetry-specification/blob/main/oteps/0265-event-vision.md)
      29              : /// and [OTEP 4430: Span Event API deprecation plan](https://github.com/open-telemetry/opentelemetry-specification/blob/main/oteps/4430-span-event-api-deprecation-plan.md),
      30              : /// span events are planned for deprecation in favor of log-based events
      31              : /// emitted via the Logs API; SDKs will provide options to render log-based
      32              : /// events as span events for compatibility.
      33              : ///
      34              : /// More information:
      35              : /// https://opentelemetry.io/docs/specs/otel/trace/sdk/
      36              : class Tracer implements APITracer {
      37              :   final TracerProvider _provider;
      38              :   final APITracer _delegate;
      39              :   final Sampler? _sampler;
      40              :   bool _enabled = true;
      41              : 
      42              :   /// Gets the sampler associated with this tracer.
      43              :   /// If no sampler was specified for this tracer, uses the provider's sampler.
      44          236 :   Sampler? get sampler => _sampler ?? _provider.sampler;
      45              : 
      46              :   /// The effective exception handling options used by [withSpan] /
      47              :   /// [withSpanAsync] when no per-call options are supplied.
      48              :   ///
      49              :   /// Falls back to the provider's [TracerProvider.spanExceptionOptions]
      50              :   /// (configured globally via `OTel.initialize(spanExceptionOptions: ...)`)
      51              :   /// and finally to a default [SpanExceptionOptions] that records the
      52              :   /// exception and sets the span status to error.
      53           11 :   SpanExceptionOptions get spanExceptionOptions =>
      54           22 :       _provider.spanExceptionOptions ?? SpanExceptionOptions.defaults;
      55              : 
      56              :   /// Private constructor for creating Tracer instances.
      57              :   ///
      58              :   /// @param provider The TracerProvider that created this Tracer
      59              :   /// @param delegate The API Tracer implementation to delegate to
      60              :   /// @param sampler Optional custom sampler for this Tracer
      61           60 :   Tracer._({
      62              :     required TracerProvider provider,
      63              :     required APITracer delegate,
      64              :     Sampler? sampler,
      65              :   })  : _provider = provider,
      66              :         _delegate = delegate,
      67              :         _sampler = sampler;
      68              : 
      69            1 :   @override
      70            2 :   String get name => _delegate.name;
      71              : 
      72            1 :   @override
      73            2 :   String? get schemaUrl => _delegate.schemaUrl;
      74              : 
      75            1 :   @override
      76            2 :   String? get version => _delegate.version;
      77              : 
      78            1 :   @override
      79            2 :   Attributes? get attributes => _delegate.attributes;
      80              : 
      81            1 :   @override
      82            2 :   set attributes(Attributes? attributes) => _delegate.attributes = attributes;
      83              : 
      84            2 :   @override
      85              :   bool isEnabled({SpanKind? kind, Context? context}) =>
      86            6 :       _enabled && _provider.hasSpanProcessors;
      87              : 
      88            5 :   @override
      89           10 :   APISpan? get currentSpan => _delegate.currentSpan;
      90              : 
      91              :   /// Sets whether this tracer is enabled.
      92              :   ///
      93              :   /// When disabled, the tracer will still create spans, but they may not be
      94              :   /// recorded or exported.
      95            2 :   set enabled(bool enable) => _enabled = enable;
      96              : 
      97              :   /// Gets the provider that created this tracer.
      98          106 :   TracerProvider get provider => _provider;
      99              : 
     100              :   /// Gets the resource associated with this tracer's provider.
     101          126 :   Resource? get resource => _provider.resource;
     102              : 
     103            0 :   @override
     104            0 :   TimeProvider get timeProvider => _delegate.timeProvider;
     105              : 
     106           10 :   @override
     107              :   T withSpan<T>(
     108              :     APISpan span,
     109              :     T Function() fn, {
     110              :     SpanExceptionOptions? exceptionOptions,
     111              :   }) {
     112              :     // Per-call options are merged field-by-field over the tracer/provider
     113              :     // default (set globally via OTel.initialize), so overriding a single flag
     114              :     // preserves the globally configured sanitizer.
     115           20 :     final options = spanExceptionOptions.mergeWith(exceptionOptions);
     116           10 :     if (OTelLog.isDebug()) {
     117           10 :       OTelLog.debug(
     118           40 :         'Tracer: withSpan called with span ${span.name}, spanId: ${span.spanContext.spanId}',
     119              :       );
     120              :     }
     121              :     // Activate the span in a new Zone via Context.runSync so the active span
     122              :     // propagates correctly across async boundaries inside fn. Wrap fn to
     123              :     // record exceptions on SDK spans.
     124              :     try {
     125           40 :       return Context.current.withSpan(span).runSync(() {
     126           10 :         if (OTelLog.isDebug()) {
     127           30 :           OTelLog.debug('Tracer: Context set with span ${span.name}');
     128              :         }
     129              :         try {
     130           10 :           final result = fn();
     131            9 :           if (OTelLog.isDebug()) {
     132            9 :             OTelLog.debug(
     133           18 :               'Tracer: Function completed in withSpan for ${span.name}',
     134              :             );
     135              :           }
     136              :           return result;
     137              :         } catch (e, stackTrace) {
     138            5 :           if (OTelLog.isError()) {
     139           15 :             OTelLog.error('Tracer: Exception in withSpan for ${span.name}: $e');
     140              :           }
     141              :           // SDK-specific exception recording only when the span is one
     142              :           // of ours. Foreign / no-op APISpans skip this branch — we
     143              :           // still activate them and rethrow.
     144            5 :           if (span is Span) {
     145            5 :             _handleSpanException(span, e, stackTrace, options);
     146              :           }
     147              :           rethrow;
     148              :         }
     149              :       });
     150              :     } finally {
     151           10 :       if (OTelLog.isDebug()) {
     152           30 :         OTelLog.debug('Tracer: withSpan completed for span ${span.name}');
     153           10 :         if (!span.isValid) {
     154            1 :           OTelLog.debug(
     155            2 :             'Tracer: Warning - span ${span.name} is invalid after withSpan operation',
     156              :           );
     157              :         }
     158              :       }
     159              :     }
     160              :   }
     161              : 
     162            8 :   @override
     163              :   Future<T> withSpanAsync<T>(
     164              :     APISpan span,
     165              :     Future<T> Function() fn, {
     166              :     SpanExceptionOptions? exceptionOptions,
     167              :   }) async {
     168              :     // Per-call options are merged field-by-field over the tracer/provider
     169              :     // default (set globally via OTel.initialize), so overriding a single flag
     170              :     // preserves the globally configured sanitizer.
     171           16 :     final options = spanExceptionOptions.mergeWith(exceptionOptions);
     172            8 :     if (OTelLog.isDebug()) {
     173            7 :       OTelLog.debug(
     174           28 :         'Tracer: withSpanAsync called with span ${span.name}, spanId: ${span.spanContext.spanId}',
     175              :       );
     176              :     }
     177              :     try {
     178           32 :       return await Context.current.withSpan(span).run(() async {
     179            8 :         if (OTelLog.isDebug()) {
     180            7 :           OTelLog.debug(
     181           14 :             'Tracer: Context set with span ${span.name} for async operation',
     182              :           );
     183              :         }
     184              :         try {
     185            8 :           return await fn();
     186              :         } catch (e, stackTrace) {
     187            3 :           if (OTelLog.isError()) {
     188            3 :             OTelLog.error(
     189            6 :               'Tracer: Exception in withSpanAsync for ${span.name}: $e',
     190              :             );
     191              :           }
     192              :           // SDK-specific exception recording only when the span is one
     193              :           // of ours. Foreign / no-op APISpans skip this branch — we
     194              :           // still activate them and rethrow.
     195            3 :           if (span is Span) {
     196            3 :             _handleSpanException(span, e, stackTrace, options);
     197              :           }
     198              :           rethrow;
     199              :         }
     200              :       });
     201              :     } finally {
     202            8 :       if (OTelLog.isDebug()) {
     203           21 :         OTelLog.debug('Tracer: withSpanAsync completed for span ${span.name}');
     204            7 :         if (!span.isValid) {
     205            1 :           OTelLog.debug(
     206            2 :             'Tracer: Warning - span ${span.name} is invalid after withSpanAsync operation',
     207              :           );
     208              :         }
     209              :       }
     210              :     }
     211              :   }
     212              : 
     213              :   /// Creates a span without making it active in any context.
     214              :   ///
     215              :   /// Per the Trace SDK spec (SDK Span creation), this goes through the
     216              :   /// same pipeline as [startSpan]: the sampler is queried and the span
     217              :   /// processors are notified. Unlike [startSpan], an explicitly provided
     218              :   /// [spanContext] is used verbatim as the new span's SpanContext
     219              :   /// (identity, flags, and TraceState) rather than only donating its
     220              :   /// trace ID — the sampler still controls IsRecording and processor
     221              :   /// delivery, and the forbidden Sampled==true with IsRecording==false
     222              :   /// combination is corrected by clearing the Sampled flag.
     223            3 :   @override
     224              :   Span createSpan({
     225              :     required String name,
     226              :     SpanContext? spanContext,
     227              :     APISpan? parentSpan,
     228              :     SpanKind kind = SpanKind.internal,
     229              :     Attributes? attributes,
     230              :     List<SpanLink>? links,
     231              :     List<SpanEvent>? spanEvents,
     232              :     DateTime? startTime,
     233              :     bool? isRecording,
     234              :     Context? context,
     235              :   }) {
     236            3 :     if (OTelLog.isDebug()) {
     237            6 :       OTelLog.debug('Tracer: Creating span with name: $name, kind: $kind');
     238              :     }
     239              : 
     240            3 :     return _startSpanInternal(
     241              :       name: name,
     242              :       context: context,
     243              :       spanContext: spanContext,
     244              :       parentSpan: parentSpan,
     245              :       kind: kind,
     246              :       attributes: attributes,
     247              :       links: links,
     248              :       spanEvents: spanEvents,
     249              :       startTime: startTime,
     250              :       isRecording: isRecording,
     251              :       honorExplicitSpanContext: true,
     252              :     );
     253              :   }
     254              : 
     255              :   /// Starts a new span.
     256              :   ///
     257              :   /// [isRecording] defaults to null, which means the sampler's decision
     258              :   /// determines whether the span records. Passing false forces a
     259              :   /// non-recording span and clears the Sampled flag (the SDK MUST NOT
     260              :   /// produce Sampled == true with IsRecording == false); passing true
     261              :   /// cannot resurrect a span the sampler decided to drop.
     262           58 :   @override
     263              :   Span startSpan(
     264              :     String name, {
     265              :     Context? context,
     266              :     SpanContext? spanContext,
     267              :     APISpan? parentSpan,
     268              :     SpanKind kind = SpanKind.internal,
     269              :     Attributes? attributes,
     270              :     List<SpanLink>? links,
     271              :     bool? isRecording,
     272              :   }) {
     273           58 :     if (OTelLog.isDebug()) {
     274          106 :       OTelLog.debug('Tracer: Starting span with name: $name, kind: $kind');
     275              :     }
     276              : 
     277           58 :     return _startSpanInternal(
     278              :       name: name,
     279              :       context: context,
     280              :       spanContext: spanContext,
     281              :       parentSpan: parentSpan,
     282              :       kind: kind,
     283              :       attributes: attributes,
     284              :       links: links,
     285              :       isRecording: isRecording,
     286              :     );
     287              :   }
     288              : 
     289              :   /// Shared SDK span-creation pipeline, per the Trace SDK spec
     290              :   /// ("SDK Span creation"): resolve the parent, generate a new SpanId,
     291              :   /// query the sampler's ShouldSample, create the span according to the
     292              :   /// decision, and notify the span processors.
     293              :   ///
     294              :   /// When [honorExplicitSpanContext] is true (the [createSpan] path) and
     295              :   /// [spanContext] is provided, that SpanContext is used verbatim for
     296              :   /// the new span instead of minting a new SpanId and deriving flags
     297              :   /// from the sampling decision; the decision still governs IsRecording
     298              :   /// and processor delivery.
     299           59 :   Span _startSpanInternal({
     300              :     required String name,
     301              :     Context? context,
     302              :     SpanContext? spanContext,
     303              :     APISpan? parentSpan,
     304              :     SpanKind kind = SpanKind.internal,
     305              :     Attributes? attributes,
     306              :     List<SpanLink>? links,
     307              :     List<SpanEvent>? spanEvents,
     308              :     DateTime? startTime,
     309              :     bool? isRecording,
     310              :     bool honorExplicitSpanContext = false,
     311              :   }) {
     312              :     // Get parent context from either the passed context or parent span.
     313              :     // Use a content-based check rather than `effectiveContext != Context.root`
     314              :     // — Context.root can carry the propagated context inside an isolate
     315              :     // spawned via Context.runIsolate (the API treats the receiving isolate's
     316              :     // root as the propagated starting context), so an identity-style check
     317              :     // would incorrectly skip parent inheritance there.
     318              :     SpanContext? parentContext;
     319              :     var effectiveParentSpan = parentSpan;
     320           58 :     final effectiveContext = context ?? Context.current;
     321              : 
     322           59 :     if (effectiveContext.span != null) {
     323           11 :       effectiveParentSpan ??= effectiveContext.span;
     324              :     }
     325           59 :     parentContext = effectiveContext.spanContext;
     326              : 
     327              :     // If no parentContext from context but we have a parentSpan, use its context
     328              :     if (parentContext == null && effectiveParentSpan != null) {
     329            6 :       parentContext = effectiveParentSpan.spanContext;
     330              :     }
     331              : 
     332              :     // Determine the trace ID to use
     333              :     TraceId traceId;
     334            8 :     if (spanContext != null && spanContext.traceId.isValid) {
     335              :       // Use provided span context's trace ID if valid
     336            4 :       traceId = spanContext.traceId;
     337              : 
     338              :       // Validate it against parent if both exist and are valid
     339            1 :       if (parentContext != null && parentContext.isValid) {
     340            2 :         if (parentContext.traceId != traceId) {
     341            2 :           throw ArgumentError(
     342              :             'Cannot create span with different trace ID than parent. '
     343            1 :             'Parent trace ID: ${parentContext.traceId}, '
     344              :             'Provided trace ID: $traceId',
     345              :           );
     346              :         }
     347              :       }
     348           12 :     } else if (parentSpan != null && parentSpan.spanContext.isValid) {
     349              :       // An explicit parentSpan takes precedence over the context's span when
     350              :       // both are provided (the parent span ID and trace ID must come from the
     351              :       // same span — using context's traceId with parentSpan's spanId would
     352              :       // produce an invalid parent reference).
     353           12 :       traceId = parentSpan.spanContext.traceId;
     354           12 :     } else if (parentContext != null && parentContext.isValid) {
     355              :       // Inherit from parent if available
     356           12 :       traceId = parentContext.traceId;
     357              :     } else {
     358              :       // Generate new trace ID for root span
     359           58 :       traceId = OTel.traceId();
     360              :     }
     361              : 
     362              :     // Determine the parent span ID
     363              :     SpanId? parentSpanId;
     364              :     if (effectiveParentSpan != null &&
     365           32 :         effectiveParentSpan.spanContext.isValid) {
     366              :       // Use effective parent span's span ID
     367           32 :       parentSpanId = effectiveParentSpan.spanContext.spanId;
     368            3 :     } else if (parentContext != null && parentContext.isValid) {
     369              :       // Use parent context's span ID
     370            3 :       parentSpanId = parentContext.spanId;
     371              :     }
     372              : 
     373              :     // Inherit trace flags and TraceState from parent — explicit parentSpan
     374              :     // wins over context for consistency with traceId resolution above.
     375              :     // Per the Trace API spec (Span creation), "the child span MUST inherit
     376              :     // all TraceState values of its parent by default"; this applies to
     377              :     // local and remote parents alike.
     378              :     TraceFlags? traceFlags;
     379              :     TraceState? parentTraceState;
     380           12 :     if (parentSpan != null && parentSpan.spanContext.isValid) {
     381           12 :       traceFlags = parentSpan.spanContext.traceFlags;
     382           12 :       parentTraceState = parentSpan.spanContext.traceState;
     383           12 :     } else if (parentContext != null && parentContext.isValid) {
     384           12 :       traceFlags = parentContext.traceFlags;
     385           12 :       parentTraceState = parentContext.traceState;
     386              :     }
     387              :     // An explicit spanContext argument can also donate a TraceState when
     388              :     // no parent supplied one (it already donates the traceId above).
     389            4 :     parentTraceState ??= spanContext?.traceState;
     390              : 
     391           59 :     if (OTelLog.isDebug()) {
     392              :       if (parentSpanId != null) {
     393           16 :         OTelLog.debug(
     394           16 :           'Creating child span: traceId=$traceId, parentSpanId=$parentSpanId',
     395              :         );
     396              :       } else {
     397          108 :         OTelLog.debug('Creating root span: traceId=$traceId');
     398              :       }
     399              :     }
     400              : 
     401              :     // Apply sampling decision if we have a sampler
     402              :     var shouldRecord = true;
     403              :     bool? sampled; // null: no sampler configured, keep inherited flags
     404           59 :     if (sampler != null) {
     405          118 :       final samplingResult = sampler!.shouldSample(
     406              :         parentContext: effectiveContext,
     407           59 :         traceId: traceId.toString(),
     408              :         name: name,
     409              :         spanKind: kind,
     410              :         attributes: attributes,
     411              :         links: links,
     412              :       );
     413              : 
     414              :       // Map the decision onto the (IsRecording, Sampled) pair, per the
     415              :       // Trace SDK spec (ShouldSample):
     416              :       //   DROP              -> IsRecording false, Sampled MUST NOT be set
     417              :       //   RECORD_ONLY       -> IsRecording true,  Sampled MUST NOT be set
     418              :       //   RECORD_AND_SAMPLE -> IsRecording true,  Sampled MUST be set
     419           59 :       switch (samplingResult.decision) {
     420           59 :         case SamplingDecision.drop:
     421              :           shouldRecord = false;
     422              :           sampled = false;
     423           59 :         case SamplingDecision.recordOnly:
     424              :           shouldRecord = true;
     425              :           sampled = false;
     426           59 :         case SamplingDecision.recordAndSample:
     427              :           shouldRecord = true;
     428              :           sampled = true;
     429              :       }
     430              : 
     431              :       // Per the Trace SDK spec (ShouldSample), the Tracestate returned
     432              :       // by the sampler is associated with the Span through the new
     433              :       // SpanContext. An explicitly empty TraceState clears it; null
     434              :       // means the sampler has no opinion (samplers written before
     435              :       // SamplingResult.traceState existed keep parent inheritance).
     436           59 :       final samplerTraceState = samplingResult.traceState;
     437              :       if (samplerTraceState != null) {
     438            1 :         parentTraceState = samplerTraceState.isEmpty ? null : samplerTraceState;
     439              :       }
     440              : 
     441              :       // Add sampler attributes if provided
     442           59 :       if (samplingResult.attributes != null) {
     443              :         if (attributes == null) {
     444            1 :           attributes = samplingResult.attributes;
     445              :         } else {
     446            1 :           attributes = attributes.copyWithAttributes(
     447            1 :             samplingResult.attributes!,
     448              :           );
     449              :         }
     450              :       }
     451              : 
     452           59 :       if (OTelLog.isDebug()) {
     453           54 :         OTelLog.debug(
     454          108 :           'Sampling decision for span $name: ${samplingResult.decision}',
     455              :         );
     456              :       }
     457              :     }
     458              : 
     459              :     // The sampler owns the recording decision. A caller may pass
     460              :     // isRecording: false to force a non-recording span, but can never
     461              :     // resurrect a dropped one (spec: DROP => IsRecording will be false).
     462              :     final recording = shouldRecord && (isRecording ?? true);
     463              : 
     464              :     // The SDK MUST NOT allow Sampled == true with IsRecording == false
     465              :     // (Trace SDK spec, Sampling — the combination causes gaps in the
     466              :     // distributed trace). A forced non-recording span is never sampled.
     467              :     if (!recording) {
     468              :       sampled = false;
     469              :     }
     470              : 
     471              :     if (sampled != null) {
     472              :       // The Sampled trace flag reflects the sampling decision only —
     473              :       // RECORD_ONLY records without setting the flag.
     474           59 :       traceFlags = OTel.traceFlags(
     475              :         sampled ? TraceFlags.SAMPLED_FLAG : TraceFlags.NONE_FLAG,
     476              :       );
     477            0 :     } else if (!recording && (traceFlags?.isSampled ?? false)) {
     478              :       // No sampler configured and flags inherited from the parent:
     479              :       // still clear Sampled on a forced non-recording span.
     480            0 :       traceFlags = OTel.traceFlags(TraceFlags.NONE_FLAG);
     481              :     }
     482              : 
     483              :     final SpanContext newSpanContext;
     484              :     if (honorExplicitSpanContext && spanContext != null) {
     485              :       // createSpan contract: an explicitly provided SpanContext is used
     486              :       // verbatim. Only the forbidden Sampled==true with
     487              :       // IsRecording==false combination is corrected (Trace SDK spec,
     488              :       // Sampling: the SDK MUST NOT allow it).
     489            0 :       newSpanContext = (!recording && spanContext.traceFlags.isSampled)
     490            0 :           ? OTel.spanContext(
     491            0 :               traceId: spanContext.traceId,
     492            0 :               spanId: spanContext.spanId,
     493            0 :               parentSpanId: spanContext.parentSpanId,
     494            0 :               traceFlags: OTel.traceFlags(TraceFlags.NONE_FLAG),
     495            0 :               traceState: spanContext.traceState,
     496            0 :               isRemote: spanContext.isRemote,
     497              :             )
     498              :           : spanContext;
     499              :     } else {
     500              :       // Always create a new span context with a new span ID
     501              :       // For root spans, ensure we set an invalid parent span ID (zeros)
     502           58 :       newSpanContext = OTel.spanContext(
     503              :         traceId: traceId,
     504           58 :         spanId: OTel.spanId(), // Always generate a new span ID
     505              :         parentSpanId: parentSpanId ??
     506           58 :             OTel.spanIdInvalid(), // Use invalid span ID for root spans
     507              :         traceFlags: traceFlags,
     508              :         traceState: parentTraceState,
     509              :       );
     510              :     }
     511              : 
     512              :     // Create the delegate span with our newly created span context.
     513              :     // (The API's startSpan forwards verbatim to createSpan; calling
     514              :     // createSpan directly also lets this pipeline carry spanEvents and
     515              :     // an explicit startTime.)
     516          118 :     final delegateSpan = _delegate.createSpan(
     517              :       name: name,
     518              :       context: effectiveContext,
     519              :       spanContext: newSpanContext,
     520              :       parentSpan: effectiveParentSpan,
     521              :       kind: kind,
     522              :       attributes: attributes,
     523              :       links: links,
     524              :       spanEvents: spanEvents,
     525              :       startTime: startTime,
     526              :       isRecording: recording,
     527              :     );
     528              : 
     529              :     // Wrap it in our SDK span which will handle processing
     530           59 :     final sdkSpan = SDKSpanCreate.create(
     531              :       delegateSpan: delegateSpan,
     532              :       sdkTracer: this,
     533              :       isRecording: recording,
     534              :     );
     535              : 
     536              :     // Notify processors. Per the Trace SDK spec (Sampling), span
     537              :     // processors MUST receive only spans with IsRecording == true.
     538              :     // OnStart receives "the parent Context of the span that the SDK
     539              :     // determined", so pass the resolved effectiveContext, never the raw
     540              :     // (possibly null) context argument.
     541              :     if (recording) {
     542          169 :       for (final processor in _provider.spanProcessors) {
     543           51 :         processor.onStart(sdkSpan, effectiveContext);
     544              :       }
     545              :     }
     546              : 
     547              :     return sdkSpan;
     548              :   }
     549              : 
     550              :   /// Like [startSpan] + [withSpan] but passes the started span to [fn]
     551              :   /// as an argument and ends the span when [fn] returns,
     552              :   /// so callers can attach attributes / events without going through
     553              :   /// `Context.current`. Routes through [withSpan] for activation;
     554              :   /// [withSpan] handles `recordException` / `setStatus(Error)` on throw,
     555              :   /// honoring [exceptionOptions].
     556            5 :   T startActiveSpan<T>({
     557              :     required String name,
     558              :     required T Function(APISpan span) fn,
     559              :     SpanKind kind = SpanKind.internal,
     560              :     Attributes? attributes,
     561              :     SpanExceptionOptions? exceptionOptions,
     562              :   }) {
     563            5 :     final span = startSpan(name, kind: kind, attributes: attributes);
     564              :     try {
     565           15 :       return withSpan(span, () => fn(span), exceptionOptions: exceptionOptions);
     566              :     } finally {
     567            5 :       span.end();
     568              :     }
     569              :   }
     570              : 
     571              :   /// Async variant of [startActiveSpan]. Routes through [withSpanAsync]
     572              :   /// for activation; [withSpanAsync] handles `recordException` /
     573              :   /// `setStatus(Error)` on throw, honoring [exceptionOptions].
     574            5 :   Future<T> startActiveSpanAsync<T>({
     575              :     required String name,
     576              :     required Future<T> Function(APISpan span) fn,
     577              :     SpanKind kind = SpanKind.internal,
     578              :     Attributes? attributes,
     579              :     SpanExceptionOptions? exceptionOptions,
     580              :   }) async {
     581            5 :     final span = startSpan(name, kind: kind, attributes: attributes);
     582              :     try {
     583            5 :       return await withSpanAsync(
     584              :         span,
     585           10 :         () => fn(span),
     586              :         exceptionOptions: exceptionOptions,
     587              :       );
     588              :     } finally {
     589            5 :       span.end();
     590              :     }
     591              :   }
     592              : 
     593              :   /// Applies [options] when [fn] throws inside [withSpan] / [withSpanAsync].
     594              :   ///
     595              :   /// Default behavior (no sanitizer): records the exception and sets the
     596              :   /// span status to [SpanStatusCode.Error], each gated by
     597              :   /// [SpanExceptionOptions.recordException] and
     598              :   /// [SpanExceptionOptions.setStatusOnException].
     599              :   ///
     600              :   /// When a [SpanExceptionOptions.exceptionSanitizer] is provided, it is
     601              :   /// invoked first and only its returned [SanitizedSpanException] values are
     602              :   /// recorded — the original exception's type, message, and stack trace are
     603              :   /// never recorded, so unsanitized data cannot leak. If the sanitizer
     604              :   /// throws, the span is marked with [SpanStatusCode.Error] using a generic
     605              :   /// description (when status updates are enabled) and the exception is not
     606              :   /// recorded.
     607              :   ///
     608              :   /// The caller always rethrows the original exception; this method never
     609              :   /// throws.
     610            5 :   void _handleSpanException(
     611              :     Span span,
     612              :     Object error,
     613              :     StackTrace stackTrace,
     614              :     SpanExceptionOptions options,
     615              :   ) {
     616            5 :     final sanitizer = options.exceptionSanitizer;
     617              :     if (sanitizer != null) {
     618              :       // Nothing to sanitize for if neither recording nor status is enabled.
     619            1 :       if (!options.recordException && !options.setStatusOnException) {
     620              :         return;
     621              :       }
     622              :       SanitizedSpanException sanitized;
     623              :       try {
     624            1 :         sanitized = sanitizer(error, stackTrace);
     625              :       } catch (sanitizerError) {
     626            1 :         if (OTelLog.isError()) {
     627            2 :           OTelLog.error(
     628              :             'Tracer: exceptionSanitizer threw while handling an exception '
     629            1 :             'on span ${span.name}: $sanitizerError',
     630              :           );
     631              :         }
     632              :         // The sanitizer failed, so we cannot safely record the original
     633              :         // (possibly sensitive) exception. Mark the span as failed with a
     634              :         // generic description instead.
     635            1 :         if (options.setStatusOnException) {
     636            1 :           span.setStatus(SpanStatusCode.Error, 'Exception sanitizer failed');
     637              :         }
     638              :         return;
     639              :       }
     640            1 :       if (options.recordException) {
     641              :         // Pass only the sanitized type/message/stacktrace. recordException
     642              :         // derives defaults from `error`, but the attribute overrides below
     643              :         // replace them, and the original stack trace is never forwarded.
     644            1 :         span.recordException(
     645              :           error,
     646            1 :           stackTrace: sanitized.stackTrace,
     647            2 :           attributes: OTel.attributesFromMap(<String, Object>{
     648            2 :             ExceptionAttributes.exceptionType.key: sanitized.type,
     649            2 :             ExceptionAttributes.exceptionMessage.key: sanitized.message,
     650              :           }),
     651              :         );
     652              :       }
     653            1 :       if (options.setStatusOnException) {
     654            1 :         span.setStatus(
     655              :           SpanStatusCode.Error,
     656            2 :           sanitized.statusDescription ?? sanitized.message,
     657              :         );
     658              :       }
     659              :       return;
     660              :     }
     661              : 
     662            5 :     if (options.recordException) {
     663            5 :       span.recordException(error, stackTrace: stackTrace);
     664              :     }
     665            5 :     if (options.setStatusOnException) {
     666           10 :       span.setStatus(SpanStatusCode.Error, error.toString());
     667              :     }
     668              :   }
     669              : }
        

Generated by: LCOV version 2.0-1