LCOV - code coverage report
Current view: top level - lib/src/metrics/storage - histogram_storage.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 94.8 % 97 92
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 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart';
       5              : 
       6              : import '../data/metric_point.dart';
       7              : import '../exemplar_filter.dart';
       8              : import '../exemplar_reservoir.dart';
       9              : import 'metric_storage.dart';
      10              : 
      11              : /// HistogramStorage is used for storing and accumulating histogram data.
      12              : class HistogramStorage<T extends num> extends HistogramStorageBase<T>
      13              :     with ExemplarSampling<T> {
      14              :   /// Map of attribute sets to histogram data.
      15              :   final Map<Attributes, _HistogramPointData<T>> _points = {};
      16              : 
      17              :   /// The bucket boundaries for this histogram.
      18              :   final List<double> boundaries;
      19              : 
      20              :   /// Whether to record min and max values.
      21              :   final bool recordMinMax;
      22              : 
      23              :   /// The start time for all points.
      24              :   final DateTime _startTime = DateTime.now();
      25              : 
      26              :   /// The exemplar filter used by this storage.
      27              :   @override
      28              :   final ExemplarFilter exemplarFilter;
      29              : 
      30              :   /// Creates a new HistogramStorage instance.
      31            9 :   HistogramStorage(
      32              :       {required this.boundaries,
      33              :       this.recordMinMax = true,
      34              :       ExemplarFilter? exemplarFilter})
      35              :       : exemplarFilter = exemplarFilter ?? const TraceBasedExemplarFilter();
      36              : 
      37              :   /// Records a measurement with the given attributes and context.
      38            6 :   @override
      39              :   void record(T value,
      40              :       [Attributes? attributes, Context? context, DateTime? timestamp]) {
      41              :     // Create a normalized key for lookup
      42            5 :     final key = attributes ?? _emptyAttributes();
      43              : 
      44              :     _HistogramPointData<T> pointData;
      45              :     // Find matching attributes
      46            6 :     final existingKey = _findMatchingKey(key);
      47           12 :     var bucketIndex = boundaries.length;
      48              :     if (existingKey != null) {
      49              :       // Update existing point
      50           10 :       pointData = _points[existingKey]!;
      51            5 :       bucketIndex = pointData.record(value);
      52              :     } else {
      53              :       // Create new point
      54            6 :       pointData = _HistogramPointData<T>(
      55            6 :         boundaries: boundaries,
      56            6 :         recordMinMax: recordMinMax,
      57           18 :         reservoir: boundaries.length > 1
      58           12 :             ? AlignedHistogramBucketExemplarReservoir(boundaries)
      59            0 :             : SimpleFixedSizeExemplarReservoir(1),
      60              :       );
      61            6 :       bucketIndex = pointData.record(value);
      62           12 :       _points[key] = pointData;
      63              :     }
      64              : 
      65           14 :     maybeOffer(pointData.reservoir, value, key, context ?? Context.current,
      66            6 :         timestamp ?? DateTime.now(), bucketIndex);
      67              :   }
      68              : 
      69              :   /// Helper to get empty attributes safely
      70            5 :   Attributes _emptyAttributes() {
      71              :     // If OTelFactory is not initialized yet, create an empty attributes directly
      72            5 :     if (OTelFactory.otelFactory == null) {
      73            0 :       return OTelAPI.attributes(); // Use the API's static method instead
      74              :     }
      75           10 :     return OTelFactory.otelFactory!.attributes();
      76              :   }
      77              : 
      78              :   /// Finds a key in the points map that equals the given key
      79            6 :   Attributes? _findMatchingKey(Attributes key) {
      80           18 :     for (final existingKey in _points.keys) {
      81            6 :       if (existingKey == key) {
      82              :         // Using the == operator which should call equals
      83              :         return existingKey;
      84              :       }
      85              :     }
      86              :     return null;
      87              :   }
      88              : 
      89              :   /// Gets the current histogram value for the given attributes.
      90              :   /// If no attributes are provided, returns a combined HistogramValue across all attribute sets.
      91            2 :   @override
      92              :   HistogramValue getValue([Attributes? attributes]) {
      93              :     if (attributes == null) {
      94              :       // Combine across all attribute sets
      95            3 :       final totalSum = _points.values.fold<num>(
      96              :         0,
      97            3 :         (sum, data) => sum + data.sum,
      98              :       );
      99            3 :       final totalCount = _points.values.fold<int>(
     100              :         0,
     101            3 :         (count, data) => count + data.count,
     102              :       );
     103              : 
     104              :       // Combine bucket counts
     105            1 :       final combinedCounts = List<int>.filled(
     106            3 :         boundaries.length + 1,
     107              :         0,
     108              :       );
     109            3 :       for (final data in _points.values) {
     110            4 :         for (var i = 0; i < data.counts.length; i++) {
     111            4 :           combinedCounts[i] += data.counts[i];
     112              :         }
     113              :       }
     114              : 
     115              :       // Find overall min and max
     116              :       num? overallMin;
     117              :       num? overallMax;
     118            3 :       if (recordMinMax && _points.isNotEmpty) {
     119            2 :         overallMin = _points.values
     120            3 :                 .map((data) => data.min)
     121            3 :                 .where((min) => min != double.infinity)
     122            1 :                 .isEmpty
     123              :             ? null
     124            2 :             : _points.values
     125            3 :                 .map((data) => data.min)
     126            3 :                 .where((min) => min != double.infinity)
     127            3 :                 .reduce((a, b) => a < b ? a : b);
     128            2 :         overallMax = _points.values
     129            3 :                 .map((data) => data.max)
     130            3 :                 .where((max) => max != double.negativeInfinity)
     131            1 :                 .isEmpty
     132              :             ? null
     133            2 :             : _points.values
     134            3 :                 .map((data) => data.max)
     135            3 :                 .where((max) => max != double.negativeInfinity)
     136            3 :                 .reduce((a, b) => a > b ? a : b);
     137              :       }
     138              : 
     139            1 :       return HistogramValue(
     140              :         sum: totalSum,
     141              :         count: totalCount,
     142            1 :         boundaries: boundaries,
     143              :         bucketCounts: combinedCounts,
     144              :         min: overallMin,
     145              :         max: overallMax,
     146              :       );
     147              :     }
     148              : 
     149              :     // Find matching attributes
     150            2 :     final existingKey = _findMatchingKey(attributes);
     151              :     if (existingKey != null) {
     152            4 :       final data = _points[existingKey]!;
     153            2 :       return HistogramValue(
     154            2 :         sum: data.sum,
     155            2 :         count: data.count,
     156            2 :         boundaries: boundaries,
     157            2 :         bucketCounts: data.counts,
     158            8 :         min: recordMinMax && data.min != double.infinity ? data.min : null,
     159            6 :         max: recordMinMax && data.max != double.negativeInfinity
     160            2 :             ? data.max
     161              :             : null,
     162              :       );
     163              :     } else {
     164              :       // Return empty histogram
     165            0 :       return HistogramValue(
     166              :         sum: 0,
     167              :         count: 0,
     168            0 :         boundaries: boundaries,
     169            0 :         bucketCounts: List<int>.filled(boundaries.length + 1, 0),
     170              :         min: null,
     171              :         max: null,
     172              :       );
     173              :     }
     174              :   }
     175              : 
     176              :   /// Collects the current set of metric points.
     177            5 :   @override
     178              :   List<MetricPoint<HistogramValue>> collectPoints() {
     179            5 :     final now = DateTime.now();
     180              : 
     181           20 :     return _points.entries.map((entry) {
     182            5 :       final data = entry.value;
     183              : 
     184              :       // Create a HistogramValue directly
     185            5 :       final histogramValue = HistogramValue(
     186            5 :         sum: data.sum,
     187            5 :         count: data.count,
     188            5 :         boundaries: boundaries,
     189            5 :         bucketCounts: data.counts,
     190           20 :         min: recordMinMax && data.min != double.infinity ? data.min : null,
     191           15 :         max: recordMinMax && data.max != double.negativeInfinity
     192            5 :             ? data.max
     193              :             : null,
     194              :       );
     195              : 
     196              :       // Create a MetricPoint<HistogramValue> - no type casting needed!
     197            5 :       return MetricPoint<HistogramValue>(
     198            5 :         attributes: entry.key,
     199            5 :         startTime: _startTime,
     200              :         endTime: now,
     201              :         value: histogramValue,
     202           15 :         exemplars: data.reservoir.collectAndReset(entry.key),
     203              :       );
     204            5 :     }).toList();
     205              :   }
     206              : 
     207              :   /// Resets all points (for delta temporality).
     208            2 :   @override
     209              :   void reset() {
     210            4 :     _points.clear();
     211              :   }
     212              : }
     213              : 
     214              : /// Data for a single histogram point.
     215              : class _HistogramPointData<T extends num> {
     216              :   /// The total count of measurements.
     217              :   int count = 0;
     218              : 
     219              :   /// The sum of all measurements.
     220              :   num sum = 0;
     221              : 
     222              :   /// The minimum value recorded.
     223              :   num min = double.infinity;
     224              : 
     225              :   /// The maximum value recorded.
     226              :   num max = double.negativeInfinity;
     227              : 
     228              :   /// The counts per bucket.
     229              :   late List<int> counts;
     230              : 
     231              :   /// The bucket boundaries.
     232              :   final List<double> boundaries;
     233              : 
     234              :   /// Whether to record min and max values.
     235              :   final bool recordMinMax;
     236              : 
     237              :   /// Reservoir for this point.
     238              :   final ExemplarReservoir reservoir;
     239              : 
     240            6 :   _HistogramPointData(
     241              :       {required this.boundaries,
     242              :       required this.recordMinMax,
     243              :       required this.reservoir}) {
     244              :     // Initialize count array with one more than boundaries
     245              :     // (for the +Inf bucket)
     246           30 :     counts = List<int>.filled(boundaries.length + 1, 0);
     247              :   }
     248              : 
     249              :   /// Records a measurement.
     250              :   /// Returns the bucket index where the measurement fell.
     251            6 :   int record(T value) {
     252           12 :     count++;
     253           12 :     sum += value;
     254              : 
     255            6 :     if (recordMinMax) {
     256              :       final num numValue = value;
     257           18 :       if (numValue < min) min = numValue;
     258           18 :       if (numValue > max) max = numValue;
     259              :     }
     260              : 
     261              :     // Find the right bucket
     262           12 :     var bucketIndex = boundaries.length; // Default to the +Inf bucket
     263           24 :     for (var i = 0; i < boundaries.length; i++) {
     264           18 :       if (value <= boundaries[i]) {
     265              :         bucketIndex = i;
     266              :         break;
     267              :       }
     268              :     }
     269              : 
     270              :     // Increment the bucket count
     271           18 :     counts[bucketIndex]++;
     272              :     return bucketIndex;
     273              :   }
     274              : }
        

Generated by: LCOV version 2.0-1