LCOV - code coverage report
Current view: top level - src/metrics/storage - histogram_storage.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 94.4 % 89 84
Test Date: 2025-11-15 13:23:01 Functions: - 0 0

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

Generated by: LCOV version 2.0-1