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