Skip to content

Commit a769bf5

Browse files
committed
Add support for timestamp encoding in exemplars
While timestamps are optional, they are required for native histograms: * https://github.com/prometheus/prometheus/blame/4aee718013/scrape/scrape.go#L1833 Old histograms can be converted to native histograms at the ingestion time, which in turn makes timestamps required for old histograms too. Benchmarking this against the baseline with no changes: * AMD EPYC 7642: ``` histogram without exemplars time: [12.389 ns 12.390 ns 12.392 ns] change: [-0.0820% -0.0532% -0.0227%] (p = 0.00 < 0.05) Change within noise threshold. histogram with exemplars (no exemplar passed) time: [28.469 ns 28.476 ns 28.483 ns] change: [+1.9145% +1.9533% +1.9954%] (p = 0.00 < 0.05) Performance has regressed. histogram with exemplars (some exemplar passed) time: [135.70 ns 135.83 ns 135.96 ns] change: [+49.325% +49.740% +50.112%] (p = 0.00 < 0.05) Performance has regressed. ``` * Apple M3 Pro: ``` histogram without exemplars time: [3.1357 ns 3.1617 ns 3.1974 ns] change: [+1.2045% +2.0927% +3.1167%] (p = 0.00 < 0.05) Performance has regressed. histogram with exemplars (no exemplar passed) time: [5.8648 ns 5.8751 ns 5.8872 ns] change: [+0.1479% +0.9875% +1.6873%] (p = 0.01 < 0.05) Change within noise threshold. histogram with exemplars (some exemplar passed) time: [69.448 ns 69.790 ns 70.192 ns] change: [+24.346% +24.897% +25.459%] (p = 0.00 < 0.05) Performance has regressed. ``` The only real change would come in the third benchmark when exemplars are actually passed, changes in the other two are due to noise. Exemplars are usually used for tracing, where there's sampling involved, so not every observation would incur a performance penalty, and only a small fraction would be affected. For cases where tracing is enabled for 100% of observations, the overhead if tracing itself would drown any changes here. Signed-off-by: Ivan Babrou <[email protected]>
1 parent 7a37c3a commit a769bf5

File tree

5 files changed

+89
-5
lines changed

5 files changed

+89
-5
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- `Family::get_or_create_owned` can access a metric in a labeled family. This
1515
method avoids the risk of runtime deadlocks at the expense of creating an
1616
owned type. See [PR 244].
17-
17+
18+
- Exemplar timestamps, which are required for `convert_classic_histograms_to_nhcb: true`
19+
in Prometheus scraping. See [PR 276].
20+
1821
[PR 244]: https://github.com/prometheus/client_rust/pull/244
1922
[PR 257]: https://github.com/prometheus/client_rust/pull/257
23+
[PR 276]: https://github.com/prometheus/client_rust/pull/276
2024

