Skip to content

Commit 8686f32

Browse files
committed
fix: add validation not to accept negative values for monotonic counters
1 parent df412fe commit 8686f32

File tree

4 files changed

+270
-8
lines changed

4 files changed

+270
-8
lines changed

opentelemetry-sdk/src/metrics/internal/precomputed_sum.rs

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use opentelemetry::KeyValue;
1+
use opentelemetry::{otel_warn, KeyValue};
22

33
use crate::metrics::data::{self, AggregatedMetrics, MetricData, SumDataPoint};
44
use crate::metrics::Temporality;
@@ -132,6 +132,16 @@ where
132132
T: Number,
133133
{
134134
fn call(&self, measurement: T, attrs: &[KeyValue]) {
135+
// Validate monotonic counter increment is non-negative
136+
if self.monotonic && measurement < T::default() {
137+
otel_warn!(
138+
name: "ObservableCounter.NegativeValue",
139+
message = "Observable counters are monotonic and can only accept non-negative values. This measurement will be dropped.",
140+
value = format!("{:?}", measurement)
141+
);
142+
return;
143+
}
144+
135145
self.filter.apply(attrs, |filtered| {
136146
self.value_map.measure(measurement, filtered);
137147
})
@@ -151,3 +161,93 @@ where
151161
(len, new.map(T::make_aggregated_metrics))
152162
}
153163
}
164+
165+
#[cfg(test)]
166+
mod tests {
167+
use super::*;
168+
169+
#[test]
170+
fn precomputed_sum_monotonic_rejects_negative_values() {
171+
let sum = PrecomputedSum::<f64>::new(
172+
Temporality::Cumulative,
173+
AttributeSetFilter::new(None),
174+
true, // monotonic = true
175+
100,
176+
);
177+
178+
Measure::call(&sum, 5.0, &[]);
179+
Measure::call(&sum, -3.0, &[]); // Should be dropped
180+
181+
let (_len, metrics) = sum.cumulative(None);
182+
assert!(metrics.is_some(), "Should have metrics");
183+
let metrics = metrics.unwrap();
184+
185+
assert!(
186+
matches!(metrics, MetricData::Sum(_)),
187+
"Expected Sum metric data"
188+
);
189+
let MetricData::Sum(sum_data) = metrics else {
190+
unreachable!()
191+
};
192+
193+
assert_eq!(sum_data.data_points.len(), 1);
194+
// Only the positive value should be recorded
195+
assert_eq!(sum_data.data_points[0].value, 5.0);
196+
}
197+
198+
#[test]
199+
fn precomputed_sum_non_monotonic_accepts_negative_values() {
200+
let sum = PrecomputedSum::<f64>::new(
201+
Temporality::Cumulative,
202+
AttributeSetFilter::new(None),
203+
false, // monotonic = false
204+
100,
205+
);
206+
207+
Measure::call(&sum, 5.0, &[]);
208+
Measure::call(&sum, -3.0, &[]); // Should be accepted
209+
210+
let (_len, metrics) = sum.cumulative(None);
211+
assert!(metrics.is_some(), "Should have metrics");
212+
let metrics = metrics.unwrap();
213+
214+
assert!(
215+
matches!(metrics, MetricData::Sum(_)),
216+
"Expected Sum metric data"
217+
);
218+
let MetricData::Sum(sum_data) = metrics else {
219+
unreachable!()
220+
};
221+
222+
// Both values should be recorded (precomputed sum overwrites, so last value wins)
223+
assert_eq!(sum_data.data_points.len(), 1);
224+
assert_eq!(sum_data.data_points[0].value, -3.0);
225+
}
226+
227+
#[test]
228+
fn precomputed_sum_monotonic_accepts_zero() {
229+
let sum = PrecomputedSum::<f64>::new(
230+
Temporality::Cumulative,
231+
AttributeSetFilter::new(None),
232+
true, // monotonic = true
233+
100,
234+
);
235+
236+
Measure::call(&sum, 0.0, &[]);
237+
238+
let (_len, metrics) = sum.cumulative(None);
239+
assert!(metrics.is_some(), "Should have metrics");
240+
let metrics = metrics.unwrap();
241+
242+
assert!(
243+
matches!(metrics, MetricData::Sum(_)),
244+
"Expected Sum metric data"
245+
);
246+
let MetricData::Sum(sum_data) = metrics else {
247+
unreachable!()
248+
};
249+
250+
assert_eq!(sum_data.data_points.len(), 1);
251+
assert_eq!(sum_data.data_points[0].value, 0.0);
252+
}
253+
}

