diff --git a/CHANGELOG.md b/CHANGELOG.md index 006d53b..e804db4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `impl Collector for std::sync::Arc`. See [PR 273]. - + [PR 244]: https://github.com/prometheus/client_rust/pull/244 [PR 257]: https://github.com/prometheus/client_rust/pull/257 [PR 273]: https://github.com/prometheus/client_rust/pull/273 @@ -25,6 +25,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - `EncodeLabelSet::encode()` now accepts a mutable reference to its encoder parameter. +- Exemplar timestamps can now be passed, which are required for `convert_classic_histograms_to_nhcb: true` + in Prometheus scraping. See [PR 276]. + +[PR 276]: https://github.com/prometheus/client_rust/pull/276 ## [0.23.1] diff --git a/Cargo.toml b/Cargo.toml index a0f5e2a..a35d145 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,10 @@ prost-build = { version = "0.12.0", optional = true } name = "baseline" harness = false +[[bench]] +name = "exemplars" +harness = false + [[bench]] name = "family" harness = false diff --git a/benches/exemplars.rs b/benches/exemplars.rs new file mode 100644 index 0000000..edbd075 --- /dev/null +++ b/benches/exemplars.rs @@ -0,0 +1,39 @@ +use std::time::SystemTime; + +use criterion::{criterion_group, criterion_main, Criterion}; +use prometheus_client::metrics::exemplar::HistogramWithExemplars; +use prometheus_client::metrics::histogram::Histogram; + +type Exemplar = Vec<(String, String)>; + +const BUCKETS: &[f64] = &[1.0, 2.0, 3.0]; + +pub fn exemplars(c: &mut Criterion) { + c.bench_function("histogram without exemplars", |b| { + let histogram = Histogram::new(BUCKETS.iter().copied()); + + b.iter(|| { + histogram.observe(1.0); + }); + }); + + c.bench_function("histogram with exemplars (no exemplar passed)", |b| { + let histogram = HistogramWithExemplars::::new(BUCKETS.iter().copied()); + + b.iter(|| { + histogram.observe(1.0, None, None); + }); + }); + + c.bench_function("histogram with exemplars (some exemplar passed)", |b| { + let histogram = HistogramWithExemplars::::new(BUCKETS.iter().copied()); + let exemplar = vec![("TraceID".to_owned(), "deadfeed".to_owned())]; + + b.iter(|| { + histogram.observe(1.0, Some(exemplar.clone()), Some(SystemTime::now())); + }); + }); +} + +criterion_group!(benches, exemplars); +criterion_main!(benches); diff --git a/src/encoding.rs b/src/encoding.rs index 6a131cd..7c25d98 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -11,6 +11,7 @@ use std::fmt::Write; use std::ops::Deref; use std::rc::Rc; use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; #[cfg(feature = "protobuf")] #[cfg_attr(docsrs, doc(cfg(feature = "protobuf")))] @@ -760,6 +761,18 @@ impl EncodeExemplarValue for u32 { } } +/// An encodable exemplar time. +pub trait EncodeExemplarTime { + /// Encode the time in the OpenMetrics text encoding. + fn encode(&self, encoder: ExemplarValueEncoder) -> Result<(), std::fmt::Error>; +} + +impl EncodeExemplarTime for SystemTime { + fn encode(&self, mut encoder: ExemplarValueEncoder) -> Result<(), std::fmt::Error> { + encoder.encode(self.duration_since(UNIX_EPOCH).unwrap().as_secs_f64()) + } +} + /// Encoder for an exemplar value. #[derive(Debug)] pub struct ExemplarValueEncoder<'a>(ExemplarValueEncoderInner<'a>); diff --git a/src/encoding/protobuf.rs b/src/encoding/protobuf.rs index ae2ff60..9f770f6 100644 --- a/src/encoding/protobuf.rs +++ b/src/encoding/protobuf.rs @@ -311,7 +311,7 @@ impl TryFrom<&Exemplar> Ok(openmetrics_data_model::Exemplar { value, - timestamp: Default::default(), + timestamp: exemplar.timestamp.map(Into::into), label: labels, }) } @@ -442,6 +442,8 @@ impl std::fmt::Write for LabelValueEncoder<'_> { #[cfg(test)] mod tests { + use prost_types::Timestamp; + use super::*; use crate::metrics::counter::Counter; use crate::metrics::exemplar::{CounterWithExemplar, HistogramWithExemplars}; @@ -454,6 +456,7 @@ mod tests { use std::collections::HashSet; use std::sync::atomic::AtomicI64; use std::sync::atomic::AtomicU64; + use std::time::SystemTime; #[test] fn encode_counter_int() { @@ -531,6 +534,9 @@ mod tests { #[test] fn encode_counter_with_exemplar() { + let now = SystemTime::now(); + let now_ts: Timestamp = now.into(); + let mut registry = Registry::default(); let counter_with_exemplar: CounterWithExemplar, f64> = @@ -541,7 +547,7 @@ mod tests { counter_with_exemplar.clone(), ); - counter_with_exemplar.inc_by(1.0, Some(vec![("user_id".to_string(), 42.0)])); + counter_with_exemplar.inc_by(1.0, Some(vec![("user_id".to_string(), 42.0)]), None); let metric_set = encode(®istry).unwrap(); @@ -563,6 +569,8 @@ mod tests { let exemplar = value.exemplar.as_ref().unwrap(); assert_eq!(1.0, exemplar.value); + assert!(exemplar.timestamp.is_none()); + let expected_label = { openmetrics_data_model::Label { name: "user_id".to_string(), @@ -573,6 +581,30 @@ mod tests { } _ => panic!("wrong value type"), } + + counter_with_exemplar.inc_by(1.0, Some(vec![("user_id".to_string(), 99.0)]), Some(now)); + + match extract_metric_point_value(&encode(®istry).unwrap()) { + openmetrics_data_model::metric_point::Value::CounterValue(value) => { + // The counter should be encoded as `DoubleValue` + let expected = openmetrics_data_model::counter_value::Total::DoubleValue(2.0); + assert_eq!(Some(expected), value.total); + + let exemplar = value.exemplar.as_ref().unwrap(); + assert_eq!(1.0, exemplar.value); + + assert_eq!(&now_ts, exemplar.timestamp.as_ref().unwrap()); + + let expected_label = { + openmetrics_data_model::Label { + name: "user_id".to_string(), + value: "99.0".to_string(), + } + }; + assert_eq!(vec![expected_label], exemplar.label); + } + _ => panic!("wrong value type"), + } } #[test] @@ -784,10 +816,14 @@ mod tests { #[test] fn encode_histogram_with_exemplars() { + let now = SystemTime::now(); + let now_ts: Timestamp = now.into(); + let mut registry = Registry::default(); let histogram = HistogramWithExemplars::new(exponential_buckets(1.0, 2.0, 10)); registry.register("my_histogram", "My histogram", histogram.clone()); - histogram.observe(1.0, Some(vec![("user_id".to_string(), 42u64)])); + + histogram.observe(1.0, Some(vec![("user_id".to_string(), 42u64)]), None); let metric_set = encode(®istry).unwrap(); @@ -805,6 +841,8 @@ mod tests { let exemplar = value.buckets.first().unwrap().exemplar.as_ref().unwrap(); assert_eq!(1.0, exemplar.value); + assert!(exemplar.timestamp.is_none()); + let expected_label = { openmetrics_data_model::Label { name: "user_id".to_string(), @@ -815,6 +853,26 @@ mod tests { } _ => panic!("wrong value type"), } + + histogram.observe(2.0, Some(vec![("user_id".to_string(), 99u64)]), Some(now)); + + match extract_metric_point_value(&encode(®istry).unwrap()) { + openmetrics_data_model::metric_point::Value::HistogramValue(value) => { + let exemplar = value.buckets.get(1).unwrap().exemplar.as_ref().unwrap(); + assert_eq!(2.0, exemplar.value); + + assert_eq!(&now_ts, exemplar.timestamp.as_ref().unwrap()); + + let expected_label = { + openmetrics_data_model::Label { + name: "user_id".to_string(), + value: "99".to_string(), + } + }; + assert_eq!(vec![expected_label], exemplar.label); + } + _ => panic!("wrong value type"), + } } #[test] diff --git a/src/encoding/text.rs b/src/encoding/text.rs index ebb453a..90f6ff4 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -37,7 +37,7 @@ //! assert_eq!(expected_msg, buffer); //! ``` -use crate::encoding::{EncodeExemplarValue, EncodeLabelSet, NoLabelSet}; +use crate::encoding::{EncodeExemplarTime, EncodeExemplarValue, EncodeLabelSet, NoLabelSet}; use crate::metrics::exemplar::Exemplar; use crate::metrics::MetricType; use crate::registry::{Prefix, Registry, Unit}; @@ -460,6 +460,15 @@ impl MetricEncoder<'_> { } .into(), )?; + if let Some(timestamp) = exemplar.timestamp { + self.writer.write_char(' ')?; + timestamp.encode( + ExemplarValueEncoder { + writer: self.writer, + } + .into(), + )?; + } Ok(()) } @@ -737,6 +746,7 @@ mod tests { use std::borrow::Cow; use std::fmt::Error; use std::sync::atomic::{AtomicI32, AtomicU32}; + use std::time::{SystemTime, UNIX_EPOCH}; #[test] fn encode_counter() { @@ -776,6 +786,8 @@ mod tests { #[test] fn encode_counter_with_exemplar() { + let now = SystemTime::now(); + let mut registry = Registry::default(); let counter_with_exemplar: CounterWithExemplar> = @@ -787,7 +799,7 @@ mod tests { counter_with_exemplar.clone(), ); - counter_with_exemplar.inc_by(1, Some(vec![("user_id".to_string(), 42)])); + counter_with_exemplar.inc_by(1, Some(vec![("user_id".to_string(), 42)]), None); let mut encoded = String::new(); encode(&mut encoded, ®istry).unwrap(); @@ -801,6 +813,23 @@ mod tests { assert_eq!(expected, encoded); parse_with_python_client(encoded); + + counter_with_exemplar.inc_by(1, Some(vec![("user_id".to_string(), 99)]), Some(now)); + + let mut encoded = String::new(); + encode(&mut encoded, ®istry).unwrap(); + + let expected = "# HELP my_counter_with_exemplar_seconds My counter with exemplar.\n" + .to_owned() + + "# TYPE my_counter_with_exemplar_seconds counter\n" + + "# UNIT my_counter_with_exemplar_seconds seconds\n" + + "my_counter_with_exemplar_seconds_total 2 # {user_id=\"99\"} 1.0 " + + dtoa::Buffer::new().format(now.duration_since(UNIX_EPOCH).unwrap().as_secs_f64()) + + "\n" + + "# EOF\n"; + assert_eq!(expected, encoded); + + parse_with_python_client(encoded); } #[test] @@ -953,29 +982,35 @@ mod tests { #[test] fn encode_histogram_with_exemplars() { + let now = SystemTime::now(); + let mut registry = Registry::default(); let histogram = HistogramWithExemplars::new(exponential_buckets(1.0, 2.0, 10)); registry.register("my_histogram", "My histogram", histogram.clone()); - histogram.observe(1.0, Some([("user_id".to_string(), 42u64)])); + + histogram.observe(1.0, Some([("user_id".to_string(), 42u64)]), Some(now)); + histogram.observe(2.0, Some([("user_id".to_string(), 99u64)]), None); let mut encoded = String::new(); encode(&mut encoded, ®istry).unwrap(); let expected = "# HELP my_histogram My histogram.\n".to_owned() + "# TYPE my_histogram histogram\n" - + "my_histogram_sum 1.0\n" - + "my_histogram_count 1\n" - + "my_histogram_bucket{le=\"1.0\"} 1 # {user_id=\"42\"} 1.0\n" - + "my_histogram_bucket{le=\"2.0\"} 1\n" - + "my_histogram_bucket{le=\"4.0\"} 1\n" - + "my_histogram_bucket{le=\"8.0\"} 1\n" - + "my_histogram_bucket{le=\"16.0\"} 1\n" - + "my_histogram_bucket{le=\"32.0\"} 1\n" - + "my_histogram_bucket{le=\"64.0\"} 1\n" - + "my_histogram_bucket{le=\"128.0\"} 1\n" - + "my_histogram_bucket{le=\"256.0\"} 1\n" - + "my_histogram_bucket{le=\"512.0\"} 1\n" - + "my_histogram_bucket{le=\"+Inf\"} 1\n" + + "my_histogram_sum 3.0\n" + + "my_histogram_count 2\n" + + "my_histogram_bucket{le=\"1.0\"} 1 # {user_id=\"42\"} 1.0 " + + dtoa::Buffer::new().format(now.duration_since(UNIX_EPOCH).unwrap().as_secs_f64()) + + "\n" + + "my_histogram_bucket{le=\"2.0\"} 2 # {user_id=\"99\"} 2.0\n" + + "my_histogram_bucket{le=\"4.0\"} 2\n" + + "my_histogram_bucket{le=\"8.0\"} 2\n" + + "my_histogram_bucket{le=\"16.0\"} 2\n" + + "my_histogram_bucket{le=\"32.0\"} 2\n" + + "my_histogram_bucket{le=\"64.0\"} 2\n" + + "my_histogram_bucket{le=\"128.0\"} 2\n" + + "my_histogram_bucket{le=\"256.0\"} 2\n" + + "my_histogram_bucket{le=\"512.0\"} 2\n" + + "my_histogram_bucket{le=\"+Inf\"} 2\n" + "# EOF\n"; assert_eq!(expected, encoded); diff --git a/src/metrics/exemplar.rs b/src/metrics/exemplar.rs index 3dfa677..40e54e6 100644 --- a/src/metrics/exemplar.rs +++ b/src/metrics/exemplar.rs @@ -16,12 +16,14 @@ use std::sync::atomic::AtomicU32; #[cfg(target_has_atomic = "64")] use std::sync::atomic::AtomicU64; use std::sync::Arc; +use std::time::SystemTime; /// An OpenMetrics exemplar. #[derive(Debug)] pub struct Exemplar { pub(crate) label_set: S, pub(crate) value: V, + pub(crate) timestamp: Option, } ///////////////////////////////////////////////////////////////////////////////// @@ -33,7 +35,7 @@ pub struct Exemplar { /// ``` /// # use prometheus_client::metrics::exemplar::CounterWithExemplar; /// let counter_with_exemplar = CounterWithExemplar::>::default(); -/// counter_with_exemplar.inc_by(1, Some(vec![("user_id".to_string(), "42".to_string())])); +/// counter_with_exemplar.inc_by(1, Some(vec![("user_id".to_string(), "42".to_string())]), None); /// let _value: (u64, _) = counter_with_exemplar.get(); /// ``` /// You can also use exemplars with families. Just wrap the exemplar in a Family. @@ -63,6 +65,7 @@ pub struct Exemplar { /// Some(TraceLabel { /// trace_id: "3a2f90c9f80b894f".to_owned(), /// }), +/// None, /// ); /// ``` #[cfg(target_has_atomic = "64")] @@ -115,12 +118,13 @@ impl> CounterWithExemplar { /// Increase the [`CounterWithExemplar`] by `v`, updating the [`Exemplar`] /// if a label set is provided, returning the previous value. - pub fn inc_by(&self, v: N, label_set: Option) -> N { + pub fn inc_by(&self, v: N, label_set: Option, timestamp: Option) -> N { let mut inner = self.inner.write(); inner.exemplar = label_set.map(|label_set| Exemplar { label_set, value: v.clone(), + timestamp, }); inner.counter.inc_by(v) @@ -175,7 +179,7 @@ where /// # use prometheus_client::metrics::exemplar::HistogramWithExemplars; /// # use prometheus_client::metrics::histogram::exponential_buckets; /// let histogram = HistogramWithExemplars::new(exponential_buckets(1.0, 2.0, 10)); -/// histogram.observe(4.2, Some(vec![("user_id".to_string(), "42".to_string())])); +/// histogram.observe(4.2, Some(vec![("user_id".to_string(), "42".to_string())]), None); /// ``` /// You can also use exemplars with families. Just wrap the exemplar in a Family. /// ``` @@ -207,6 +211,7 @@ where /// Some(TraceLabel { /// trace_id: "3a2f90c9f80b894f".to_owned(), /// }), +/// None, /// ); /// ``` #[derive(Debug)] @@ -247,7 +252,7 @@ impl HistogramWithExemplars { /// Observe the given value, optionally providing a label set and thus /// setting the [`Exemplar`] value. - pub fn observe(&self, v: f64, label_set: Option) { + pub fn observe(&self, v: f64, label_set: Option, timestamp: Option) { let mut inner = self.inner.write(); let bucket = inner.histogram.observe_and_bucket(v); if let (Some(bucket), Some(label_set)) = (bucket, label_set) { @@ -256,6 +261,7 @@ impl HistogramWithExemplars { Exemplar { label_set, value: v, + timestamp, }, ); }