diff --git a/CHANGELOG.md b/CHANGELOG.md index 80654b5e..54b03a75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.25.0] + +### Added + +- Added an off-by-default `protobuf-protox` feature to build protobuf support + without requiring an external `protoc` binary. + +### Changed + +- Updated `prost`, `prost-build`, and `prost-types` dependencies to `v0.14`. +- The `prometheus_protobuf` feature generates and encodes Prometheus + `io.prometheus.client` protobuf messages from `metrics.proto` rather than the + OpenMetrics protobuf data model. + See [Issue](https://github.com/prometheus/OpenMetrics/issues/296) for more context. + The `protobuf` and `openmetrics_protobuf` features retain the old, deprecated, OpenMetrics protobuf support which will + be removed in a future release. + ## [0.24.1] ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e20a178b..64337755 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,6 +6,9 @@ The `build.rs` script in this library depends upon the [Protocol Buffers compiler][protoc]. Be sure that `protoc` is installed and available within your `PATH`. +If you enable the off-by-default `protobuf-protox` feature, the build uses +`protox` instead and does not require `protoc`. + [protoc]: https://docs.rs/prost-build/latest/prost_build/#sourcing-protoc ## Python Dependencies diff --git a/Cargo.toml b/Cargo.toml index c7b0ffab..7ef44a57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "prometheus-client" -version = "0.24.1" +version = "0.25.0" authors = ["Max Inden "] edition = "2021" description = "Open Metrics client library allowing users to natively instrument applications." @@ -12,7 +12,10 @@ documentation = "https://docs.rs/prometheus-client" [features] default = [] -protobuf = ["dep:prost", "dep:prost-types", "dep:prost-build"] +prometheus_protobuf = ["dep:prost", "dep:prost-types", "dep:prost-build"] +openmetrics_protobuf = ["dep:prost", "dep:prost-types", "dep:prost-build"] +protobuf = ["openmetrics_protobuf"] +protobuf-protox = ["dep:protox"] # This feature provides additional APIs for testing downstream code using # `prometheus-client`. @@ -29,8 +32,8 @@ dtoa = "1.0" itoa = "1.0" parking_lot = "0.12" prometheus-client-derive-encode = { version = "0.5.0", path = "derive-encode" } -prost = { version = "0.12.0", optional = true } -prost-types = { version = "0.12.0", optional = true } +prost = { version = "0.14", optional = true } +prost-types = { version = "0.14", optional = true } [dev-dependencies] async-std = { version = "1", features = ["attributes"] } @@ -49,7 +52,8 @@ hyper-util = { version = "0.1.3", features = ["tokio"] } http-body-util = "0.1.1" [build-dependencies] -prost-build = { version = "0.12.0", optional = true } +prost-build = { version = "0.14", optional = true } +protox = { version = "0.9.1", optional = true } [[bench]] name = "baseline" @@ -73,7 +77,7 @@ required-features = [] name = "proto" path = "benches/encoding/proto.rs" harness = false -required-features = ["protobuf"] +required-features = ["prometheus_protobuf"] # Passing arguments to the docsrs builder in order to properly document cfg's. # More information: https://docs.rs/about/builds#cross-compiling diff --git a/benches/encoding/proto.rs b/benches/encoding/proto.rs index da3a182d..5f62eedd 100644 --- a/benches/encoding/proto.rs +++ b/benches/encoding/proto.rs @@ -2,7 +2,7 @@ // https://github.com/tikv/rust-prometheus/blob/ab1ca7285d3463504381a5025ae1951e020d6796/benches/text_encoder.rs:write use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use prometheus_client::encoding::protobuf; +use prometheus_client::encoding::prometheus_protobuf; use prometheus_client::metrics::counter::Counter; use prometheus_client::metrics::family::Family; use prometheus_client::metrics::histogram::{exponential_buckets, Histogram}; @@ -75,7 +75,7 @@ pub fn proto(c: &mut Criterion) { } b.iter(|| { - let metric_set = protobuf::encode(®istry).unwrap(); + let metric_set = prometheus_protobuf::encode(®istry).unwrap(); black_box(metric_set); }) }); diff --git a/build.rs b/build.rs index d10b0197..5afe84c7 100644 --- a/build.rs +++ b/build.rs @@ -1,11 +1,36 @@ -use std::io::Result; - -fn main() -> Result<()> { - #[cfg(feature = "protobuf")] - prost_build::compile_protos( - &["src/encoding/proto/openmetrics_data_model.proto"], - &["src/encoding/proto/"], - )?; +use std::error::Error; + +fn main() -> Result<(), Box> { + #[cfg(any(feature = "prometheus_protobuf", feature = "openmetrics_protobuf"))] + compile_protos()?; + + Ok(()) +} + +#[allow(clippy::vec_init_then_push)] // False positive due to feature flags +#[cfg(any(feature = "prometheus_protobuf", feature = "openmetrics_protobuf"))] +fn compile_protos() -> Result<(), Box> { + let mut protos = Vec::new(); + + #[cfg(feature = "prometheus_protobuf")] + protos.push("src/encoding/proto/metrics.proto"); + #[cfg(feature = "openmetrics_protobuf")] + protos.push("src/encoding/proto/openmetrics_data_model.proto"); + + let includes = ["src/encoding/proto/"]; + + #[cfg(feature = "protobuf-protox")] + prost_build::compile_fds(protox::compile(&protos, includes)?)?; + + #[cfg(not(feature = "protobuf-protox"))] + prost_build::compile_protos(&protos, &includes)?; + + for path in &protos { + println!("cargo:rerun-if-changed={}", path); + } + for path in &includes { + println!("cargo:rerun-if-changed={}", path); + } Ok(()) } diff --git a/derive-encode/Cargo.toml b/derive-encode/Cargo.toml index 064cce7d..5fd04bcd 100644 --- a/derive-encode/Cargo.toml +++ b/derive-encode/Cargo.toml @@ -17,7 +17,7 @@ quote = "1" syn = "2" [dev-dependencies] -prometheus-client = { path = "../", features = ["protobuf"] } +prometheus-client = { path = "../", features = ["prometheus_protobuf"] } trybuild = "1" [lib] diff --git a/derive-encode/tests/lib.rs b/derive-encode/tests/lib.rs index 5d0910f3..ec30a562 100644 --- a/derive-encode/tests/lib.rs +++ b/derive-encode/tests/lib.rs @@ -47,8 +47,8 @@ fn basic_flow() { mod protobuf { use crate::{Labels, Method}; - use prometheus_client::encoding::protobuf::encode; - use prometheus_client::encoding::protobuf::openmetrics_data_model; + use prometheus_client::encoding::prometheus_protobuf::encode; + use prometheus_client::encoding::prometheus_protobuf::prometheus_data_model; use prometheus_client::metrics::counter::Counter; use prometheus_client::metrics::family::Family; use prometheus_client::registry::Registry; @@ -67,17 +67,15 @@ mod protobuf { }) .inc(); - // Encode all metrics in the registry in the OpenMetrics protobuf format. - let mut metric_set = encode(®istry).unwrap(); - let mut family: openmetrics_data_model::MetricFamily = - metric_set.metric_families.pop().unwrap(); - let metric: openmetrics_data_model::Metric = family.metrics.pop().unwrap(); + let mut metric_families = encode(®istry).unwrap(); + let mut family: prometheus_data_model::MetricFamily = metric_families.pop().unwrap(); + let metric: prometheus_data_model::Metric = family.metric.pop().unwrap(); - let method = &metric.labels[0]; + let method = &metric.label[0]; assert_eq!("method", method.name); assert_eq!("Get", method.value); - let path = &metric.labels[1]; + let path = &metric.label[1]; assert_eq!("path", path.name); assert_eq!("/metrics", path.value); } @@ -96,13 +94,11 @@ mod protobuf { }) .inc(); - // Encode all metrics in the registry in the OpenMetrics protobuf format. - let mut metric_set = encode(®istry).unwrap(); - let mut family: openmetrics_data_model::MetricFamily = - metric_set.metric_families.pop().unwrap(); - let metric: openmetrics_data_model::Metric = family.metrics.pop().unwrap(); + let mut metric_families = encode(®istry).unwrap(); + let mut family: prometheus_data_model::MetricFamily = metric_families.pop().unwrap(); + let metric: prometheus_data_model::Metric = family.metric.pop().unwrap(); - let label = &metric.labels[0]; + let label = &metric.label[0]; assert_eq!("method", label.name); assert_eq!("Get", label.value); } diff --git a/src/encoding.rs b/src/encoding.rs index 44ec0efd..8226c974 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -1,5 +1,7 @@ //! Exposition format implementations. +#![allow(deprecated)] + pub use prometheus_client_derive_encode::*; use crate::metrics::exemplar::Exemplar; @@ -13,17 +15,32 @@ use std::rc::Rc; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; -#[cfg(feature = "protobuf")] -#[cfg_attr(docsrs, doc(cfg(feature = "protobuf")))] -pub mod protobuf; +#[cfg(feature = "openmetrics_protobuf")] +#[cfg_attr(docsrs, doc(cfg(feature = "openmetrics_protobuf")))] +#[cfg_attr( + not(test), + deprecated( + note = "OpenMetrics protobuf support is deprecated. Use prometheus_protobuf instead." + ) +)] +pub mod openmetrics_protobuf; +#[cfg(feature = "prometheus_protobuf")] +#[cfg_attr(docsrs, doc(cfg(feature = "prometheus_protobuf")))] +pub mod prometheus_protobuf; pub mod text; +#[cfg(feature = "openmetrics_protobuf")] +#[deprecated(note = "Use openmetrics_protobuf instead.")] +pub use openmetrics_protobuf as protobuf; + macro_rules! for_both_mut { ($self:expr, $inner:ident, $pattern:pat, $fn:expr) => { match &mut $self.0 { $inner::Text($pattern) => $fn, - #[cfg(feature = "protobuf")] + #[cfg(feature = "openmetrics_protobuf")] $inner::Protobuf($pattern) => $fn, + #[cfg(feature = "prometheus_protobuf")] + $inner::PrometheusProtobuf($pattern) => $fn, } }; } @@ -32,8 +49,10 @@ macro_rules! for_both { ($self:expr, $inner:ident, $pattern:pat, $fn:expr) => { match $self.0 { $inner::Text($pattern) => $fn, - #[cfg(feature = "protobuf")] + #[cfg(feature = "openmetrics_protobuf")] $inner::Protobuf($pattern) => $fn, + #[cfg(feature = "prometheus_protobuf")] + $inner::PrometheusProtobuf($pattern) => $fn, } }; } @@ -81,8 +100,11 @@ pub struct DescriptorEncoder<'a>(DescriptorEncoderInner<'a>); enum DescriptorEncoderInner<'a> { Text(text::DescriptorEncoder<'a>), - #[cfg(feature = "protobuf")] - Protobuf(protobuf::DescriptorEncoder<'a>), + #[cfg(feature = "openmetrics_protobuf")] + Protobuf(openmetrics_protobuf::DescriptorEncoder<'a>), + + #[cfg(feature = "prometheus_protobuf")] + PrometheusProtobuf(prometheus_protobuf::DescriptorEncoder<'a>), } impl<'a> From> for DescriptorEncoder<'a> { @@ -91,13 +113,20 @@ impl<'a> From> for DescriptorEncoder<'a> { } } -#[cfg(feature = "protobuf")] -impl<'a> From> for DescriptorEncoder<'a> { - fn from(e: protobuf::DescriptorEncoder<'a>) -> Self { +#[cfg(feature = "openmetrics_protobuf")] +impl<'a> From> for DescriptorEncoder<'a> { + fn from(e: openmetrics_protobuf::DescriptorEncoder<'a>) -> Self { Self(DescriptorEncoderInner::Protobuf(e)) } } +#[cfg(feature = "prometheus_protobuf")] +impl<'a> From> for DescriptorEncoder<'a> { + fn from(e: prometheus_protobuf::DescriptorEncoder<'a>) -> Self { + Self(DescriptorEncoderInner::PrometheusProtobuf(e)) + } +} + impl DescriptorEncoder<'_> { pub(crate) fn with_prefix_and_labels<'s>( &'s mut self, @@ -138,8 +167,11 @@ pub struct MetricEncoder<'a>(MetricEncoderInner<'a>); enum MetricEncoderInner<'a> { Text(text::MetricEncoder<'a>), - #[cfg(feature = "protobuf")] - Protobuf(protobuf::MetricEncoder<'a>), + #[cfg(feature = "openmetrics_protobuf")] + Protobuf(openmetrics_protobuf::MetricEncoder<'a>), + + #[cfg(feature = "prometheus_protobuf")] + PrometheusProtobuf(prometheus_protobuf::MetricEncoder<'a>), } impl<'a> From> for MetricEncoder<'a> { @@ -148,13 +180,20 @@ impl<'a> From> for MetricEncoder<'a> { } } -#[cfg(feature = "protobuf")] -impl<'a> From> for MetricEncoder<'a> { - fn from(e: protobuf::MetricEncoder<'a>) -> Self { +#[cfg(feature = "openmetrics_protobuf")] +impl<'a> From> for MetricEncoder<'a> { + fn from(e: openmetrics_protobuf::MetricEncoder<'a>) -> Self { Self(MetricEncoderInner::Protobuf(e)) } } +#[cfg(feature = "prometheus_protobuf")] +impl<'a> From> for MetricEncoder<'a> { + fn from(e: prometheus_protobuf::MetricEncoder<'a>) -> Self { + Self(MetricEncoderInner::PrometheusProtobuf(e)) + } +} + impl MetricEncoder<'_> { /// Encode a counter. pub fn encode_counter< @@ -225,8 +264,10 @@ pub struct LabelSetEncoder<'a>(LabelSetEncoderInner<'a>); #[derive(Debug)] enum LabelSetEncoderInner<'a> { Text(text::LabelSetEncoder<'a>), - #[cfg(feature = "protobuf")] - Protobuf(protobuf::LabelSetEncoder<'a>), + #[cfg(feature = "openmetrics_protobuf")] + Protobuf(openmetrics_protobuf::LabelSetEncoder<'a>), + #[cfg(feature = "prometheus_protobuf")] + PrometheusProtobuf(prometheus_protobuf::LabelSetEncoder<'a>), } impl<'a> From> for LabelSetEncoder<'a> { @@ -235,13 +276,20 @@ impl<'a> From> for LabelSetEncoder<'a> { } } -#[cfg(feature = "protobuf")] -impl<'a> From> for LabelSetEncoder<'a> { - fn from(e: protobuf::LabelSetEncoder<'a>) -> Self { +#[cfg(feature = "openmetrics_protobuf")] +impl<'a> From> for LabelSetEncoder<'a> { + fn from(e: openmetrics_protobuf::LabelSetEncoder<'a>) -> Self { Self(LabelSetEncoderInner::Protobuf(e)) } } +#[cfg(feature = "prometheus_protobuf")] +impl<'a> From> for LabelSetEncoder<'a> { + fn from(e: prometheus_protobuf::LabelSetEncoder<'a>) -> Self { + Self(LabelSetEncoderInner::PrometheusProtobuf(e)) + } +} + impl LabelSetEncoder<'_> { /// Encode the given label. pub fn encode_label(&mut self) -> LabelEncoder<'_> { @@ -314,8 +362,10 @@ pub struct LabelEncoder<'a>(LabelEncoderInner<'a>); #[derive(Debug)] enum LabelEncoderInner<'a> { Text(text::LabelEncoder<'a>), - #[cfg(feature = "protobuf")] - Protobuf(protobuf::LabelEncoder<'a>), + #[cfg(feature = "openmetrics_protobuf")] + Protobuf(openmetrics_protobuf::LabelEncoder<'a>), + #[cfg(feature = "prometheus_protobuf")] + PrometheusProtobuf(prometheus_protobuf::LabelEncoder<'a>), } impl<'a> From> for LabelEncoder<'a> { @@ -324,13 +374,20 @@ impl<'a> From> for LabelEncoder<'a> { } } -#[cfg(feature = "protobuf")] -impl<'a> From> for LabelEncoder<'a> { - fn from(e: protobuf::LabelEncoder<'a>) -> Self { +#[cfg(feature = "openmetrics_protobuf")] +impl<'a> From> for LabelEncoder<'a> { + fn from(e: openmetrics_protobuf::LabelEncoder<'a>) -> Self { Self(LabelEncoderInner::Protobuf(e)) } } +#[cfg(feature = "prometheus_protobuf")] +impl<'a> From> for LabelEncoder<'a> { + fn from(e: prometheus_protobuf::LabelEncoder<'a>) -> Self { + Self(LabelEncoderInner::PrometheusProtobuf(e)) + } +} + impl LabelEncoder<'_> { /// Encode a label. pub fn encode_label_key(&mut self) -> Result, std::fmt::Error> { @@ -371,8 +428,10 @@ pub struct LabelKeyEncoder<'a>(LabelKeyEncoderInner<'a>); #[derive(Debug)] enum LabelKeyEncoderInner<'a> { Text(text::LabelKeyEncoder<'a>), - #[cfg(feature = "protobuf")] - Protobuf(protobuf::LabelKeyEncoder<'a>), + #[cfg(feature = "openmetrics_protobuf")] + Protobuf(openmetrics_protobuf::LabelKeyEncoder<'a>), + #[cfg(feature = "prometheus_protobuf")] + PrometheusProtobuf(prometheus_protobuf::LabelKeyEncoder<'a>), } impl<'a> From> for LabelKeyEncoder<'a> { @@ -381,13 +440,20 @@ impl<'a> From> for LabelKeyEncoder<'a> { } } -#[cfg(feature = "protobuf")] -impl<'a> From> for LabelKeyEncoder<'a> { - fn from(e: protobuf::LabelKeyEncoder<'a>) -> Self { +#[cfg(feature = "openmetrics_protobuf")] +impl<'a> From> for LabelKeyEncoder<'a> { + fn from(e: openmetrics_protobuf::LabelKeyEncoder<'a>) -> Self { Self(LabelKeyEncoderInner::Protobuf(e)) } } +#[cfg(feature = "prometheus_protobuf")] +impl<'a> From> for LabelKeyEncoder<'a> { + fn from(e: prometheus_protobuf::LabelKeyEncoder<'a>) -> Self { + Self(LabelKeyEncoderInner::PrometheusProtobuf(e)) + } +} + impl std::fmt::Write for LabelKeyEncoder<'_> { fn write_str(&mut self, s: &str) -> std::fmt::Result { for_both_mut!(self, LabelKeyEncoderInner, e, e.write_str(s)) @@ -468,8 +534,10 @@ pub struct LabelValueEncoder<'a>(LabelValueEncoderInner<'a>); #[derive(Debug)] enum LabelValueEncoderInner<'a> { Text(text::LabelValueEncoder<'a>), - #[cfg(feature = "protobuf")] - Protobuf(protobuf::LabelValueEncoder<'a>), + #[cfg(feature = "openmetrics_protobuf")] + Protobuf(openmetrics_protobuf::LabelValueEncoder<'a>), + #[cfg(feature = "prometheus_protobuf")] + PrometheusProtobuf(prometheus_protobuf::LabelValueEncoder<'a>), } impl<'a> From> for LabelValueEncoder<'a> { @@ -478,13 +546,20 @@ impl<'a> From> for LabelValueEncoder<'a> { } } -#[cfg(feature = "protobuf")] -impl<'a> From> for LabelValueEncoder<'a> { - fn from(e: protobuf::LabelValueEncoder<'a>) -> Self { +#[cfg(feature = "openmetrics_protobuf")] +impl<'a> From> for LabelValueEncoder<'a> { + fn from(e: openmetrics_protobuf::LabelValueEncoder<'a>) -> Self { LabelValueEncoder(LabelValueEncoderInner::Protobuf(e)) } } +#[cfg(feature = "prometheus_protobuf")] +impl<'a> From> for LabelValueEncoder<'a> { + fn from(e: prometheus_protobuf::LabelValueEncoder<'a>) -> Self { + LabelValueEncoder(LabelValueEncoderInner::PrometheusProtobuf(e)) + } +} + impl std::fmt::Write for LabelValueEncoder<'_> { fn write_str(&mut self, s: &str) -> std::fmt::Result { for_both_mut!(self, LabelValueEncoderInner, e, e.write_str(s)) @@ -611,10 +686,7 @@ impl EncodeGaugeValue for i64 { impl EncodeGaugeValue for u64 { fn encode(&self, encoder: &mut GaugeValueEncoder) -> Result<(), std::fmt::Error> { - // Between forcing end users to do endless as i64 for things that are - // clearly valid i64 and having one error case for rarely used protobuf when - // a gauge is set to >i64::MAX, the latter seems like the right choice. - encoder.encode_i64(i64::try_from(*self).map_err(|_err| std::fmt::Error)?) + encoder.encode_u64(*self) } } @@ -658,8 +730,10 @@ pub struct GaugeValueEncoder<'a>(GaugeValueEncoderInner<'a>); #[derive(Debug)] enum GaugeValueEncoderInner<'a> { Text(text::GaugeValueEncoder<'a>), - #[cfg(feature = "protobuf")] - Protobuf(protobuf::GaugeValueEncoder<'a>), + #[cfg(feature = "openmetrics_protobuf")] + Protobuf(openmetrics_protobuf::GaugeValueEncoder<'a>), + #[cfg(feature = "prometheus_protobuf")] + PrometheusProtobuf(prometheus_protobuf::GaugeValueEncoder<'a>), } impl GaugeValueEncoder<'_> { @@ -667,6 +741,10 @@ impl GaugeValueEncoder<'_> { for_both_mut!(self, GaugeValueEncoderInner, e, e.encode_u32(v)) } + fn encode_u64(&mut self, v: u64) -> Result<(), std::fmt::Error> { + for_both_mut!(self, GaugeValueEncoderInner, e, e.encode_u64(v)) + } + fn encode_i64(&mut self, v: i64) -> Result<(), std::fmt::Error> { for_both_mut!(self, GaugeValueEncoderInner, e, e.encode_i64(v)) } @@ -682,13 +760,20 @@ impl<'a> From> for GaugeValueEncoder<'a> { } } -#[cfg(feature = "protobuf")] -impl<'a> From> for GaugeValueEncoder<'a> { - fn from(e: protobuf::GaugeValueEncoder<'a>) -> Self { +#[cfg(feature = "openmetrics_protobuf")] +impl<'a> From> for GaugeValueEncoder<'a> { + fn from(e: openmetrics_protobuf::GaugeValueEncoder<'a>) -> Self { GaugeValueEncoder(GaugeValueEncoderInner::Protobuf(e)) } } +#[cfg(feature = "prometheus_protobuf")] +impl<'a> From> for GaugeValueEncoder<'a> { + fn from(e: prometheus_protobuf::GaugeValueEncoder<'a>) -> Self { + GaugeValueEncoder(GaugeValueEncoderInner::PrometheusProtobuf(e)) + } +} + /// An encodable counter value. pub trait EncodeCounterValue { /// Encode the given instance in the OpenMetrics text encoding. @@ -733,8 +818,10 @@ pub struct CounterValueEncoder<'a>(CounterValueEncoderInner<'a>); #[derive(Debug)] enum CounterValueEncoderInner<'a> { Text(text::CounterValueEncoder<'a>), - #[cfg(feature = "protobuf")] - Protobuf(protobuf::CounterValueEncoder<'a>), + #[cfg(feature = "openmetrics_protobuf")] + Protobuf(openmetrics_protobuf::CounterValueEncoder<'a>), + #[cfg(feature = "prometheus_protobuf")] + PrometheusProtobuf(prometheus_protobuf::CounterValueEncoder<'a>), } impl<'a> From> for CounterValueEncoder<'a> { @@ -743,13 +830,20 @@ impl<'a> From> for CounterValueEncoder<'a> { } } -#[cfg(feature = "protobuf")] -impl<'a> From> for CounterValueEncoder<'a> { - fn from(e: protobuf::CounterValueEncoder<'a>) -> Self { +#[cfg(feature = "openmetrics_protobuf")] +impl<'a> From> for CounterValueEncoder<'a> { + fn from(e: openmetrics_protobuf::CounterValueEncoder<'a>) -> Self { CounterValueEncoder(CounterValueEncoderInner::Protobuf(e)) } } +#[cfg(feature = "prometheus_protobuf")] +impl<'a> From> for CounterValueEncoder<'a> { + fn from(e: prometheus_protobuf::CounterValueEncoder<'a>) -> Self { + CounterValueEncoder(CounterValueEncoderInner::PrometheusProtobuf(e)) + } +} + impl CounterValueEncoder<'_> { fn encode_f64(&mut self, v: f64) -> Result<(), std::fmt::Error> { for_both_mut!(self, CounterValueEncoderInner, e, e.encode_f64(v)) @@ -809,8 +903,10 @@ pub struct ExemplarValueEncoder<'a>(ExemplarValueEncoderInner<'a>); #[derive(Debug)] enum ExemplarValueEncoderInner<'a> { Text(text::ExemplarValueEncoder<'a>), - #[cfg(feature = "protobuf")] - Protobuf(protobuf::ExemplarValueEncoder<'a>), + #[cfg(feature = "openmetrics_protobuf")] + Protobuf(openmetrics_protobuf::ExemplarValueEncoder<'a>), + #[cfg(feature = "prometheus_protobuf")] + PrometheusProtobuf(prometheus_protobuf::ExemplarValueEncoder<'a>), } impl<'a> From> for ExemplarValueEncoder<'a> { @@ -819,13 +915,20 @@ impl<'a> From> for ExemplarValueEncoder<'a> { } } -#[cfg(feature = "protobuf")] -impl<'a> From> for ExemplarValueEncoder<'a> { - fn from(e: protobuf::ExemplarValueEncoder<'a>) -> Self { +#[cfg(feature = "openmetrics_protobuf")] +impl<'a> From> for ExemplarValueEncoder<'a> { + fn from(e: openmetrics_protobuf::ExemplarValueEncoder<'a>) -> Self { ExemplarValueEncoder(ExemplarValueEncoderInner::Protobuf(e)) } } +#[cfg(feature = "prometheus_protobuf")] +impl<'a> From> for ExemplarValueEncoder<'a> { + fn from(e: prometheus_protobuf::ExemplarValueEncoder<'a>) -> Self { + ExemplarValueEncoder(ExemplarValueEncoderInner::PrometheusProtobuf(e)) + } +} + impl ExemplarValueEncoder<'_> { fn encode(&mut self, v: f64) -> Result<(), std::fmt::Error> { for_both_mut!(self, ExemplarValueEncoderInner, e, e.encode(v)) diff --git a/src/encoding/protobuf.rs b/src/encoding/openmetrics_protobuf.rs similarity index 99% rename from src/encoding/protobuf.rs rename to src/encoding/openmetrics_protobuf.rs index 9f770f60..c7334c50 100644 --- a/src/encoding/protobuf.rs +++ b/src/encoding/openmetrics_protobuf.rs @@ -1,7 +1,7 @@ //! Open Metrics protobuf implementation. //! //! ``` -//! # use prometheus_client::encoding::protobuf::encode; +//! # use prometheus_client::encoding::openmetrics_protobuf::encode; //! # use prometheus_client::metrics::counter::Counter; //! # use prometheus_client::registry::Registry; //! # @@ -327,6 +327,10 @@ impl GaugeValueEncoder<'_> { self.encode_i64(v as i64) } + pub fn encode_u64(&mut self, v: u64) -> Result<(), std::fmt::Error> { + self.encode_i64(i64::try_from(v).map_err(|_err| std::fmt::Error)?) + } + pub fn encode_i64(&mut self, v: i64) -> Result<(), std::fmt::Error> { *self.value = openmetrics_data_model::gauge_value::Value::IntValue(v); Ok(()) @@ -441,6 +445,7 @@ impl std::fmt::Write for LabelValueEncoder<'_> { } #[cfg(test)] +#[allow(deprecated)] mod tests { use prost_types::Timestamp; diff --git a/src/encoding/prometheus_protobuf.rs b/src/encoding/prometheus_protobuf.rs new file mode 100644 index 00000000..54252bd0 --- /dev/null +++ b/src/encoding/prometheus_protobuf.rs @@ -0,0 +1,839 @@ +//! Prometheus protobuf implementation. +//! +//! ``` +//! # use prometheus_client::encoding::prometheus_protobuf::encode; +//! # use prometheus_client::metrics::counter::Counter; +//! # use prometheus_client::registry::Registry; +//! # +//! # // Create registry and counter and register the latter with the former. +//! # let mut registry = Registry::default(); +//! # let counter: Counter = Counter::default(); +//! # registry.register( +//! # "my_counter", +//! # "This is my counter", +//! # counter.clone(), +//! # ); +//! # counter.inc(); +//! // Returns `Vec`, the top-level container type. Please refer to [metrics.proto](https://github.com/prometheus/prometheus/blob/main/prompb/io/prometheus/client/metrics.proto) for details. +//! let metric_families = encode(®istry).unwrap(); +//! +//! let family = metric_families.first().unwrap(); +//! assert_eq!("my_counter_total", family.name); +//! assert_eq!("This is my counter.", family.help); +//! ``` +//! +//! For wire-format exposition, serialize each returned `MetricFamily` with +//! length-delimited protobuf framing. [`encode_to_vec`] provides the exact +//! payload used with +//! `application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited`. + +// Allowing some lints here as the `io.prometheus.client.rs` file is generated. +#[allow(missing_docs, clippy::derive_partial_eq_without_eq)] +/// Data models generated from Prometheus `io.prometheus.client` protobuf. +pub mod prometheus_data_model { + include!(concat!(env!("OUT_DIR"), "/io.prometheus.client.rs")); +} + +use prost::Message; +use std::{borrow::Cow, collections::HashMap}; + +use crate::metrics::MetricType; +use crate::registry::{Registry, Unit}; +use crate::{metrics::exemplar::Exemplar, registry::Prefix}; + +use super::{EncodeCounterValue, EncodeExemplarValue, EncodeGaugeValue, EncodeLabelSet}; + +/// Encode the metrics registered with the provided [`Registry`] into +/// Prometheus `MetricFamily` messages. +pub fn encode( + registry: &Registry, +) -> Result, std::fmt::Error> { + let mut metric_families = Vec::new(); + let mut descriptor_encoder = DescriptorEncoder::new(&mut metric_families).into(); + registry.encode(&mut descriptor_encoder)?; + Ok(metric_families) +} + +/// Encode the metrics registered with the provided [`Registry`] into a +/// length-delimited Prometheus protobuf payload. +pub fn encode_to_vec(registry: &Registry) -> Result, EncodeError> { + let metric_families = encode(registry)?; + let mut encoded = Vec::new(); + + for metric_family in metric_families { + metric_family.encode_length_delimited(&mut encoded)?; + } + + Ok(encoded) +} + +/// Errors returned by [`encode_to_vec`]. +#[derive(Debug)] +pub enum EncodeError { + /// A metric failed to encode into the intermediate protobuf data model. + Fmt(std::fmt::Error), + /// The generated protobuf message failed to serialize. + Protobuf(prost::EncodeError), +} + +impl std::fmt::Display for EncodeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + EncodeError::Fmt(_) => f.write_str("failed to encode metrics into Prometheus protobuf"), + EncodeError::Protobuf(err) => err.fmt(f), + } + } +} + +impl std::error::Error for EncodeError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + EncodeError::Fmt(err) => Some(err), + EncodeError::Protobuf(err) => Some(err), + } + } +} + +impl From for EncodeError { + fn from(err: std::fmt::Error) -> Self { + EncodeError::Fmt(err) + } +} + +impl From for EncodeError { + fn from(err: prost::EncodeError) -> Self { + EncodeError::Protobuf(err) + } +} + +impl From for prometheus_data_model::MetricType { + fn from(metric_type: MetricType) -> Self { + match metric_type { + MetricType::Counter => prometheus_data_model::MetricType::Counter, + MetricType::Gauge => prometheus_data_model::MetricType::Gauge, + MetricType::Histogram => prometheus_data_model::MetricType::Histogram, + // Prometheus does not have a dedicated info type; expose it as the + // conventional `_info` gauge with value `1`. + MetricType::Info => prometheus_data_model::MetricType::Gauge, + MetricType::Unknown => prometheus_data_model::MetricType::Untyped, + } + } +} + +fn metric_family_name( + prefix: Option<&Prefix>, + name: &str, + unit: Option<&Unit>, + metric_type: MetricType, +) -> String { + let mut full_name = String::new(); + + if let Some(prefix) = prefix { + full_name.push_str(prefix.as_str()); + full_name.push('_'); + } + + full_name.push_str(name); + + if let Some(unit) = unit { + full_name.push('_'); + full_name.push_str(unit.as_str()); + } + + match metric_type { + MetricType::Counter => full_name.push_str("_total"), + MetricType::Info => full_name.push_str("_info"), + MetricType::Gauge | MetricType::Histogram | MetricType::Unknown => {} + } + + full_name +} + +/// Metric Descriptor encoder for protobuf encoding. +#[derive(Debug)] +pub(crate) struct DescriptorEncoder<'a> { + metric_families: &'a mut Vec, + prefix: Option<&'a Prefix>, + labels: &'a [(Cow<'static, str>, Cow<'static, str>)], +} + +impl DescriptorEncoder<'_> { + pub(crate) fn new( + metric_families: &mut Vec, + ) -> DescriptorEncoder<'_> { + DescriptorEncoder { + metric_families, + prefix: Default::default(), + labels: Default::default(), + } + } + + pub(crate) fn with_prefix_and_labels<'s>( + &'s mut self, + prefix: Option<&'s Prefix>, + labels: &'s [(Cow<'static, str>, Cow<'static, str>)], + ) -> DescriptorEncoder<'s> { + DescriptorEncoder { + prefix, + labels, + metric_families: self.metric_families, + } + } + + pub fn encode_descriptor<'s>( + &'s mut self, + name: &str, + help: &str, + unit: Option<&Unit>, + metric_type: MetricType, + ) -> Result, std::fmt::Error> { + let family = prometheus_data_model::MetricFamily { + name: metric_family_name(self.prefix, name, unit, metric_type), + r#type: prometheus_data_model::MetricType::from(metric_type) as i32, + unit: unit + .map(|unit| unit.as_str().to_string()) + .unwrap_or_default(), + help: help.to_string(), + ..Default::default() + }; + let mut labels = vec![]; + self.labels.encode( + &mut LabelSetEncoder { + labels: &mut labels, + } + .into(), + )?; + self.metric_families.push(family); + + Ok(MetricEncoder { + family: &mut self + .metric_families + .last_mut() + .expect("previous push") + .metric, + metric_type, + labels, + }) + } +} + +/// Encoder for protobuf encoding. +/// +/// This is an inner type for [`super::MetricEncoder`]. +#[derive(Debug)] +pub(crate) struct MetricEncoder<'f> { + /// OpenMetrics metric type of the metric. + metric_type: MetricType, + /// Vector of OpenMetrics metrics to which encoded metrics are added. + family: &'f mut Vec, + /// Labels to be added to each metric. + labels: Vec, +} + +impl MetricEncoder<'_> { + pub fn encode_counter< + S: EncodeLabelSet, + CounterValue: EncodeCounterValue, + ExemplarValue: EncodeExemplarValue, + >( + &mut self, + v: &CounterValue, + exemplar: Option<&Exemplar>, + ) -> Result<(), std::fmt::Error> { + let mut value = 0.0; + let mut e = CounterValueEncoder { value: &mut value }.into(); + v.encode(&mut e)?; + + self.family.push(prometheus_data_model::Metric { + label: self.labels.clone(), + counter: Some(prometheus_data_model::Counter { + value, + exemplar: exemplar.map(TryInto::try_into).transpose()?, + start_timestamp: None, + }), + ..Default::default() + }); + + Ok(()) + } + + pub fn encode_gauge( + &mut self, + v: &GaugeValue, + ) -> Result<(), std::fmt::Error> { + let mut value = 0.0; + let mut e = GaugeValueEncoder { value: &mut value }.into(); + v.encode(&mut e)?; + + self.family.push(prometheus_data_model::Metric { + label: self.labels.clone(), + gauge: Some(prometheus_data_model::Gauge { value }), + ..Default::default() + }); + + Ok(()) + } + + pub fn encode_info( + &mut self, + label_set: &impl super::EncodeLabelSet, + ) -> Result<(), std::fmt::Error> { + let mut labels = self.labels.clone(); + label_set.encode( + &mut LabelSetEncoder { + labels: &mut labels, + } + .into(), + )?; + + self.family.push(prometheus_data_model::Metric { + label: labels, + gauge: Some(prometheus_data_model::Gauge { value: 1.0 }), + ..Default::default() + }); + + Ok(()) + } + + pub fn encode_family( + &mut self, + label_set: &S, + ) -> Result, std::fmt::Error> { + let mut labels = self.labels.clone(); + label_set.encode( + &mut LabelSetEncoder { + labels: &mut labels, + } + .into(), + )?; + + Ok(MetricEncoder { + metric_type: self.metric_type, + family: self.family, + labels, + }) + } + + pub fn encode_histogram( + &mut self, + sum: f64, + count: u64, + buckets: &[(f64, u64)], + exemplars: Option<&HashMap>>, + ) -> Result<(), std::fmt::Error> { + let mut cumulative_count = 0; + let bucket = buckets + .iter() + .enumerate() + .map(|(i, (upper_bound, count))| { + cumulative_count += count; + Ok(prometheus_data_model::Bucket { + cumulative_count, + // not needed; if set would override cumulative_count. + cumulative_count_float: 0.0, + upper_bound: *upper_bound, + exemplar: exemplars + .and_then(|exemplars| exemplars.get(&i).map(|exemplar| exemplar.try_into())) + .transpose()?, + }) + }) + .collect::, std::fmt::Error>>()?; + + self.family.push(prometheus_data_model::Metric { + label: self.labels.clone(), + histogram: Some(prometheus_data_model::Histogram { + sample_count: count, + sample_count_float: 0.0, + sample_sum: sum, + bucket, + start_timestamp: None, + ..Default::default() + }), + ..Default::default() + }); + + Ok(()) + } +} + +impl TryFrom<&Exemplar> + for prometheus_data_model::Exemplar +{ + type Error = std::fmt::Error; + + fn try_from(exemplar: &Exemplar) -> Result { + let mut value = f64::default(); + exemplar + .value + .encode(ExemplarValueEncoder { value: &mut value }.into())?; + + let mut label = vec![]; + exemplar + .label_set + .encode(&mut LabelSetEncoder { labels: &mut label }.into())?; + + Ok(prometheus_data_model::Exemplar { + label, + value, + timestamp: exemplar.timestamp.map(Into::into), + }) + } +} + +#[derive(Debug)] +pub(crate) struct GaugeValueEncoder<'a> { + value: &'a mut f64, +} + +impl GaugeValueEncoder<'_> { + pub fn encode_u32(&mut self, v: u32) -> Result<(), std::fmt::Error> { + self.encode_f64(f64::from(v)) + } + + pub fn encode_u64(&mut self, v: u64) -> Result<(), std::fmt::Error> { + self.encode_f64(v as f64) + } + + pub fn encode_i64(&mut self, v: i64) -> Result<(), std::fmt::Error> { + self.encode_f64(v as f64) + } + + pub fn encode_f64(&mut self, v: f64) -> Result<(), std::fmt::Error> { + *self.value = v; + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct ExemplarValueEncoder<'a> { + value: &'a mut f64, +} + +impl ExemplarValueEncoder<'_> { + pub fn encode(&mut self, v: f64) -> Result<(), std::fmt::Error> { + *self.value = v; + Ok(()) + } +} + +impl From<&(K, V)> for prometheus_data_model::LabelPair { + fn from(kv: &(K, V)) -> Self { + prometheus_data_model::LabelPair { + name: kv.0.to_string(), + value: kv.1.to_string(), + } + } +} + +#[derive(Debug)] +pub(crate) struct CounterValueEncoder<'a> { + value: &'a mut f64, +} + +impl CounterValueEncoder<'_> { + pub fn encode_f64(&mut self, v: f64) -> Result<(), std::fmt::Error> { + *self.value = v; + Ok(()) + } + + pub fn encode_u64(&mut self, v: u64) -> Result<(), std::fmt::Error> { + *self.value = v as f64; + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct LabelSetEncoder<'a> { + labels: &'a mut Vec, +} + +impl LabelSetEncoder<'_> { + pub fn encode_label(&mut self) -> LabelEncoder<'_> { + LabelEncoder { + labels: self.labels, + } + } +} + +#[derive(Debug)] +pub(crate) struct LabelEncoder<'a> { + labels: &'a mut Vec, +} + +impl LabelEncoder<'_> { + pub fn encode_label_key(&mut self) -> Result, std::fmt::Error> { + self.labels + .push(prometheus_data_model::LabelPair::default()); + + Ok(LabelKeyEncoder { + label: self.labels.last_mut().expect("To find pushed label."), + }) + } +} + +#[derive(Debug)] +pub(crate) struct LabelKeyEncoder<'a> { + label: &'a mut prometheus_data_model::LabelPair, +} + +impl std::fmt::Write for LabelKeyEncoder<'_> { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.label.name.write_str(s) + } +} + +impl<'a> LabelKeyEncoder<'a> { + pub fn encode_label_value(self) -> Result, std::fmt::Error> { + Ok(LabelValueEncoder { + label_value: &mut self.label.value, + }) + } +} + +#[derive(Debug)] +pub(crate) struct LabelValueEncoder<'a> { + label_value: &'a mut String, +} + +impl LabelValueEncoder<'_> { + pub fn finish(self) -> Result<(), std::fmt::Error> { + Ok(()) + } +} + +impl std::fmt::Write for LabelValueEncoder<'_> { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.label_value.write_str(s) + } +} + +#[cfg(test)] +mod tests { + use prost_types::Timestamp; + + use super::*; + use crate::metrics::counter::Counter; + use crate::metrics::exemplar::{CounterWithExemplar, HistogramWithExemplars}; + use crate::metrics::family::Family; + use crate::metrics::gauge::Gauge; + use crate::metrics::histogram::{exponential_buckets, Histogram}; + use crate::metrics::info::Info; + use crate::registry::Unit; + use std::borrow::Cow; + use std::collections::HashSet; + use std::sync::atomic::AtomicI64; + use std::sync::atomic::AtomicU64; + use std::time::SystemTime; + + #[test] + fn encode_counter_int() { + let counter: Counter = Counter::default(); + let mut registry = Registry::default(); + registry.register("my_counter", "My counter", counter.clone()); + counter.inc(); + + let metric_families = encode(®istry).unwrap(); + let family = metric_families.first().unwrap(); + assert_eq!("my_counter_total", family.name); + assert_eq!("My counter.", family.help); + assert_eq!( + prometheus_data_model::MetricType::Counter as i32, + family.r#type + ); + + let metric = family.metric.first().unwrap(); + assert_eq!(1.0, metric.counter.as_ref().unwrap().value); + } + + #[test] + fn encode_counter_with_unit() { + let counter: Counter = Counter::default(); + let mut registry = Registry::default(); + registry.register_with_unit("my_counter", "My counter", Unit::Seconds, counter); + + let metric_families = encode(®istry).unwrap(); + let family = metric_families.first().unwrap(); + assert_eq!("my_counter_seconds_total", family.name); + assert_eq!("seconds", family.unit); + } + + #[test] + fn encode_counter_with_exemplar() { + let now = SystemTime::now(); + let now_ts: Timestamp = now.into(); + + let mut registry = Registry::default(); + let counter: CounterWithExemplar, f64> = CounterWithExemplar::default(); + registry.register("my_counter", "My counter", counter.clone()); + + counter.inc_by(1.0, Some(vec![("user_id".to_string(), 42.0)]), None); + + let metric_families = encode(®istry).unwrap(); + let exemplar = metric_families[0].metric[0] + .counter + .as_ref() + .unwrap() + .exemplar + .as_ref() + .unwrap(); + assert_eq!(1.0, exemplar.value); + assert_eq!(None, exemplar.timestamp); + assert_eq!("user_id", exemplar.label[0].name); + assert_eq!("42.0", exemplar.label[0].value); + + counter.inc_by(1.0, Some(vec![("user_id".to_string(), 99.0)]), Some(now)); + + let metric_families = encode(®istry).unwrap(); + let counter = metric_families[0].metric[0].counter.as_ref().unwrap(); + assert_eq!(2.0, counter.value); + let exemplar = counter.exemplar.as_ref().unwrap(); + assert_eq!(1.0, exemplar.value); + assert_eq!(Some(now_ts), exemplar.timestamp.clone()); + assert_eq!("99.0", exemplar.label[0].value); + } + + #[test] + fn encode_gauge() { + let gauge = Gauge::::default(); + let mut registry = Registry::default(); + registry.register("my_gauge", "My gauge", gauge.clone()); + gauge.inc(); + + let metric_families = encode(®istry).unwrap(); + let family = metric_families.first().unwrap(); + assert_eq!("my_gauge", family.name.as_str()); + assert_eq!( + prometheus_data_model::MetricType::Gauge as i32, + family.r#type + ); + assert_eq!(1.0, family.metric[0].gauge.as_ref().unwrap().value); + } + + #[test] + fn encode_gauge_u64_max() { + let gauge = Gauge::::default(); + let mut registry = Registry::default(); + registry.register("my_gauge", "My gauge", gauge.clone()); + gauge.set(u64::MAX); + + let metric_families = encode(®istry).unwrap(); + assert_eq!( + u64::MAX as f64, + metric_families[0].metric[0].gauge.as_ref().unwrap().value + ); + } + + #[test] + fn encode_counter_family() { + let mut registry = Registry::default(); + + let family = Family::, Counter>::default(); + registry.register("my_counter_family", "My counter family", family.clone()); + + family + .get_or_create(&vec![ + ("method".to_string(), "GET".to_string()), + ("status".to_string(), "200".to_string()), + ]) + .inc(); + + family + .get_or_create(&vec![ + ("method".to_string(), "POST".to_string()), + ("status".to_string(), "200".to_string()), + ]) + .inc(); + + let metric_families = encode(®istry).unwrap(); + let family = metric_families.first().unwrap(); + assert_eq!("my_counter_family_total", family.name.as_str()); + assert_eq!(2, family.metric.len()); + + let mut potential_method_value = HashSet::new(); + potential_method_value.insert("GET"); + potential_method_value.insert("POST"); + + let metric = family.metric.first().unwrap(); + assert_eq!(2, metric.label.len()); + assert_eq!("method", metric.label[0].name); + assert!(potential_method_value.remove(metric.label[0].value.as_str())); + assert_eq!("status", metric.label[1].name); + assert_eq!("200", metric.label[1].value); + + let metric2 = &family.metric[1]; + assert_eq!(2, metric2.label.len()); + assert_eq!("method", metric2.label[0].name); + assert!(potential_method_value.remove(metric2.label[0].value.as_str())); + assert_eq!("status", metric2.label[1].name); + assert_eq!("200", metric2.label[1].value); + } + + #[test] + fn encode_counter_family_with_prefix_and_label() { + let mut registry = Registry::default(); + let sub_registry = registry.sub_registry_with_prefix("my_prefix"); + let sub_sub_registry = sub_registry + .sub_registry_with_label((Cow::Borrowed("my_key"), Cow::Borrowed("my_value"))); + let family = Family::, Counter>::default(); + sub_sub_registry.register("my_counter_family", "My counter family", family.clone()); + + family + .get_or_create(&vec![ + ("method".to_string(), "GET".to_string()), + ("status".to_string(), "200".to_string()), + ]) + .inc(); + + let metric_families = encode(®istry).unwrap(); + let family = metric_families.first().unwrap(); + assert_eq!("my_prefix_my_counter_family_total", family.name.as_str()); + + let metric = family.metric.first().unwrap(); + assert_eq!("my_key", metric.label[0].name); + assert_eq!("my_value", metric.label[0].value); + assert_eq!("method", metric.label[1].name); + assert_eq!("GET", metric.label[1].value); + assert_eq!("status", metric.label[2].name); + assert_eq!("200", metric.label[2].value); + } + + #[test] + fn encode_histogram() { + let mut registry = Registry::default(); + let histogram = Histogram::new(exponential_buckets(1.0, 2.0, 10)); + registry.register("my_histogram", "My histogram", histogram.clone()); + histogram.observe(1.0); + + let metric_families = encode(®istry).unwrap(); + let family = metric_families.first().unwrap(); + assert_eq!("my_histogram", family.name.as_str()); + assert_eq!( + prometheus_data_model::MetricType::Histogram as i32, + family.r#type + ); + + let histogram = family.metric[0].histogram.as_ref().unwrap(); + assert_eq!(1, histogram.sample_count); + assert_eq!(1.0, histogram.sample_sum); + assert_eq!(11, histogram.bucket.len()); + assert_eq!(1, histogram.bucket[0].cumulative_count); + assert_eq!(1.0, histogram.bucket[0].upper_bound); + assert_eq!(f64::MAX, histogram.bucket.last().unwrap().upper_bound); + } + + #[test] + fn encode_histogram_with_exemplars() { + let now = SystemTime::now(); + let now_ts: Timestamp = now.into(); + + let histogram = HistogramWithExemplars::new(exponential_buckets(1.0, 2.0, 10)); + let mut registry = Registry::default(); + registry.register("my_histogram", "My histogram", histogram.clone()); + + histogram.observe(1.0, Some(vec![("user_id".to_string(), 42u64)]), None); + + let metric_families = encode(®istry).unwrap(); + let exemplar = metric_families[0].metric[0] + .histogram + .as_ref() + .unwrap() + .bucket[0] + .exemplar + .as_ref() + .unwrap(); + assert_eq!(1.0, exemplar.value); + assert_eq!(None, exemplar.timestamp); + assert_eq!("42", exemplar.label[0].value); + + histogram.observe(2.0, Some(vec![("user_id".to_string(), 99u64)]), Some(now)); + + let metric_families = encode(®istry).unwrap(); + let exemplar = metric_families[0].metric[0] + .histogram + .as_ref() + .unwrap() + .bucket[1] + .exemplar + .as_ref() + .unwrap(); + assert_eq!(2.0, exemplar.value); + assert_eq!(Some(now_ts), exemplar.timestamp.clone()); + assert_eq!("99", exemplar.label[0].value); + } + + #[test] + fn encode_family_and_counter_and_histogram() { + let mut registry = Registry::default(); + + let counter_family = Family::, Counter>::default(); + let histogram_family = + Family::, Histogram>::new_with_constructor(|| { + Histogram::new(exponential_buckets(1.0, 2.0, 10)) + }); + + registry.register("my_family_counter", "My counter", counter_family.clone()); + registry.register( + "my_family_histogram", + "My histogram", + histogram_family.clone(), + ); + + counter_family + .get_or_create(&vec![("path".to_string(), "/".to_string())]) + .inc(); + + histogram_family + .get_or_create(&vec![("path".to_string(), "/".to_string())]) + .observe(1.0); + + let counter: Counter = Counter::default(); + registry.register("my_counter", "My counter", counter.clone()); + counter.inc(); + + let histogram = Histogram::new(exponential_buckets(1.0, 2.0, 10)); + registry.register("my_histogram", "My histogram", histogram.clone()); + histogram.observe(1.0); + + let metric_families = encode(®istry).unwrap(); + assert_eq!("my_family_counter_total", metric_families[0].name); + assert_eq!("my_family_histogram", metric_families[1].name); + assert_eq!("my_counter_total", metric_families[2].name); + assert_eq!("my_histogram", metric_families[3].name); + } + + #[test] + fn encode_info() { + let info = Info::new(vec![("os".to_string(), "GNU/linux".to_string())]); + let mut registry = Registry::default(); + registry.register("my_info_metric", "My info metric", info); + + let metric_families = encode(®istry).unwrap(); + let family = metric_families.first().unwrap(); + assert_eq!("my_info_metric_info", family.name.as_str()); + assert_eq!( + prometheus_data_model::MetricType::Gauge as i32, + family.r#type + ); + + let metric = family.metric.first().unwrap(); + assert_eq!(1.0, metric.gauge.as_ref().unwrap().value); + assert_eq!("os", metric.label[0].name); + assert_eq!("GNU/linux", metric.label[0].value); + } + + #[test] + fn encode_to_vec_length_delimited() { + let counter: Counter = Counter::default(); + let mut registry = Registry::default(); + registry.register("my_counter", "My counter", counter.clone()); + counter.inc(); + + let payload = encode_to_vec(®istry).unwrap(); + let family = + prometheus_data_model::MetricFamily::decode_length_delimited(payload.as_slice()) + .unwrap(); + + assert_eq!("my_counter_total", family.name); + assert_eq!(1.0, family.metric[0].counter.as_ref().unwrap().value); + } +} diff --git a/src/encoding/proto/metrics.proto b/src/encoding/proto/metrics.proto new file mode 100644 index 00000000..a0027ff9 --- /dev/null +++ b/src/encoding/proto/metrics.proto @@ -0,0 +1,153 @@ +// Copyright 2013 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package io.prometheus.client; +option go_package = "io_prometheus_client"; + +import "google/protobuf/timestamp.proto"; + +message LabelPair { + string name = 1; + string value = 2; +} + +enum MetricType { + // COUNTER must use the Metric field "counter". + COUNTER = 0; + // GAUGE must use the Metric field "gauge". + GAUGE = 1; + // SUMMARY must use the Metric field "summary". + SUMMARY = 2; + // UNTYPED must use the Metric field "untyped". + UNTYPED = 3; + // HISTOGRAM must use the Metric field "histogram". + HISTOGRAM = 4; + // GAUGE_HISTOGRAM must use the Metric field "histogram". + GAUGE_HISTOGRAM = 5; +} + +message Gauge { + double value = 1; +} + +message Counter { + double value = 1; + Exemplar exemplar = 2; + + google.protobuf.Timestamp start_timestamp = 3; +} + +message Quantile { + double quantile = 1; + double value = 2; +} + +message Summary { + uint64 sample_count = 1; + double sample_sum = 2; + repeated Quantile quantile = 3; + + google.protobuf.Timestamp start_timestamp = 4; +} + +message Untyped { + double value = 1; +} + +message Histogram { + uint64 sample_count = 1; + double sample_count_float = 4; // Overrides sample_count if > 0. + double sample_sum = 2; + // Buckets for the classic histogram. + repeated Bucket bucket = 3; // Ordered in increasing order of upper_bound, +Inf bucket is optional. + + google.protobuf.Timestamp start_timestamp = 15; + + // Everything below here is for native histograms (formerly known as sparse histograms). + + // schema defines the bucket schema. Currently, valid numbers are -4 <= n <= 8. + // They are all for base-2 bucket schemas, where 1 is a bucket boundary in each case, and + // then each power of two is divided into 2^n logarithmic buckets. + // Or in other words, each bucket boundary is the previous boundary times 2^(2^-n). + // In the future, more bucket schemas may be added using numbers < -4 or > 8. + sint32 schema = 5; + double zero_threshold = 6; // Breadth of the zero bucket. + uint64 zero_count = 7; // Count in zero bucket. + double zero_count_float = 8; // Overrides sb_zero_count if > 0. + + // Negative buckets for the native histogram. + repeated BucketSpan negative_span = 9; + // Use either "negative_delta" or "negative_count", the former for + // regular histograms with integer counts, the latter for float + // histograms. + repeated sint64 negative_delta = 10; // Count delta of each bucket compared to previous one (or to zero for 1st bucket). + repeated double negative_count = 11; // Absolute count of each bucket. + + // Positive buckets for the native histogram. + // Use a no-op span (offset 0, length 0) for a native histogram without any + // observations yet and with a zero_threshold of 0. Otherwise, it would be + // indistinguishable from a classic histogram. + repeated BucketSpan positive_span = 12; + // Use either "positive_delta" or "positive_count", the former for + // regular histograms with integer counts, the latter for float + // histograms. + repeated sint64 positive_delta = 13; // Count delta of each bucket compared to previous one (or to zero for 1st bucket). + repeated double positive_count = 14; // Absolute count of each bucket. + + // Only used for native histograms. These exemplars MUST have a timestamp. + repeated Exemplar exemplars = 16; +} + +message Bucket { + uint64 cumulative_count = 1; // Cumulative in increasing order. + double cumulative_count_float = 4; // Overrides cumulative_count if > 0. + double upper_bound = 2; // Inclusive. + Exemplar exemplar = 3; +} + +// A BucketSpan defines a number of consecutive buckets in a native +// histogram with their offset. Logically, it would be more +// straightforward to include the bucket counts in the Span. However, +// the protobuf representation is more compact in the way the data is +// structured here (with all the buckets in a single array separate +// from the Spans). +message BucketSpan { + sint32 offset = 1; // Gap to previous span, or starting point for 1st span (which can be negative). + uint32 length = 2; // Length of consecutive buckets. +} + +message Exemplar { + repeated LabelPair label = 1; + double value = 2; + google.protobuf.Timestamp timestamp = 3; // OpenMetrics-style. +} + +message Metric { + repeated LabelPair label = 1; + Gauge gauge = 2; + Counter counter = 3; + Summary summary = 4; + Untyped untyped = 5; + Histogram histogram = 7; + int64 timestamp_ms = 6; +} + +message MetricFamily { + string name = 1; + string help = 2; + MetricType type = 3; + repeated Metric metric = 4; + string unit = 5; +} \ No newline at end of file diff --git a/src/encoding/text.rs b/src/encoding/text.rs index fd050f42..bc3f650b 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -602,6 +602,12 @@ impl GaugeValueEncoder<'_> { Ok(()) } + pub fn encode_u64(&mut self, v: u64) -> Result<(), std::fmt::Error> { + self.writer.write_str(" ")?; + self.writer.write_str(itoa::Buffer::new().format(v))?; + Ok(()) + } + pub fn encode_i64(&mut self, v: i64) -> Result<(), std::fmt::Error> { self.writer.write_str(" ")?; self.writer.write_str(itoa::Buffer::new().format(v))?;