opentelemetry-sdk/src/metrics/internal/sum.rs

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::metrics::data::{self, AggregatedMetrics, MetricData, SumDataPoint};
22
use crate::metrics::Temporality;
3-
use opentelemetry::KeyValue;
3+
use opentelemetry::{otel_warn, KeyValue};
44

55
use super::aggregate::{AggregateTimeInitiator, AttributeSetFilter};
66
use super::{Aggregator, AtomicTracker, ComputeAggregation, Measure, Number};
@@ -149,6 +149,16 @@ where
149149
T: Number,
150150
{
151151
fn call(&self, measurement: T, attrs: &[KeyValue]) {
152+
// Validate monotonic counter increment is non-negative
153+
if self.monotonic && measurement < T::default() {
154+
otel_warn!(
155+
name: "Counter.NegativeValue",
156+
message = "Counters are monotonic and can only accept non-negative values. This measurement will be dropped.",
157+
value = format!("{:?}", measurement)
158+
);
159+
return;
160+
}
161+
152162
self.filter.apply(attrs, |filtered| {
153163
self.value_map.measure(measurement, filtered);
154164
})
@@ -168,3 +178,121 @@ where
168178
(len, new.map(T::make_aggregated_metrics))
169179
}
170180
}
181+
182+
#[cfg(test)]
183+
mod tests {
184+
use super::*;
185+
186+
#[test]
187+
fn sum_monotonic_rejects_negative_values() {
188+
let sum = Sum::<f64>::new(
189+
Temporality::Cumulative,
190+
AttributeSetFilter::new(None),
191+
true, // monotonic = true
192+
100,
193+
);
194+
195+
Measure::call(&sum, 5.0, &[]);
196+
Measure::call(&sum, -3.0, &[]);
197+
198+
let (_len, metrics) = sum.cumulative(None);
199+
assert!(metrics.is_some(), "Should have metrics");
200+
let metrics = metrics.unwrap();
201+
202+
assert!(
203+
matches!(metrics, MetricData::Sum(_)),
204+
"Expected Sum metric data"
205+
);
206+
let MetricData::Sum(sum_data) = metrics else {
207+
unreachable!()
208+
};
209+
210+
assert_eq!(sum_data.data_points.len(), 1);
211+
assert_eq!(sum_data.data_points[0].value, 5.0);
212+
}
213+
214+
#[test]
215+
fn sum_non_monotonic_accepts_negative_values() {
216+
// Create a non-monotonic sum (up-down counter)
217+
let sum = Sum::<f64>::new(
218+
Temporality::Cumulative,
219+
AttributeSetFilter::new(None),
220+
false, // monotonic = false
221+
100,
222+
);
223+
224+
Measure::call(&sum, 5.0, &[]);
225+
Measure::call(&sum, -3.0, &[]);
226+
227+
let (_len, metrics) = sum.cumulative(None);
228+
assert!(metrics.is_some(), "Should have metrics");
229+
let metrics = metrics.unwrap();
230+
231+
assert!(
232+
matches!(metrics, MetricData::Sum(_)),
233+
"Expected Sum metric data"
234+
);
235+
let MetricData::Sum(sum_data) = metrics else {
236+
unreachable!()
237+
};
238+
239+
assert_eq!(sum_data.data_points.len(), 1);
240+
// Both values should be summed: 5.0 + (-3.0) = 2.0
241+
assert_eq!(sum_data.data_points[0].value, 2.0);
242+
}
243+
244+
#[test]
245+
fn sum_monotonic_accepts_zero() {
246+
let sum = Sum::<f64>::new(
247+
Temporality::Cumulative,
248+
AttributeSetFilter::new(None),
249+
true,
250+
100,
251+
);
252+
253+
Measure::call(&sum, 0.0, &[]);
254+
255+
let (_len, metrics) = sum.cumulative(None);
256+
assert!(metrics.is_some(), "Should have metrics");
257+
let metrics = metrics.unwrap();
258+
259+
assert!(
260+
matches!(metrics, MetricData::Sum(_)),
261+
"Expected Sum metric data"
262+
);
263+
let MetricData::Sum(sum_data) = metrics else {
264+
unreachable!()
265+
};
266+
267+
assert_eq!(sum_data.data_points.len(), 1);
268+
assert_eq!(sum_data.data_points[0].value, 0.0);
269+
}
270+
271+
#[test]
272+
fn sum_monotonic_rejects_negative_i64() {
273+
let sum = Sum::<i64>::new(
274+
Temporality::Cumulative,
275+
AttributeSetFilter::new(None),
276+
true, // monotonic = true
277+
100,
278+
);
279+
280+
Measure::call(&sum, 10, &[]);
281+
Measure::call(&sum, -5, &[]);
282+
283+
let (_len, metrics) = sum.cumulative(None);
284+
assert!(metrics.is_some(), "Should have metrics");
285+
let metrics = metrics.unwrap();
286+
287+
assert!(
288+
matches!(metrics, MetricData::Sum(_)),
289+
"Expected Sum metric data"
290+
);
291+
let MetricData::Sum(sum_data) = metrics else {
292+
unreachable!()
293+
};
294+
295+
assert_eq!(sum_data.data_points.len(), 1);
296+
assert_eq!(sum_data.data_points[0].value, 10);
297+
}
298+
}

