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 : }
|