Skip to content

Commit 0355911

Browse files
authored
feat: support timestamp encoding in exemplars (#276)
Signed-off-by: Ivan Babrou <[email protected]>
1 parent 970606a commit 0355911

File tree

7 files changed

+183
-24
lines changed

7 files changed

+183
-24
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717

1818
- `impl<T: Collector> Collector for std::sync::Arc<T>`.
1919
See [PR 273].
20-
20+
2121
[PR 244]: https://github.com/prometheus/client_rust/pull/244
2222
[PR 257]: https://github.com/prometheus/client_rust/pull/257
2323
[PR 273]: https://github.com/prometheus/client_rust/pull/273
2424

2525
### Changed
2626

2727
- `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
2832

2933
## [0.23.1]
3034

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ prost-build = { version = "0.12.0", optional = true }
4848
name = "baseline"
4949
harness = false
5050

51+
[[bench]]
52+
name = "exemplars"
53+
harness = false
54+
5155
[[bench]]
5256
name = "family"
5357
harness = false

benches/exemplars.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use std::time::SystemTime;
2+
3+
use criterion::{criterion_group, criterion_main, Criterion};
4+
use prometheus_client::metrics::exemplar::HistogramWithExemplars;
5+
use prometheus_client::metrics::histogram::Histogram;
6+
7+
type Exemplar = Vec<(String, String)>;
8+
9+
const BUCKETS: &[f64] = &[1.0, 2.0, 3.0];
10+
11+
pub fn exemplars(c: &mut Criterion) {
12+
c.bench_function("histogram without exemplars", |b| {
13+
let histogram = Histogram::new(BUCKETS.iter().copied());
14+
15+
b.iter(|| {
16+
histogram.observe(1.0);
17+
});
18+
});
19+
20+
c.bench_function("histogram with exemplars (no exemplar passed)", |b| {
21+
let histogram = HistogramWithExemplars::<Exemplar>::new(BUCKETS.iter().copied());
22+
23+
b.iter(|| {
24+
histogram.observe(1.0, None, None);
25+
});
26+
});
27+
28+
c.bench_function("histogram with exemplars (some exemplar passed)", |b| {
29+
let histogram = HistogramWithExemplars::<Exemplar>::new(BUCKETS.iter().copied());
30+
let exemplar = vec![("TraceID".to_owned(), "deadfeed".to_owned())];
31+
32+
b.iter(|| {
33+
histogram.observe(1.0, Some(exemplar.clone()), Some(SystemTime::now()));
34+
});
35+
});
36+
}
37+
38+
criterion_group!(benches, exemplars);
39+
criterion_main!(benches);

src/encoding.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use std::fmt::Write;
1111
use std::ops::Deref;
1212
use std::rc::Rc;
1313
use std::sync::Arc;
14+
use std::time::{SystemTime, UNIX_EPOCH};
1415

1516
#[cfg(feature = "protobuf")]
1617
#[cfg_attr(docsrs, doc(cfg(feature = "protobuf")))]
@@ -760,6 +761,18 @@ impl EncodeExemplarValue for u32 {
760761
}
761762
}
762763

764+
/// An encodable exemplar time.
765+
pub trait EncodeExemplarTime {
766+
/// Encode the time in the OpenMetrics text encoding.
767+
fn encode(&self, encoder: ExemplarValueEncoder) -> Result<(), std::fmt::Error>;
768+
}
769+
770+
impl EncodeExemplarTime for SystemTime {
771+
fn encode(&self, mut encoder: ExemplarValueEncoder) -> Result<(), std::fmt::Error> {
772+
encoder.encode(self.duration_since(UNIX_EPOCH).unwrap().as_secs_f64())
773+
}
774+
}
775+
763776
/// Encoder for an exemplar value.
764777
#[derive(Debug)]
765778
pub struct ExemplarValueEncoder<'a>(ExemplarValueEncoderInner<'a>);

src/encoding/protobuf.rs

Lines changed: 61 additions & 3 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: Default::default(),
314+
timestamp: exemplar.timestamp.map(Into::into),
315315
label: labels,
316316
})
317317
}
@@ -442,6 +442,8 @@ impl std::fmt::Write for LabelValueEncoder<'_> {
442442

443443
#[cfg(test)]
444444
mod tests {
445+
use prost_types::Timestamp;
446+
445447
use super::*;
446448
use crate::metrics::counter::Counter;
447449
use crate::metrics::exemplar::{CounterWithExemplar, HistogramWithExemplars};
@@ -454,6 +456,7 @@ mod tests {
454456
use std::collections::HashSet;
455457
use std::sync::atomic::AtomicI64;
456458
use std::sync::atomic::AtomicU64;
459+
use std::time::SystemTime;
457460

458461
#[test]
459462
fn encode_counter_int() {
@@ -531,6 +534,9 @@ mod tests {
531534

532535
#[test]
533536
fn encode_counter_with_exemplar() {
537+
let now = SystemTime::now();
538+
let now_ts: Timestamp = now.into();
539+
534540
let mut registry = Registry::default();
535541

536542
let counter_with_exemplar: CounterWithExemplar<Vec<(String, f64)>, f64> =
@@ -541,7 +547,7 @@ mod tests {
541547
counter_with_exemplar.clone(),
542548
);
543549

544-
counter_with_exemplar.inc_by(1.0, Some(vec![("user_id".to_string(), 42.0)]));
550+
counter_with_exemplar.inc_by(1.0, Some(vec![("user_id".to_string(), 42.0)]), None);
545551

546552
let metric_set = encode(&registry).unwrap();
547553

@@ -563,6 +569,8 @@ mod tests {
563569
let exemplar = value.exemplar.as_ref().unwrap();
564570
assert_eq!(1.0, exemplar.value);
565571

572+
assert!(exemplar.timestamp.is_none());
573+
566574
let expected_label = {
567575
openmetrics_data_model::Label {
568576
name: "user_id".to_string(),
@@ -573,6 +581,30 @@ mod tests {
573581
}
574582
_ => panic!("wrong value type"),
575583
}
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+
}
576608
}
577609

578610
#[test]
@@ -784,10 +816,14 @@ mod tests {
784816

785817
#[test]
786818
fn encode_histogram_with_exemplars() {
819+
let now = SystemTime::now();
820+
let now_ts: Timestamp = now.into();
821+
787822
let mut registry = Registry::default();
788823
let histogram = HistogramWithExemplars::new(exponential_buckets(1.0, 2.0, 10));
789824
registry.register("my_histogram", "My histogram", histogram.clone());
790-
histogram.observe(1.0, Some(vec![("user_id".to_string(), 42u64)]));
825+
826+
histogram.observe(1.0, Some(vec![("user_id".to_string(), 42u64)]), None);
791827

792828
let metric_set = encode(&registry).unwrap();
793829

@@ -805,6 +841,8 @@ mod tests {
805841
let exemplar = value.buckets.first().unwrap().exemplar.as_ref().unwrap();
806842
assert_eq!(1.0, exemplar.value);
807843

844+
assert!(exemplar.timestamp.is_none());
845+
808846
let expected_label = {
809847
openmetrics_data_model::Label {
810848
name: "user_id".to_string(),
@@ -815,6 +853,26 @@ mod tests {
815853
}
816854
_ => panic!("wrong value type"),
817855
}
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+
}
818876
}
819877

820878
#[test]

src/encoding/text.rs

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
//! assert_eq!(expected_msg, buffer);
3838
//! ```
3939
40-
use crate::encoding::{EncodeExemplarValue, EncodeLabelSet, NoLabelSet};
40+
use crate::encoding::{EncodeExemplarTime, EncodeExemplarValue, EncodeLabelSet, NoLabelSet};
4141
use crate::metrics::exemplar::Exemplar;
4242
use crate::metrics::MetricType;
4343
use crate::registry::{Prefix, Registry, Unit};
@@ -460,6 +460,15 @@ impl MetricEncoder<'_> {
460460
}
461461
.into(),
462462
)?;
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+
}
463472
Ok(())
464473
}
465474