opentelemetry/src/metrics/instruments/counter.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ use std::sync::Arc;
44

55
use super::SyncInstrument;
66

7-
/// An instrument that records increasing values.
7+
/// A monotonic instrument that records increasing values.
8+
///
9+
/// Counters are used to measure values that only increase over time, such as the number
10+
/// of requests received, bytes sent, or errors encountered. Only non-negative values
11+
/// should be recorded. Negative values violate the monotonic property and will be
12+
/// dropped by the SDK with a warning.
13+
///
14+
/// # Cloning
815
///
916
/// [`Counter`] can be cloned to create multiple handles to the same instrument. If a [`Counter`] needs to be shared,
1017
/// users are recommended to clone the [`Counter`] instead of creating duplicate [`Counter`]s for the same metric. Creating
@@ -29,12 +36,31 @@ impl<T> Counter<T> {
2936
}
3037

3138
/// Records an increment to the counter.
39+
///
40+
/// # Arguments
41+
///
42+
/// * `value` - A non-negative value to add to the counter. According to the
43+
/// OpenTelemetry specification, counters are monotonic instruments that record
44+
/// increasing values. Passing a negative value violates this contract.
45+
///
46+
/// * `attributes` - A set of key-value pairs that describe the measurement context.
47+
///
48+
/// # Behavior with negative values
49+
///
50+
/// The API does not validate the value, but the SDK implementation will log a warning
51+
/// and drop negative values to maintain the monotonic property of counters. Applications
52+
/// should ensure only non-negative values are passed to this method.
3253
pub fn add(&self, value: T, attributes: &[KeyValue]) {
3354
self.0.measure(value, attributes)
3455
}
3556
}
3657

37-
/// An async instrument that records increasing values.
58+
/// A monotonic asynchronous instrument that records increasing values.
59+
///
60+
/// Observable counters are used to measure values that only increase over time and are
61+
/// observed via callbacks, such as process CPU time or total memory usage. Only non-negative
62+
/// values should be recorded. Negative values violate the monotonic property and will be
63+
/// dropped by the SDK with a warning.
3864
#[derive(Clone)]
3965
#[non_exhaustive]
4066
pub struct ObservableCounter<T> {

opentelemetry/src/metrics/mod.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,33 @@ pub use noop::NoopMeterProvider;
1818

1919
/// SDK implemented trait for creating instruments
2020
pub trait InstrumentProvider {
21-
/// creates an instrument for recording increasing values.
21+
/// Creates a monotonic counter for recording increasing values.
22+
///
23+
/// Counters only accept non-negative values. Negative values will be dropped by the SDK.
2224
fn u64_counter(&self, _builder: InstrumentBuilder<'_, Counter<u64>>) -> Counter<u64> {
2325
Counter::new(Arc::new(noop::NoopSyncInstrument::new()))
2426
}
2527

26-
/// creates an instrument for recording increasing values.
28+
/// Creates a monotonic counter for recording increasing values.
29+
///
30+
/// Counters only accept non-negative values. Negative values will be dropped by the SDK.
2731
fn f64_counter(&self, _builder: InstrumentBuilder<'_, Counter<f64>>) -> Counter<f64> {
2832
Counter::new(Arc::new(noop::NoopSyncInstrument::new()))
2933
}
3034

31-
/// creates an instrument for recording increasing values via callback.
35+
/// Creates a monotonic observable counter for recording increasing values via callback.
36+
///
37+
/// Observable counters only accept non-negative values. Negative values will be dropped by the SDK.
3238
fn u64_observable_counter(
3339
&self,
3440
_builder: AsyncInstrumentBuilder<'_, ObservableCounter<u64>, u64>,
3541
) -> ObservableCounter<u64> {
3642
ObservableCounter::new()
3743
}
3844

39-
/// creates an instrument for recording increasing values via callback.
45+
/// Creates a monotonic observable counter for recording increasing values via callback.
46+
///
47+
/// Observable counters only accept non-negative values. Negative values will be dropped by the SDK.
4048
fn f64_observable_counter(
4149
&self,
4250
_builder: AsyncInstrumentBuilder<'_, ObservableCounter<f64>, f64>,

0 commit comments

Comments
 (0)