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 : }
|