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