2125
### Changed
2226

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: 31 additions & 1 deletion
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: Some(exemplar.time.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> =
@@ -543,6 +549,14 @@ mod tests {
543549

544550
counter_with_exemplar.inc_by(1.0, Some(vec![("user_id".to_string(), 42.0)]));
545551

552+
counter_with_exemplar
553+
.inner
554+
.write()
555+
.exemplar
556+
.as_mut()
557+
.unwrap()
558+
.time = now;
559+
546560
let metric_set = encode(&registry).unwrap();
547561

548562
let family = metric_set.metric_families.first().unwrap();
@@ -563,6 +577,8 @@ mod tests {
563577
let exemplar = value.exemplar.as_ref().unwrap();
564578
assert_eq!(1.0, exemplar.value);
565579

580+
assert_eq!(&now_ts, exemplar.timestamp.as_ref().unwrap());
581+
566582
let expected_label = {
567583
openmetrics_data_model::Label {
568584
name: "user_id".to_string(),
@@ -784,11 +800,23 @@ mod tests {
784800

785801
#[test]
786802
fn encode_histogram_with_exemplars() {
803+
let now = SystemTime::now();
804+
let now_ts: Timestamp = now.into();
805+
787806
let mut registry = Registry::default();
788807
let histogram = HistogramWithExemplars::new(exponential_buckets(1.0, 2.0, 10));
789808
registry.register("my_histogram", "My histogram", histogram.clone());
790809
histogram.observe(1.0, Some(vec![("user_id".to_string(), 42u64)]));
791810

811+
histogram
812+
.inner
813+
.write()
814+
.exemplars
815+
.get_mut(&0)
816+
.as_mut()
817+
.unwrap()
818+
.time = now;
819+
792820
let metric_set = encode(&registry).unwrap();
793821

794822
let family = metric_set.metric_families.first().unwrap();
@@ -805,6 +833,8 @@ mod tests {
805833
let exemplar = value.buckets.first().unwrap().exemplar.as_ref().unwrap();
806834
assert_eq!(1.0, exemplar.value);
807835

836+
assert_eq!(&now_ts, exemplar.timestamp.as_ref().unwrap());
837+
808838
let expected_label = {
809839
openmetrics_data_model::Label {
810840
name: "user_id".to_string(),

src/encoding/text.rs

Lines changed: 36 additions & 3 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,13 @@ 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+
)?;
463470
Ok(())
464471
}
465472

@@ -737,6 +744,7 @@ mod tests {
737744
use std::borrow::Cow;
738745
use std::fmt::Error;
739746
use std::sync::atomic::{AtomicI32, AtomicU32};
747+
use std::time::{SystemTime, UNIX_EPOCH};
740748

741749
#[test]
742750
fn encode_counter() {
@@ -776,6 +784,8 @@ mod tests {
776784

777785
#[test]
778786
fn encode_counter_with_exemplar() {
787+
let now = SystemTime::now();
788+
779789
let mut registry = Registry::default();
780790

781791
let counter_with_exemplar: CounterWithExemplar<Vec<(String, u64)>> =
@@ -789,14 +799,24 @@ mod tests {
789799

790800
counter_with_exemplar.inc_by(1, Some(vec![("user_id".to_string(), 42)]));
791801

802+
counter_with_exemplar
803+
.inner
804+
.write()
805+
.exemplar
806+
.as_mut()
807+
.unwrap()
808+
.time = now;
809+
792810
let mut encoded = String::new();
793811
encode(&mut encoded, &registry).unwrap();
794812

795813
let expected = "# HELP my_counter_with_exemplar_seconds My counter with exemplar.\n"
796814
.to_owned()
797815
+ "# TYPE my_counter_with_exemplar_seconds counter\n"
798816
+ "# UNIT my_counter_with_exemplar_seconds seconds\n"
799-
+ "my_counter_with_exemplar_seconds_total 1 # {user_id=\"42\"} 1.0\n"
817+
+ "my_counter_with_exemplar_seconds_total 1 # {user_id=\"42\"} 1.0 "
818+
+ &dtoa::Buffer::new().format(now.duration_since(UNIX_EPOCH).unwrap().as_secs_f64())
819+
+ "\n"
800820
+ "# EOF\n";
801821
assert_eq!(expected, encoded);
802822

@@ -953,19 +973,32 @@ mod tests {
953973

954974
#[test]
955975
fn encode_histogram_with_exemplars() {
976+
let now = SystemTime::now();
977+
956978
let mut registry = Registry::default();
957979
let histogram = HistogramWithExemplars::new(exponential_buckets(1.0, 2.0, 10));
958980
registry.register("my_histogram", "My histogram", histogram.clone());
959981
histogram.observe(1.0, Some([("user_id".to_string(), 42u64)]));
960982

983+
histogram
984+
.inner
985+
.write()
986+
.exemplars
987+
.get_mut(&0)
988+
.as_mut()
989+
.unwrap()
990+
.time = now;
991+
961992
let mut encoded = String::new();
962993
encode(&mut encoded, &registry).unwrap();
963994

964995
let expected = "# HELP my_histogram My histogram.\n".to_owned()
965996
+ "# TYPE my_histogram histogram\n"
966997
+ "my_histogram_sum 1.0\n"
967998
+ "my_histogram_count 1\n"
968-
+ "my_histogram_bucket{le=\"1.0\"} 1 # {user_id=\"42\"} 1.0\n"
999+
+ "my_histogram_bucket{le=\"1.0\"} 1 # {user_id=\"42\"} 1.0 "
1000+
+ &dtoa::Buffer::new().format(now.duration_since(UNIX_EPOCH).unwrap().as_secs_f64())
1001+
+ "\n"
9691002
+ "my_histogram_bucket{le=\"2.0\"} 1\n"
9701003
+ "my_histogram_bucket{le=\"4.0\"} 1\n"
9711004
+ "my_histogram_bucket{le=\"8.0\"} 1\n"

src/metrics/exemplar.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ use std::sync::atomic::AtomicU32;
1616
#[cfg(target_has_atomic = "64")]
1717
use std::sync::atomic::AtomicU64;
1818
use std::sync::Arc;
19+
use std::time::SystemTime;
1920

2021
/// An OpenMetrics exemplar.
2122
#[derive(Debug)]
2223
pub struct Exemplar<S, V> {
2324
pub(crate) label_set: S,
2425
pub(crate) value: V,
26+
pub(crate) time: SystemTime,
2527
}
2628

2729
/////////////////////////////////////////////////////////////////////////////////
@@ -121,6 +123,7 @@ impl<S, N: Clone, A: counter::Atomic<N>> CounterWithExemplar<S, N, A> {
121123
inner.exemplar = label_set.map(|label_set| Exemplar {
122124
label_set,
123125
value: v.clone(),
126+
time: SystemTime::now(),
124127
});
125128

126129
inner.counter.inc_by(v)
@@ -256,6 +259,7 @@ impl<S> HistogramWithExemplars<S> {
256259
Exemplar {
257260
label_set,
258261
value: v,
262+
time: SystemTime::now(),
259263
},
260264
);
261265
}

0 commit comments

Comments
 (0)