Skip to content

Commit b698bfa

Browse files
committed
Pass exemplar timestamp explicitly
This allows having exemplars without timestamps, which are cheaper. It also allows amortization of timestamping if multiple measurements share time. Signed-off-by: Ivan Babrou <[email protected]>
1 parent b017ad8 commit b698bfa

File tree

5 files changed

+104
-71
lines changed

5 files changed

+104
-71
lines changed

CHANGELOG.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
- `impl<T: Collector> Collector for std::sync::Arc<T>`.
1919
See [PR 273].
2020

21-
- Exemplar timestamps, which are required for `convert_classic_histograms_to_nhcb: true`
22-
in Prometheus scraping. See [PR 276].
23-
2421
[PR 244]: https://github.com/prometheus/client_rust/pull/244
2522
[PR 257]: https://github.com/prometheus/client_rust/pull/257
2623
[PR 273]: https://github.com/prometheus/client_rust/pull/273
27-
[PR 276]: https://github.com/prometheus/client_rust/pull/276
2824

2925
### Changed
3026

3127
- `EncodeLabelSet::encode()` now accepts a mutable reference to its encoder parameter.
28+
- Exemplar timestamps can now be passed, which are required for `convert_classic_histograms_to_nhcb: true`
29+
in Prometheus scraping. See [PR 276].
30+
31+
[PR 276]: https://github.com/prometheus/client_rust/pull/276
3232

3333
## [0.23.1]
3434

benches/exemplars.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::time::SystemTime;
2+
13
use criterion::{criterion_group, criterion_main, Criterion};
24
use prometheus_client::metrics::exemplar::HistogramWithExemplars;
35
use prometheus_client::metrics::histogram::Histogram;
@@ -19,7 +21,7 @@ pub fn exemplars(c: &mut Criterion) {
1921
let histogram = HistogramWithExemplars::<Exemplar>::new(BUCKETS.iter().copied());
2022

2123
b.iter(|| {
22-
histogram.observe(1.0, None);
24+
histogram.observe(1.0, None, None);
2325
});
2426
});
2527

@@ -28,7 +30,7 @@ pub fn exemplars(c: &mut Criterion) {
2830
let exemplar = vec![("TraceID".to_owned(), "deadfeed".to_owned())];
2931

3032
b.iter(|| {
31-
histogram.observe(1.0, Some(exemplar.clone()));
33+
histogram.observe(1.0, Some(exemplar.clone()), Some(SystemTime::now()));
3234
});
3335
});
3436
}

src/encoding/protobuf.rs

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ impl<S: EncodeLabelSet, V: EncodeExemplarValue> TryFrom<&Exemplar<S, V>>
311311

312312
Ok(openmetrics_data_model::Exemplar {
313313
value,
314-
timestamp: Some(exemplar.time.into()),
314+
timestamp: exemplar.timestamp.map(Into::into),
315315
label: labels,
316316
})
317317
}
@@ -547,15 +547,7 @@ mod tests {
547547
counter_with_exemplar.clone(),
548548
);
549549

550-
counter_with_exemplar.inc_by(1.0, Some(vec![("user_id".to_string(), 42.0)]));
551-
552-
counter_with_exemplar
553-
.inner
554-
.write()
555-
.exemplar
556-
.as_mut()
557-
.unwrap()
558-
.time = now;
550+
counter_with_exemplar.inc_by(1.0, Some(vec![("user_id".to_string(), 42.0)]), None);
559551

560552
let metric_set = encode(&registry).unwrap();
561553