@@ -737,6 +746,7 @@ mod tests {
737746
use std::borrow::Cow;
738747
use std::fmt::Error;
739748
use std::sync::atomic::{AtomicI32, AtomicU32};
749+
use std::time::{SystemTime, UNIX_EPOCH};
740750

741751
#[test]
742752
fn encode_counter() {
@@ -776,6 +786,8 @@ mod tests {
776786

777787
#[test]
778788
fn encode_counter_with_exemplar() {
789+
let now = SystemTime::now();
790+
779791
let mut registry = Registry::default();
780792

781793
let counter_with_exemplar: CounterWithExemplar<Vec<(String, u64)>> =
@@ -787,7 +799,7 @@ mod tests {
787799
counter_with_exemplar.clone(),
788800
);
789801

790-
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);
791803

792804
let mut encoded = String::new();
793805
encode(&mut encoded, &registry).unwrap();
@@ -801,6 +813,23 @@ mod tests {
801813
assert_eq!(expected, encoded);
802814

803815
parse_with_python_client(encoded);
816+
817+
counter_with_exemplar.inc_by(1, Some(vec![("user_id".to_string(), 99)]), Some(now));
818+
819+
let mut encoded = String::new();
820+
encode(&mut encoded, &registry).unwrap();
821+
822+
let expected = "# HELP my_counter_with_exemplar_seconds My counter with exemplar.\n"
823+
.to_owned()
824+
+ "# TYPE my_counter_with_exemplar_seconds counter\n"
825+
+ "# UNIT my_counter_with_exemplar_seconds seconds\n"
826+
+ "my_counter_with_exemplar_seconds_total 2 # {user_id=\"99\"} 1.0 "
827+
+ dtoa::Buffer::new().format(now.duration_since(UNIX_EPOCH).unwrap().as_secs_f64())
828+
+ "\n"
829+
+ "# EOF\n";
830+
assert_eq!(expected, encoded);
831+
832+
parse_with_python_client(encoded);
804833
}
805834

806835
#[test]
@@ -953,29 +982,35 @@ mod tests {
953982

954983
#[test]
955984
fn encode_histogram_with_exemplars() {
985+
let now = SystemTime::now();
986+
956987
let mut registry = Registry::default();
957988
let histogram = HistogramWithExemplars::new(exponential_buckets(1.0, 2.0, 10));
958989
registry.register("my_histogram", "My histogram", histogram.clone());
959-
histogram.observe(1.0, Some([("user_id".to_string(), 42u64)]));
990+
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);
960993

961994
let mut encoded = String::new();
962995
encode(&mut encoded, &registry).unwrap();
963996

964997
let expected = "# HELP my_histogram My histogram.\n".to_owned()
965998
+ "# TYPE my_histogram histogram\n"
966-
+ "my_histogram_sum 1.0\n"
967-
+ "my_histogram_count 1\n"
968-
+ "my_histogram_bucket{le=\"1.0\"} 1 # {user_id=\"42\"} 1.0\n"
969-
+ "my_histogram_bucket{le=\"2.0\"} 1\n"
970-
+ "my_histogram_bucket{le=\"4.0\"} 1\n"
971-
+ "my_histogram_bucket{le=\"8.0\"} 1\n"
972-
+ "my_histogram_bucket{le=\"16.0\"} 1\n"
973-
+ "my_histogram_bucket{le=\"32.0\"} 1\n"
974-
+ "my_histogram_bucket{le=\"64.0\"} 1\n"
975-
+ "my_histogram_bucket{le=\"128.0\"} 1\n"
976-
+ "my_histogram_bucket{le=\"256.0\"} 1\n"
977-
+ "my_histogram_bucket{le=\"512.0\"} 1\n"
978-
+ "my_histogram_bucket{le=\"+Inf\"} 1\n"
999+
+ "my_histogram_sum 3.0\n"
1000+
+ "my_histogram_count 2\n"
1001+
+ "my_histogram_bucket{le=\"1.0\"} 1 # {user_id=\"42\"} 1.0 "
1002+
+ dtoa::Buffer::new().format(now.duration_since(UNIX_EPOCH).unwrap().as_secs_f64())
1003+
+ "\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"
9791014
+ "# EOF\n";
9801015
assert_eq!(expected, encoded);
9811016

0 commit comments

Comments
 (0)