From 9c5810d080964c3fe2d4d49214ea3cdd65ccd1fc Mon Sep 17 00:00:00 2001 From: Max Inden Date: Sun, 16 Oct 2022 09:44:46 +0100 Subject: [PATCH] encoding/: Adopt serde style encoding Adopt encoding style similar to serde. `EncodeXXX` for a type that can be encoded (see serde's `Serialize`) and `XXXEncoder` for a supported encoding i.e. text and protobuf (see serde's `Serializer`). - Compatible with Rust's additive features. Enabling the `protobuf` feature does not change type signatures. - `EncodeMetric` is trait object safe, and thus `Registry` can use dynamic dispatch. - Implement a single encoding trait `EncodeMetric` per metric type instead of one implementation per metric AND per encoding format. - Leverage `std::fmt::Write` instead of `std::io::Write`. The OpenMetrics text format has to be unicode compliant. The OpenMetrics Protobuf format requires labels to be valid strings. Signed-off-by: Max Inden --- Cargo.toml | 4 +- benches/encoding/proto.rs | 2 +- benches/encoding/text.rs | 8 +- derive-encode/Cargo.toml | 3 - derive-encode/src/lib.rs | 152 ++--- derive-encode/tests/lib.rs | 62 +- examples/actix-web.rs | 11 +- examples/custom-metric.rs | 28 +- examples/hyper.rs | 24 +- examples/tide.rs | 10 +- src/encoding.rs | 553 +++++++++++++++++- src/encoding/{proto.rs => protobuf.rs} | 674 ++++++++------------- src/encoding/text.rs | 779 +++++++++++-------------- src/lib.rs | 10 +- src/metrics.rs | 13 + src/metrics/counter.rs | 16 + src/metrics/exemplar.rs | 45 +- src/metrics/family.rs | 36 +- src/metrics/gauge.rs | 23 +- src/metrics/histogram.rs | 13 + src/metrics/info.rs | 18 +- src/registry.rs | 59 +- 22 files changed, 1435 insertions(+), 1108 deletions(-) rename src/encoding/{proto.rs => protobuf.rs} (57%) diff --git a/Cargo.toml b/Cargo.toml index 88bc056..0b07d29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,8 @@ homepage = "https://github.com/prometheus/client_rust" documentation = "https://docs.rs/prometheus-client" [features] -protobuf = ["dep:prost", "dep:prost-types", "dep:prost-build", "dep:void", "prometheus-client-derive-encode/protobuf"] +default = [] +protobuf = ["dep:prost", "dep:prost-types", "dep:prost-build"] [workspace] members = ["derive-encode"] @@ -23,7 +24,6 @@ parking_lot = "0.12" prometheus-client-derive-encode = { version = "0.3.0", path = "derive-encode" } prost = { version = "0.9.0", optional = true } prost-types = { version = "0.9.0", optional = true } -void = { version = "1.0", optional = true } [dev-dependencies] async-std = { version = "1", features = ["attributes"] } diff --git a/benches/encoding/proto.rs b/benches/encoding/proto.rs index 4de33a1..71e173c 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::proto::{encode, EncodeMetric}; +use prometheus_client::encoding::protobuf::{encode, EncodeMetric}; use prometheus_client::encoding::Encode; use prometheus_client::metrics::counter::Counter; use prometheus_client::metrics::family::Family; diff --git a/benches/encoding/text.rs b/benches/encoding/text.rs index 1ab6be5..0d650fc 100644 --- a/benches/encoding/text.rs +++ b/benches/encoding/text.rs @@ -35,7 +35,7 @@ pub fn text(c: &mut Criterion) { } impl prometheus_client::encoding::text::Encode for Status { - fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> { + fn encode(&self, writer: &mut dyn Write) -> Result<(), std::fmt::Error> { let status = match self { Status::Two => b"200", Status::Four => b"400", @@ -47,14 +47,14 @@ pub fn text(c: &mut Criterion) { } #[cfg(feature = "protobuf")] - impl prometheus_client::encoding::proto::EncodeLabels for Status { - fn encode(&self, labels: &mut Vec) { + impl prometheus_client::encoding::protobuf::EncodeLabels for Status { + fn encode(&self, labels: &mut Vec) { let value = match self { Status::Two => "200".to_string(), Status::Four => "400".to_string(), Status::Five => "500".to_string(), }; - labels.push(prometheus_client::encoding::proto::Label { + labels.push(prometheus_client::encoding::protobuf::Label { name: "status".to_string(), value, }); diff --git a/derive-encode/Cargo.toml b/derive-encode/Cargo.toml index 9762fd3..55b5f37 100644 --- a/derive-encode/Cargo.toml +++ b/derive-encode/Cargo.toml @@ -9,9 +9,6 @@ repository = "https://github.com/prometheus/client_rust" homepage = "https://github.com/prometheus/client_rust" documentation = "https://docs.rs/prometheus-client-derive-text-encode" -[features] -protobuf = [] - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/derive-encode/src/lib.rs b/derive-encode/src/lib.rs index d6f6eba..b48b3f7 100644 --- a/derive-encode/src/lib.rs +++ b/derive-encode/src/lib.rs @@ -1,21 +1,27 @@ -extern crate proc_macro; +#![deny(dead_code)] +#![deny(missing_docs)] +#![deny(unused)] +#![forbid(unsafe_code)] +#![warn(missing_debug_implementations)] + +//! Derive crate for [`prometheus_client`]. use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; use syn::DeriveInput; -#[proc_macro_derive(Encode)] -pub fn derive_encode(input: TokenStream) -> TokenStream { +/// Derive [`prometheus_client::encoding::EncodeLabelSet`]. +#[proc_macro_derive(EncodeLabelSet)] +pub fn derive_encode_label_set(input: TokenStream) -> TokenStream { let ast: DeriveInput = syn::parse(input).unwrap(); let name = &ast.ident; - let body = match ast.clone().data { + let body: TokenStream2 = match ast.clone().data { syn::Data::Struct(s) => match s.fields { syn::Fields::Named(syn::FieldsNamed { named, .. }) => named .into_iter() - .enumerate() - .map(|(i, f)| { + .map(|f| { let ident = f.ident.unwrap(); let ident_string = KEYWORD_IDENTIFIERS .iter() @@ -23,16 +29,15 @@ pub fn derive_encode(input: TokenStream) -> TokenStream { .map(|pair| pair.0.to_string()) .unwrap_or_else(|| ident.to_string()); - let maybe_comma = if i == 0 { - TokenStream2::default() - } else { - quote! { writer.write_all(b",")?; } - }; quote! { - #maybe_comma - writer.write_all(concat!(#ident_string, "=\"").as_bytes())?; - prometheus_client::encoding::text::Encode::encode(&self.#ident, writer)?; - writer.write_all(b"\"")?; + let mut label_encoder = encoder.encode_label(); + let mut label_key_encoder = label_encoder.encode_label_key()?; + EncodeLabelKey::encode(&#ident_string, &mut label_key_encoder)?; + + let mut label_value_encoder = label_key_encoder.encode_label_value()?; + EncodeLabelValue::encode(&self.#ident, &mut label_value_encoder)?; + + label_value_encoder.finish()?; } }) .collect(), @@ -41,29 +46,19 @@ pub fn derive_encode(input: TokenStream) -> TokenStream { } syn::Fields::Unit => panic!("Can not derive Encode for struct with unit field."), }, - syn::Data::Enum(syn::DataEnum { variants, .. }) => { - let match_arms: TokenStream2 = variants - .into_iter() - .map(|v| { - let ident = v.ident; - quote! { - #name::#ident => writer.write_all(stringify!(#ident).as_bytes())?, - } - }) - .collect(); - - quote! { - match self { - #match_arms - } - } + syn::Data::Enum(syn::DataEnum { .. }) => { + panic!("Can not derive Encode for enum.") } syn::Data::Union(_) => panic!("Can not derive Encode for union."), }; let gen = quote! { - impl prometheus_client::encoding::text::Encode for #name { - fn encode(&self, writer: &mut dyn std::io::Write) -> std::result::Result<(), std::io::Error> { + impl prometheus_client::encoding::EncodeLabelSet for #name { + fn encode(&self, mut encoder: prometheus_client::encoding::LabelSetEncoder) -> std::result::Result<(), std::fmt::Error> { + use prometheus_client::encoding::EncodeLabel; + use prometheus_client::encoding::EncodeLabelKey; + use prometheus_client::encoding::EncodeLabelValue; + #body Ok(()) @@ -71,91 +66,52 @@ pub fn derive_encode(input: TokenStream) -> TokenStream { } }; - #[cfg(feature = "protobuf")] - let gen = { - let protobuf = derive_protobuf_encode(ast); - quote! { - #gen - - #protobuf - } - }; - gen.into() } -#[cfg(feature = "protobuf")] -fn derive_protobuf_encode(ast: DeriveInput) -> TokenStream2 { +/// Derive [`prometheus_client::encoding::EncodeLabelValue`]. +#[proc_macro_derive(EncodeLabelValue)] +pub fn derive_encode_label_value(input: TokenStream) -> TokenStream { + let ast: DeriveInput = syn::parse(input).unwrap(); let name = &ast.ident; - match ast.data { - syn::Data::Struct(s) => match s.fields { - syn::Fields::Named(syn::FieldsNamed { named, .. }) => { - let push_labels: TokenStream2 = named - .into_iter() - .map(|f| { - let ident = f.ident.unwrap(); - let ident_string = KEYWORD_IDENTIFIERS - .iter() - .find(|pair| ident == pair.1) - .map(|pair| pair.0.to_string()) - .unwrap_or_else(|| ident.to_string()); - - quote! { - let mut label = { - let mut labels = vec![]; - prometheus_client::encoding::proto::EncodeLabels::encode(&self.#ident, &mut labels); - debug_assert_eq!(1, labels.len(), "Labels encoded from {} should have only one label.", #ident_string); - labels.pop().expect("should have an element") - }; - // Override the label name with the field name of this struct. - label.name = #ident_string.to_string(); - labels.push(label); - } - }) - .collect(); - - quote! { - impl prometheus_client::encoding::proto::EncodeLabels for #name { - fn encode(&self, labels: &mut Vec) { - #push_labels - } - } - } - } - syn::Fields::Unnamed(_) => { - panic!("Can not derive Encode for struct with unnamed fields.") - } - syn::Fields::Unit => panic!("Can not derive Encode for struct with unit field."), - }, + let body = match ast.clone().data { + syn::Data::Struct(_) => { + panic!("Can not derive EncodeLabel for struct.") + } syn::Data::Enum(syn::DataEnum { variants, .. }) => { let match_arms: TokenStream2 = variants .into_iter() .map(|v| { let ident = v.ident; quote! { - #name::#ident => { - let mut label = prometheus_client::encoding::proto::Label::default(); - label.name = stringify!(#name).to_string(); - label.value = stringify!(#ident).to_string(); - labels.push(label); - } + #name::#ident => encoder.write_str(stringify!(#ident))?, } }) .collect(); quote! { - impl prometheus_client::encoding::proto::EncodeLabels for #name { - fn encode(&self, labels: &mut Vec) { - match self { - #match_arms - }; - } + match self { + #match_arms } } } syn::Data::Union(_) => panic!("Can not derive Encode for union."), - } + }; + + let gen = quote! { + impl prometheus_client::encoding::EncodeLabelValue for #name { + fn encode(&self, encoder: &mut prometheus_client::encoding::LabelValueEncoder) -> std::result::Result<(), std::fmt::Error> { + use std::fmt::Write; + + #body + + Ok(()) + } + } + }; + + gen.into() } // Copied from https://github.com/djc/askama (MIT and APACHE licensed) and diff --git a/derive-encode/tests/lib.rs b/derive-encode/tests/lib.rs index 7056aa2..446af0b 100644 --- a/derive-encode/tests/lib.rs +++ b/derive-encode/tests/lib.rs @@ -1,16 +1,16 @@ use prometheus_client::encoding::text::encode; -use prometheus_client::encoding::Encode; +use prometheus_client::encoding::{EncodeLabelSet, EncodeLabelValue}; use prometheus_client::metrics::counter::Counter; use prometheus_client::metrics::family::Family; use prometheus_client::registry::Registry; -#[derive(Clone, Hash, PartialEq, Eq, Encode)] +#[derive(Clone, Hash, PartialEq, Eq, EncodeLabelSet, Debug)] struct Labels { method: Method, path: String, } -#[derive(Clone, Hash, PartialEq, Eq, Encode)] +#[derive(Clone, Hash, PartialEq, Eq, EncodeLabelValue, Debug)] enum Method { Get, #[allow(dead_code)] @@ -33,20 +33,20 @@ fn basic_flow() { .inc(); // Encode all metrics in the registry in the text format. - let mut buffer = vec![]; + let mut buffer = String::new(); encode(&mut buffer, ®istry).unwrap(); let expected = "# HELP my_counter This is my counter.\n".to_owned() + "# TYPE my_counter counter\n" + "my_counter_total{method=\"Get\",path=\"/metrics\"} 1\n" + "# EOF\n"; - assert_eq!(expected, String::from_utf8(buffer).unwrap()); + assert_eq!(expected, buffer); } -#[cfg(feature = "protobuf")] mod protobuf { use crate::{Labels, Method}; - use prometheus_client::encoding::proto::encode; + use prometheus_client::encoding::protobuf::encode; + use prometheus_client::encoding::protobuf::openmetrics_data_model; use prometheus_client::metrics::counter::Counter; use prometheus_client::metrics::family::Family; use prometheus_client::registry::Registry; @@ -66,10 +66,10 @@ mod protobuf { .inc(); // Encode all metrics in the registry in the OpenMetrics protobuf format. - let mut metric_set = encode(®istry); - let mut family: prometheus_client::encoding::proto::MetricFamily = + let mut metric_set = encode(®istry).unwrap(); + let mut family: openmetrics_data_model::MetricFamily = metric_set.metric_families.pop().unwrap(); - let metric: prometheus_client::encoding::proto::Metric = family.metrics.pop().unwrap(); + let metric: openmetrics_data_model::Metric = family.metrics.pop().unwrap(); let method = &metric.labels[0]; assert_eq!("method", method.name); @@ -83,27 +83,32 @@ mod protobuf { #[test] fn enums() { let mut registry = Registry::default(); - let family = Family::::default(); + let family = Family::::default(); registry.register("my_counter", "This is my counter", family.clone()); // Record a single HTTP GET request. - family.get_or_create(&Method::Get).inc(); + family + .get_or_create(&Labels { + method: Method::Get, + path: "/metrics".to_string(), + }) + .inc(); // Encode all metrics in the registry in the OpenMetrics protobuf format. - let mut metric_set = encode(®istry); - let mut family: prometheus_client::encoding::proto::MetricFamily = + let mut metric_set = encode(®istry).unwrap(); + let mut family: openmetrics_data_model::MetricFamily = metric_set.metric_families.pop().unwrap(); - let metric: prometheus_client::encoding::proto::Metric = family.metrics.pop().unwrap(); + let metric: openmetrics_data_model::Metric = family.metrics.pop().unwrap(); let label = &metric.labels[0]; - assert_eq!("Method", label.name); + assert_eq!("method", label.name); assert_eq!("Get", label.value); } } #[test] fn remap_keyword_identifiers() { - #[derive(Encode, Hash, Clone, Eq, PartialEq)] + #[derive(EncodeLabelSet, Hash, Clone, Eq, PartialEq, Debug)] struct Labels { // `r#type` is problematic as `r#` is not a valid OpenMetrics label name // but one needs to use keyword identifier syntax (aka. raw identifiers) @@ -114,17 +119,20 @@ fn remap_keyword_identifiers() { r#type: u64, } - let labels = Labels { r#type: 42 }; + let mut registry = Registry::default(); + let family = Family::::default(); + registry.register("my_counter", "This is my counter", family.clone()); - let mut buffer = vec![]; + // Record a single HTTP GET request. + family.get_or_create(&Labels { r#type: 42 }).inc(); - { - use prometheus_client::encoding::text::Encode; - labels.encode(&mut buffer).unwrap(); - } + // Encode all metrics in the registry in the text format. + let mut buffer = String::new(); + encode(&mut buffer, ®istry).unwrap(); - assert_eq!( - "type=\"42\"".to_string(), - String::from_utf8(buffer).unwrap() - ); + let expected = "# HELP my_counter This is my counter.\n".to_owned() + + "# TYPE my_counter counter\n" + + "my_counter_total{type=\"42\"} 1\n" + + "# EOF\n"; + assert_eq!(expected, buffer); } diff --git a/examples/actix-web.rs b/examples/actix-web.rs index 5b59cd2..d01a102 100644 --- a/examples/actix-web.rs +++ b/examples/actix-web.rs @@ -2,18 +2,18 @@ use std::sync::Mutex; use actix_web::{web, App, HttpResponse, HttpServer, Responder, Result}; use prometheus_client::encoding::text::encode; -use prometheus_client::encoding::Encode; +use prometheus_client::encoding::{EncodeLabelSet, EncodeLabelValue}; use prometheus_client::metrics::counter::Counter; use prometheus_client::metrics::family::Family; use prometheus_client::registry::Registry; -#[derive(Clone, Debug, Hash, PartialEq, Eq, Encode)] +#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelValue)] pub enum Method { Get, Post, } -#[derive(Clone, Debug, Hash, PartialEq, Eq, Encode)] +#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] pub struct MethodLabels { pub method: Method, } @@ -34,9 +34,8 @@ pub struct AppState { pub async fn metrics_handler(state: web::Data>) -> Result { let state = state.lock().unwrap(); - let mut buf = Vec::new(); - encode(&mut buf, &state.registry)?; - let body = std::str::from_utf8(buf.as_slice()).unwrap().to_string(); + let mut body = String::new(); + encode(&mut body, &state.registry).unwrap(); Ok(HttpResponse::Ok() .content_type("application/openmetrics-text; version=1.0.0; charset=utf-8") .body(body)) diff --git a/examples/custom-metric.rs b/examples/custom-metric.rs index fe8f3ce..4e2084d 100644 --- a/examples/custom-metric.rs +++ b/examples/custom-metric.rs @@ -1,4 +1,4 @@ -use prometheus_client::encoding::text::{encode, EncodeMetric, Encoder}; +use prometheus_client::encoding::{text::encode, EncodeMetric, MetricEncoder}; use prometheus_client::metrics::MetricType; use prometheus_client::registry::Registry; @@ -11,28 +11,20 @@ use prometheus_client::registry::Registry; struct MyCustomMetric {} impl EncodeMetric for MyCustomMetric { - fn encode(&self, mut encoder: Encoder) -> Result<(), std::io::Error> { + fn encode(&self, mut encoder: MetricEncoder) -> Result<(), std::fmt::Error> { // This method is called on each Prometheus server scrape. Allowing you // to execute whatever logic is needed to generate and encode your // custom metric. // - // While the `Encoder`'s builder pattern should guide you well and makes - // many mistakes impossible at the type level, do keep in mind that - // "with great power comes great responsibility". E.g. every CPU cycle - // spend in this method delays the response send to the Prometheus - // server. - - encoder - .no_suffix()? - .no_bucket()? - .encode_value(rand::random::())? - .no_exemplar()?; - - Ok(()) + // Do keep in mind that "with great power comes great responsibility". + // E.g. every CPU cycle spend in this method delays the response send to + // the Prometheus server. + + encoder.encode_counter::<(), _, u64>(rand::random::(), None) } fn metric_type(&self) -> prometheus_client::metrics::MetricType { - MetricType::Unknown + MetricType::Counter } } @@ -46,8 +38,8 @@ fn main() { metric, ); - let mut encoded = Vec::new(); + let mut encoded = String::new(); encode(&mut encoded, ®istry).unwrap(); - println!("Scrape output:\n{:?}", String::from_utf8(encoded).unwrap()); + println!("Scrape output:\n{:?}", encoded); } diff --git a/examples/hyper.rs b/examples/hyper.rs index 93884ca..f5a4009 100644 --- a/examples/hyper.rs +++ b/examples/hyper.rs @@ -59,17 +59,19 @@ pub fn make_handler( move |_req: Request| { let reg = registry.clone(); Box::pin(async move { - let mut buf = Vec::new(); - encode(&mut buf, ®.clone()).map(|_| { - let body = Body::from(buf); - Response::builder() - .header( - hyper::header::CONTENT_TYPE, - "application/openmetrics-text; version=1.0.0; charset=utf-8", - ) - .body(body) - .unwrap() - }) + let mut buf = String::new(); + encode(&mut buf, ®.clone()) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) + .map(|_| { + let body = Body::from(buf); + Response::builder() + .header( + hyper::header::CONTENT_TYPE, + "application/openmetrics-text; version=1.0.0; charset=utf-8", + ) + .body(body) + .unwrap() + }) }) } } diff --git a/examples/tide.rs b/examples/tide.rs index 677105d..68cacac 100644 --- a/examples/tide.rs +++ b/examples/tide.rs @@ -1,5 +1,5 @@ -use prometheus_client::encoding::text::encode; -use prometheus_client::encoding::Encode; +use prometheus_client::encoding::EncodeLabelValue; +use prometheus_client::encoding::{text::encode, EncodeLabelSet}; use prometheus_client::metrics::counter::Counter; use prometheus_client::metrics::family::Family; use prometheus_client::registry::Registry; @@ -31,7 +31,7 @@ async fn main() -> std::result::Result<(), std::io::Error> { app.at("/").get(|_| async { Ok("Hello, world!") }); app.at("/metrics") .get(|req: tide::Request| async move { - let mut encoded = Vec::new(); + let mut encoded = String::new(); encode(&mut encoded, &req.state().registry).unwrap(); let response = tide::Response::builder(200) .body(encoded) @@ -44,13 +44,13 @@ async fn main() -> std::result::Result<(), std::io::Error> { Ok(()) } -#[derive(Clone, Debug, Hash, PartialEq, Eq, Encode)] +#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] struct Labels { method: Method, path: String, } -#[derive(Clone, Debug, Hash, PartialEq, Eq, Encode)] +#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelValue)] enum Method { Get, Put, diff --git a/src/encoding.rs b/src/encoding.rs index 8d9a0c6..d3a725f 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -2,6 +2,557 @@ pub use prometheus_client_derive_encode::*; +use crate::metrics::exemplar::Exemplar; +use crate::metrics::MetricType; +use std::borrow::Cow; +use std::collections::HashMap; +use std::fmt::Write; +use std::ops::Deref; + #[cfg(feature = "protobuf")] -pub mod proto; +pub mod protobuf; pub mod text; + +/// Trait implemented by each metric type, e.g. [`Counter`], to implement its encoding in the OpenMetric text format. +pub trait EncodeMetric { + /// Encode the given instance in the OpenMetrics text encoding. + fn encode(&self, encoder: MetricEncoder<'_, '_>) -> Result<(), std::fmt::Error>; + + /// The OpenMetrics metric type of the instance. + // One can not use [`TypedMetric`] directly, as associated constants are not + // object safe and thus can not be used with dynamic dispatching. + fn metric_type(&self) -> MetricType; +} + +impl EncodeMetric for Box { + fn encode(&self, encoder: MetricEncoder) -> Result<(), std::fmt::Error> { + self.deref().encode(encoder) + } + + fn metric_type(&self) -> MetricType { + self.deref().metric_type() + } +} + +/// Encoder for a metric. +/// +// `MetricEncoder` does not take a trait parameter for `writer` and `labels` +// because `EncodeMetric` which uses `MetricEncoder` needs to be usable as a +// trait object in order to be able to register different metric types with a +// `Registry`. Trait objects can not use type parameters. +// +// TODO: Alternative solutions to the above are very much appreciated. +#[derive(Debug)] +pub struct MetricEncoder<'a, 'b>(MetricEncoderInner<'a, 'b>); + +#[derive(Debug)] +enum MetricEncoderInner<'a, 'b> { + Text(text::MetricEncoder<'a, 'b>), + + #[cfg(feature = "protobuf")] + Protobuf(protobuf::MetricEncoder<'a>), +} + +impl<'a, 'b> From> for MetricEncoder<'a, 'b> { + fn from(e: text::MetricEncoder<'a, 'b>) -> Self { + Self(MetricEncoderInner::Text(e)) + } +} + +#[cfg(feature = "protobuf")] +impl<'a, 'b> From> for MetricEncoder<'a, 'b> { + fn from(e: protobuf::MetricEncoder<'a>) -> Self { + Self(MetricEncoderInner::Protobuf(e)) + } +} + +macro_rules! for_both_mut { + ($self:expr, $inner:ident, $pattern:pat, $fn:expr) => { + match &mut $self.0 { + $inner::Text($pattern) => $fn, + #[cfg(feature = "protobuf")] + $inner::Protobuf($pattern) => $fn, + } + }; +} + +macro_rules! for_both { + ($self:expr, $inner:ident, $pattern:pat, $fn:expr) => { + match $self.0 { + $inner::Text($pattern) => $fn, + #[cfg(feature = "protobuf")] + $inner::Protobuf($pattern) => $fn, + } + }; +} + +impl<'a, 'b> MetricEncoder<'a, 'b> { + /// Encode a counter. + pub fn encode_counter< + S: EncodeLabelSet, + CounterValue: EncodeCounterValue, + ExemplarValue: EncodeExemplarValue, + >( + &mut self, + v: CounterValue, + exemplar: Option<&Exemplar>, + ) -> Result<(), std::fmt::Error> { + for_both_mut!(self, MetricEncoderInner, e, e.encode_counter(v, exemplar)) + } + + /// Encode a gauge. + pub fn encode_gauge(&mut self, v: impl EncodeGaugeValue) -> Result<(), std::fmt::Error> { + for_both_mut!(self, MetricEncoderInner, e, e.encode_gauge(v)) + } + + /// Encode an info. + pub fn encode_info(&mut self, label_set: &impl EncodeLabelSet) -> Result<(), std::fmt::Error> { + for_both_mut!(self, MetricEncoderInner, e, e.encode_info(label_set)) + } + + /// Encode a histogram. + pub fn encode_histogram( + &mut self, + sum: f64, + count: u64, + buckets: &[(f64, u64)], + exemplars: Option<&HashMap>>, + ) -> Result<(), std::fmt::Error> { + for_both_mut!( + self, + MetricEncoderInner, + e, + e.encode_histogram(sum, count, buckets, exemplars) + ) + } + + /// Encode a metric family. + pub fn encode_family<'c, 'd, S: EncodeLabelSet>( + &'c mut self, + label_set: &'d S, + ) -> Result, std::fmt::Error> { + for_both_mut!( + self, + MetricEncoderInner, + e, + e.encode_family(label_set).map(Into::into) + ) + } +} + +/// An encodable label set. +pub trait EncodeLabelSet { + /// Encode oneself into the given encoder. + fn encode(&self, encoder: LabelSetEncoder) -> Result<(), std::fmt::Error>; +} + +impl<'a> From> for LabelSetEncoder<'a> { + fn from(e: text::LabelSetEncoder<'a>) -> Self { + Self(LabelSetEncoderInner::Text(e)) + } +} + +/// Encoder for a label set. +#[derive(Debug)] +pub struct LabelSetEncoder<'a>(LabelSetEncoderInner<'a>); + +#[derive(Debug)] +enum LabelSetEncoderInner<'a> { + Text(text::LabelSetEncoder<'a>), + #[cfg(feature = "protobuf")] + Protobuf(protobuf::LabelSetEncoder<'a>), +} + +#[cfg(feature = "protobuf")] +impl<'a> From> for LabelSetEncoder<'a> { + fn from(e: protobuf::LabelSetEncoder<'a>) -> Self { + Self(LabelSetEncoderInner::Protobuf(e)) + } +} + +impl<'a> LabelSetEncoder<'a> { + /// Encode the given label. + pub fn encode_label(&mut self) -> LabelEncoder { + for_both_mut!(self, LabelSetEncoderInner, e, e.encode_label().into()) + } +} + +/// An encodable label. +pub trait EncodeLabel { + /// Encode oneself into the given encoder. + fn encode(&self, encoder: LabelEncoder) -> Result<(), std::fmt::Error>; +} + +/// Encoder for a label. +#[derive(Debug)] +pub struct LabelEncoder<'a>(LabelEncoderInner<'a>); + +#[derive(Debug)] +enum LabelEncoderInner<'a> { + Text(text::LabelEncoder<'a>), + #[cfg(feature = "protobuf")] + Protobuf(protobuf::LabelEncoder<'a>), +} + +impl<'a> From> for LabelEncoder<'a> { + fn from(e: text::LabelEncoder<'a>) -> Self { + Self(LabelEncoderInner::Text(e)) + } +} + +#[cfg(feature = "protobuf")] +impl<'a> From> for LabelEncoder<'a> { + fn from(e: protobuf::LabelEncoder<'a>) -> Self { + Self(LabelEncoderInner::Protobuf(e)) + } +} + +impl<'a> LabelEncoder<'a> { + /// Encode a label. + pub fn encode_label_key(&mut self) -> Result { + for_both_mut!( + self, + LabelEncoderInner, + e, + e.encode_label_key().map(Into::into) + ) + } +} + +/// An encodable label key. +pub trait EncodeLabelKey { + /// Encode oneself into the given encoder. + fn encode(&self, encoder: &mut LabelKeyEncoder) -> Result<(), std::fmt::Error>; +} + +/// Encoder for a label key. +#[derive(Debug)] +pub struct LabelKeyEncoder<'a>(LabelKeyEncoderInner<'a>); + +#[derive(Debug)] +enum LabelKeyEncoderInner<'a> { + Text(text::LabelKeyEncoder<'a>), + #[cfg(feature = "protobuf")] + Protobuf(protobuf::LabelKeyEncoder<'a>), +} + +impl<'a> From> for LabelKeyEncoder<'a> { + fn from(e: text::LabelKeyEncoder<'a>) -> Self { + Self(LabelKeyEncoderInner::Text(e)) + } +} + +#[cfg(feature = "protobuf")] +impl<'a> From> for LabelKeyEncoder<'a> { + fn from(e: protobuf::LabelKeyEncoder<'a>) -> Self { + Self(LabelKeyEncoderInner::Protobuf(e)) + } +} + +impl<'a> std::fmt::Write for LabelKeyEncoder<'a> { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + for_both_mut!(self, LabelKeyEncoderInner, e, e.write_str(s)) + } +} + +impl<'a> LabelKeyEncoder<'a> { + /// Encode a label value. + pub fn encode_label_value(self) -> Result, std::fmt::Error> { + for_both!( + self, + LabelKeyEncoderInner, + e, + e.encode_label_value().map(LabelValueEncoder::from) + ) + } +} + +/// An encodable label value. +pub trait EncodeLabelValue { + /// Encode oneself into the given encoder. + fn encode(&self, encoder: &mut LabelValueEncoder) -> Result<(), std::fmt::Error>; +} + +/// Encoder for a label value. +#[derive(Debug)] +pub struct LabelValueEncoder<'a>(LabelValueEncoderInner<'a>); + +impl<'a> From> for LabelValueEncoder<'a> { + fn from(e: text::LabelValueEncoder<'a>) -> Self { + LabelValueEncoder(LabelValueEncoderInner::Text(e)) + } +} + +#[cfg(feature = "protobuf")] +impl<'a> From> for LabelValueEncoder<'a> { + fn from(e: protobuf::LabelValueEncoder<'a>) -> Self { + LabelValueEncoder(LabelValueEncoderInner::Protobuf(e)) + } +} + +#[derive(Debug)] +enum LabelValueEncoderInner<'a> { + Text(text::LabelValueEncoder<'a>), + #[cfg(feature = "protobuf")] + Protobuf(protobuf::LabelValueEncoder<'a>), +} + +impl<'a> LabelValueEncoder<'a> { + /// Finish encoding the label value. + pub fn finish(self) -> Result<(), std::fmt::Error> { + for_both!(self, LabelValueEncoderInner, e, e.finish()) + } +} + +impl<'a> std::fmt::Write for LabelValueEncoder<'a> { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + for_both_mut!(self, LabelValueEncoderInner, e, e.write_str(s)) + } +} + +impl EncodeLabelSet for [T; N] { + fn encode(&self, encoder: LabelSetEncoder) -> Result<(), std::fmt::Error> { + self.as_ref().encode(encoder) + } +} + +impl EncodeLabelSet for &[T] { + fn encode(&self, mut encoder: LabelSetEncoder) -> Result<(), std::fmt::Error> { + if self.is_empty() { + return Ok(()); + } + + for label in self.iter() { + label.encode(encoder.encode_label())? + } + + Ok(()) + } +} + +impl EncodeLabelSet for Vec { + fn encode(&self, encoder: LabelSetEncoder) -> Result<(), std::fmt::Error> { + self.as_slice().encode(encoder) + } +} + +impl EncodeLabelSet for () { + fn encode(&self, _encoder: LabelSetEncoder) -> Result<(), std::fmt::Error> { + Ok(()) + } +} + +impl EncodeLabel for (K, V) { + fn encode(&self, mut encoder: LabelEncoder) -> Result<(), std::fmt::Error> { + let (key, value) = self; + + let mut label_key_encoder = encoder.encode_label_key()?; + key.encode(&mut label_key_encoder)?; + + let mut label_value_encoder = label_key_encoder.encode_label_value()?; + value.encode(&mut label_value_encoder)?; + label_value_encoder.finish()?; + + Ok(()) + } +} + +impl EncodeLabelKey for &str { + fn encode(&self, encoder: &mut LabelKeyEncoder) -> Result<(), std::fmt::Error> { + encoder.write_str(self)?; + Ok(()) + } +} +impl EncodeLabelValue for &str { + fn encode(&self, encoder: &mut LabelValueEncoder) -> Result<(), std::fmt::Error> { + encoder.write_str(self)?; + Ok(()) + } +} + +impl EncodeLabelKey for String { + fn encode(&self, encoder: &mut LabelKeyEncoder) -> Result<(), std::fmt::Error> { + EncodeLabelKey::encode(&self.as_str(), encoder) + } +} +impl EncodeLabelValue for String { + fn encode(&self, encoder: &mut LabelValueEncoder) -> Result<(), std::fmt::Error> { + EncodeLabelValue::encode(&self.as_str(), encoder) + } +} + +impl<'a> EncodeLabelKey for Cow<'a, str> { + fn encode(&self, encoder: &mut LabelKeyEncoder) -> Result<(), std::fmt::Error> { + EncodeLabelKey::encode(&self.as_ref(), encoder) + } +} + +impl<'a> EncodeLabelValue for Cow<'a, str> { + fn encode(&self, encoder: &mut LabelValueEncoder) -> Result<(), std::fmt::Error> { + EncodeLabelValue::encode(&self.as_ref(), encoder) + } +} + +impl EncodeLabelValue for f64 { + fn encode(&self, encoder: &mut LabelValueEncoder) -> Result<(), std::fmt::Error> { + encoder.write_str(dtoa::Buffer::new().format(*self)) + } +} + +impl EncodeLabelValue for u64 { + fn encode(&self, encoder: &mut LabelValueEncoder) -> Result<(), std::fmt::Error> { + encoder.write_str(itoa::Buffer::new().format(*self)) + } +} + +/// An encodable gauge value. +pub trait EncodeGaugeValue { + /// Encode the given instance in the OpenMetrics text encoding. + fn encode(&self, encoder: &mut GaugeValueEncoder) -> Result<(), std::fmt::Error>; +} + +impl EncodeGaugeValue for i64 { + fn encode(&self, encoder: &mut GaugeValueEncoder) -> Result<(), std::fmt::Error> { + encoder.encode_i64(*self) + } +} + +impl EncodeGaugeValue for f64 { + fn encode(&self, encoder: &mut GaugeValueEncoder) -> Result<(), std::fmt::Error> { + encoder.encode_f64(*self) + } +} + +/// Encoder for a gauge value. +#[derive(Debug)] +pub struct GaugeValueEncoder<'a>(GaugeValueEncoderInner<'a>); + +#[derive(Debug)] +enum GaugeValueEncoderInner<'a> { + Text(text::GaugeValueEncoder<'a>), + #[cfg(feature = "protobuf")] + Protobuf(protobuf::GaugeValueEncoder<'a>), +} + +impl<'a> GaugeValueEncoder<'a> { + fn encode_f64(&mut self, v: f64) -> Result<(), std::fmt::Error> { + for_both_mut!(self, GaugeValueEncoderInner, e, e.encode_f64(v)) + } + + fn encode_i64(&mut self, v: i64) -> Result<(), std::fmt::Error> { + for_both_mut!(self, GaugeValueEncoderInner, e, e.encode_i64(v)) + } +} + +impl<'a> From> for GaugeValueEncoder<'a> { + fn from(e: text::GaugeValueEncoder<'a>) -> Self { + GaugeValueEncoder(GaugeValueEncoderInner::Text(e)) + } +} + +#[cfg(feature = "protobuf")] +impl<'a> From> for GaugeValueEncoder<'a> { + fn from(e: protobuf::GaugeValueEncoder<'a>) -> Self { + GaugeValueEncoder(GaugeValueEncoderInner::Protobuf(e)) + } +} + +/// An encodable counter value. +pub trait EncodeCounterValue { + /// Encode the given instance in the OpenMetrics text encoding. + fn encode(&self, encoder: &mut CounterValueEncoder) -> Result<(), std::fmt::Error>; +} + +impl EncodeCounterValue for u64 { + fn encode(&self, encoder: &mut CounterValueEncoder) -> Result<(), std::fmt::Error> { + encoder.encode_u64(*self) + } +} + +impl EncodeCounterValue for f64 { + fn encode(&self, encoder: &mut CounterValueEncoder) -> Result<(), std::fmt::Error> { + encoder.encode_f64(*self) + } +} + +/// Encoder for a counter value. +#[derive(Debug)] +pub struct CounterValueEncoder<'a>(CounterValueEncoderInner<'a>); + +#[derive(Debug)] +enum CounterValueEncoderInner<'a> { + Text(text::CounterValueEncoder<'a>), + #[cfg(feature = "protobuf")] + Protobuf(protobuf::CounterValueEncoder<'a>), +} + +impl<'a> CounterValueEncoder<'a> { + fn encode_f64(&mut self, v: f64) -> Result<(), std::fmt::Error> { + for_both_mut!(self, CounterValueEncoderInner, e, e.encode_f64(v)) + } + + fn encode_u64(&mut self, v: u64) -> Result<(), std::fmt::Error> { + for_both_mut!(self, CounterValueEncoderInner, e, e.encode_u64(v)) + } +} + +/// An encodable exemplar value. +pub trait EncodeExemplarValue { + /// Encode the given instance in the OpenMetrics text encoding. + fn encode(&self, encoder: ExemplarValueEncoder) -> Result<(), std::fmt::Error>; +} + +impl EncodeExemplarValue for f64 { + fn encode(&self, mut encoder: ExemplarValueEncoder) -> Result<(), std::fmt::Error> { + encoder.encode(*self) + } +} + +impl EncodeExemplarValue for u64 { + fn encode(&self, mut encoder: ExemplarValueEncoder) -> Result<(), std::fmt::Error> { + encoder.encode(*self as f64) + } +} + +impl<'a> From> for CounterValueEncoder<'a> { + fn from(e: text::CounterValueEncoder<'a>) -> Self { + CounterValueEncoder(CounterValueEncoderInner::Text(e)) + } +} + +#[cfg(feature = "protobuf")] +impl<'a> From> for CounterValueEncoder<'a> { + fn from(e: protobuf::CounterValueEncoder<'a>) -> Self { + CounterValueEncoder(CounterValueEncoderInner::Protobuf(e)) + } +} + +/// Encoder for an exemplar value. +#[derive(Debug)] +pub struct ExemplarValueEncoder<'a>(ExemplarValueEncoderInner<'a>); + +#[derive(Debug)] +enum ExemplarValueEncoderInner<'a> { + Text(text::ExemplarValueEncoder<'a>), + #[cfg(feature = "protobuf")] + Protobuf(protobuf::ExemplarValueEncoder<'a>), +} + +impl<'a> ExemplarValueEncoder<'a> { + fn encode(&mut self, v: f64) -> Result<(), std::fmt::Error> { + for_both_mut!(self, ExemplarValueEncoderInner, e, e.encode(v)) + } +} + +impl<'a> From> for ExemplarValueEncoder<'a> { + fn from(e: text::ExemplarValueEncoder<'a>) -> Self { + ExemplarValueEncoder(ExemplarValueEncoderInner::Text(e)) + } +} + +#[cfg(feature = "protobuf")] +impl<'a> From> for ExemplarValueEncoder<'a> { + fn from(e: protobuf::ExemplarValueEncoder<'a>) -> Self { + ExemplarValueEncoder(ExemplarValueEncoderInner::Protobuf(e)) + } +} diff --git a/src/encoding/proto.rs b/src/encoding/protobuf.rs similarity index 57% rename from src/encoding/proto.rs rename to src/encoding/protobuf.rs index f183551..ef6b6ac 100644 --- a/src/encoding/proto.rs +++ b/src/encoding/protobuf.rs @@ -1,7 +1,7 @@ //! Open Metrics protobuf implementation. //! //! ``` -//! # use prometheus_client::encoding::proto::encode; +//! # use prometheus_client::encoding::protobuf::encode; //! # use prometheus_client::metrics::counter::Counter; //! # use prometheus_client::registry::Registry; //! # @@ -15,7 +15,7 @@ //! # ); //! # counter.inc(); //! // Returns `MetricSet`, the top-level container type. Please refer to [openmetrics_data_model.proto](https://github.com/OpenObservability/OpenMetrics/blob/main/proto/openmetrics_data_model.proto) for details. -//! let metric_set = encode(®istry); +//! let metric_set = encode(®istry).unwrap(); //! //! let family = metric_set.metric_families.first().unwrap(); //! assert_eq!("my_counter", family.name); @@ -30,27 +30,17 @@ pub mod openmetrics_data_model { include!(concat!(env!("OUT_DIR"), "/openmetrics.rs")); } -use crate::metrics::counter::Counter; -use crate::metrics::exemplar::{CounterWithExemplar, Exemplar, HistogramWithExemplars}; -use crate::metrics::family::{Family, MetricConstructor}; -use crate::metrics::gauge::Gauge; -use crate::metrics::histogram::Histogram; -use crate::metrics::info::Info; -use crate::metrics::{counter, gauge, MetricType, TypedMetric}; -use crate::registry::Registry; use std::collections::HashMap; -use std::ops::Deref; -use void::Void; -pub use openmetrics_data_model::*; -pub use prometheus_client_derive_encode::*; +use crate::metrics::exemplar::Exemplar; +use crate::metrics::MetricType; +use crate::registry::Registry; + +use super::{EncodeExemplarValue, EncodeLabelSet}; /// Encode the metrics registered with the provided [`Registry`] into MetricSet /// using the OpenMetrics protobuf format. -pub fn encode(registry: &Registry) -> openmetrics_data_model::MetricSet -where - M: EncodeMetric, -{ +pub fn encode(registry: &Registry) -> Result { let mut metric_set = openmetrics_data_model::MetricSet::default(); for (desc, metric) in registry.iter() { @@ -58,7 +48,7 @@ where name: desc.name().to_string(), r#type: { let metric_type: openmetrics_data_model::MetricType = - EncodeMetric::metric_type(metric.as_ref()).into(); + super::EncodeMetric::metric_type(metric.as_ref()).into(); metric_type as i32 }, unit: if let Some(unit) = desc.unit() { @@ -71,13 +61,25 @@ where }; let mut labels = vec![]; - desc.labels().encode(&mut labels); - EncodeMetric::encode(metric.as_ref(), labels, &mut family.metrics); + desc.labels().encode( + LabelSetEncoder { + labels: &mut labels, + } + .into(), + )?; + + let encoder = MetricEncoder { + family: &mut family.metrics, + metric_type: super::EncodeMetric::metric_type(metric.as_ref()), + labels: &mut labels, + }; + + super::EncodeMetric::encode(metric.as_ref(), encoder.into())?; metric_set.metric_families.push(family); } - metric_set + Ok(metric_set) } impl From for openmetrics_data_model::MetricType { @@ -92,439 +94,287 @@ impl From for openmetrics_data_model::MetricType { } } -/// Trait implemented by each metric type, e.g. [`Counter`], to implement its encoding. -pub trait EncodeMetric { - /// Encode to OpenMetrics protobuf encoding. - fn encode( - &self, - labels: Vec, - family: &mut Vec, - ); - - /// The OpenMetrics metric type of the instance. - fn metric_type(&self) -> MetricType; +#[derive(Debug)] +pub(crate) struct MetricEncoder<'a> { + metric_type: MetricType, + family: &'a mut Vec, + labels: &'a mut Vec, } -impl EncodeMetric for Box { - fn encode( - &self, - labels: Vec, - family: &mut Vec, - ) { - self.deref().encode(labels, family) - } +impl<'a> MetricEncoder<'a> { + pub fn encode_counter( + &mut self, + v: impl super::EncodeCounterValue, + exemplar: Option<&Exemplar>, + ) -> Result<(), std::fmt::Error> { + let mut value = openmetrics_data_model::counter_value::Total::IntValue(0); + let mut e = CounterValueEncoder { value: &mut value }.into(); + v.encode(&mut e)?; - fn metric_type(&self) -> MetricType { - self.deref().metric_type() + self.family.push(openmetrics_data_model::Metric { + labels: self.labels.clone(), + metric_points: vec![openmetrics_data_model::MetricPoint { + value: Some(openmetrics_data_model::metric_point::Value::CounterValue( + openmetrics_data_model::CounterValue { + total: Some(value), + exemplar: exemplar.map(|e| e.try_into()).transpose()?, + ..Default::default() + }, + )), + ..Default::default() + }], + }); + + Ok(()) } -} -/// Trait to implement its label encoding in the OpenMetrics protobuf format. -pub trait EncodeLabels { - /// Encode the given instance into Labels in the OpenMetrics protobuf - /// encoding. - fn encode(&self, labels: &mut Vec); -} + pub fn encode_gauge(&mut self, v: impl super::EncodeGaugeValue) -> Result<(), std::fmt::Error> { + let mut value = openmetrics_data_model::gauge_value::Value::IntValue(0); + let mut e = GaugeValueEncoder { value: &mut value }.into(); + v.encode(&mut e)?; -impl From<&(K, V)> for openmetrics_data_model::Label { - fn from(kv: &(K, V)) -> Self { - openmetrics_data_model::Label { - name: kv.0.to_string(), - value: kv.1.to_string(), - } - } -} + self.family.push(openmetrics_data_model::Metric { + labels: self.labels.clone(), + metric_points: vec![openmetrics_data_model::MetricPoint { + value: Some(openmetrics_data_model::metric_point::Value::GaugeValue( + openmetrics_data_model::GaugeValue { value: Some(value) }, + )), + ..Default::default() + }], + }); -impl EncodeLabels for f64 { - fn encode(&self, labels: &mut Vec) { - labels.push(openmetrics_data_model::Label { - name: self.to_string(), - value: self.to_string(), - }) + Ok(()) } -} -impl EncodeLabels for u64 { - fn encode(&self, labels: &mut Vec) { - labels.push(openmetrics_data_model::Label { - name: self.to_string(), - value: self.to_string(), - }) - } -} + pub fn encode_info( + &mut self, + label_set: &impl super::EncodeLabelSet, + ) -> Result<(), std::fmt::Error> { + let mut info_labels = vec![]; + label_set.encode( + LabelSetEncoder { + labels: &mut info_labels, + } + .into(), + )?; + + self.family.push(openmetrics_data_model::Metric { + labels: self.labels.clone(), + metric_points: vec![openmetrics_data_model::MetricPoint { + value: Some(openmetrics_data_model::metric_point::Value::InfoValue( + openmetrics_data_model::InfoValue { info: info_labels }, + )), + ..Default::default() + }], + }); -impl EncodeLabels for u32 { - fn encode(&self, labels: &mut Vec) { - labels.push(openmetrics_data_model::Label { - name: self.to_string(), - value: self.to_string(), - }) + Ok(()) } -} -impl EncodeLabels for String { - fn encode(&self, labels: &mut Vec) { - labels.push(openmetrics_data_model::Label { - name: self.clone(), - value: self.clone(), - }) - } -} + pub fn encode_family<'b, S: EncodeLabelSet>( + &'b mut self, + label_set: &S, + ) -> Result, std::fmt::Error> { + label_set.encode( + LabelSetEncoder { + labels: &mut self.labels, + } + .into(), + )?; -impl EncodeLabels for Vec -where - for<'a> &'a T: Into, -{ - fn encode(&self, labels: &mut Vec) { - self.as_slice().encode(labels); + Ok(MetricEncoder { + metric_type: self.metric_type, + family: self.family, + labels: self.labels, + }) } -} -impl EncodeLabels for [T] -where - for<'a> &'a T: Into, -{ - fn encode(&self, labels: &mut Vec) { - labels.extend(self.iter().map(|t| t.into())); - } -} + pub fn encode_histogram( + &mut self, + sum: f64, + count: u64, + buckets: &[(f64, u64)], + exemplars: Option<&HashMap>>, + ) -> Result<(), std::fmt::Error> { + let buckets = buckets + .into_iter() + .enumerate() + .map(|(i, (upper_bound, count))| { + Ok(openmetrics_data_model::histogram_value::Bucket { + upper_bound: *upper_bound, + count: *count, + exemplar: exemplars + .and_then(|exemplars| exemplars.get(&i).map(|exemplar| exemplar.try_into())) + .transpose()?, + ..Default::default() + }) + }) + .collect::, _>>()?; + + self.family.push(openmetrics_data_model::Metric { + labels: self.labels.clone(), + metric_points: vec![openmetrics_data_model::MetricPoint { + value: Some(openmetrics_data_model::metric_point::Value::HistogramValue( + openmetrics_data_model::HistogramValue { + count, + created: None, + buckets, + sum: Some(openmetrics_data_model::histogram_value::Sum::DoubleValue( + sum, + )), + }, + )), + ..Default::default() + }], + }); -impl EncodeLabels for Void { - fn encode(&self, _labels: &mut Vec) { - void::unreachable(*self); + Ok(()) } } -fn encode_exemplar(exemplar: &Exemplar) -> openmetrics_data_model::Exemplar -where - N: Clone, - S: EncodeLabels, - f64: From, // required because Exemplar.value is defined as `double` in protobuf +impl TryFrom<&Exemplar> + for openmetrics_data_model::Exemplar { - let mut exemplar_proto = openmetrics_data_model::Exemplar { - value: exemplar.value.clone().into(), - ..Default::default() - }; - exemplar.label_set.encode(&mut exemplar_proto.label); + type Error = std::fmt::Error; - exemplar_proto -} - -///////////////////////////////////////////////////////////////////////////////// -// Counter - -/// Trait to implement its counter value encoding in the OpenMetrics protobuf -/// format. -pub trait EncodeCounterValue { - /// Encode the given instance into counter value in the OpenMetrics protobuf - /// encoding. - fn encode(&self) -> openmetrics_data_model::counter_value::Total; -} + fn try_from(exemplar: &Exemplar) -> Result { + let mut value = f64::default(); + exemplar + .value + .encode(ExemplarValueEncoder { value: &mut value }.into())?; -impl EncodeCounterValue for u64 { - fn encode(&self) -> openmetrics_data_model::counter_value::Total { - openmetrics_data_model::counter_value::Total::IntValue(*self) - } -} + let mut labels = vec![]; + exemplar.label_set.encode( + LabelSetEncoder { + labels: &mut labels, + } + .into(), + )?; -impl EncodeCounterValue for f64 { - fn encode(&self) -> openmetrics_data_model::counter_value::Total { - openmetrics_data_model::counter_value::Total::DoubleValue(*self) + Ok(openmetrics_data_model::Exemplar { + value, + timestamp: Default::default(), + label: labels, + }) } } -impl EncodeMetric for Counter -where - N: EncodeCounterValue, - A: counter::Atomic, -{ - fn encode( - &self, - labels: Vec, - family: &mut Vec, - ) { - let mut metric = encode_counter_with_maybe_exemplar(self.get(), None); - metric.labels = labels; - - family.push(metric); - } - - fn metric_type(&self) -> MetricType { - Self::TYPE - } +#[derive(Debug)] +pub(crate) struct GaugeValueEncoder<'a> { + value: &'a mut openmetrics_data_model::gauge_value::Value, } -impl EncodeMetric for CounterWithExemplar -where - S: EncodeLabels, - N: Clone + EncodeCounterValue, - A: counter::Atomic, - f64: From, -{ - fn encode( - &self, - labels: Vec, - family: &mut Vec, - ) { - let (value, exemplar) = self.get(); - let exemplar_proto = exemplar.as_ref().map(|e| encode_exemplar(e)); - let mut metric = encode_counter_with_maybe_exemplar(value, exemplar_proto); - metric.labels = labels; - - family.push(metric); +impl<'a> GaugeValueEncoder<'a> { + pub fn encode_i64(&mut self, v: i64) -> Result<(), std::fmt::Error> { + *self.value = openmetrics_data_model::gauge_value::Value::IntValue(v); + Ok(()) } - fn metric_type(&self) -> MetricType { - Counter::::TYPE + pub fn encode_f64(&mut self, v: f64) -> Result<(), std::fmt::Error> { + *self.value = openmetrics_data_model::gauge_value::Value::DoubleValue(v); + Ok(()) } } -fn encode_counter_with_maybe_exemplar( - value: N, - exemplar: Option, -) -> openmetrics_data_model::Metric -where - N: EncodeCounterValue, -{ - openmetrics_data_model::Metric { - metric_points: { - let metric_point = openmetrics_data_model::MetricPoint { - value: { - Some(openmetrics_data_model::metric_point::Value::CounterValue( - openmetrics_data_model::CounterValue { - total: Some(value.encode()), - exemplar, - ..Default::default() - }, - )) - }, - ..Default::default() - }; - - vec![metric_point] - }, - ..Default::default() - } -} - -///////////////////////////////////////////////////////////////////////////////// -// Gauge - -/// Trait to implement its gauge value encoding in the OpenMetrics protobuf -/// format. -pub trait EncodeGaugeValue { - /// Encode the given instance into gauge value in the OpenMetrics protobuf - /// encoding. - fn encode(&self) -> openmetrics_data_model::gauge_value::Value; +#[derive(Debug)] +pub(crate) struct ExemplarValueEncoder<'a> { + value: &'a mut f64, } -// GaugeValue.int_value is defined as `int64` in protobuf -impl EncodeGaugeValue for i64 { - fn encode(&self) -> openmetrics_data_model::gauge_value::Value { - openmetrics_data_model::gauge_value::Value::IntValue(*self) +impl<'a> ExemplarValueEncoder<'a> { + pub fn encode(&mut self, v: f64) -> Result<(), std::fmt::Error> { + *self.value = v; + Ok(()) } } -impl EncodeGaugeValue for u64 { - fn encode(&self) -> openmetrics_data_model::gauge_value::Value { - openmetrics_data_model::gauge_value::Value::IntValue(*self as i64) +impl From<&(K, V)> for openmetrics_data_model::Label { + fn from(kv: &(K, V)) -> Self { + openmetrics_data_model::Label { + name: kv.0.to_string(), + value: kv.1.to_string(), + } } } -impl EncodeGaugeValue for f64 { - fn encode(&self) -> openmetrics_data_model::gauge_value::Value { - openmetrics_data_model::gauge_value::Value::DoubleValue(*self) - } +#[derive(Debug)] +pub(crate) struct CounterValueEncoder<'a> { + value: &'a mut openmetrics_data_model::counter_value::Total, } -impl EncodeMetric for Gauge -where - N: EncodeGaugeValue, - A: gauge::Atomic, -{ - fn encode( - &self, - labels: Vec, - family: &mut Vec, - ) { - let metric = openmetrics_data_model::Metric { - metric_points: { - let metric_point = openmetrics_data_model::MetricPoint { - value: { - Some(openmetrics_data_model::metric_point::Value::GaugeValue( - openmetrics_data_model::GaugeValue { - value: Some(self.get().encode()), - }, - )) - }, - ..Default::default() - }; - - vec![metric_point] - }, - labels, - }; - - family.push(metric) +impl<'a> CounterValueEncoder<'a> { + pub fn encode_f64(&mut self, v: f64) -> Result<(), std::fmt::Error> { + *self.value = openmetrics_data_model::counter_value::Total::DoubleValue(v); + Ok(()) } - fn metric_type(&self) -> MetricType { - Self::TYPE + pub fn encode_u64(&mut self, v: u64) -> Result<(), std::fmt::Error> { + *self.value = openmetrics_data_model::counter_value::Total::IntValue(v); + Ok(()) } } -///////////////////////////////////////////////////////////////////////////////// -// Family +#[derive(Debug)] +pub(crate) struct LabelSetEncoder<'a> { + labels: &'a mut Vec, +} -impl EncodeMetric for Family -where - S: EncodeLabels + Clone + std::hash::Hash + Eq, - M: EncodeMetric + TypedMetric, - C: MetricConstructor, -{ - fn encode( - &self, - labels: Vec, - family: &mut Vec, - ) { - for (label_set, metric) in self.read().iter() { - let mut labels = labels.clone(); - label_set.encode(&mut labels); - metric.encode(labels, family) +impl<'a> LabelSetEncoder<'a> { + pub fn encode_label(&mut self) -> LabelEncoder { + LabelEncoder { + labels: self.labels, } } - - fn metric_type(&self) -> MetricType { - M::TYPE - } } -///////////////////////////////////////////////////////////////////////////////// -// Histogram - -impl EncodeMetric for Histogram { - fn encode( - &self, - labels: Vec, - family: &mut Vec, - ) { - let (sum, count, buckets) = self.get(); - // TODO: Would be better to use never type instead of `Void`. - let mut metric = encode_histogram_with_maybe_exemplars::(sum, count, &buckets, None); - metric.labels = labels; +#[derive(Debug)] +pub(crate) struct LabelEncoder<'a> { + labels: &'a mut Vec, +} - family.push(metric) - } +impl<'a> LabelEncoder<'a> { + pub fn encode_label_key(&mut self) -> Result { + self.labels.push(openmetrics_data_model::Label::default()); - fn metric_type(&self) -> MetricType { - Self::TYPE + Ok(LabelKeyEncoder { + label: self.labels.last_mut().expect("To find pushed label."), + }) } } -impl EncodeMetric for HistogramWithExemplars -where - S: EncodeLabels, -{ - fn encode( - &self, - labels: Vec, - family: &mut Vec, - ) { - let inner = self.inner(); - let (sum, count, buckets) = inner.histogram.get(); - let mut metric = - encode_histogram_with_maybe_exemplars(sum, count, &buckets, Some(&inner.exemplars)); - metric.labels = labels; - - family.push(metric) - } +#[derive(Debug)] +pub(crate) struct LabelKeyEncoder<'a> { + label: &'a mut openmetrics_data_model::Label, +} - fn metric_type(&self) -> MetricType { - Histogram::TYPE +impl<'a> std::fmt::Write for LabelKeyEncoder<'a> { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.label.name.write_str(s) } } -fn encode_histogram_with_maybe_exemplars( - sum: f64, - count: u64, - buckets: &[(f64, u64)], - exemplars: Option<&HashMap>>, -) -> openmetrics_data_model::Metric -where - S: EncodeLabels, -{ - openmetrics_data_model::Metric { - metric_points: { - let metric_point = openmetrics_data_model::MetricPoint { - value: { - let mut histogram_value = openmetrics_data_model::HistogramValue { - sum: Some(openmetrics_data_model::histogram_value::Sum::DoubleValue( - sum, - )), - count, - ..Default::default() - }; - - let mut cummulative = 0; - for (i, (upper_bound, count)) in buckets.iter().enumerate() { - cummulative += count; - let bucket = openmetrics_data_model::histogram_value::Bucket { - count: cummulative, - upper_bound: *upper_bound, - exemplar: exemplars - .and_then(|es| es.get(&i)) - .map(|exemplar| encode_exemplar(exemplar)), - }; - histogram_value.buckets.push(bucket); - } - Some(openmetrics_data_model::metric_point::Value::HistogramValue( - histogram_value, - )) - }, - ..Default::default() - }; - - vec![metric_point] - }, - ..Default::default() +impl<'a> LabelKeyEncoder<'a> { + pub fn encode_label_value(self) -> Result, std::fmt::Error> { + Ok(LabelValueEncoder { + label_value: &mut self.label.value, + }) } } -///////////////////////////////////////////////////////////////////////////////// -// Info - -impl EncodeMetric for Info -where - S: EncodeLabels, -{ - fn encode( - &self, - mut labels: Vec, - family: &mut Vec, - ) { - let metric = openmetrics_data_model::Metric { - metric_points: { - let metric_point = openmetrics_data_model::MetricPoint { - value: { - self.0.encode(&mut labels); - - Some(openmetrics_data_model::metric_point::Value::InfoValue( - openmetrics_data_model::InfoValue { info: labels }, - )) - }, - ..Default::default() - }; - - vec![metric_point] - }, - ..Default::default() - }; +#[derive(Debug)] +pub(crate) struct LabelValueEncoder<'a> { + label_value: &'a mut String, +} - family.push(metric); +impl<'a> LabelValueEncoder<'a> { + pub fn finish(self) -> Result<(), std::fmt::Error> { + Ok(()) } +} - fn metric_type(&self) -> MetricType { - Self::TYPE +impl<'a> std::fmt::Write for LabelValueEncoder<'a> { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.label_value.write_str(s) } } @@ -548,7 +398,7 @@ mod tests { registry.register("my_counter", "My counter", counter.clone()); counter.inc(); - let metric_set = encode(®istry); + let metric_set = encode(®istry).unwrap(); let family = metric_set.metric_families.first().unwrap(); assert_eq!("my_counter", family.name); @@ -578,7 +428,7 @@ mod tests { registry.register("my_counter", "My counter", counter.clone()); counter.inc(); - let metric_set = encode(®istry); + let metric_set = encode(®istry).unwrap(); let family = metric_set.metric_families.first().unwrap(); assert_eq!("my_counter", family.name); @@ -607,7 +457,7 @@ mod tests { let counter: Counter = Counter::default(); registry.register_with_unit("my_counter", "My counter", Unit::Seconds, counter); - let metric_set = encode(®istry); + let metric_set = encode(®istry).unwrap(); let family = metric_set.metric_families.first().unwrap(); assert_eq!("my_counter", family.name); @@ -629,7 +479,7 @@ mod tests { counter_with_exemplar.inc_by(1.0, Some(vec![("user_id".to_string(), 42.0)])); - let metric_set = encode(®istry); + let metric_set = encode(®istry).unwrap(); let family = metric_set.metric_families.first().unwrap(); assert_eq!("my_counter_with_exemplar", family.name); @@ -652,7 +502,7 @@ mod tests { let expected_label = { openmetrics_data_model::Label { name: "user_id".to_string(), - value: "42".to_string(), + value: "42.0".to_string(), } }; assert_eq!(vec![expected_label], exemplar.label); @@ -668,7 +518,7 @@ mod tests { registry.register("my_gauge", "My gauge", gauge.clone()); gauge.inc(); - let metric_set = encode(®istry); + let metric_set = encode(®istry).unwrap(); let family = metric_set.metric_families.first().unwrap(); assert_eq!("my_gauge", family.name); assert_eq!("My gauge.", family.help); @@ -700,7 +550,7 @@ mod tests { ]) .inc(); - let metric_set = encode(®istry); + let metric_set = encode(®istry).unwrap(); let family = metric_set.metric_families.first().unwrap(); assert_eq!("my_counter_family", family.name); @@ -745,7 +595,7 @@ mod tests { ]) .inc(); - let metric_set = encode(®istry); + let metric_set = encode(®istry).unwrap(); let family = metric_set.metric_families.first().unwrap(); assert_eq!("my_prefix_my_counter_family", family.name); @@ -783,7 +633,7 @@ mod tests { registry.register("my_histogram", "My histogram", histogram.clone()); histogram.observe(1.0); - let metric_set = encode(®istry); + let metric_set = encode(®istry).unwrap(); let family = metric_set.metric_families.first().unwrap(); assert_eq!("my_histogram", family.name); @@ -816,7 +666,7 @@ mod tests { registry.register("my_histogram", "My histogram", histogram.clone()); histogram.observe(1.0, Some(vec![("user_id".to_string(), 42u64)])); - let metric_set = encode(®istry); + let metric_set = encode(®istry).unwrap(); let family = metric_set.metric_families.first().unwrap(); assert_eq!("my_histogram", family.name); @@ -846,7 +696,7 @@ mod tests { #[test] fn encode_family_counter_histogram() { - let mut registry = Registry::>::default(); + let mut registry = Registry::default(); let counter_family = Family::, Counter>::default(); let histogram_family = @@ -854,12 +704,8 @@ mod tests { Histogram::new(exponential_buckets(1.0, 2.0, 10)) }); - registry.register("my_counter", "My counter", Box::new(counter_family.clone())); - registry.register( - "my_histogram", - "My histogram", - Box::new(histogram_family.clone()), - ); + registry.register("my_counter", "My counter", counter_family.clone()); + registry.register("my_histogram", "My histogram", histogram_family.clone()); counter_family .get_or_create(&vec![("path".to_string(), "/".to_string())]) @@ -869,14 +715,14 @@ mod tests { .get_or_create(&vec![("path".to_string(), "/".to_string())]) .observe(1.0); - let metric_set = encode(®istry); + let metric_set = encode(®istry).unwrap(); assert_eq!("my_counter", metric_set.metric_families[0].name); assert_eq!("my_histogram", metric_set.metric_families[1].name); } #[test] fn encode_family_and_counter_and_histogram() { - let mut registry = Registry::>::default(); + let mut registry = Registry::default(); // Family let counter_family = Family::, Counter>::default(); @@ -885,15 +731,11 @@ mod tests { Histogram::new(exponential_buckets(1.0, 2.0, 10)) }); - registry.register( - "my_family_counter", - "My counter", - Box::new(counter_family.clone()), - ); + registry.register("my_family_counter", "My counter", counter_family.clone()); registry.register( "my_family_histogram", "My histogram", - Box::new(histogram_family.clone()), + histogram_family.clone(), ); counter_family @@ -906,15 +748,15 @@ mod tests { // Counter let counter: Counter = Counter::default(); - registry.register("my_counter", "My counter", Box::new(counter.clone())); + registry.register("my_counter", "My counter", counter.clone()); counter.inc(); // Histogram let histogram = Histogram::new(exponential_buckets(1.0, 2.0, 10)); - registry.register("my_histogram", "My histogram", Box::new(histogram.clone())); + registry.register("my_histogram", "My histogram", histogram.clone()); histogram.observe(1.0); - let metric_set = encode(®istry); + let metric_set = encode(®istry).unwrap(); assert_eq!("my_family_counter", metric_set.metric_families[0].name); assert_eq!("my_family_histogram", metric_set.metric_families[1].name); } @@ -925,7 +767,7 @@ mod tests { let info = Info::new(vec![("os".to_string(), "GNU/linux".to_string())]); registry.register("my_info_metric", "My info metric", info); - let metric_set = encode(®istry); + let metric_set = encode(®istry).unwrap(); let family = metric_set.metric_families.first().unwrap(); assert_eq!("my_info_metric", family.name); diff --git a/src/encoding/text.rs b/src/encoding/text.rs index 7504b97..4b9b7d7 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -14,584 +14,481 @@ //! # counter.clone(), //! # ); //! # counter.inc(); -//! let mut buffer = vec![]; +//! let mut buffer = String::new(); //! encode(&mut buffer, ®istry).unwrap(); //! //! let expected = "# HELP my_counter This is my counter.\n".to_owned() + //! "# TYPE my_counter counter\n" + //! "my_counter_total 1\n" + //! "# EOF\n"; -//! assert_eq!(expected, String::from_utf8(buffer).unwrap()); +//! assert_eq!(expected, buffer); //! ``` -use crate::metrics::counter::{self, Counter}; -use crate::metrics::exemplar::{CounterWithExemplar, Exemplar, HistogramWithExemplars}; -use crate::metrics::family::{Family, MetricConstructor}; -use crate::metrics::gauge::{self, Gauge}; -use crate::metrics::histogram::Histogram; -use crate::metrics::info::Info; -use crate::metrics::{MetricType, TypedMetric}; +use crate::encoding::{EncodeExemplarValue, EncodeLabelSet, EncodeMetric}; +use crate::metrics::exemplar::Exemplar; use crate::registry::{Registry, Unit}; use std::borrow::Cow; use std::collections::HashMap; -use std::io::Write; -use std::ops::Deref; +use std::fmt::Write; /// Encode the metrics registered with the provided [`Registry`] into the /// provided [`Write`]r using the OpenMetrics text format. -pub fn encode(writer: &mut W, registry: &Registry) -> Result<(), std::io::Error> +pub fn encode(writer: &mut W, registry: &Registry) -> Result<(), std::fmt::Error> where W: Write, { for (desc, metric) in registry.iter() { - writer.write_all(b"# HELP ")?; - writer.write_all(desc.name().as_bytes())?; + writer.write_str("# HELP ")?; + writer.write_str(desc.name())?; if let Some(unit) = desc.unit() { - writer.write_all(b"_")?; - unit.encode(writer)?; + writer.write_str("_")?; + writer.write_str(unit.as_str())?; } - writer.write_all(b" ")?; - writer.write_all(desc.help().as_bytes())?; - writer.write_all(b"\n")?; + writer.write_str(" ")?; + writer.write_str(desc.help())?; + writer.write_str("\n")?; - writer.write_all(b"# TYPE ")?; - writer.write_all(desc.name().as_bytes())?; + writer.write_str("# TYPE ")?; + writer.write_str(desc.name())?; if let Some(unit) = desc.unit() { - writer.write_all(b"_")?; - unit.encode(writer)?; + writer.write_str("_")?; + writer.write_str(unit.as_str())?; } - writer.write_all(b" ")?; - EncodeMetric::metric_type(metric.as_ref()).encode(writer)?; - writer.write_all(b"\n")?; + writer.write_str(" ")?; + writer.write_str(EncodeMetric::metric_type(metric.as_ref()).as_str())?; + writer.write_str("\n")?; if let Some(unit) = desc.unit() { - writer.write_all(b"# UNIT ")?; - writer.write_all(desc.name().as_bytes())?; - writer.write_all(b"_")?; - unit.encode(writer)?; - writer.write_all(b" ")?; - unit.encode(writer)?; - writer.write_all(b"\n")?; + writer.write_str("# UNIT ")?; + writer.write_str(desc.name())?; + writer.write_str("_")?; + writer.write_str(unit.as_str())?; + writer.write_str(" ")?; + writer.write_str(unit.as_str())?; + writer.write_str("\n")?; } - let encoder = Encoder { + let encoder = MetricEncoder { writer, name: desc.name(), unit: desc.unit(), const_labels: desc.labels(), - labels: None, - }; + family_labels: None, + } + .into(); EncodeMetric::encode(metric.as_ref(), encoder)?; } - writer.write_all(b"# EOF\n")?; + writer.write_str("# EOF\n")?; Ok(()) } -/// OpenMetrics text encoding for a value. -pub trait Encode { - /// Encode to OpenMetrics text encoding. - fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error>; +/// Helper type for [`EncodeMetric`], see [`EncodeMetric::encode`]. +pub(crate) struct MetricEncoder<'a, 'b> { + writer: &'a mut dyn Write, + name: &'a str, + unit: &'a Option, + const_labels: &'a [(Cow<'static, str>, Cow<'static, str>)], + family_labels: Option<&'b dyn super::EncodeLabelSet>, } -impl Encode for f64 { - fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> { - writer.write_all(dtoa::Buffer::new().format(*self).as_bytes())?; - Ok(()) - } -} +impl<'a, 'b> std::fmt::Debug for MetricEncoder<'a, 'b> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut labels = String::new(); + if let Some(l) = self.family_labels { + l.encode(LabelSetEncoder::new(&mut labels).into())?; + } -impl Encode for u64 { - fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> { - writer.write_all(itoa::Buffer::new().format(*self).as_bytes())?; - Ok(()) + f.debug_struct("Encoder") + .field("name", &self.name) + .field("unit", &self.unit) + .field("const_labels", &self.const_labels) + .field("labels", &labels.as_str()) + .finish() } } -impl Encode for u32 { - fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> { - writer.write_all(itoa::Buffer::new().format(*self).as_bytes())?; - Ok(()) - } -} +impl<'a, 'b> MetricEncoder<'a, 'b> { + pub fn encode_counter( + &mut self, + v: impl super::EncodeCounterValue, + exemplar: Option<&Exemplar>, + ) -> Result<(), std::fmt::Error> { + self.write_name_and_unit()?; -impl Encode for &[T] { - fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> { - if self.is_empty() { - return Ok(()); - } + self.write_suffix("total")?; - let mut iter = self.iter().peekable(); - while let Some(x) = iter.next() { - x.encode(writer)?; + self.encode_labels::<()>(None)?; - if iter.peek().is_some() { - writer.write_all(b",")?; + v.encode( + &mut CounterValueEncoder { + writer: self.writer, } + .into(), + )?; + + if let Some(exemplar) = exemplar { + self.encode_exemplar(exemplar)?; } + self.newline()?; + Ok(()) } -} -impl Encode for Vec { - fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> { - self.as_slice().encode(writer) - } -} + pub fn encode_gauge(&mut self, v: impl super::EncodeGaugeValue) -> Result<(), std::fmt::Error> { + self.write_name_and_unit()?; -impl Encode for (K, V) { - fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> { - let (key, value) = self; + self.encode_labels::<()>(None)?; - key.encode(writer)?; - writer.write_all(b"=\"")?; + v.encode( + &mut GaugeValueEncoder { + writer: self.writer, + } + .into(), + )?; - value.encode(writer)?; - writer.write_all(b"\"")?; + self.newline()?; Ok(()) } -} -impl Encode for &str { - fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> { - // TODO: Can we do better? - writer.write_all(self.as_bytes())?; - Ok(()) - } -} + pub fn encode_info(&mut self, label_set: &S) -> Result<(), std::fmt::Error> { + self.write_name_and_unit()?; -impl Encode for String { - fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> { - self.as_str().encode(writer) - } -} + self.write_suffix("info")?; -impl<'a> Encode for Cow<'a, str> { - fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> { - self.as_ref().encode(writer) - } -} + self.encode_labels(Some(label_set))?; -impl Encode for MetricType { - fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> { - let t = match self { - MetricType::Counter => "counter", - MetricType::Gauge => "gauge", - MetricType::Histogram => "histogram", - MetricType::Info => "info", - MetricType::Unknown => "unknown", - }; - - writer.write_all(t.as_bytes())?; - Ok(()) - } -} + self.writer.write_str(" ")?; + self.writer.write_str(itoa::Buffer::new().format(1))?; + + self.newline()?; -impl Encode for Unit { - fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> { - writer.write_all(self.as_str().as_bytes())?; Ok(()) } -} -impl Encode for () { - fn encode(&self, _writer: &mut dyn Write) -> Result<(), std::io::Error> { - Ok(()) + /// Encode a set of labels. Used by wrapper metric types like [`Family`]. + pub fn encode_family<'c, 'd, S: EncodeLabelSet>( + &'c mut self, + label_set: &'d S, + ) -> Result, std::fmt::Error> { + debug_assert!(self.family_labels.is_none()); + + Ok(MetricEncoder { + writer: self.writer, + name: self.name, + unit: self.unit, + const_labels: self.const_labels, + family_labels: Some(label_set), + }) } -} -/// Helper type for [`EncodeMetric`], see [`EncodeMetric::encode`]. -/// -// `Encoder` does not take a trait parameter for `writer` and `labels` because -// `EncodeMetric` which uses `Encoder` needs to be usable as a trait object in -// order to be able to register different metric types with a `Registry`. Trait -// objects can not use type parameters. -// -// TODO: Alternative solutions to the above are very much appreciated. -#[allow(missing_debug_implementations)] -pub struct Encoder<'a, 'b> { - writer: &'a mut dyn Write, - name: &'a str, - unit: &'a Option, - const_labels: &'a [(Cow<'static, str>, Cow<'static, str>)], - labels: Option<&'b dyn Encode>, -} + pub fn encode_histogram( + &mut self, + sum: f64, + count: u64, + buckets: &[(f64, u64)], + exemplars: Option<&HashMap>>, + ) -> Result<(), std::fmt::Error> { + self.write_name_and_unit()?; + self.write_suffix("sum")?; + self.encode_labels::<()>(None)?; + self.writer.write_str(" ")?; + self.writer.write_str(dtoa::Buffer::new().format(sum))?; + self.newline()?; -impl<'a, 'b> Encoder<'a, 'b> { - /// Encode a metric suffix, e.g. in the case of [`Counter`] the suffic `_total`. - pub fn encode_suffix(&mut self, suffix: &'static str) -> Result { self.write_name_and_unit()?; + self.write_suffix("count")?; + self.encode_labels::<()>(None)?; + self.writer.write_str(" ")?; + self.writer.write_str(itoa::Buffer::new().format(count))?; + self.newline()?; - self.writer.write_all(b"_")?; - self.writer.write_all(suffix.as_bytes()).map(|_| ())?; + let mut cummulative = 0; + for (i, (upper_bound, count)) in buckets.iter().enumerate() { + cummulative += count; - self.encode_labels() - } + self.write_name_and_unit()?; + self.write_suffix("bucket")?; - /// Signal that the metric has no suffix. - pub fn no_suffix(&mut self) -> Result { - self.write_name_and_unit()?; + if *upper_bound == f64::MAX { + self.encode_labels(Some(&[("le", "+Inf")]))?; + } else { + self.encode_labels(Some(&[("le", *upper_bound)]))?; + } + + self.writer.write_str(" ")?; + self.writer + .write_str(itoa::Buffer::new().format(cummulative))?; + + if let Some(exemplar) = exemplars.and_then(|e| e.get(&i)) { + self.encode_exemplar(exemplar)? + } + + self.newline()?; + } - self.encode_labels() + Ok(()) } - fn write_name_and_unit(&mut self) -> Result<(), std::io::Error> { - self.writer.write_all(self.name.as_bytes())?; + /// Encode an exemplar for the given metric. + fn encode_exemplar( + &mut self, + exemplar: &Exemplar, + ) -> Result<(), std::fmt::Error> { + self.writer.write_str(" # {")?; + exemplar + .label_set + .encode(LabelSetEncoder::new(self.writer).into())?; + self.writer.write_str("} ")?; + exemplar.value.encode( + ExemplarValueEncoder { + writer: self.writer, + } + .into(), + )?; + Ok(()) + } + + fn newline(&mut self) -> Result<(), std::fmt::Error> { + self.writer.write_str("\n") + } + fn write_name_and_unit(&mut self) -> Result<(), std::fmt::Error> { + self.writer.write_str(self.name)?; if let Some(unit) = self.unit { - self.writer.write_all(b"_")?; - unit.encode(self.writer)?; + self.writer.write_str("_")?; + self.writer.write_str(unit.as_str())?; } Ok(()) } + fn write_suffix(&mut self, suffix: &'static str) -> Result<(), std::fmt::Error> { + self.writer.write_str("_")?; + self.writer.write_str(suffix)?; + + Ok(()) + } + // TODO: Consider caching the encoded labels for Histograms as they stay the // same but are currently encoded multiple times. - fn encode_labels(&mut self) -> Result { - let mut opened_curly_brackets = false; + fn encode_labels( + &mut self, + additional_labels: Option<&S>, + ) -> Result<(), std::fmt::Error> { + if self.const_labels.is_empty() + && additional_labels.is_none() + && self.family_labels.is_none() + { + return Ok(()); + } - if !self.const_labels.is_empty() { - self.writer.write_all(b"{")?; - opened_curly_brackets = true; + self.writer.write_str("{")?; - self.const_labels.encode(self.writer)?; - } + self.const_labels + .encode(LabelSetEncoder::new(self.writer).into())?; - if let Some(labels) = &self.labels { - if opened_curly_brackets { - self.writer.write_all(b",")?; - } else { - opened_curly_brackets = true; - self.writer.write_all(b"{")?; + if let Some(additional_labels) = additional_labels { + if !self.const_labels.is_empty() { + self.writer.write_str(",")?; } - labels.encode(self.writer)?; - } - Ok(BucketEncoder { - opened_curly_brackets, - writer: self.writer, - }) - } + additional_labels.encode(LabelSetEncoder::new(self.writer).into())?; + } - /// Encode a set of labels. Used by wrapper metric types like [`Family`]. - pub fn with_label_set<'c, 'd>(&'c mut self, label_set: &'d dyn Encode) -> Encoder<'c, 'd> { - debug_assert!(self.labels.is_none()); + if let Some(labels) = &self.family_labels { + if !self.const_labels.is_empty() || additional_labels.is_some() { + self.writer.write_str(",")?; + } - Encoder { - writer: self.writer, - name: self.name, - unit: self.unit, - const_labels: self.const_labels, - labels: Some(label_set), + labels.encode(LabelSetEncoder::new(self.writer).into())?; } + + self.writer.write_str("}")?; + + Ok(()) } } -/// Used to encode an OpenMetrics Histogram bucket. -#[allow(missing_debug_implementations)] -#[must_use] -pub struct BucketEncoder<'a> { +pub(crate) struct CounterValueEncoder<'a> { writer: &'a mut dyn Write, - opened_curly_brackets: bool, } -impl<'a> BucketEncoder<'a> { - /// Encode a bucket. Used for the [`Histogram`] metric type. - pub fn encode_bucket(&mut self, upper_bound: f64) -> Result { - if self.opened_curly_brackets { - self.writer.write_all(b",")?; - } else { - self.writer.write_all(b"{")?; - } - - self.writer.write_all(b"le=\"")?; - if upper_bound == f64::MAX { - self.writer.write_all(b"+Inf")?; - } else { - upper_bound.encode(self.writer)?; - } - self.writer.write_all(b"\"}")?; +impl<'a> std::fmt::Debug for CounterValueEncoder<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CounterValueEncoder").finish() + } +} - Ok(ValueEncoder { - writer: self.writer, - }) +impl<'a> CounterValueEncoder<'a> { + pub fn encode_f64(&mut self, v: f64) -> Result<(), std::fmt::Error> { + self.writer.write_str(" ")?; + self.writer.write_str(dtoa::Buffer::new().format(v))?; + Ok(()) } - /// Signal that the metric type has no bucket. - pub fn no_bucket(&mut self) -> Result { - if self.opened_curly_brackets { - self.writer.write_all(b"}")?; - } - Ok(ValueEncoder { - writer: self.writer, - }) + 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(()) } } -/// Used to encode an OpenMetrics metric value. -#[allow(missing_debug_implementations)] -#[must_use] -pub struct ValueEncoder<'a> { +pub(crate) struct GaugeValueEncoder<'a> { writer: &'a mut dyn Write, } -impl<'a> ValueEncoder<'a> { - /// Encode the metric value. E.g. in the case of [`Counter`] the - /// monotonically increasing counter value. - pub fn encode_value(&mut self, v: V) -> Result { - self.writer.write_all(b" ")?; - v.encode(self.writer)?; - Ok(ExemplarEncoder { - writer: self.writer, - }) +impl<'a> std::fmt::Debug for GaugeValueEncoder<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GaugeValueEncoder").finish() } } -/// Used to encode an OpenMetrics Exemplar. -#[allow(missing_debug_implementations)] -#[must_use] -pub struct ExemplarEncoder<'a> { - writer: &'a mut dyn Write, -} - -impl<'a> ExemplarEncoder<'a> { - /// Encode an exemplar for the given metric. - pub fn encode_exemplar( - &mut self, - exemplar: &Exemplar, - ) -> Result<(), std::io::Error> { - self.writer.write_all(b" # {")?; - exemplar.label_set.encode(self.writer)?; - self.writer.write_all(b"} ")?; - exemplar.value.encode(self.writer)?; - self.writer.write_all(b"\n")?; +impl<'a> GaugeValueEncoder<'a> { + pub fn encode_f64(&mut self, v: f64) -> Result<(), std::fmt::Error> { + self.writer.write_str(" ")?; + self.writer.write_str(dtoa::Buffer::new().format(v))?; Ok(()) } - /// Signal that the metric type has no exemplar. - pub fn no_exemplar(&mut self) -> Result<(), std::io::Error> { - self.writer.write_all(b"\n")?; + 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))?; Ok(()) } } -/// Trait implemented by each metric type, e.g. [`Counter`], to implement its encoding in the OpenMetric text format. -pub trait EncodeMetric { - /// Encode the given instance in the OpenMetrics text encoding. - fn encode(&self, encoder: Encoder) -> Result<(), std::io::Error>; - - /// The OpenMetrics metric type of the instance. - // One can not use [`TypedMetric`] directly, as associated constants are not - // object safe and thus can not be used with dynamic dispatching. - fn metric_type(&self) -> MetricType; +pub(crate) struct ExemplarValueEncoder<'a> { + writer: &'a mut dyn Write, } -impl EncodeMetric for Box { - fn encode(&self, encoder: Encoder) -> Result<(), std::io::Error> { - self.deref().encode(encoder) +impl<'a> std::fmt::Debug for ExemplarValueEncoder<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ExemplarValueEncoder").finish() } +} - fn metric_type(&self) -> MetricType { - self.deref().metric_type() +impl<'a> ExemplarValueEncoder<'a> { + pub fn encode(&mut self, v: f64) -> Result<(), std::fmt::Error> { + self.writer.write_str(dtoa::Buffer::new().format(v)) } } -///////////////////////////////////////////////////////////////////////////////// -// Counter - -impl EncodeMetric for Counter -where - N: Encode, - A: counter::Atomic, -{ - fn encode(&self, encoder: Encoder) -> Result<(), std::io::Error> { - // TODO: Would be better to use never type instead of `()`. - encode_counter_with_maybe_exemplar::<(), _>(self.get(), None, encoder) - } +pub(crate) struct LabelSetEncoder<'a> { + writer: &'a mut dyn Write, + first: bool, +} - fn metric_type(&self) -> MetricType { - Self::TYPE +impl<'a> std::fmt::Debug for LabelSetEncoder<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LabelSetEncoder") + .field("first", &self.first) + .finish() } } -// TODO: S, V, N, A are hard to grasp. -impl EncodeMetric for CounterWithExemplar -where - S: Encode, - N: Encode + Clone, - A: counter::Atomic, -{ - fn encode(&self, encoder: Encoder) -> Result<(), std::io::Error> { - let (value, exemplar) = self.get(); - encode_counter_with_maybe_exemplar(value, exemplar.as_ref(), encoder) +impl<'a> LabelSetEncoder<'a> { + fn new(writer: &'a mut dyn Write) -> Self { + Self { + writer, + first: true, + } } - fn metric_type(&self) -> MetricType { - Counter::::TYPE + pub fn encode_label(&mut self) -> LabelEncoder { + let first = self.first; + self.first = false; + LabelEncoder { + writer: self.writer, + first, + } } } -fn encode_counter_with_maybe_exemplar( - value: N, - exemplar: Option<&Exemplar>, - mut encoder: Encoder, -) -> Result<(), std::io::Error> -where - S: Encode, - N: Encode, -{ - let mut bucket_encoder = encoder.encode_suffix("total")?; - let mut value_encoder = bucket_encoder.no_bucket()?; - let mut exemplar_encoder = value_encoder.encode_value(value)?; - - match exemplar { - Some(exemplar) => exemplar_encoder.encode_exemplar(exemplar)?, - None => exemplar_encoder.no_exemplar()?, - } - - Ok(()) +pub(crate) struct LabelEncoder<'a> { + writer: &'a mut dyn Write, + first: bool, } -///////////////////////////////////////////////////////////////////////////////// -// Gauge - -impl EncodeMetric for Gauge -where - N: Encode, - A: gauge::Atomic, -{ - fn encode(&self, mut encoder: Encoder) -> Result<(), std::io::Error> { - encoder - .no_suffix()? - .no_bucket()? - .encode_value(self.get())? - .no_exemplar()?; - - Ok(()) - } - fn metric_type(&self) -> MetricType { - Self::TYPE +impl<'a> std::fmt::Debug for LabelEncoder<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LabelEncoder") + .field("first", &self.first) + .finish() } } -///////////////////////////////////////////////////////////////////////////////// -// Family - -impl EncodeMetric for Family -where - S: Clone + std::hash::Hash + Eq + Encode, - M: EncodeMetric + TypedMetric, - C: MetricConstructor, -{ - fn encode(&self, mut encoder: Encoder) -> Result<(), std::io::Error> { - let guard = self.read(); - for (label_set, m) in guard.iter() { - let encoder = encoder.with_label_set(label_set); - m.encode(encoder)?; +impl<'a> LabelEncoder<'a> { + pub fn encode_label_key(&mut self) -> Result { + if !self.first { + self.writer.write_str(",")?; } - Ok(()) - } - - fn metric_type(&self) -> MetricType { - M::TYPE + Ok(LabelKeyEncoder { + writer: self.writer, + }) } } -///////////////////////////////////////////////////////////////////////////////// -// Histogram +pub(crate) struct LabelKeyEncoder<'a> { + writer: &'a mut dyn Write, +} -impl EncodeMetric for Histogram { - fn encode(&self, encoder: Encoder) -> Result<(), std::io::Error> { - let (sum, count, buckets) = self.get(); - // TODO: Would be better to use never type instead of `()`. - encode_histogram_with_maybe_exemplars::<()>(sum, count, &buckets, None, encoder) +impl<'a> std::fmt::Debug for LabelKeyEncoder<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LabelKeyEncoder").finish() } +} - fn metric_type(&self) -> MetricType { - Self::TYPE +impl<'a> LabelKeyEncoder<'a> { + pub fn encode_label_value(self) -> Result, std::fmt::Error> { + self.writer.write_str("=\"")?; + Ok(LabelValueEncoder { + writer: self.writer, + }) } } -impl EncodeMetric for HistogramWithExemplars { - fn encode(&self, encoder: Encoder) -> Result<(), std::io::Error> { - let inner = self.inner(); - let (sum, count, buckets) = inner.histogram.get(); - encode_histogram_with_maybe_exemplars(sum, count, &buckets, Some(&inner.exemplars), encoder) +impl<'a> std::fmt::Write for LabelKeyEncoder<'a> { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.writer.write_str(s) } +} - fn metric_type(&self) -> MetricType { - Histogram::TYPE - } +pub(crate) struct LabelValueEncoder<'a> { + writer: &'a mut dyn Write, } -fn encode_histogram_with_maybe_exemplars( - sum: f64, - count: u64, - buckets: &[(f64, u64)], - exemplars: Option<&HashMap>>, - mut encoder: Encoder, -) -> Result<(), std::io::Error> { - encoder - .encode_suffix("sum")? - .no_bucket()? - .encode_value(sum)? - .no_exemplar()?; - encoder - .encode_suffix("count")? - .no_bucket()? - .encode_value(count)? - .no_exemplar()?; - - let mut cummulative = 0; - for (i, (upper_bound, count)) in buckets.iter().enumerate() { - cummulative += count; - let mut bucket_encoder = encoder.encode_suffix("bucket")?; - let mut value_encoder = bucket_encoder.encode_bucket(*upper_bound)?; - let mut exemplar_encoder = value_encoder.encode_value(cummulative)?; - - match exemplars.and_then(|es| es.get(&i)) { - Some(exemplar) => exemplar_encoder.encode_exemplar(exemplar)?, - None => exemplar_encoder.no_exemplar()?, - } +impl<'a> std::fmt::Debug for LabelValueEncoder<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LabelValueEncoder").finish() } - - Ok(()) } -///////////////////////////////////////////////////////////////////////////////// -// Info - -impl EncodeMetric for Info -where - S: Clone + std::hash::Hash + Eq + Encode, -{ - fn encode(&self, mut encoder: Encoder) -> Result<(), std::io::Error> { - encoder - .with_label_set(&self.0) - .encode_suffix("info")? - .no_bucket()? - .encode_value(1u32)? - .no_exemplar()?; - - Ok(()) +impl<'a> LabelValueEncoder<'a> { + pub fn finish(self) -> Result<(), std::fmt::Error> { + self.writer.write_str("\"") } +} - fn metric_type(&self) -> MetricType { - Self::TYPE +impl<'a> std::fmt::Write for LabelValueEncoder<'a> { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.writer.write_str(s) } } #[cfg(test)] mod tests { use super::*; - use crate::metrics::counter::Counter; + use crate::metrics::exemplar::HistogramWithExemplars; + use crate::metrics::family::Family; use crate::metrics::gauge::Gauge; - use crate::metrics::histogram::exponential_buckets; + use crate::metrics::histogram::{exponential_buckets, Histogram}; + use crate::metrics::info::Info; + use crate::metrics::{counter::Counter, exemplar::CounterWithExemplar}; use pyo3::{prelude::*, types::PyModule}; use std::borrow::Cow; @@ -601,11 +498,11 @@ mod tests { let mut registry = Registry::default(); registry.register("my_counter", "My counter", counter); - let mut encoded = Vec::new(); + let mut encoded = String::new(); encode(&mut encoded, ®istry).unwrap(); - parse_with_python_client(String::from_utf8(encoded).unwrap()); + parse_with_python_client(encoded); } #[test] @@ -614,7 +511,7 @@ mod tests { let counter: Counter = Counter::default(); registry.register_with_unit("my_counter", "My counter", Unit::Seconds, counter); - let mut encoded = Vec::new(); + let mut encoded = String::new(); encode(&mut encoded, ®istry).unwrap(); let expected = "# HELP my_counter_seconds My counter.\n".to_owned() @@ -622,16 +519,16 @@ mod tests { + "# UNIT my_counter_seconds seconds\n" + "my_counter_seconds_total 0\n" + "# EOF\n"; - assert_eq!(expected, String::from_utf8(encoded.clone()).unwrap()); + assert_eq!(expected, encoded); - parse_with_python_client(String::from_utf8(encoded).unwrap()); + parse_with_python_client(encoded); } #[test] fn encode_counter_with_exemplar() { let mut registry = Registry::default(); - let counter_with_exemplar: CounterWithExemplar<(String, u64)> = + let counter_with_exemplar: CounterWithExemplar> = CounterWithExemplar::default(); registry.register_with_unit( "my_counter_with_exemplar", @@ -640,20 +537,20 @@ mod tests { counter_with_exemplar.clone(), ); - counter_with_exemplar.inc_by(1, Some(("user_id".to_string(), 42))); + counter_with_exemplar.inc_by(1, Some(vec![("user_id".to_string(), 42)])); - let mut encoded = Vec::new(); + 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 1 # {user_id=\"42\"} 1\n" + + "my_counter_with_exemplar_seconds_total 1 # {user_id=\"42\"} 1.0\n" + "# EOF\n"; - assert_eq!(expected, String::from_utf8(encoded.clone()).unwrap()); + assert_eq!(expected, encoded); - parse_with_python_client(String::from_utf8(encoded).unwrap()); + parse_with_python_client(encoded); } #[test] @@ -662,11 +559,11 @@ mod tests { let gauge: Gauge = Gauge::default(); registry.register("my_gauge", "My gauge", gauge); - let mut encoded = Vec::new(); + let mut encoded = String::new(); encode(&mut encoded, ®istry).unwrap(); - parse_with_python_client(String::from_utf8(encoded).unwrap()); + parse_with_python_client(encoded); } #[test] @@ -682,11 +579,11 @@ mod tests { ]) .inc(); - let mut encoded = Vec::new(); + let mut encoded = String::new(); encode(&mut encoded, ®istry).unwrap(); - parse_with_python_client(String::from_utf8(encoded).unwrap()); + parse_with_python_client(encoded); } #[test] @@ -705,7 +602,7 @@ mod tests { ]) .inc(); - let mut encoded = Vec::new(); + let mut encoded = String::new(); encode(&mut encoded, ®istry).unwrap(); @@ -714,9 +611,9 @@ mod tests { + "# TYPE my_prefix_my_counter_family counter\n" + "my_prefix_my_counter_family_total{my_key=\"my_value\",method=\"GET\",status=\"200\"} 1\n" + "# EOF\n"; - assert_eq!(expected, String::from_utf8(encoded.clone()).unwrap()); + assert_eq!(expected, encoded); - parse_with_python_client(String::from_utf8(encoded).unwrap()); + parse_with_python_client(encoded); } #[test] @@ -725,16 +622,16 @@ mod tests { let info = Info::new(vec![("os".to_string(), "GNU/linux".to_string())]); registry.register("my_info_metric", "My info metric", info); - let mut encoded = Vec::new(); + let mut encoded = String::new(); encode(&mut encoded, ®istry).unwrap(); let expected = "# HELP my_info_metric My info metric.\n".to_owned() + "# TYPE my_info_metric info\n" + "my_info_metric_info{os=\"GNU/linux\"} 1\n" + "# EOF\n"; - assert_eq!(expected, String::from_utf8(encoded.clone()).unwrap()); + assert_eq!(expected, encoded); - parse_with_python_client(String::from_utf8(encoded).unwrap()); + parse_with_python_client(encoded); } #[test] @@ -744,11 +641,11 @@ mod tests { registry.register("my_histogram", "My histogram", histogram.clone()); histogram.observe(1.0); - let mut encoded = Vec::new(); + let mut encoded = String::new(); encode(&mut encoded, ®istry).unwrap(); - parse_with_python_client(String::from_utf8(encoded).unwrap()); + parse_with_python_client(encoded); } #[test] @@ -764,11 +661,11 @@ mod tests { ]) .observe(1.0); - let mut encoded = Vec::new(); + let mut encoded = String::new(); encode(&mut encoded, ®istry).unwrap(); - parse_with_python_client(String::from_utf8(encoded).unwrap()); + parse_with_python_client(encoded); } #[test] @@ -776,9 +673,9 @@ mod tests { 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)])); - let mut encoded = Vec::new(); + let mut encoded = String::new(); encode(&mut encoded, ®istry).unwrap(); let expected = "# HELP my_histogram My histogram.\n".to_owned() @@ -797,9 +694,9 @@ mod tests { + "my_histogram_bucket{le=\"512.0\"} 1\n" + "my_histogram_bucket{le=\"+Inf\"} 1\n" + "# EOF\n"; - assert_eq!(expected, String::from_utf8(encoded.clone()).unwrap()); + assert_eq!(expected, encoded); - parse_with_python_client(String::from_utf8(encoded).unwrap()); + parse_with_python_client(encoded); } fn parse_with_python_client(input: String) { @@ -825,7 +722,7 @@ def parse(input): parser .getattr("parse") .expect("`parse` to exist.") - .call1((input,)) + .call1((input.clone(),)) .map_err(|e| e.to_string()) .unwrap(); }) diff --git a/src/lib.rs b/src/lib.rs index 061b5cf..e64d60b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,7 +13,7 @@ //! # Examples //! //! ``` -//! use prometheus_client::encoding::Encode; +//! use prometheus_client::encoding::{EncodeLabelSet, EncodeLabelValue}; //! use prometheus_client::encoding::text::encode; //! use prometheus_client::metrics::counter::{Atomic, Counter}; //! use prometheus_client::metrics::family::Family; @@ -30,7 +30,7 @@ //! // //! // You could as well use `(String, String)` to represent a label set, //! // instead of the custom type below. -//! #[derive(Clone, Debug, Hash, PartialEq, Eq, Encode)] +//! #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] //! struct Labels { //! // Use your own enum types to represent label values. //! method: Method, @@ -38,7 +38,7 @@ //! path: String, //! }; //! -//! #[derive(Clone, Debug, Hash, PartialEq, Eq, Encode)] +//! #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelValue)] //! enum Method { //! GET, //! PUT, @@ -65,14 +65,14 @@ //! // When a monitoring system like Prometheus scrapes the local node, encode //! // all metrics in the registry in the text format, and send the encoded //! // metrics back. -//! let mut buffer = vec![]; +//! let mut buffer = String::new(); //! encode(&mut buffer, ®istry).unwrap(); //! //! let expected = "# HELP http_requests Number of HTTP requests received.\n".to_owned() + //! "# TYPE http_requests counter\n" + //! "http_requests_total{method=\"GET\",path=\"/metrics\"} 1\n" + //! "# EOF\n"; -//! assert_eq!(expected, String::from_utf8(buffer).unwrap()); +//! assert_eq!(expected, buffer); //! ``` //! See [examples] directory for more. //! diff --git a/src/metrics.rs b/src/metrics.rs index 647fa5c..cd38952 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -28,3 +28,16 @@ pub enum MetricType { // StateSet, // Summary } + +impl MetricType { + /// Returns the given metric type's str representation. + pub fn as_str(&self) -> &str { + match self { + MetricType::Counter => "counter", + MetricType::Gauge => "gauge", + MetricType::Histogram => "histogram", + MetricType::Info => "info", + MetricType::Unknown => "unknown", + } + } +} diff --git a/src/metrics/counter.rs b/src/metrics/counter.rs index bd886c5..e5929a1 100644 --- a/src/metrics/counter.rs +++ b/src/metrics/counter.rs @@ -2,6 +2,8 @@ //! //! See [`Counter`] for details. +use crate::encoding::{EncodeMetric, MetricEncoder}; + use super::{MetricType, TypedMetric}; use std::marker::PhantomData; #[cfg(not(any(target_arch = "mips", target_arch = "powerpc")))] @@ -171,6 +173,20 @@ impl TypedMetric for Counter { const TYPE: MetricType = MetricType::Counter; } +impl EncodeMetric for Counter +where + N: crate::encoding::EncodeCounterValue, + A: Atomic, +{ + fn encode(&self, mut encoder: MetricEncoder) -> Result<(), std::fmt::Error> { + encoder.encode_counter::<(), _, u64>(self.get(), None) + } + + fn metric_type(&self) -> MetricType { + Self::TYPE + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/metrics/exemplar.rs b/src/metrics/exemplar.rs index a2cffc0..9950d95 100644 --- a/src/metrics/exemplar.rs +++ b/src/metrics/exemplar.rs @@ -2,6 +2,10 @@ //! //! See [`CounterWithExemplar`] and [`HistogramWithExemplars`] for details. +use crate::encoding::{ + EncodeCounterValue, EncodeExemplarValue, EncodeLabelSet, EncodeMetric, MetricEncoder, +}; + use super::counter::{self, Counter}; use super::histogram::Histogram; use super::{MetricType, TypedMetric}; @@ -37,13 +41,13 @@ pub struct Exemplar { /// # use prometheus_client::metrics::exemplar::CounterWithExemplar; /// # use prometheus_client::metrics::histogram::exponential_buckets; /// # use prometheus_client::metrics::family::Family; -/// # use prometheus_client_derive_encode::Encode; -/// #[derive(Clone, Hash, PartialEq, Eq, Encode, Debug, Default)] +/// # use prometheus_client_derive_encode::EncodeLabelSet; +/// #[derive(Clone, Hash, PartialEq, Eq, EncodeLabelSet, Debug, Default)] /// pub struct ResultLabel { /// pub result: String, /// } /// -/// #[derive(Clone, Hash, PartialEq, Eq, Encode, Debug, Default)] +/// #[derive(Clone, Hash, PartialEq, Eq, EncodeLabelSet, Debug, Default)] /// pub struct TraceLabel { /// pub trace_id: String, /// } @@ -144,6 +148,23 @@ impl> CounterWithExemplar { } } +// TODO: S, V, N, A are hard to grasp. +impl EncodeMetric for crate::metrics::exemplar::CounterWithExemplar +where + S: EncodeLabelSet, + N: EncodeCounterValue + EncodeExemplarValue + Clone, + A: counter::Atomic, +{ + fn encode(&self, mut encoder: MetricEncoder) -> Result<(), std::fmt::Error> { + let (value, exemplar) = self.get(); + encoder.encode_counter(value, exemplar.as_ref()) + } + + fn metric_type(&self) -> MetricType { + Counter::::TYPE + } +} + ///////////////////////////////////////////////////////////////////////////////// // Histogram @@ -161,13 +182,13 @@ impl> CounterWithExemplar { /// # use prometheus_client::metrics::exemplar::HistogramWithExemplars; /// # use prometheus_client::metrics::histogram::exponential_buckets; /// # use prometheus_client::metrics::family::Family; -/// # use prometheus_client_derive_encode::Encode; -/// #[derive(Clone, Hash, PartialEq, Eq, Encode, Debug, Default)] +/// # use prometheus_client::encoding::EncodeLabelSet; +/// #[derive(Clone, Hash, PartialEq, Eq, EncodeLabelSet, Debug, Default)] /// pub struct ResultLabel { /// pub result: String, /// } /// -/// #[derive(Clone, Hash, PartialEq, Eq, Encode, Debug, Default)] +/// #[derive(Clone, Hash, PartialEq, Eq, EncodeLabelSet, Debug, Default)] /// pub struct TraceLabel { /// pub trace_id: String, /// } @@ -244,3 +265,15 @@ impl HistogramWithExemplars { self.inner.read() } } + +impl EncodeMetric for HistogramWithExemplars { + fn encode(&self, mut encoder: MetricEncoder) -> Result<(), std::fmt::Error> { + let inner = self.inner(); + let (sum, count, buckets) = inner.histogram.get(); + encoder.encode_histogram(sum, count, &buckets, Some(&inner.exemplars)) + } + + fn metric_type(&self) -> MetricType { + Histogram::TYPE + } +} diff --git a/src/metrics/family.rs b/src/metrics/family.rs index 0dbf51e..8054828 100644 --- a/src/metrics/family.rs +++ b/src/metrics/family.rs @@ -2,6 +2,8 @@ //! //! See [`Family`] for details. +use crate::encoding::{EncodeLabelSet, EncodeMetric, MetricEncoder}; + use super::{MetricType, TypedMetric}; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard}; use std::collections::HashMap; @@ -41,14 +43,14 @@ use std::sync::Arc; /// family.get_or_create(&vec![("method".to_owned(), "GET".to_owned())]).inc(); /// /// # // Encode all metrics in the registry in the text format. -/// # let mut buffer = vec![]; +/// # let mut buffer = String::new(); /// # encode(&mut buffer, ®istry).unwrap(); /// # /// # let expected = "# HELP my_counter This is my counter.\n".to_owned() + /// # "# TYPE my_counter counter\n" + /// # "my_counter_total{method=\"GET\"} 1\n" + /// # "# EOF\n"; -/// # assert_eq!(expected, String::from_utf8(buffer).unwrap()); +/// # assert_eq!(expected, buffer); /// ``` /// /// ### [`Family`] with custom type for performance and/or type safety @@ -57,7 +59,7 @@ use std::sync::Arc; /// [`Encode`](crate::encoding::text::Encode) implementation. /// /// ``` -/// # use prometheus_client::encoding::Encode; +/// # use prometheus_client::encoding::{EncodeLabelSet, EncodeLabelValue}; /// # use prometheus_client::encoding::text::encode; /// # use prometheus_client::metrics::counter::{Atomic, Counter}; /// # use prometheus_client::metrics::family::Family; @@ -65,12 +67,12 @@ use std::sync::Arc; /// # use std::io::Write; /// # /// # let mut registry = Registry::default(); -/// #[derive(Clone, Debug, Hash, PartialEq, Eq, Encode)] +/// #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] /// struct Labels { /// method: Method, /// }; /// -/// #[derive(Clone, Debug, Hash, PartialEq, Eq, Encode)] +/// #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelValue)] /// enum Method { /// GET, /// PUT, @@ -87,14 +89,14 @@ use std::sync::Arc; /// family.get_or_create(&Labels { method: Method::GET }).inc(); /// # /// # // Encode all metrics in the registry in the text format. -/// # let mut buffer = vec![]; +/// # let mut buffer = String::new(); /// # encode(&mut buffer, ®istry).unwrap(); /// # /// # let expected = "# HELP my_counter This is my counter.\n".to_owned() + /// # "# TYPE my_counter counter\n" + /// # "my_counter_total{method=\"GET\"} 1\n" + /// # "# EOF\n"; -/// # assert_eq!(expected, String::from_utf8(buffer).unwrap()); +/// # assert_eq!(expected, buffer); /// ``` // TODO: Consider exposing hash algorithm. pub struct Family M> { @@ -300,6 +302,26 @@ impl TypedMetric for Family { const TYPE: MetricType = ::TYPE; } +impl EncodeMetric for Family +where + S: Clone + std::hash::Hash + Eq + EncodeLabelSet, + M: EncodeMetric + TypedMetric, + C: MetricConstructor, +{ + fn encode(&self, mut encoder: MetricEncoder) -> Result<(), std::fmt::Error> { + let guard = self.read(); + for (label_set, m) in guard.iter() { + let encoder = encoder.encode_family(label_set)?; + m.encode(encoder)?; + } + Ok(()) + } + + fn metric_type(&self) -> MetricType { + M::TYPE + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/metrics/gauge.rs b/src/metrics/gauge.rs index 297872f..60b1dc5 100644 --- a/src/metrics/gauge.rs +++ b/src/metrics/gauge.rs @@ -2,6 +2,8 @@ //! //! See [`Gauge`] for details. +use crate::encoding::{EncodeGaugeValue, EncodeMetric, MetricEncoder}; + use super::{MetricType, TypedMetric}; use std::marker::PhantomData; #[cfg(not(any(target_arch = "mips", target_arch = "powerpc")))] @@ -25,8 +27,8 @@ use std::sync::Arc; /// ``` /// # use prometheus_client::metrics::gauge::Gauge; /// let gauge: Gauge = Gauge::default(); -/// gauge.set(42u64); -/// let _value: u64 = gauge.get(); +/// gauge.set(42); +/// let _value = gauge.get(); /// ``` /// /// ## Using [`AtomicU64`] as storage and [`f64`] on the interface @@ -40,7 +42,7 @@ use std::sync::Arc; /// ``` #[cfg(not(any(target_arch = "mips", target_arch = "powerpc")))] #[derive(Debug)] -pub struct Gauge { +pub struct Gauge { value: Arc, phantom: PhantomData, } @@ -48,7 +50,7 @@ pub struct Gauge { /// Open Metrics [`Gauge`] to record current measurements. #[cfg(any(target_arch = "mips", target_arch = "powerpc"))] #[derive(Debug)] -pub struct Gauge { +pub struct Gauge { value: Arc, phantom: PhantomData, } @@ -265,6 +267,19 @@ impl TypedMetric for Gauge { const TYPE: MetricType = MetricType::Gauge; } +impl EncodeMetric for Gauge +where + N: EncodeGaugeValue, + A: Atomic, +{ + fn encode(&self, mut encoder: MetricEncoder) -> Result<(), std::fmt::Error> { + encoder.encode_gauge(self.get()) + } + fn metric_type(&self) -> MetricType { + Self::TYPE + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/metrics/histogram.rs b/src/metrics/histogram.rs index 5922a28..66f4496 100644 --- a/src/metrics/histogram.rs +++ b/src/metrics/histogram.rs @@ -2,6 +2,8 @@ //! //! See [`Histogram`] for details. +use crate::encoding::{EncodeMetric, MetricEncoder}; + use super::{MetricType, TypedMetric}; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard}; use std::iter::{self, once}; @@ -128,6 +130,17 @@ pub fn linear_buckets(start: f64, width: f64, length: u16) -> impl Iterator Result<(), std::fmt::Error> { + let (sum, count, buckets) = self.get(); + encoder.encode_histogram::<()>(sum, count, &buckets, None) + } + + fn metric_type(&self) -> MetricType { + Self::TYPE + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/metrics/info.rs b/src/metrics/info.rs index 6201cdf..6ab127c 100644 --- a/src/metrics/info.rs +++ b/src/metrics/info.rs @@ -2,7 +2,10 @@ //! //! See [`Info`] for details. -use crate::metrics::{MetricType, TypedMetric}; +use crate::{ + encoding::{EncodeLabelSet, EncodeMetric, MetricEncoder}, + metrics::{MetricType, TypedMetric}, +}; /// Open Metrics [`Info`] metric "to expose textual information which SHOULD NOT /// change during process lifetime". @@ -25,3 +28,16 @@ impl Info { impl TypedMetric for Info { const TYPE: MetricType = MetricType::Info; } + +impl EncodeMetric for Info +where + S: Clone + std::hash::Hash + Eq + EncodeLabelSet, +{ + fn encode(&self, mut encoder: MetricEncoder) -> Result<(), std::fmt::Error> { + encoder.encode_info(&self.0) + } + + fn metric_type(&self) -> MetricType { + Self::TYPE + } +} diff --git a/src/registry.rs b/src/registry.rs index e69ad3b..39777f2 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -19,7 +19,7 @@ use std::borrow::Cow; /// users might want to use their custom types. /// /// ``` -/// # use prometheus_client::encoding::text::{encode, EncodeMetric}; +/// # use prometheus_client::encoding::text::encode; /// # use prometheus_client::metrics::counter::{Atomic as _, Counter}; /// # use prometheus_client::metrics::gauge::{Atomic as _, Gauge}; /// # use prometheus_client::registry::Registry; @@ -42,7 +42,7 @@ use std::borrow::Cow; /// ); /// /// # // Encode all metrics in the registry in the text format. -/// # let mut buffer = vec![]; +/// # let mut buffer = String::new(); /// # encode(&mut buffer, ®istry).unwrap(); /// # /// # let expected = "# HELP my_counter This is my counter.\n".to_owned() + @@ -52,9 +52,9 @@ use std::borrow::Cow; /// # "# TYPE my_gauge gauge\n" + /// # "my_gauge 0\n" + /// # "# EOF\n"; -/// # assert_eq!(expected, String::from_utf8(buffer).unwrap()); +/// # assert_eq!(expected, buffer); /// ``` -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Registry { prefix: Option, labels: Vec<(Cow<'static, str>, Cow<'static, str>)>, @@ -62,17 +62,6 @@ pub struct Registry { sub_registries: Vec, } -impl Default for Registry { - fn default() -> Self { - Self { - prefix: None, - labels: Default::default(), - metrics: Default::default(), - sub_registries: vec![], - } - } -} - impl Registry { /// Creates a new default [`Registry`] with the given prefix. pub fn with_prefix(prefix: impl Into) -> Self { @@ -169,7 +158,6 @@ impl Registry { self.metrics.push((descriptor, Box::new(metric))); } - // TODO: Update doc. /// Create a sub-registry to register metrics with a common prefix. /// /// Say you would like to prefix one set of metrics with `subsystem_a` and @@ -367,44 +355,11 @@ impl Unit { } } -// TODO: Instead of depending on dynamic dispatch for all metrics, we could use -// static dispatch for the most common ones and only fall back to dynamic -// dispatch. That said, this needs to be proven performant in a benchmark first. /// Super trait representing an abstract Prometheus metric. -#[cfg(not(feature = "protobuf"))] -pub trait Metric: - crate::encoding::text::EncodeMetric + Send + Sync + std::fmt::Debug + 'static -{ -} +pub trait Metric: crate::encoding::EncodeMetric + Send + Sync + std::fmt::Debug + 'static {} -/// Super trait representing an abstract Prometheus metric. -#[cfg(feature = "protobuf")] -pub trait Metric: - crate::encoding::text::EncodeMetric - + crate::encoding::proto::EncodeMetric - + Send - + Sync - + std::fmt::Debug - + 'static -{ -} - -#[cfg(not(feature = "protobuf"))] -impl Metric for T where - T: crate::encoding::text::EncodeMetric + Send + Sync + std::fmt::Debug + 'static -{ -} - -#[cfg(feature = "protobuf")] -impl Metric for T where - T: crate::encoding::text::EncodeMetric - + crate::encoding::proto::EncodeMetric - + Send - + Sync - + std::fmt::Debug - + 'static -{ -} +impl Metric for T where T: crate::encoding::EncodeMetric + Send + Sync + std::fmt::Debug + 'static +{} #[cfg(test)] mod tests {