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 : /// Storage implementation for sum-based metrics like Counter and UpDownCounter.
11 : ///
12 : /// SumStorage accumulates measurements for sum-based instruments. It maintains
13 : /// separate accumulated values for each unique set of attributes, and provides
14 : /// methods to collect the current state as metric points.
15 : ///
16 : /// This storage implementation supports both monotonic sums (like Counter)
17 : /// and non-monotonic sums (like UpDownCounter).
18 : ///
19 : /// More information:
20 : /// https://opentelemetry.io/docs/specs/otel/metrics/sdk/#the-temporality-of-instruments
21 : class SumStorage<T extends num> extends NumericStorage<T> {
22 : /// Map of attribute sets to accumulated values.
23 : final Map<Attributes?, _SumPointData<T>> _points = {};
24 :
25 : /// Whether the sum is monotonic (only increases).
26 : ///
27 : /// Monotonic sums only accept positive increments and are
28 : /// appropriate for counters that never decrease.
29 : final bool isMonotonic;
30 :
31 : /// The start time for all points.
32 : ///
33 : /// This is used for cumulative temporality reporting.
34 : final DateTime _startTime = DateTime.now();
35 :
36 : /// Creates a new SumStorage instance.
37 : ///
38 : /// @param isMonotonic Whether this storage is for a monotonic sum
39 15 : SumStorage({
40 : required this.isMonotonic,
41 : });
42 :
43 : /// Records a measurement with the given attributes.
44 : ///
45 : /// For synchronous instruments, this is a delta that gets added to the existing value.
46 : /// For asynchronous instruments, this should be the absolute value.
47 : ///
48 : /// @param value The value to record
49 : /// @param attributes Optional attributes to associate with this measurement
50 14 : @override
51 : void record(T value, [Attributes? attributes]) {
52 : // Check constraints for monotonic counters
53 25 : if (isMonotonic && value < 0) {
54 0 : print('Warning: Negative value $value provided to monotonic sum storage. '
55 : 'This will be ignored.');
56 : return;
57 : }
58 :
59 : // Check if we already have an entry for these attributes
60 28 : if (_points.containsKey(attributes)) {
61 : // Add to existing data point
62 24 : _points[attributes]!.add(value);
63 : } else {
64 : // Create new data point
65 42 : _points[attributes] = _SumPointData<T>(
66 : value: value,
67 14 : lastUpdateTime: DateTime.now(),
68 : );
69 : }
70 : }
71 :
72 : /// Gets the current value for the given attributes.
73 : ///
74 : /// If no attributes are provided, returns the sum across all attribute sets.
75 : ///
76 : /// @param attributes Optional attributes to filter by
77 : /// @return The current accumulated value
78 6 : @override
79 : T getValue([Attributes? attributes]) {
80 : num result;
81 :
82 : if (attributes == null) {
83 : // Sum of all values across all attribute sets
84 36 : result = _points.values.fold<num>(0, (sum, data) => sum + data.value);
85 12 : } else if (_points.containsKey(attributes)) {
86 : // Return the value for the specific attributes
87 18 : result = _points[attributes]!.value;
88 : } else {
89 : // No entry for these attributes
90 : result = 0;
91 : }
92 :
93 : // Convert to the appropriate generic type
94 6 : if (T == int) {
95 5 : return result.toInt() as T;
96 3 : } else if (T == double) {
97 2 : return result.toDouble() as T;
98 : } else {
99 : return result as T;
100 : }
101 : }
102 :
103 : /// Collects the current set of metric points.
104 : ///
105 : /// This method is used by the instrument to collect all current
106 : /// sum values as metric points for export.
107 : ///
108 : /// @return A list of metric points containing the current values
109 10 : @override
110 : List<MetricPoint<T>> collectPoints() {
111 10 : final now = DateTime.now();
112 :
113 40 : return _points.entries.map((entry) {
114 : // Convert null attributes to empty attributes for MetricPoint
115 18 : final attributes = entry.key ?? OTelFactory.otelFactory!.attributes();
116 :
117 : // Convert numeric value to the specific generic type T
118 : final T typedValue;
119 10 : if (T == int) {
120 27 : typedValue = entry.value.value.toInt() as T;
121 3 : } else if (T == double) {
122 6 : typedValue = entry.value.value.toDouble() as T;
123 : } else {
124 2 : typedValue = entry.value.value;
125 : }
126 :
127 10 : return MetricPoint<T>.sum(
128 : attributes: attributes,
129 10 : startTime: _startTime,
130 : time: now,
131 : value: typedValue,
132 10 : isMonotonic: isMonotonic,
133 20 : exemplars: entry.value.exemplars,
134 : );
135 10 : }).toList();
136 : }
137 :
138 : /// Resets all points (for delta temporality).
139 : ///
140 : /// This method clears all accumulated values. It is used when
141 : /// reporting with delta temporality to reset the accumulation
142 : /// after each export.
143 5 : @override
144 : void reset() {
145 10 : _points.clear();
146 : }
147 :
148 : /// Adds an exemplar to a specific point.
149 : ///
150 : /// Exemplars are example measurements that provide additional
151 : /// context about specific observations.
152 : ///
153 : /// @param exemplar The exemplar to add
154 : /// @param attributes The attributes identifying the point to add the exemplar to
155 0 : @override
156 : void addExemplar(Exemplar exemplar, [Attributes? attributes]) {
157 0 : if (_points.containsKey(attributes)) {
158 0 : _points[attributes]!.exemplars.add(exemplar);
159 : }
160 : }
161 : }
162 :
163 : /// Internal class representing data for a single sum point.
164 : ///
165 : /// This class tracks the accumulated value, last update time,
166 : /// and exemplars for a specific combination of attributes.
167 : class _SumPointData<T extends num> {
168 : /// The accumulated value.
169 : T value;
170 :
171 : /// The time this point was last updated.
172 : DateTime lastUpdateTime;
173 :
174 : /// Exemplars for this point.
175 : final List<Exemplar> exemplars = [];
176 :
177 : /// Creates a new _SumPointData instance.
178 : ///
179 : /// @param value The initial value
180 : /// @param lastUpdateTime The time of the initial value
181 14 : _SumPointData({
182 : required this.value,
183 : required this.lastUpdateTime,
184 : });
185 :
186 : /// Adds a value to this point (for synchronous counters).
187 : ///
188 : /// @param delta The value to add to the accumulated value
189 8 : void add(T delta) {
190 : // Handle the addition with proper type conversion
191 8 : if (T == int) {
192 28 : value = (value + delta).toInt() as T;
193 3 : } else if (T == double) {
194 8 : value = (value + delta).toDouble() as T;
195 : } else {
196 3 : value = (value + delta) as T;
197 : }
198 :
199 16 : lastUpdateTime = DateTime.now();
200 : }
201 :
202 : /// Sets the value directly (for asynchronous counters).
203 : ///
204 : /// @param newValue The new absolute value to set
205 0 : void setValue(T newValue) {
206 0 : value = newValue;
207 0 : lastUpdateTime = DateTime.now();
208 : }
209 :
210 0 : @override
211 0 : String toString() => 'SumPointData(value: $value)';
212 : }
|