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

Generated by: LCOV version 2.0-1