Skip to content

Commit 9435495

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 <github@ivan.computer>
1 parent b017ad8 commit 9435495

5 files changed

Lines changed: 106 additions & 72 deletions

File tree

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: 9 additions & 7 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
/////////////////////////////////////////////////////////////////////////////////
@@ -35,7 +35,7 @@ pub struct Exemplar<S, V> {
3535
/// ```
3636
/// # use prometheus_client::metrics::exemplar::CounterWithExemplar;
3737
/// let counter_with_exemplar = CounterWithExemplar::<Vec<(String, String)>>::default();
38-
/// counter_with_exemplar.inc_by(1, Some(vec![("user_id".to_string(), "42".to_string())]));
38+
/// counter_with_exemplar.inc_by(1, Some(vec![("user_id".to_string(), "42".to_string())]), None);
3939
/// let _value: (u64, _) = counter_with_exemplar.get();
4040
/// ```
4141
/// You can also use exemplars with families. Just wrap the exemplar in a Family.
@@ -65,6 +65,7 @@ pub struct Exemplar<S, V> {
6565
/// Some(TraceLabel {
6666
/// trace_id: "3a2f90c9f80b894f".to_owned(),
6767
/// }),
68+
/// None,
6869
/// );
6970
/// ```
7071
#[cfg(target_has_atomic = "64")]
@@ -117,13 +118,13 @@ impl<S, N: Clone, A: counter::Atomic<N>> CounterWithExemplar<S, N, A> {
117118

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

123124
inner.exemplar = label_set.map(|label_set| Exemplar {
124125
label_set,
125126
value: v.clone(),
126-
time: SystemTime::now(),
127+
timestamp,
127128
});
128129

129130
inner.counter.inc_by(v)
@@ -178,7 +179,7 @@ where
178179
/// # use prometheus_client::metrics::exemplar::HistogramWithExemplars;
179180
/// # use prometheus_client::metrics::histogram::exponential_buckets;
180181
/// 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())]));
182+
/// histogram.observe(4.2, Some(vec![("user_id".to_string(), "42".to_string())]), None);
182183
/// ```
183184
/// You can also use exemplars with families. Just wrap the exemplar in a Family.
184185
/// ```
@@ -210,6 +211,7 @@ where
210211
/// Some(TraceLabel {
211212
/// trace_id: "3a2f90c9f80b894f".to_owned(),
212213
/// }),
214+
/// None,
213215
/// );
214216
/// ```
215217
#[derive(Debug)]
@@ -250,7 +252,7 @@ impl<S> HistogramWithExemplars<S> {
250252

251253
/// Observe the given value, optionally providing a label set and thus
252254
/// setting the [`Exemplar`] value.
253-
pub fn observe(&self, v: f64, label_set: Option<S>) {
255+
pub fn observe(&self, v: f64, label_set: Option<S>, timestamp: Option<SystemTime>) {
254256
let mut inner = self.inner.write();
255257
let bucket = inner.histogram.observe_and_bucket(v);
256258
if let (Some(bucket), Some(label_set)) = (bucket, label_set) {
@@ -259,7 +261,7 @@ impl<S> HistogramWithExemplars<S> {
259261
Exemplar {
260262
label_set,
261263
value: v,
262-
time: SystemTime::now(),
264+
timestamp,
263265
},
264266
);
265267
}

0 commit comments

Comments
 (0)