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

Generated by: LCOV version 2.0-1