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 : /// 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 24 : SumStorage({required this.isMonotonic});
40 :
41 : /// Records a measurement with the given attributes.
42 : ///
43 : /// For synchronous instruments, this is a delta that gets added to the existing value.
44 : /// For asynchronous instruments, this should be the absolute value.
45 : ///
46 : /// @param value The value to record
47 : /// @param attributes Optional attributes to associate with this measurement
48 22 : @override
49 : void record(T value, [Attributes? attributes]) {
50 : // Check constraints for monotonic counters
51 41 : if (isMonotonic && value < 0) {
52 2 : print(
53 : 'Warning: Negative value $value provided to monotonic sum storage. '
54 : 'This will be ignored.',
55 : );
56 : return;
57 : }
58 :
59 : // Check if we already have an entry for these attributes
60 44 : if (_points.containsKey(attributes)) {
61 : // Add to existing data point
62 30 : _points[attributes]!.add(value);
63 : } else {
64 : // Create new data point
65 66 : _points[attributes] = _SumPointData<T>(
66 : value: value,
67 22 : 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 10 : @override
79 : T getValue([Attributes? attributes]) {
80 : num result;
81 :
82 : if (attributes == null) {
83 : // Sum of all values across all attribute sets
84 48 : result = _points.values.fold<num>(0, (sum, data) => sum + data.value);
85 18 : } else if (_points.containsKey(attributes)) {
86 : // Return the value for the specific attributes
87 24 : 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 10 : if (T == int) {
95 8 : return result.toInt() as T;
96 6 : } else if (T == double) {
97 5 : 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 15 : @override
110 : List<MetricPoint<T>> collectPoints() {
111 15 : final now = DateTime.now();
112 :
113 60 : return _points.entries.map((entry) {
114 : // Convert null attributes to empty attributes for MetricPoint
115 25 : final attributes = entry.key ?? OTelFactory.otelFactory!.attributes();
116 :
117 : // Convert numeric value to the specific generic type T
118 : final T typedValue;
119 15 : if (T == int) {
120 42 : typedValue = entry.value.value.toInt() as T;
121 4 : } else if (T == double) {
122 9 : typedValue = entry.value.value.toDouble() as T;
123 : } else {
124 2 : typedValue = entry.value.value;
125 : }
126 :
127 15 : return MetricPoint<T>.sum(
128 : attributes: attributes,
129 15 : startTime: _startTime,
130 : time: now,
131 : value: typedValue,
132 15 : isMonotonic: isMonotonic,
133 30 : exemplars: entry.value.exemplars,
134 : );
135 15 : }).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 11 : @override
144 : void reset() {
145 22 : _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 1 : @override
156 : void addExemplar(Exemplar exemplar, [Attributes? attributes]) {
157 2 : if (_points.containsKey(attributes)) {
158 4 : _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 22 : _SumPointData({required this.value, required this.lastUpdateTime});
182 :
183 : /// Adds a value to this point (for synchronous counters).
184 : ///
185 : /// @param delta The value to add to the accumulated value
186 10 : void add(T delta) {
187 : // Handle the addition with proper type conversion
188 10 : if (T == int) {
189 36 : value = (value + delta).toInt() as T;
190 4 : } else if (T == double) {
191 12 : value = (value + delta).toDouble() as T;
192 : } else {
193 3 : value = (value + delta) as T;
194 : }
195 :
196 20 : lastUpdateTime = DateTime.now();
197 : }
198 :
199 : /// Sets the value directly (for asynchronous counters).
200 : ///
201 : /// @param newValue The new absolute value to set
202 0 : void setValue(T newValue) {
203 0 : value = newValue;
204 0 : lastUpdateTime = DateTime.now();
205 : }
206 :
207 0 : @override
208 0 : String toString() => 'SumPointData(value: $value)';
209 : }
|