diff --git a/example/sampling.dart b/example/sampling.dart new file mode 100644 index 00000000..11f79b13 --- /dev/null +++ b/example/sampling.dart @@ -0,0 +1,91 @@ +// Copyright 2021-2022 Workiva. +// Licensed under the Apache License, Version 2.0. Please see https://github.com/Workiva/opentelemetry-dart/blob/master/LICENSE for more information + +import 'package:collection/collection.dart'; +import 'package:opentelemetry/api.dart' + show + Attribute, + Context, + SpanKind, + SpanLink, + TraceId, + registerGlobalTracerProvider, + spanContextFromContext; +import 'package:opentelemetry/sdk.dart' + show + ConsoleExporter, + Decision, + ParentBasedSampler, + ReadOnlySpan, + ReadWriteSpan, + Sampler, + SamplingResult, + SimpleSpanProcessor, + TracerProviderBase; + +final Attribute samplingOffAttribute = + Attribute.fromInt('sampling.priority', 0); + +class SpanSamplingPrioritySampler implements Sampler { + @override + SamplingResult shouldSample( + Context parentContext, + TraceId traceId, + String name, + SpanKind spanKind, + List attributes, + List links) { + final decision = attributes.firstWhereOrNull((element) => + element.key == 'sampling.priority' && element.value == 0) != + null + ? Decision.recordOnly + : Decision.recordAndSample; + + return SamplingResult( + decision, attributes, spanContextFromContext(parentContext).traceState); + } + + @override + String get description => 'SpanSamplingPrioritySampler'; +} + +class PrintingSpanProcessor extends SimpleSpanProcessor { + PrintingSpanProcessor(super.exporter); + + @override + void onStart(ReadWriteSpan span, Context parentContext) { + print('Span started: ${span.name}'); + super.onStart(span, parentContext); + } + + @override + void onEnd(ReadOnlySpan span) { + print('Span ended: ${span.name}'); + super.onEnd(span); + } + + @override + void shutdown() { + print('Shutting down'); + super.shutdown(); + } + + @override + void forceFlush() {} +} + +void main(List args) async { + final sampler = ParentBasedSampler(SpanSamplingPrioritySampler()); + final tp = TracerProviderBase( + processors: [PrintingSpanProcessor(ConsoleExporter())], sampler: sampler); + registerGlobalTracerProvider(tp); + + final tracer = tp.getTracer('instrumentation-name'); + + tracer.startSpan('span-not-sampled', attributes: [ + samplingOffAttribute, + ]).end(); + tracer.startSpan('span-sampled').end(); + + tp.shutdown(); +} diff --git a/lib/src/api/open_telemetry.dart b/lib/src/api/open_telemetry.dart index a99d75d1..488db6e5 100644 --- a/lib/src/api/open_telemetry.dart +++ b/lib/src/api/open_telemetry.dart @@ -81,6 +81,7 @@ Future trace(String name, Future Function() fn, {api.Context? context, api.Tracer? tracer, bool newRoot = false, + List spanAttributes = const [], api.SpanKind spanKind = api.SpanKind.internal, List spanLinks = const []}) async { context ??= api.Context.current; @@ -89,6 +90,7 @@ Future trace(String name, Future Function() fn, final span = tracer.startSpan(name, // TODO: use start span option `newRoot` instead context: newRoot ? api.Context.root : context, + attributes: spanAttributes, kind: spanKind, links: spanLinks); try { @@ -116,6 +118,7 @@ T traceSync(String name, T Function() fn, {api.Context? context, api.Tracer? tracer, bool newRoot = false, + List spanAttributes = const [], api.SpanKind spanKind = api.SpanKind.internal, List spanLinks = const []}) { context ??= api.Context.current; @@ -124,6 +127,7 @@ T traceSync(String name, T Function() fn, final span = tracer.startSpan(name, // TODO: use start span option `newRoot` instead context: newRoot ? api.Context.root : context, + attributes: spanAttributes, kind: spanKind, links: spanLinks); try { diff --git a/lib/src/api/span_processors/span_processor.dart b/lib/src/api/span_processors/span_processor.dart index 52923497..391e08f3 100644 --- a/lib/src/api/span_processors/span_processor.dart +++ b/lib/src/api/span_processors/span_processor.dart @@ -4,7 +4,7 @@ import '../../../api.dart' as api; @Deprecated( - 'This class will be moved to the SDK package in 0.19.0. Use [SpanExporter] from SDK instead.') + 'This class will be moved to the SDK package in 0.19.0. Use [SpanProcessor] from SDK instead.') abstract class SpanProcessor { void onStart(api.Span span, api.Context parentContext); diff --git a/lib/src/api/trace/span_context.dart b/lib/src/api/trace/span_context.dart index cc23de0f..efa699b4 100644 --- a/lib/src/api/trace/span_context.dart +++ b/lib/src/api/trace/span_context.dart @@ -5,39 +5,29 @@ import '../../../api.dart' as api; /// Representation of the context of an individual span. class SpanContext { - final api.SpanId _spanId; - final api.TraceId _traceId; - final int _traceFlags; - final api.TraceState _traceState; - final bool _isRemote; - - api.TraceId get traceId => _traceId; - - api.SpanId get spanId => _spanId; - - int get traceFlags => _traceFlags; - - api.TraceState get traceState => _traceState; + final api.TraceId traceId; + final api.SpanId spanId; + final int traceFlags; + final api.TraceState traceState; + final bool isRemote; bool get isValid => spanId.isValid && traceId.isValid; /// Construct a [SpanContext]. - SpanContext(this._traceId, this._spanId, this._traceFlags, this._traceState) - : _isRemote = false; + SpanContext(this.traceId, this.spanId, this.traceFlags, this.traceState) + : isRemote = false; - /// Construct a [SpanContext] representing an operation which originated - /// from a remote source. + /// Construct a [SpanContext] representing an operation which originated from + /// a remote source. SpanContext.remote( - this._traceId, this._spanId, this._traceFlags, this._traceState) - : _isRemote = true; + this.traceId, this.spanId, this.traceFlags, this.traceState) + : isRemote = true; /// Construct an invalid [SpanContext]. SpanContext.invalid() - : _spanId = api.SpanId.invalid(), - _traceId = api.TraceId.invalid(), - _traceFlags = api.TraceFlags.none, - _traceState = api.TraceState.empty(), - _isRemote = false; - - bool get isRemote => _isRemote; + : spanId = api.SpanId.invalid(), + traceId = api.TraceId.invalid(), + traceFlags = api.TraceFlags.none, + traceState = api.TraceState.empty(), + isRemote = false; } diff --git a/lib/src/sdk/trace/exporters/collector_exporter.dart b/lib/src/sdk/trace/exporters/collector_exporter.dart index f41000a5..7a54e2be 100644 --- a/lib/src/sdk/trace/exporters/collector_exporter.dart +++ b/lib/src/sdk/trace/exporters/collector_exporter.dart @@ -268,6 +268,8 @@ class CollectorExporter implements sdk.SpanExporter { return pb_common.AnyValue(); } + @Deprecated( + 'This method will be removed in 0.19.0. Use [SpanProcessor] instead.') @override void forceFlush() { return; diff --git a/lib/src/sdk/trace/exporters/console_exporter.dart b/lib/src/sdk/trace/exporters/console_exporter.dart index 6413a647..450295f5 100644 --- a/lib/src/sdk/trace/exporters/console_exporter.dart +++ b/lib/src/sdk/trace/exporters/console_exporter.dart @@ -33,6 +33,8 @@ class ConsoleExporter implements sdk.SpanExporter { _printSpans(spans); } + @Deprecated( + 'This method will be removed in 0.19.0. Use [SpanProcessor] instead.') @override void forceFlush() { return; diff --git a/lib/src/sdk/trace/exporters/span_exporter.dart b/lib/src/sdk/trace/exporters/span_exporter.dart index eca1e281..5e5414ed 100644 --- a/lib/src/sdk/trace/exporters/span_exporter.dart +++ b/lib/src/sdk/trace/exporters/span_exporter.dart @@ -1,11 +1,13 @@ // Copyright 2021-2022 Workiva. // Licensed under the Apache License, Version 2.0. Please see https://github.com/Workiva/opentelemetry-dart/blob/master/LICENSE for more information -import '../../../../sdk.dart' as sdk; +import '../read_only_span.dart'; abstract class SpanExporter { - void export(List spans); + void export(List spans); + @Deprecated( + 'This method will be removed in 0.19.0. Use [SpanProcessor] instead.') void forceFlush(); void shutdown(); diff --git a/lib/src/sdk/trace/span_processors/batch_processor.dart b/lib/src/sdk/trace/span_processors/batch_processor.dart index 1eac0601..b9145d7d 100644 --- a/lib/src/sdk/trace/span_processors/batch_processor.dart +++ b/lib/src/sdk/trace/span_processors/batch_processor.dart @@ -6,19 +6,23 @@ import 'dart:math'; import 'package:logging/logging.dart'; -import '../../../../api.dart' as api; -import '../../../../sdk.dart' as sdk; - -class BatchSpanProcessor implements sdk.SpanProcessor { +import '../../../api/context/context.dart'; +import '../../../api/trace/trace_flags.dart'; +import '../exporters/span_exporter.dart'; +import '../read_only_span.dart'; +import '../read_write_span.dart'; +import 'span_processor.dart'; + +class BatchSpanProcessor implements SpanProcessor { static const int _DEFAULT_MAXIMUM_BATCH_SIZE = 512; static const int _DEFAULT_MAXIMUM_QUEUE_SIZE = 2048; static const int _DEFAULT_EXPORT_DELAY = 5000; - final sdk.SpanExporter _exporter; + final SpanExporter _exporter; final Logger _log = Logger('opentelemetry.BatchSpanProcessor'); final int _maxExportBatchSize; final int _maxQueueSize; - final List _spanBuffer = []; + final List _spanBuffer = []; late final Timer _timer; @@ -41,11 +45,10 @@ class BatchSpanProcessor implements sdk.SpanProcessor { while (_spanBuffer.isNotEmpty) { _exportBatch(_timer); } - _exporter.forceFlush(); } @override - void onEnd(sdk.ReadOnlySpan span) { + void onEnd(ReadOnlySpan span) { if (_isShutdown) { return; } @@ -53,7 +56,7 @@ class BatchSpanProcessor implements sdk.SpanProcessor { } @override - void onStart(sdk.ReadWriteSpan span, api.Context parentContext) {} + void onStart(ReadWriteSpan span, Context parentContext) {} @override void shutdown() { @@ -63,7 +66,7 @@ class BatchSpanProcessor implements sdk.SpanProcessor { _exporter.shutdown(); } - void _addToBuffer(sdk.ReadOnlySpan span) { + void _addToBuffer(ReadOnlySpan span) { if (_spanBuffer.length >= _maxQueueSize) { // Buffer is full, drop span. _log.warning( @@ -71,7 +74,11 @@ class BatchSpanProcessor implements sdk.SpanProcessor { return; } - _spanBuffer.add(span); + final isSampled = + span.spanContext.traceFlags & TraceFlags.sampled == TraceFlags.sampled; + if (isSampled) { + _spanBuffer.add(span); + } } void _exportBatch(Timer timer) { diff --git a/lib/src/sdk/trace/span_processors/simple_processor.dart b/lib/src/sdk/trace/span_processors/simple_processor.dart index a39e6aa2..e0846752 100644 --- a/lib/src/sdk/trace/span_processors/simple_processor.dart +++ b/lib/src/sdk/trace/span_processors/simple_processor.dart @@ -1,35 +1,40 @@ // Copyright 2021-2022 Workiva. // Licensed under the Apache License, Version 2.0. Please see https://github.com/Workiva/opentelemetry-dart/blob/master/LICENSE for more information -import '../../../../api.dart' as api; -import '../../../../sdk.dart' as sdk; - -class SimpleSpanProcessor implements sdk.SpanProcessor { - final sdk.SpanExporter _exporter; +import '../../../api/context/context.dart'; +import '../../../api/trace/trace_flags.dart'; +import '../exporters/span_exporter.dart'; +import '../read_only_span.dart'; +import '../read_write_span.dart'; +import 'span_processor.dart'; + +class SimpleSpanProcessor implements SpanProcessor { + final SpanExporter _exporter; bool _isShutdown = false; SimpleSpanProcessor(this._exporter); @override - void forceFlush() { - _exporter.forceFlush(); - } + void forceFlush() {} @override - void onEnd(sdk.ReadOnlySpan span) { + void onEnd(ReadOnlySpan span) { if (_isShutdown) { return; } - _exporter.export([span]); + final isSampled = + span.spanContext.traceFlags & TraceFlags.sampled == TraceFlags.sampled; + if (isSampled) { + _exporter.export([span]); + } } @override - void onStart(sdk.ReadWriteSpan span, api.Context parentContext) {} + void onStart(ReadWriteSpan span, Context parentContext) {} @override void shutdown() { - forceFlush(); _isShutdown = true; _exporter.shutdown(); } diff --git a/test/sdk/trace/span_processors/batch_processor_test.dart b/test/sdk/trace/span_processors/batch_processor_test.dart new file mode 100644 index 00000000..a24a7de5 --- /dev/null +++ b/test/sdk/trace/span_processors/batch_processor_test.dart @@ -0,0 +1,111 @@ +// Copyright 2021-2022 Workiva. +// Licensed under the Apache License, Version 2.0. Please see https://github.com/Workiva/opentelemetry-dart/blob/master/LICENSE for more information + +@TestOn('vm') +import 'package:opentelemetry/api.dart' + show SpanContext, SpanId, SpanKind, TraceFlags, TraceId, TraceState; +import 'package:opentelemetry/sdk.dart'; +import 'package:opentelemetry/src/sdk/trace/span.dart'; +import 'package:test/test.dart'; + +class TestSpanExporter extends SpanExporter { + final spans = []; + + bool isShutdown = false; + + @override + void export(List spans) { + this.spans.addAll(spans); + } + + @override + void forceFlush() {} + + @override + void shutdown() { + isShutdown = true; + } +} + +void main() { + final sampledSpanContext = SpanContext(TraceId([1, 2, 3]), SpanId([7, 8, 9]), + TraceFlags.sampled, TraceState.empty()); + final sampledSpan = Span( + 'foo', + sampledSpanContext, + SpanId([4, 5, 6]), + [], + DateTimeTimeProvider(), + Resource([]), + InstrumentationScope( + 'library_name', 'library_version', 'url://schema', []), + SpanKind.client, + [], + SpanLimits(), + DateTimeTimeProvider().now); + + final unsampledSpanContext = SpanContext(TraceId([1, 2, 3]), + SpanId([7, 8, 9]), TraceFlags.none, TraceState.empty()); + final unsampledSpan = Span( + 'foo', + unsampledSpanContext, + SpanId([4, 5, 6]), + [], + DateTimeTimeProvider(), + Resource([]), + InstrumentationScope( + 'library_name', 'library_version', 'url://schema', []), + SpanKind.client, + [], + SpanLimits(), + DateTimeTimeProvider().now); + + late TestSpanExporter exporter; + late BatchSpanProcessor processor; + + setUp(() { + exporter = TestSpanExporter(); + processor = BatchSpanProcessor(exporter, + maxExportBatchSize: 2, scheduledDelayMillis: 60 * 60 * 1000); + }); + + group('onEnd', () { + test('adds sampled span to buffer', () { + processor.onEnd(sampledSpan); + expect(exporter.spans, isEmpty); + processor.forceFlush(); + expect(exporter.spans, [sampledSpan]); + }); + + test('does not add unsampled span to buffer', () { + processor.onEnd(unsampledSpan); + expect(exporter.spans, isEmpty); + processor.forceFlush(); + expect(exporter.spans, isEmpty); + }); + + test('does not add span to buffer after shutdown', () { + processor + ..shutdown() + ..onEnd(sampledSpan) + ..forceFlush(); + expect(exporter.spans, isEmpty); + }); + }); + + group('forceFlush', () { + test('flushes buffer', () { + processor + ..onEnd(sampledSpan) + ..forceFlush(); + expect(exporter.spans, [sampledSpan]); + }); + }); + + group('shutdown', () { + test('calls exporter.shutdown()', () { + processor.shutdown(); + expect(exporter.isShutdown, isTrue); + }); + }); +} diff --git a/test/sdk/trace/span_processors/simple_processor_test.dart b/test/sdk/trace/span_processors/simple_processor_test.dart new file mode 100644 index 00000000..c1c06da0 --- /dev/null +++ b/test/sdk/trace/span_processors/simple_processor_test.dart @@ -0,0 +1,96 @@ +// Copyright 2021-2022 Workiva. +// Licensed under the Apache License, Version 2.0. Please see https://github.com/Workiva/opentelemetry-dart/blob/master/LICENSE for more information + +@TestOn('vm') +import 'package:opentelemetry/api.dart' + show SpanContext, SpanId, SpanKind, TraceFlags, TraceId, TraceState; +import 'package:opentelemetry/sdk.dart'; +import 'package:opentelemetry/src/sdk/trace/span.dart'; +import 'package:test/test.dart'; + +class TestSpanExporter extends SpanExporter { + final spans = []; + + bool isShutdown = false; + + @override + void export(List spans) { + this.spans.addAll(spans); + } + + @override + void forceFlush() {} + + @override + void shutdown() { + isShutdown = true; + } +} + +void main() { + final sampledSpanContext = SpanContext(TraceId([1, 2, 3]), SpanId([7, 8, 9]), + TraceFlags.sampled, TraceState.empty()); + final sampledSpan = Span( + 'foo', + sampledSpanContext, + SpanId([4, 5, 6]), + [], + DateTimeTimeProvider(), + Resource([]), + InstrumentationScope( + 'library_name', 'library_version', 'url://schema', []), + SpanKind.client, + [], + SpanLimits(), + DateTimeTimeProvider().now); + + final unsampledSpanContext = SpanContext(TraceId([1, 2, 3]), + SpanId([7, 8, 9]), TraceFlags.none, TraceState.empty()); + final unsampledSpan = Span( + 'foo', + unsampledSpanContext, + SpanId([4, 5, 6]), + [], + DateTimeTimeProvider(), + Resource([]), + InstrumentationScope( + 'library_name', 'library_version', 'url://schema', []), + SpanKind.client, + [], + SpanLimits(), + DateTimeTimeProvider().now); + + late TestSpanExporter exporter; + late SimpleSpanProcessor processor; + + setUp(() { + exporter = TestSpanExporter(); + processor = SimpleSpanProcessor(exporter); + }); + + group('onEnd', () { + test('exports sampled span', () { + processor.onEnd(sampledSpan); + expect(exporter.spans, [sampledSpan]); + }); + + test('does not export unsampled span', () { + processor.onEnd(unsampledSpan); + expect(exporter.spans, isEmpty); + }); + + test('does not export after shutdown', () { + processor + ..shutdown() + ..onEnd(sampledSpan); + expect(exporter.spans, isEmpty); + }); + }); + + group('shutdown', () { + test('calls exporter.shutdown()', () { + processor.shutdown(); + expect(exporter.isShutdown, isTrue); + }); + }); +} diff --git a/test/unit/mocks.dart b/test/unit/mocks.dart index 37000188..f53b4f6c 100644 --- a/test/unit/mocks.dart +++ b/test/unit/mocks.dart @@ -5,7 +5,6 @@ import 'package:http/http.dart' as http; import 'package:mocktail/mocktail.dart'; import 'package:opentelemetry/src/api/context/context.dart'; import 'package:opentelemetry/src/api/trace/span.dart'; -import 'package:opentelemetry/src/sdk/trace/exporters/span_exporter.dart'; import 'package:opentelemetry/src/sdk/trace/read_only_span.dart'; import 'package:opentelemetry/src/sdk/trace/span_processors/span_processor.dart'; @@ -17,6 +16,4 @@ class MockSpan extends Mock implements Span {} class MockReadOnlySpan extends Mock implements ReadOnlySpan {} -class MockSpanExporter extends Mock implements SpanExporter {} - class MockSpanProcessor extends Mock implements SpanProcessor {} diff --git a/test/unit/sdk/span_processors/batch_processor_test.dart b/test/unit/sdk/span_processors/batch_processor_test.dart deleted file mode 100644 index 681f7d8c..00000000 --- a/test/unit/sdk/span_processors/batch_processor_test.dart +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2021-2022 Workiva. -// Licensed under the Apache License, Version 2.0. Please see https://github.com/Workiva/opentelemetry-dart/blob/master/LICENSE for more information - -@TestOn('vm') -import 'package:mocktail/mocktail.dart'; -import 'package:opentelemetry/src/sdk/trace/exporters/span_exporter.dart'; -import 'package:opentelemetry/src/sdk/trace/read_only_span.dart'; -import 'package:opentelemetry/src/sdk/trace/span_processors/batch_processor.dart'; -import 'package:test/test.dart'; - -import '../../mocks.dart'; - -void main() { - late BatchSpanProcessor processor; - late SpanExporter mockExporter; - late ReadOnlySpan mockSpan1, mockSpan2, mockSpan3; - - setUp(() { - mockSpan1 = MockReadOnlySpan(); - mockSpan2 = MockReadOnlySpan(); - mockSpan3 = MockReadOnlySpan(); - - mockExporter = MockSpanExporter(); - processor = BatchSpanProcessor(mockExporter, - maxExportBatchSize: 2, scheduledDelayMillis: 100); - }); - - tearDown(() { - processor.shutdown(); - reset(mockExporter); - }); - - test('forceFlush', () { - processor - ..onEnd(mockSpan1) - ..onEnd(mockSpan2) - ..onEnd(mockSpan3) - ..forceFlush(); - - verify(() => mockExporter.export([mockSpan1, mockSpan2])).called(1); - verify(() => mockExporter.export([mockSpan3])).called(1); - verify(() => mockExporter.forceFlush()).called(1); - }); - - test('shutdown shuts exporter down', () { - processor.shutdown(); - - verify(() => mockExporter.shutdown()).called(1); - verify(() => mockExporter.forceFlush()).called(1); - }); -} diff --git a/test/unit/sdk/span_processors/simple_processor_test.dart b/test/unit/sdk/span_processors/simple_processor_test.dart deleted file mode 100644 index d4248de0..00000000 --- a/test/unit/sdk/span_processors/simple_processor_test.dart +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2021-2022 Workiva. -// Licensed under the Apache License, Version 2.0. Please see https://github.com/Workiva/opentelemetry-dart/blob/master/LICENSE for more information - -@TestOn('vm') -import 'package:mocktail/mocktail.dart'; -import 'package:opentelemetry/sdk.dart' as sdk; -import 'package:test/test.dart'; - -import '../../mocks.dart'; - -void main() { - late sdk.SpanExporter exporter; - late sdk.SimpleSpanProcessor processor; - late sdk.ReadOnlySpan span; - - setUp(() { - exporter = MockSpanExporter(); - processor = sdk.SimpleSpanProcessor(exporter); - span = MockReadOnlySpan(); - }); - - test('executes export', () { - processor.onEnd(span); - - verify(() => exporter.export([span])).called(1); - }); - - test('flushes exporter on forced flush', () { - processor.forceFlush(); - - verify(() => exporter.forceFlush()).called(1); - }); - - test('does not export if shut down', () { - processor - ..shutdown() - ..onEnd(span); - - verify(() => exporter.shutdown()).called(1); - verify(() => exporter.forceFlush()).called(1); - verifyNever(() => exporter.export([span])); - }); -}