@@ -577,7 +569,7 @@ mod tests {
577569
let exemplar = value.exemplar.as_ref().unwrap();
578570
assert_eq!(1.0, exemplar.value);
579571

580-
assert_eq!(&now_ts, exemplar.timestamp.as_ref().unwrap());
572+
assert!(exemplar.timestamp.is_none());
581573

582574
let expected_label = {
583575
openmetrics_data_model::Label {
@@ -589,6 +581,30 @@ mod tests {
589581
}
590582
_ => panic!("wrong value type"),
591583
}
584+
585+
counter_with_exemplar.inc_by(1.0, Some(vec![("user_id".to_string(), 99.0)]), Some(now));
586+
587+
match extract_metric_point_value(&encode(&registry).unwrap()) {
588+
openmetrics_data_model::metric_point::Value::CounterValue(value) => {
589+
// The counter should be encoded as `DoubleValue`
590+
let expected = openmetrics_data_model::counter_value::Total::DoubleValue(2.0);
591+
assert_eq!(Some(expected), value.total);
592+
593+
let exemplar = value.exemplar.as_ref().unwrap();
594+
assert_eq!(1.0, exemplar.value);
595+
596+
assert_eq!(&now_ts, exemplar.timestamp.as_ref().unwrap());
597+
598+
let expected_label = {
599+
openmetrics_data_model::Label {
600+
name: "user_id".to_string(),
601+
value: "99.0".to_string(),
602+
}
603+
};
604+
assert_eq!(vec![expected_label], exemplar.label);
605+
}
606+
_ => panic!("wrong value type"),
607+
}
592608
}
593609

594610
#[test]
@@ -806,16 +822,8 @@ mod tests {
806822
let mut registry = Registry::default();
807823
let histogram = HistogramWithExemplars::new(exponential_buckets(1.0, 2.0, 10));
808824
registry.register("my_histogram", "My histogram", histogram.clone());
809-
histogram.observe(1.0, Some(vec![("user_id".to_string(), 42u64)]));
810-
811-
histogram
812-
.inner
813-
.write()
814-
.exemplars
815-
.get_mut(&0)
816-
.as_mut()
817-
.unwrap()
818-
.time = now;
825+
826+
histogram.observe(1.0, Some(vec![("user_id".to_string(), 42u64)]), None);
819827

820828
let metric_set = encode(&registry).unwrap();
821829

@@ -833,7 +841,7 @@ mod tests {
833841
let exemplar = value.buckets.first().unwrap().exemplar.as_ref().unwrap();
834842
assert_eq!(1.0, exemplar.value);
835843

836-
assert_eq!(&now_ts, exemplar.timestamp.as_ref().unwrap());
844+
assert!(exemplar.timestamp.is_none());
837845

838846
let expected_label = {
839847
openmetrics_data_model::Label {
@@ -845,6 +853,26 @@ mod tests {
845853
}
846854
_ => panic!("wrong value type"),
847855
}
856+
857+
histogram.observe(2.0, Some(vec![("user_id".to_string(), 99u64)]), Some(now));
858+
859+
match extract_metric_point_value(&encode(&registry).unwrap()) {
860+
openmetrics_data_model::metric_point::Value::HistogramValue(value) => {
861+
let exemplar = value.buckets.get(1).unwrap().exemplar.as_ref().unwrap();
862+
assert_eq!(2.0, exemplar.value);
863+
864+
assert_eq!(&now_ts, exemplar.timestamp.as_ref().unwrap());
865+
866+
let expected_label = {
867+
openmetrics_data_model::Label {
868+
name: "user_id".to_string(),
869+
value: "99".to_string(),
870+
}
871+
};
872+
assert_eq!(vec![expected_label], exemplar.label);
873+
}
874+
_ => panic!("wrong value type"),
875+
}
848876
}
849877

850878
#[test]

src/encoding/text.rs

Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -460,13 +460,15 @@ impl MetricEncoder<'_> {
460460
}
461461
.into(),
462462
)?;
463-
self.writer.write_char(' ')?;
464-
exemplar.time.encode(
465-
ExemplarValueEncoder {
466-
writer: self.writer,
467-
}
468-
.into(),
469-
)?;
463+
if let Some(timestamp) = exemplar.timestamp {
464+
self.writer.write_char(' ')?;
465+
timestamp.encode(
466+
ExemplarValueEncoder {
467+
writer: self.writer,
468+
}
469+
.into(),
470+
)?;
471+
}
470472
Ok(())
471473
}
472474

@@ -797,15 +799,22 @@ mod tests {
797799
counter_with_exemplar.clone(),
798800
);
799801

