LCOV - code coverage report
Current view: top level - lib/src/trace/export - batch_span_processor.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 100.0 % 66 66
Test Date: 2026-08-27 23:42:02 Functions: - 0 0

            Line data    Source code
       1              : // Copyright The OpenTelemetry Authors
       2              : // SPDX-License-Identifier: Apache-2.0
       3              : 
       4              : import 'dart:async';
       5              : import 'dart:collection';
       6              : 
       7              : import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart';
       8              : import 'package:synchronized/synchronized.dart';
       9              : 
      10              : import '../../environment/otel_env.dart';
      11              : import '../span.dart';
      12              : import '../span_processor.dart';
      13              : import 'span_exporter.dart';
      14              : 
      15              : /// Configuration for the [BatchSpanProcessor].
      16              : ///
      17              : /// This class configures how the batch span processor behaves, including
      18              : /// queue size limits, export scheduling, and batch size parameters.
      19              : class BatchSpanProcessorConfig {
      20              :   /// Stand-in for "no limit": `OTEL_BSP_EXPORT_TIMEOUT=0` means no timeout
      21              :   /// per spec, represented as the max milliseconds in a 32-bit integer
      22              :   /// (~24.8 days). Web-safe (as microseconds it is far below JS's 2^53
      23              :   /// safe-integer limit, so it behaves identically on VM, dart2js, and wasm)
      24              :   /// and within Dart timer limits for `Future.timeout()`.
      25              :   static const Duration noLimit = Duration(milliseconds: 0x7FFFFFFF);
      26              : 
      27              :   /// The maximum queue size for spans. After this is reached,
      28              :   /// spans will be dropped.
      29              :   final int maxQueueSize;
      30              : 
      31              :   /// The delay between two consecutive exports.
      32              :   final Duration scheduleDelay;
      33              : 
      34              :   /// The maximum batch size of spans that can be exported at once.
      35              :   final int maxExportBatchSize;
      36              : 
      37              :   /// The amount of time to wait for an export to complete before timing out.
      38              :   final Duration exportTimeout;
      39              : 
      40              :   /// Creates a new configuration for a [BatchSpanProcessor].
      41              :   ///
      42              :   /// [maxQueueSize] The maximum number of spans that can be queued for export. Default is 2048.
      43              :   ///    If this limit is reached, additional spans will be dropped.
      44              :   /// [scheduleDelay] The time interval between two consecutive exports. Default is 5 seconds.
      45              :   ///    This controls how frequently batches are sent to the exporter.
      46              :   /// [maxExportBatchSize] The maximum number of spans to export in a single batch. Default is 512.
      47              :   ///    This helps control resource usage during export operations.
      48              :   /// [exportTimeout] The maximum time to wait for an export operation to complete. Default is 30 seconds.
      49              :   ///    After this time, export operations will be considered failed.
      50          172 :   const BatchSpanProcessorConfig({
      51              :     this.maxQueueSize = 2048,
      52              :     this.scheduleDelay = const Duration(milliseconds: 5000),
      53              :     this.maxExportBatchSize = 512,
      54              :     this.exportTimeout = const Duration(seconds: 30),
      55              :   });
      56              : 
      57              :   /// Creates a configuration by reading `OTEL_BSP_*` environment variables
      58              :   /// via [OTelEnv]. Falls back to standard OTel defaults if variables are
      59              :   /// missing or invalid.
      60              :   ///
      61              :   /// Invalid or out-of-range values emit an [OTelLog.warn] diagnostic and
      62              :   /// fall back to the spec default.
      63          136 :   factory BatchSpanProcessorConfig.fromBspEnvironmentValues(
      64              :       BspEnvironmentValues env) {
      65              :     var queueSize = env.maxQueueSize ?? 2048;
      66              :     var batchSize = env.maxExportBatchSize ?? 512;
      67              : 
      68              :     // --- scheduleDelay ---
      69              :     // Spec type: Duration. Zero is valid ("export as fast as possible").
      70              :     // Negative values MUST warn and fall back to default.
      71              :     Duration scheduleDelay;
      72              :     if (env.scheduleDelay != null) {
      73              :       final delay = env.scheduleDelay!;
      74            2 :       if (delay.inMilliseconds >= 0) {
      75              :         scheduleDelay = delay;
      76              :       } else {
      77            1 :         if (OTelLog.isWarn()) {
      78            2 :           OTelLog.warn('BatchSpanProcessorConfig: Negative '
      79            1 :               'OTEL_BSP_SCHEDULE_DELAY (${delay.inMilliseconds} ms) is '
      80              :               'invalid per spec, using default 5000 ms.');
      81              :         }
      82              :         scheduleDelay = const Duration(milliseconds: 5000);
      83              :       }
      84              :     } else {
      85              :       scheduleDelay = const Duration(milliseconds: 5000);
      86              :     }
      87              : 
      88              :     // --- exportTimeout ---
      89              :     // Spec type: Timeout. Zero means "no limit" — substitute a very large
      90              :     // duration. Negative values MUST warn and fall back to default.
      91              :     Duration exportTimeout;
      92              :     if (env.exportTimeout != null) {
      93              :       final timeout = env.exportTimeout!;
      94            2 :       if (timeout.inMilliseconds == 0) {
      95              :         exportTimeout = noLimit;
      96            2 :       } else if (timeout.inMilliseconds > 0) {
      97              :         exportTimeout = timeout;
      98              :       } else {
      99            1 :         if (OTelLog.isWarn()) {
     100            2 :           OTelLog.warn('BatchSpanProcessorConfig: Negative '
     101            1 :               'OTEL_BSP_EXPORT_TIMEOUT (${timeout.inMilliseconds} ms) is '
     102              :               'invalid per spec, using default 30000 ms.');
     103              :         }
     104              :         exportTimeout = const Duration(milliseconds: 30000);
     105              :       }
     106              :     } else {
     107              :       exportTimeout = const Duration(milliseconds: 30000);
     108              :     }
     109              : 
     110              :     // --- Validation Logic ---
     111          136 :     if (queueSize <= 0) {
     112            1 :       if (OTelLog.isWarn()) {
     113            2 :         OTelLog.warn('BatchSpanProcessorConfig: Non-positive '
     114              :             'OTEL_BSP_MAX_QUEUE_SIZE ($queueSize) is invalid per spec, '
     115              :             'using default 2048.');
     116              :       }
     117              :       queueSize = 2048;
     118              :     }
     119          136 :     if (batchSize <= 0) {
     120            1 :       if (OTelLog.isWarn()) {
     121            2 :         OTelLog.warn('BatchSpanProcessorConfig: Non-positive '
     122              :             'OTEL_BSP_MAX_EXPORT_BATCH_SIZE ($batchSize) is invalid per '
     123              :             'spec, using default 512.');
     124              :       }
     125              :       batchSize = 512;
     126              :     }
     127              :     // Spec rule: maxExportBatchSize must be less than or equal to maxQueueSize
     128          136 :     if (batchSize > queueSize) {
     129              :       batchSize = queueSize;
     130              :     }
     131              : 
     132          136 :     return BatchSpanProcessorConfig(
     133              :       maxQueueSize: queueSize,
     134              :       maxExportBatchSize: batchSize,
     135              :       scheduleDelay: scheduleDelay,
     136              :       exportTimeout: exportTimeout,
     137              :     );
     138              :   }
     139              : 
     140              :   /// Creates a configuration by reading `OTEL_BSP_*` environment variables
     141              :   /// via [OTelEnv]. Falls back to standard OTel defaults if variables are
     142              :   /// missing or invalid.
     143              :   ///
     144              :   /// | Environment Variable              | Type     | Default  | Notes                                                    |
     145              :   /// |-----------------------------------|----------|----------|----------------------------------------------------------|
     146              :   /// | `OTEL_BSP_SCHEDULE_DELAY`         | Duration | `5000`   | Delay between exports (ms). 0 is valid (export ASAP).    |
     147              :   /// | `OTEL_BSP_EXPORT_TIMEOUT`         | Timeout  | `30000`  | Export timeout (ms). 0 means no limit.                   |
     148              :   /// | `OTEL_BSP_MAX_QUEUE_SIZE`         | Integer  | `2048`   | Maximum span queue size.                                 |
     149              :   /// | `OTEL_BSP_MAX_EXPORT_BATCH_SIZE`  | Integer  | `512`    | Maximum batch size. Must be ≤ `MAX_QUEUE_SIZE`.          |
     150              :   ///
     151              :   /// Invalid or out-of-range values emit an [OTelLog.warn] diagnostic and
     152              :   /// fall back to the spec default.
     153            1 :   factory BatchSpanProcessorConfig.fromEnvironment() {
     154            1 :     return BatchSpanProcessorConfig.fromBspEnvironmentValues(
     155            1 :         OTelEnv.getBspConfig());
     156              :   }
     157              : }
     158              : 
     159              : /// A [SpanProcessor] that batches spans before export.
     160              : ///
     161              : /// This processor collects finished spans in a queue and exports them in batches
     162              : /// at regular intervals, improving efficiency compared to exporting each span
     163              : /// individually. Spans are added to a queue when they end, and periodically sent
     164              : /// to the configured exporter in batches according to the configured schedule.
     165              : ///
     166              : /// The batch behavior can be tuned using [BatchSpanProcessorConfig] to control
     167              : /// batch size, queue limits, and export timing.
     168              : class BatchSpanProcessor implements SpanProcessor {
     169              :   /// The exporter used to send spans to the backend
     170              :   final SpanExporter exporter;
     171              : 
     172              :   /// Configuration for the batch processor behavior
     173              :   final BatchSpanProcessorConfig _config;
     174              : 
     175              :   /// Queue of spans waiting to be exported
     176              :   final Queue<Span> _spanQueue = Queue<Span>();
     177              : 
     178              :   /// Whether the processor has been shut down
     179              :   bool _isShutdown = false;
     180              : 
     181              :   /// Timer for scheduling periodic exports
     182              :   Timer? _timer;
     183              : 
     184              :   /// Lock for synchronizing queue access
     185              :   final _lock = Lock();
     186              : 
     187              :   /// Creates a new BatchSpanProcessor with the specified exporter and configuration.
     188              :   ///
     189              :   /// The BatchSpanProcessor collects finished spans in a queue and exports them in batches
     190              :   /// at regular intervals. This improves efficiency compared to exporting each span individually.
     191              :   ///
     192              :   /// A timer is started when this processor is created based on the [config]'s scheduleDelay.
     193              :   /// The timer triggers periodic batch exports of completed spans to the configured exporter.
     194              :   ///
     195              :   /// When the maximum queue size is reached, new spans will be dropped and not exported.
     196              :   ///
     197              :   /// This processor does not modify spans on start or when their names are updated,
     198              :   /// it only processes spans when they end.
     199              :   ///
     200              :   /// If an error occurs during export, it will be logged but not propagated.
     201              :   ///
     202              :   /// [exporter] The SpanExporter to use for exporting batches of spans
     203              :   /// [config] Optional configuration for the batch processor
     204          137 :   BatchSpanProcessor(this.exporter, [BatchSpanProcessorConfig? config])
     205              :       : _config = config ?? const BatchSpanProcessorConfig() {
     206          554 :     _timer = Timer.periodic(_config.scheduleDelay, (_) async {
     207              :       try {
     208            6 :         await _exportBatch();
     209              :         // Exporter errors are caught inside _exportSingleBatch; this is a
     210              :         // last-resort guard so an unexpected failure can never become an
     211              :         // unhandled async error inside a periodic timer.
     212              :         // coverage:ignore-start
     213              :       } catch (e) {
     214              :         if (OTelLog.isError()) OTelLog.error('Error in batch export timer: $e');
     215              :       }
     216              :       // coverage:ignore-end
     217              :     });
     218              :   }
     219              : 
     220           39 :   @override
     221              :   Future<void> onEnd(Span span) async {
     222           39 :     if (_isShutdown) {
     223              :       return;
     224              :     }
     225              : 
     226              :     // Per the Trace SDK spec (Sampling), span exporters MUST receive
     227              :     // spans with the Sampled flag set and SHOULD NOT receive the ones
     228              :     // that do not (e.g. RECORD_ONLY spans record but are not exported).
     229          117 :     if (!span.spanContext.traceFlags.isSampled) {
     230            2 :       if (OTelLog.isDebug()) {
     231            2 :         OTelLog.debug(
     232            6 :           'BatchSpanProcessor: Skipping enqueue - span ${span.spanContext.spanId} is not sampled',
     233              :         );
     234              :       }
     235              :       return;
     236              :     }
     237              : 
     238          114 :     return _lock.synchronized(() {
     239          190 :       if (_spanQueue.length >= _config.maxQueueSize) {
     240            1 :         if (OTelLog.isDebug()) {
     241            1 :           OTelLog.debug('BatchSpanProcessor queue full - dropping span');
     242              :         }
     243              :         return;
     244              :       }
     245           76 :       _spanQueue.add(span);
     246              :     });
     247              :   }
     248              : 
     249           43 :   @override
     250              :   Future<void> onStart(Span span, Context? parentContext) async {
     251              :     // Nothing to do on start
     252              :   }
     253              : 
     254            1 :   @override
     255              :   Future<void> onNameUpdate(Span span, String newName) async {
     256              :     // Nothing to do on name update
     257              :   }
     258              : 
     259              :   /// Periodic export path: called by the timer. Exports up to one batch
     260              :   /// of [BatchSpanProcessorConfig.maxExportBatchSize] spans and returns.
     261              :   /// No-op once [_isShutdown] is set.
     262            6 :   Future<void> _exportBatch() async {
     263            6 :     if (_isShutdown) {
     264              :       return;
     265              :     }
     266            6 :     await _exportSingleBatch();
     267              :   }
     268              : 
     269              :   /// Pulls up to [BatchSpanProcessorConfig.maxExportBatchSize] spans
     270              :   /// off the queue and hands them to the exporter. Bypasses the
     271              :   /// [_isShutdown] check so it remains usable from inside [shutdown]
     272              :   /// (which sets the flag last). Returns true if any spans were
     273              :   /// exported, false if the queue was empty or the exporter threw.
     274           39 :   Future<bool> _exportSingleBatch() async {
     275           39 :     final spansToExport = <Span>[];
     276              : 
     277          117 :     await _lock.synchronized(() {
     278          195 :       final batchSize = _spanQueue.length > _config.maxExportBatchSize
     279            4 :           ? _config.maxExportBatchSize
     280           78 :           : _spanQueue.length;
     281           76 :       for (var i = 0; i < batchSize; i++) {
     282           74 :         if (_spanQueue.isEmpty) break;
     283          111 :         spansToExport.add(_spanQueue.removeFirst());
     284              :       }
     285              :     });
     286              : 
     287           39 :     if (spansToExport.isEmpty) {
     288              :       return false;
     289              :     }
     290              : 
     291              :     try {
     292          185 :       await exporter.export(spansToExport).timeout(_config.exportTimeout);
     293              :       return true;
     294              :     } catch (e) {
     295           34 :       if (OTelLog.isError()) {
     296           68 :         OTelLog.error('Error exporting batch of spans: $e');
     297              :       }
     298              :       // Bail out — don't loop forever on a broken exporter.
     299              :       return false;
     300              :     }
     301              :   }
     302              : 
     303              :   /// Drains the queue completely, in batches. Used by [forceFlush] and
     304              :   /// [shutdown]. Stops on the first export failure (so a broken
     305              :   /// exporter can't wedge shutdown forever).
     306          136 :   Future<void> _drainQueue() async {
     307          272 :     while (_spanQueue.isNotEmpty) {
     308           36 :       final exported = await _exportSingleBatch();
     309              :       if (!exported) break;
     310              :     }
     311              :   }
     312              : 
     313          130 :   @override
     314              :   Future<void> forceFlush() async {
     315          130 :     if (_isShutdown) {
     316              :       return;
     317              :     }
     318          130 :     await _drainQueue();
     319              :   }
     320              : 
     321          136 :   @override
     322              :   Future<void> shutdown() async {
     323          136 :     if (_isShutdown) {
     324              :       return;
     325              :     }
     326              : 
     327              :     // Stop the periodic timer first so it can't race with our drain.
     328          272 :     _timer?.cancel();
     329              : 
     330              :     // Drain queued spans BEFORE setting `_isShutdown` — otherwise the
     331              :     // `_isShutdown` early-return inside `_exportBatch` would skip them.
     332              :     // `_drainQueue` calls `_exportSingleBatch` directly so it's
     333              :     // unaffected by the flag in any case, but the ordering keeps the
     334              :     // behavior obvious.
     335          136 :     await _drainQueue();
     336              : 
     337          136 :     _isShutdown = true;
     338              : 
     339              :     // Shutdown the exporter
     340          272 :     await exporter.shutdown();
     341              :   }
     342              : }
        

Generated by: LCOV version 2.0-1