Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- `impl<T: Collector> Collector for std::sync::Arc<T>`.
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

### 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]

Expand Down
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 39 additions & 0 deletions benches/exemplars.rs
Original file line number Diff line number Diff line change
@@ -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::<Exemplar>::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::<Exemplar>::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);
13 changes: 13 additions & 0 deletions src/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")))]
Expand Down Expand Up @@ -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>);
Expand Down
64 changes: 61 additions & 3 deletions src/encoding/protobuf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ impl<S: EncodeLabelSet, V: EncodeExemplarValue> TryFrom<&Exemplar<S, V>>

Ok(openmetrics_data_model::Exemplar {
value,
timestamp: Default::default(),
timestamp: exemplar.timestamp.map(Into::into),
label: labels,
})
}
Expand Down Expand Up @@ -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};
Expand All @@ -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() {
Expand Down Expand Up @@ -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<Vec<(String, f64)>, f64> =
Expand All @@ -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(&registry).unwrap();

Expand All @@ -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(),
Expand All @@ -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(&registry).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]
Expand Down Expand Up @@ -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(&registry).unwrap();

Expand All @@ -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(),
Expand All @@ -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(&registry).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]
Expand Down
67 changes: 51 additions & 16 deletions src/encoding/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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(())
}

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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<Vec<(String, u64)>> =
Expand All @@ -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, &registry).unwrap();
Expand All @@ -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, &registry).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]
Expand Down Expand Up @@ -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, &registry).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);

Expand Down
Loading
Loading