800-
counter_with_exemplar.inc_by(1, Some(vec![("user_id".to_string(), 42)]));
802+
counter_with_exemplar.inc_by(1, Some(vec![("user_id".to_string(), 42)]), None);
803+
804+
let mut encoded = String::new();
805+
encode(&mut encoded, &registry).unwrap();
806+
807+
let expected = "# HELP my_counter_with_exemplar_seconds My counter with exemplar.\n"
808+
.to_owned()
809+
+ "# TYPE my_counter_with_exemplar_seconds counter\n"
810+
+ "# UNIT my_counter_with_exemplar_seconds seconds\n"
811+
+ "my_counter_with_exemplar_seconds_total 1 # {user_id=\"42\"} 1.0\n"
812+
+ "# EOF\n";
813+
assert_eq!(expected, encoded);
814+
815+
parse_with_python_client(encoded);
801816

802-
counter_with_exemplar
803-
.inner
804-
.write()
805-
.exemplar
806-
.as_mut()
807-
.unwrap()
808-
.time = now;
817+
counter_with_exemplar.inc_by(1, Some(vec![("user_id".to_string(), 99)]), Some(now));
809818

810819
let mut encoded = String::new();
811820
encode(&mut encoded, &registry).unwrap();
@@ -814,7 +823,7 @@ mod tests {
814823
.to_owned()
815824
+ "# TYPE my_counter_with_exemplar_seconds counter\n"
816825
+ "# UNIT my_counter_with_exemplar_seconds seconds\n"
817-
+ "my_counter_with_exemplar_seconds_total 1 # {user_id=\"42\"} 1.0 "
826+
+ "my_counter_with_exemplar_seconds_total 2 # {user_id=\"99\"} 1.0 "
818827
+ dtoa::Buffer::new().format(now.duration_since(UNIX_EPOCH).unwrap().as_secs_f64())
819828
+ "\n"
820829
+ "# EOF\n";
@@ -978,37 +987,30 @@ mod tests {
978987
let mut registry = Registry::default();
979988
let histogram = HistogramWithExemplars::new(exponential_buckets(1.0, 2.0, 10));
980989
registry.register("my_histogram", "My histogram", histogram.clone());
981-
histogram.observe(1.0, Some([("user_id".to_string(), 42u64)]));
982990

983-
histogram
984-
.inner
985-
.write()
986-
.exemplars
987-
.get_mut(&0)
988-
.as_mut()
989-
.unwrap()
990-
.time = now;
991+
histogram.observe(1.0, Some([("user_id".to_string(), 42u64)]), Some(now));
992+
histogram.observe(2.0, Some([("user_id".to_string(), 99u64)]), None);
991993

992994
let mut encoded = String::new();
993995
encode(&mut encoded, &registry).unwrap();
994996

995997
let expected = "# HELP my_histogram My histogram.\n".to_owned()
996998
+ "# TYPE my_histogram histogram\n"
997-
+ "my_histogram_sum 1.0\n"
998-
+ "my_histogram_count 1\n"
999+
+ "my_histogram_sum 3.0\n"
1000+
+ "my_histogram_count 2\n"
9991001
+ "my_histogram_bucket{le=\"1.0\"} 1 # {user_id=\"42\"} 1.0 "
10001002
+ dtoa::Buffer::new().format(now.duration_since(UNIX_EPOCH).unwrap().as_secs_f64())
10011003
+ "\n"
1002-
+ "my_histogram_bucket{le=\"2.0\"} 1\n"
1003-
+ "my_histogram_bucket{le=\"4.0\"} 1\n"
1004-
+ "my_histogram_bucket{le=\"8.0\"} 1\n"
1005-
+ "my_histogram_bucket{le=\"16.0\"} 1\n"
1006-
+ "my_histogram_bucket{le=\"32.0\"} 1\n"
1007-
+ "my_histogram_bucket{le=\"64.0\"} 1\n"
1008-
+ "my_histogram_bucket{le=\"128.0\"} 1\n"
1009-
+ "my_histogram_bucket{le=\"256.0\"} 1\n"
1010-
+ "my_histogram_bucket{le=\"512.0\"} 1\n"
1011-
+ "my_histogram_bucket{le=\"+Inf\"} 1\n"
1004+
+ "my_histogram_bucket{le=\"2.0\"} 2 # {user_id=\"99\"} 2.0\n"
1005+
+ "my_histogram_bucket{le=\"4.0\"} 2\n"
1006+
+ "my_histogram_bucket{le=\"8.0\"} 2\n"
1007+
+ "my_histogram_bucket{le=\"16.0\"} 2\n"
1008+
+ "my_histogram_bucket{le=\"32.0\"} 2\n"
1009+
+ "my_histogram_bucket{le=\"64.0\"} 2\n"
1010+
+ "my_histogram_bucket{le=\"128.0\"} 2\n"
1011+
+ "my_histogram_bucket{le=\"256.0\"} 2\n"
1012+
+ "my_histogram_bucket{le=\"512.0\"} 2\n"
1013+
+ "my_histogram_bucket{le=\"+Inf\"} 2\n"
10121014
+ "# EOF\n";
10131015
assert_eq!(expected, encoded);
10141016

src/metrics/exemplar.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use std::time::SystemTime;
2323
pub struct Exemplar<S, V> {
2424
pub(crate) label_set: S,
2525
pub(crate) value: V,
26-
pub(crate) time: SystemTime,
26+
pub(crate) timestamp: Option<SystemTime>,
2727
}
2828

2929
/////////////////////////////////////////////////////////////////////////////////
@@ -117,13 +117,13 @@ impl<S, N: Clone, A: counter::Atomic<N>> CounterWithExemplar<S, N, A> {
117117

118118
/// Increase the [`CounterWithExemplar`] by `v`, updating the [`Exemplar`]
119119
/// if a label set is provided, returning the previous value.
120-
pub fn inc_by(&self, v: N, label_set: Option<S>) -> N {
120+
pub fn inc_by(&self, v: N, label_set: Option<S>, timestamp: Option<SystemTime>) -> N {
121121
let mut inner = self.inner.write();
122122

123123
inner.exemplar = label_set.map(|label_set| Exemplar {
124124
label_set,
125125
value: v.clone(),
126-
time: SystemTime::now(),
126+
timestamp,
127127
});
128128

129129
inner.counter.inc_by(v)
@@ -178,7 +178,7 @@ where
178178
/// # use prometheus_client::metrics::exemplar::HistogramWithExemplars;
179179
/// # use prometheus_client::metrics::histogram::exponential_buckets;
180180
/// let histogram = HistogramWithExemplars::new(exponential_buckets(1.0, 2.0, 10));
181-
/// histogram.observe(4.2, Some(vec![("user_id".to_string(), "42".to_string())]));
181+
/// histogram.observe(4.2, Some(vec![("user_id".to_string(), "42".to_string())]), None);
182182
/// ```
183183
/// You can also use exemplars with families. Just wrap the exemplar in a Family.
184184
/// ```
@@ -210,6 +210,7 @@ where
210210
/// Some(TraceLabel {
211211
/// trace_id: "3a2f90c9f80b894f".to_owned(),
212212
/// }),
213+
/// None,
213214
/// );
214215
/// ```
215216
#[derive(Debug)]
@@ -250,7 +251,7 @@ impl<S> HistogramWithExemplars<S> {
250251

251252
/// Observe the given value, optionally providing a label set and thus
252253
/// setting the [`Exemplar`] value.
253-
pub fn observe(&self, v: f64, label_set: Option<S>) {
254+
pub fn observe(&self, v: f64, label_set: Option<S>, timestamp: Option<SystemTime>) {
254255
let mut inner = self.inner.write();
255256
let bucket = inner.histogram.observe_and_bucket(v);
256257
if let (Some(bucket), Some(label_set)) = (bucket, label_set) {
@@ -259,7 +260,7 @@ impl<S> HistogramWithExemplars<S> {
259260
Exemplar {
260261
label_set,
261262
value: v,
262-
time: SystemTime::now(),
263+
timestamp,
263264
},
264265
);
265266
}

0 commit comments

Comments
 (0)