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