From 83dd0069c8a97676361340d960e6d122d3a4cb4f Mon Sep 17 00:00:00 2001 From: Max Inden Date: Sat, 10 Dec 2022 12:43:38 +0100 Subject: [PATCH] encoding/: Adopt serde style encoding (#105) 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 --- CHANGELOG.md | 13 +- Cargo.toml | 4 +- benches/encoding/proto.rs | 46 +- benches/encoding/text.rs | 48 +- derive-encode/Cargo.toml | 3 - derive-encode/src/lib.rs | 152 ++--- derive-encode/tests/lib.rs | 62 +- examples/actix-web.rs | 19 +- examples/custom-metric.rs | 29 +- examples/hyper.rs | 26 +- examples/tide.rs | 12 +- src/encoding.rs | 555 ++++++++++++++++- src/encoding/{proto.rs => protobuf.rs} | 677 ++++++++------------- src/encoding/text.rs | 796 +++++++++++-------------- src/lib.rs | 12 +- src/metrics.rs | 13 + src/metrics/counter.rs | 16 + src/metrics/exemplar.rs | 45 +- src/metrics/family.rs | 50 +- src/metrics/gauge.rs | 84 ++- src/metrics/histogram.rs | 13 + src/metrics/info.rs | 18 +- src/registry.rs | 107 ++-- 23 files changed, 1562 insertions(+), 1238 deletions(-) rename src/encoding/{proto.rs => protobuf.rs} (56%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56ad62b..236f15d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,11 +16,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Move`Encode` trait from `prometheus_client::encoding::text` to `prometheus_client::encoding`. See [PR 83]. +- Always use dynamic dispatch on `Registry`, i.e. remove generic type parameter `M` from `Registry`. See [PR 105]. +- Refactor encoding. See [PR 105]. + - Introducing separate traits to encode + - value (e.g. `EncodeCounterValue`) + - label set (`EncodeLabelSet`), derivable for structs via `prometheus-client-derive-encode` + - label (`EncodeLabel`) + - label key (`EncodeLabelKey`) + - label value (`EncodeLabelValue`), derivable for enums via `prometheus-client-derive-encode` + - Encode as UTF-8 strings, not bytes. I.e. use `std::fmt::Write` instead of `std::io::Write`. +- Use signed integers for `Gauge` for compliance with OpenMetrics protobuf + format. See [PR 105]. [PR 83]: https://github.com/prometheus/client_rust/pull/83 [PR 85]: https://github.com/prometheus/client_rust/pull/85 [PR 96]: https://github.com/prometheus/client_rust/pull/96 +[PR 105]: https://github.com/prometheus/client_rust/pull/105 ## [0.18.1] diff --git a/Cargo.toml b/Cargo.toml index a42a237..fcad770 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.11.0", optional = true } prost-types = { version = "0.11.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..6817b65 100644 --- a/benches/encoding/proto.rs +++ b/benches/encoding/proto.rs @@ -2,68 +2,64 @@ // 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::Encode; +use prometheus_client::encoding::protobuf; use prometheus_client::metrics::counter::Counter; use prometheus_client::metrics::family::Family; use prometheus_client::metrics::histogram::{exponential_buckets, Histogram}; use prometheus_client::registry::Registry; -use std::fmt::{Display, Formatter}; +use prometheus_client_derive_encode::{EncodeLabelSet, EncodeLabelValue}; pub fn proto(c: &mut Criterion) { c.bench_function("encode", |b| { - #[derive(Clone, Hash, PartialEq, Eq, Encode)] - struct Labels { + #[derive(Clone, Hash, PartialEq, Eq, EncodeLabelSet, Debug)] + struct CounterLabels { path: String, method: Method, some_number: u64, } - #[derive(Clone, Hash, PartialEq, Eq, Encode)] + #[derive(Clone, Hash, PartialEq, Eq, EncodeLabelValue, Debug)] enum Method { Get, #[allow(dead_code)] Put, } - impl Display for Method { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Method::Get => write!(f, "Get"), - Method::Put => write!(f, "Put"), - } - } + #[derive(Clone, Hash, PartialEq, Eq, EncodeLabelSet, Debug)] + struct HistogramLabels { + region: Region, } - #[derive(Clone, Hash, PartialEq, Eq, Encode)] + #[derive(Clone, Hash, PartialEq, Eq, EncodeLabelValue, Debug)] enum Region { Africa, #[allow(dead_code)] Asia, } - let mut registry = Registry::>::default(); + let mut registry = Registry::default(); for i in 0..100 { - let counter_family = Family::::default(); - let histogram_family = Family::::new_with_constructor(|| { - Histogram::new(exponential_buckets(1.0, 2.0, 10)) - }); + let counter_family = Family::::default(); + let histogram_family = + Family::::new_with_constructor(|| { + Histogram::new(exponential_buckets(1.0, 2.0, 10)) + }); registry.register( format!("my_counter{}", i), "My counter", - Box::new(counter_family.clone()), + counter_family.clone(), ); registry.register( format!("my_histogram{}", i), "My histogram", - Box::new(histogram_family.clone()), + histogram_family.clone(), ); for j in 0_u32..100 { counter_family - .get_or_create(&Labels { + .get_or_create(&CounterLabels { path: format!("/path/{}", i), method: Method::Get, some_number: j.into(), @@ -71,13 +67,15 @@ pub fn proto(c: &mut Criterion) { .inc(); histogram_family - .get_or_create(&Region::Africa) + .get_or_create(&HistogramLabels { + region: Region::Africa, + }) .observe(j.into()); } } b.iter(|| { - let metric_set = encode(®istry); + let metric_set = protobuf::encode(®istry).unwrap(); black_box(metric_set); }) }); diff --git a/benches/encoding/text.rs b/benches/encoding/text.rs index 1ab6be5..079cf52 100644 --- a/benches/encoding/text.rs +++ b/benches/encoding/text.rs @@ -1,31 +1,30 @@ // Benchmark inspired by https://github.com/tikv/rust-prometheus/blob/ab1ca7285d3463504381a5025ae1951e020d6796/benches/text_encoder.rs use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use prometheus_client::encoding::text::{encode, EncodeMetric}; -use prometheus_client::encoding::Encode; +use prometheus_client::encoding::{self, EncodeLabelSet, EncodeLabelValue, LabelValueEncoder}; use prometheus_client::metrics::counter::Counter; use prometheus_client::metrics::family::Family; use prometheus_client::metrics::histogram::{exponential_buckets, Histogram}; use prometheus_client::registry::Registry; -use std::io::Write; +use std::fmt::Write; pub fn text(c: &mut Criterion) { c.bench_function("encode", |b| { - #[derive(Clone, Hash, PartialEq, Eq, Encode)] + #[derive(Clone, Hash, PartialEq, Eq, EncodeLabelSet, Debug)] struct Labels { method: Method, status: Status, some_number: u64, } - #[derive(Clone, Hash, PartialEq, Eq, Encode)] + #[derive(Clone, Hash, PartialEq, Eq, EncodeLabelValue, Debug)] enum Method { Get, #[allow(dead_code)] Put, } - #[derive(Clone, Hash, PartialEq, Eq)] + #[derive(Clone, Hash, PartialEq, Eq, Debug)] enum Status { Two, #[allow(dead_code)] @@ -34,34 +33,19 @@ pub fn text(c: &mut Criterion) { Five, } - impl prometheus_client::encoding::text::Encode for Status { - fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> { + impl prometheus_client::encoding::EncodeLabelValue for Status { + fn encode(&self, writer: &mut LabelValueEncoder) -> Result<(), std::fmt::Error> { let status = match self { - Status::Two => b"200", - Status::Four => b"400", - Status::Five => b"500", + Status::Two => "200", + Status::Four => "400", + Status::Five => "500", }; - writer.write_all(status)?; + writer.write_str(status)?; Ok(()) } } - #[cfg(feature = "protobuf")] - impl prometheus_client::encoding::proto::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 { - name: "status".to_string(), - value, - }); - } - } - - let mut registry = Registry::>::default(); + let mut registry = Registry::default(); for i in 0..100 { let counter_family = Family::::default(); @@ -72,12 +56,12 @@ pub fn text(c: &mut Criterion) { registry.register( format!("my_counter_{}", i), "My counter", - Box::new(counter_family.clone()), + counter_family.clone(), ); registry.register( format!("my_histogram_{}", i), "My histogram", - Box::new(histogram_family.clone()), + histogram_family.clone(), ); for j in 0u32..100 { @@ -98,10 +82,10 @@ pub fn text(c: &mut Criterion) { } } - let mut buffer = vec![]; + let mut buffer = String::new(); b.iter(|| { - encode(&mut buffer, ®istry).unwrap(); + encoding::text::encode(&mut buffer, ®istry).unwrap(); black_box(&mut buffer); }) }); 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..381c5bf 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 80144aa..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, Hash, PartialEq, Eq, Encode)] +#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelValue)] pub enum Method { Get, Post, } -#[derive(Clone, 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)) @@ -55,11 +54,9 @@ async fn main() -> std::io::Result<()> { let mut state = AppState { registry: Registry::default(), }; - state.registry.register( - "requests", - "Count of requests", - Box::new(metrics.requests.clone()), - ); + state + .registry + .register("requests", "Count of requests", metrics.requests.clone()); let state = web::Data::new(Mutex::new(state)); HttpServer::new(move || { diff --git a/examples/custom-metric.rs b/examples/custom-metric.rs index 7fcda08..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; @@ -7,31 +7,24 @@ use prometheus_client::registry::Registry; /// Related to the concept of "Custom Collectors" in other implementations. /// /// [`MyCustomMetric`] generates and encodes a random number on each scrape. +#[derive(Debug)] 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 } } @@ -45,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 404b6c1..f5a4009 100644 --- a/examples/hyper.rs +++ b/examples/hyper.rs @@ -21,7 +21,7 @@ async fn main() { registry.register( "requests", "How many requests the application has received", - Box::new(request_counter.clone()), + request_counter.clone(), ); // Spawn a server to serve the OpenMetrics endpoint. @@ -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 244db06..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, Hash, PartialEq, Eq, Encode)] +#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] struct Labels { method: Method, path: String, } -#[derive(Clone, Hash, PartialEq, Eq, Encode)] +#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelValue)] enum Method { Get, Put, @@ -58,7 +58,7 @@ enum Method { #[derive(Clone)] struct State { - registry: Arc>>, + registry: Arc, } #[derive(Default)] diff --git a/src/encoding.rs b/src/encoding.rs index 8d9a0c6..7859b79 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -2,6 +2,559 @@ 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`](crate::metrics::counter::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 56% rename from src/encoding/proto.rs rename to src/encoding/protobuf.rs index 1230ee4..554d6f1 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,34 +30,25 @@ 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() { let mut family = openmetrics_data_model::MetricFamily { name: desc.name().to_string(), r#type: { - let metric_type: openmetrics_data_model::MetricType = metric.metric_type().into(); + let metric_type: openmetrics_data_model::MetricType = + super::EncodeMetric::metric_type(metric.as_ref()).into(); metric_type as i32 }, unit: if let Some(unit) = desc.unit() { @@ -70,13 +61,25 @@ where }; let mut labels = vec![]; - desc.labels().encode(&mut labels); - metric.encode(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 { @@ -91,444 +94,286 @@ 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() + }], + }); -/// Trait combining [`EncodeMetric`] and [`Send`]. -pub trait SendEncodeMetric: EncodeMetric + Send {} + Ok(()) + } -impl SendEncodeMetric for T {} + 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)?; -/// 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); -} + 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 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(), - } + Ok(()) } -} -impl EncodeLabels for f64 { - 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 u64 { - fn encode(&self, labels: &mut Vec) { - labels.push(openmetrics_data_model::Label { - name: self.to_string(), - value: self.to_string(), - }) + Ok(()) } -} -impl EncodeLabels for u32 { - fn encode(&self, labels: &mut Vec) { - labels.push(openmetrics_data_model::Label { - name: self.to_string(), - value: self.to_string(), - }) - } -} + pub fn encode_family<'b, S: EncodeLabelSet>( + &'b mut self, + label_set: &S, + ) -> Result, std::fmt::Error> { + label_set.encode( + LabelSetEncoder { + labels: self.labels, + } + .into(), + )?; -impl EncodeLabels for String { - fn encode(&self, labels: &mut Vec) { - labels.push(openmetrics_data_model::Label { - name: self.clone(), - value: self.clone(), + Ok(MetricEncoder { + metric_type: self.metric_type, + family: self.family, + labels: self.labels, }) } -} -impl EncodeLabels for Vec -where - for<'a> &'a T: Into, -{ - fn encode(&self, labels: &mut Vec) { - self.as_slice().encode(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 + .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()?, + }) + }) + .collect::, std::fmt::Error>>()?; + + 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 + fn try_from(exemplar: &Exemplar) -> Result { + let mut value = f64::default(); + exemplar + .value + .encode(ExemplarValueEncoder { value: &mut value }.into())?; -/// 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; -} + let mut labels = vec![]; + exemplar.label_set.encode( + LabelSetEncoder { + labels: &mut labels, + } + .into(), + )?; -impl EncodeCounterValue for u64 { - fn encode(&self) -> openmetrics_data_model::counter_value::Total { - openmetrics_data_model::counter_value::Total::IntValue(*self) + Ok(openmetrics_data_model::Exemplar { + value, + timestamp: Default::default(), + label: labels, + }) } } -impl EncodeCounterValue for f64 { - fn encode(&self) -> openmetrics_data_model::counter_value::Total { - openmetrics_data_model::counter_value::Total::DoubleValue(*self) - } +#[derive(Debug)] +pub(crate) struct GaugeValueEncoder<'a> { + value: &'a mut openmetrics_data_model::gauge_value::Value, } -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); +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 { - Self::TYPE + pub fn encode_f64(&mut self, v: f64) -> Result<(), std::fmt::Error> { + *self.value = openmetrics_data_model::gauge_value::Value::DoubleValue(v); + Ok(()) } } -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); - } - - fn metric_type(&self) -> MetricType { - Counter::::TYPE - } +#[derive(Debug)] +pub(crate) struct ExemplarValueEncoder<'a> { + value: &'a mut f64, } -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() +impl<'a> ExemplarValueEncoder<'a> { + pub fn encode(&mut self, v: f64) -> Result<(), std::fmt::Error> { + *self.value = v; + Ok(()) } } -///////////////////////////////////////////////////////////////////////////////// -// 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; -} - -// 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 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 u64 { - fn encode(&self) -> openmetrics_data_model::gauge_value::Value { - openmetrics_data_model::gauge_value::Value::IntValue(*self as i64) - } +#[derive(Debug)] +pub(crate) struct CounterValueEncoder<'a> { + value: &'a mut openmetrics_data_model::counter_value::Total, } -impl EncodeGaugeValue for f64 { - fn encode(&self) -> openmetrics_data_model::gauge_value::Value { - openmetrics_data_model::gauge_value::Value::DoubleValue(*self) +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(()) } -} -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) - } - - 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) } } @@ -552,7 +397,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); @@ -582,7 +427,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); @@ -611,7 +456,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); @@ -633,7 +478,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); @@ -656,7 +501,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); @@ -672,7 +517,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); @@ -704,7 +549,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); @@ -749,7 +594,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); @@ -787,7 +632,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); @@ -820,7 +665,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); @@ -850,7 +695,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 = @@ -858,12 +703,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())]) @@ -873,14 +714,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(); @@ -889,15 +730,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 @@ -910,15 +747,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); } @@ -929,7 +766,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 754b08a..5d5e337 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -14,600 +14,482 @@ //! # 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, - M: EncodeMetric, { 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" ")?; - metric.metric_type().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(); - metric.encode(encoder)?; + 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`](crate::metrics::family::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))?; - self.encode_labels() + if let Some(exemplar) = exemplars.and_then(|e| e.get(&i)) { + self.encode_exemplar(exemplar)? + } + + self.newline()?; + } + + 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(()) } - // 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 write_suffix(&mut self, suffix: &'static str) -> Result<(), std::fmt::Error> { + self.writer.write_str("_")?; + self.writer.write_str(suffix)?; - if !self.const_labels.is_empty() { - self.writer.write_all(b"{")?; - opened_curly_brackets = true; + Ok(()) + } - self.const_labels.encode(self.writer)?; + // TODO: Consider caching the encoded labels for Histograms as they stay the + // same but are currently encoded multiple times. + 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 let Some(labels) = &self.labels { - if opened_curly_brackets { - self.writer.write_all(b",")?; - } else { - opened_curly_brackets = true; - self.writer.write_all(b"{")?; - } - labels.encode(self.writer)?; - } + self.writer.write_str("{")?; - Ok(BucketEncoder { - opened_curly_brackets, - writer: self.writer, - }) - } + self.const_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(additional_labels) = additional_labels { + if !self.const_labels.is_empty() { + self.writer.write_str(",")?; + } - Encoder { - writer: self.writer, - name: self.name, - unit: self.unit, - const_labels: self.const_labels, - labels: Some(label_set), + additional_labels.encode(LabelSetEncoder::new(self.writer).into())?; } - } -} -/// Used to encode an OpenMetrics Histogram bucket. -#[allow(missing_debug_implementations)] -#[must_use] -pub struct BucketEncoder<'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"{")?; - } + if let Some(labels) = &self.family_labels { + if !self.const_labels.is_empty() || additional_labels.is_some() { + self.writer.write_str(",")?; + } - self.writer.write_all(b"le=\"")?; - if upper_bound == f64::MAX { - self.writer.write_all(b"+Inf")?; - } else { - upper_bound.encode(self.writer)?; + labels.encode(LabelSetEncoder::new(self.writer).into())?; } - self.writer.write_all(b"\"}")?; - Ok(ValueEncoder { - writer: self.writer, - }) - } + self.writer.write_str("}")?; - /// 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, - }) + Ok(()) } } -/// Used to encode an OpenMetrics metric value. -#[allow(missing_debug_implementations)] -#[must_use] -pub struct ValueEncoder<'a> { +pub(crate) struct CounterValueEncoder<'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 CounterValueEncoder<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CounterValueEncoder").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> 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 exemplar. - pub fn no_exemplar(&mut self) -> Result<(), std::io::Error> { - self.writer.write_all(b"\n")?; + 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(()) } } -/// 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 GaugeValueEncoder<'a> { + writer: &'a mut dyn Write, } -impl EncodeMetric for Box { - fn encode(&self, encoder: Encoder) -> Result<(), std::io::Error> { - self.deref().encode(encoder) - } - - fn metric_type(&self) -> MetricType { - self.deref().metric_type() +impl<'a> std::fmt::Debug for GaugeValueEncoder<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GaugeValueEncoder").finish() } } -/// Trait combining [`EncodeMetric`], [`Send`] and [`Sync`]. -pub trait SendSyncEncodeMetric: EncodeMetric + Send + Sync {} - -impl SendSyncEncodeMetric for T {} - -impl EncodeMetric for Box { - fn encode(&self, encoder: Encoder) -> Result<(), std::io::Error> { - self.deref().encode(encoder) +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(()) } - fn metric_type(&self) -> MetricType { - self.deref().metric_type() + 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(()) } } -///////////////////////////////////////////////////////////////////////////////// -// Counter +pub(crate) struct ExemplarValueEncoder<'a> { + writer: &'a mut dyn Write, +} -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) +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::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)) } } -// 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) - } +pub(crate) struct LabelSetEncoder<'a> { + writer: &'a mut dyn Write, + first: bool, +} - fn metric_type(&self) -> MetricType { - Counter::::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() } } -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()?, +impl<'a> LabelSetEncoder<'a> { + fn new(writer: &'a mut dyn Write) -> Self { + Self { + writer, + first: true, + } } - Ok(()) + pub fn encode_label(&mut self) -> LabelEncoder { + let first = self.first; + self.first = false; + LabelEncoder { + writer: self.writer, + first, + } + } } -///////////////////////////////////////////////////////////////////////////////// -// 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()?; +pub(crate) struct LabelEncoder<'a> { + writer: &'a mut dyn Write, + first: bool, +} - 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; @@ -617,11 +499,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] @@ -630,7 +512,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() @@ -638,16 +520,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", @@ -656,20 +538,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] @@ -678,11 +560,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] @@ -698,11 +580,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] @@ -721,7 +603,7 @@ mod tests { ]) .inc(); - let mut encoded = Vec::new(); + let mut encoded = String::new(); encode(&mut encoded, ®istry).unwrap(); @@ -730,9 +612,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] @@ -741,16 +623,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] @@ -760,11 +642,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] @@ -780,11 +662,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] @@ -792,9 +674,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() @@ -813,9 +695,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) { @@ -841,7 +723,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 dbc70f3..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, 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, Hash, PartialEq, Eq, Encode)] +//! #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelValue)] //! enum Method { //! GET, //! PUT, @@ -54,7 +54,7 @@ //! "http_requests", //! // And the metric help text. //! "Number of HTTP requests received", -//! Box::new(http_requests.clone()), +//! http_requests.clone(), //! ); //! //! // Somewhere in your business logic record a single HTTP GET request. @@ -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 bb16376..bd49015 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, RwLockWriteGuard}; use std::collections::HashMap; @@ -41,23 +43,24 @@ 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 /// -/// Using `Encode` derive macro to generate -/// [`Encode`](crate::encoding::text::Encode) implementation. +/// Using `EncodeLabelSet` and `EncodeLabelValue` derive macro to generate +/// [`EncodeLabelSet`] for `struct`s and +/// [`EncodeLabelValue`](crate::encoding::EncodeLabelValue) for `enum`s. /// /// ``` -/// # 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 +68,12 @@ use std::sync::Arc; /// # use std::io::Write; /// # /// # let mut registry = Registry::default(); -/// #[derive(Clone, Hash, PartialEq, Eq, Encode)] +/// #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] /// struct Labels { /// method: Method, /// }; /// -/// #[derive(Clone, Hash, PartialEq, Eq, Encode)] +/// #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelValue)] /// enum Method { /// GET, /// PUT, @@ -87,17 +90,16 @@ 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. -#[derive(Debug)] pub struct Family M> { metrics: Arc>>, /// Function that when called constructs a new metric. @@ -111,6 +113,14 @@ pub struct Family M> { constructor: C, } +impl std::fmt::Debug for Family { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Family") + .field("metrics", &self.metrics) + .finish() + } +} + /// A constructor for creating new metrics in a [`Family`] when calling /// [`Family::get_or_create`]. Such constructor is provided via /// [`Family::new_with_constructor`]. @@ -296,6 +306,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..86b4503 100644 --- a/src/metrics/gauge.rs +++ b/src/metrics/gauge.rs @@ -2,11 +2,13 @@ //! //! See [`Gauge`] for details. +use crate::encoding::{EncodeGaugeValue, EncodeMetric, MetricEncoder}; + use super::{MetricType, TypedMetric}; use std::marker::PhantomData; +use std::sync::atomic::{AtomicI32, Ordering}; #[cfg(not(any(target_arch = "mips", target_arch = "powerpc")))] use std::sync::atomic::{AtomicI64, AtomicU64}; -use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::Arc; /// Open Metrics [`Gauge`] to record current measurements. @@ -15,18 +17,18 @@ use std::sync::Arc; /// /// [`Gauge`] is generic over the actual data type tracking the [`Gauge`] state /// as well as the data type used to interact with the [`Gauge`]. Out of -/// convenience the generic type parameters are set to use an [`AtomicU64`] as a -/// storage and [`u64`] on the interface by default. +/// convenience the generic type parameters are set to use an [`AtomicI64`] as a +/// storage and [`i64`] on the interface by default. /// /// # Examples /// -/// ## Using [`AtomicU64`] as storage and [`u64`] on the interface +/// ## Using [`AtomicI64`] as storage and [`i64`] on the interface /// /// ``` /// # 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, } @@ -133,54 +135,54 @@ pub trait Atomic { } #[cfg(not(any(target_arch = "mips", target_arch = "powerpc")))] -impl Atomic for AtomicU64 { - fn inc(&self) -> u64 { +impl Atomic for AtomicI64 { + fn inc(&self) -> i64 { self.inc_by(1) } - fn inc_by(&self, v: u64) -> u64 { + fn inc_by(&self, v: i64) -> i64 { self.fetch_add(v, Ordering::Relaxed) } - fn dec(&self) -> u64 { + fn dec(&self) -> i64 { self.dec_by(1) } - fn dec_by(&self, v: u64) -> u64 { + fn dec_by(&self, v: i64) -> i64 { self.fetch_sub(v, Ordering::Relaxed) } - fn set(&self, v: u64) -> u64 { + fn set(&self, v: i64) -> i64 { self.swap(v, Ordering::Relaxed) } - fn get(&self) -> u64 { + fn get(&self) -> i64 { self.load(Ordering::Relaxed) } } -impl Atomic for AtomicU32 { - fn inc(&self) -> u32 { +impl Atomic for AtomicI32 { + fn inc(&self) -> i32 { self.inc_by(1) } - fn inc_by(&self, v: u32) -> u32 { + fn inc_by(&self, v: i32) -> i32 { self.fetch_add(v, Ordering::Relaxed) } - fn dec(&self) -> u32 { + fn dec(&self) -> i32 { self.dec_by(1) } - fn dec_by(&self, v: u32) -> u32 { + fn dec_by(&self, v: i32) -> i32 { self.fetch_sub(v, Ordering::Relaxed) } - fn set(&self, v: u32) -> u32 { + fn set(&self, v: i32) -> i32 { self.swap(v, Ordering::Relaxed) } - fn get(&self) -> u32 { + fn get(&self) -> i32 { self.load(Ordering::Relaxed) } } @@ -234,37 +236,23 @@ impl Atomic for AtomicU64 { } } -#[cfg(not(any(target_arch = "mips", target_arch = "powerpc")))] -impl Atomic for AtomicI64 { - fn inc(&self) -> i64 { - self.inc_by(1) - } - - fn inc_by(&self, v: i64) -> i64 { - self.fetch_add(v, Ordering::Relaxed) - } - - fn dec(&self) -> i64 { - self.dec_by(1) - } - - fn dec_by(&self, v: i64) -> i64 { - self.fetch_sub(v, Ordering::Relaxed) - } +impl TypedMetric for Gauge { + const TYPE: MetricType = MetricType::Gauge; +} - fn set(&self, v: i64) -> i64 { - self.swap(v, Ordering::Relaxed) +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 get(&self) -> i64 { - self.load(Ordering::Relaxed) + fn metric_type(&self) -> MetricType { + Self::TYPE } } -impl TypedMetric for Gauge { - const TYPE: MetricType = MetricType::Gauge; -} - #[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 b88dc46..7badf73 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -19,16 +19,13 @@ 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; /// # /// // Create a metric registry. -/// // -/// // Note the angle brackets to make sure to use the default (dynamic -/// // dispatched boxed metric) for the generic type parameter. -/// let mut registry = ::default(); +/// let mut registry = Registry::default(); /// /// let counter: Counter = Counter::default(); /// let gauge: Gauge = Gauge::default(); @@ -36,16 +33,16 @@ use std::borrow::Cow; /// registry.register( /// "my_counter", /// "This is my counter", -/// Box::new(counter.clone()), +/// counter.clone(), /// ); /// registry.register( /// "my_gauge", /// "This is my gauge", -/// Box::new(gauge.clone()), +/// gauge.clone(), /// ); /// /// # // 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() + @@ -55,28 +52,17 @@ 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)] -pub struct Registry> { +#[derive(Debug, Default)] +pub struct Registry { prefix: Option, labels: Vec<(Cow<'static, str>, Cow<'static, str>)>, - metrics: Vec<(Descriptor, M)>, - sub_registries: Vec>, -} - -impl Default for Registry { - fn default() -> Self { - Self { - prefix: None, - labels: Default::default(), - metrics: Default::default(), - sub_registries: vec![], - } - } + metrics: Vec<(Descriptor, Box)>, + sub_registries: Vec, } -impl Registry { +impl Registry { /// Creates a new default [`Registry`] with the given prefix. pub fn with_prefix(prefix: impl Into) -> Self { Self { @@ -103,12 +89,17 @@ impl Registry { /// # use prometheus_client::metrics::counter::{Atomic as _, Counter}; /// # use prometheus_client::registry::{Registry, Unit}; /// # - /// let mut registry: Registry = Registry::default(); - /// let counter = Counter::default(); + /// let mut registry = Registry::default(); + /// let counter: Counter = Counter::default(); /// /// registry.register("my_counter", "This is my counter", counter.clone()); /// ``` - pub fn register, H: Into>(&mut self, name: N, help: H, metric: M) { + pub fn register, H: Into>( + &mut self, + name: N, + help: H, + metric: impl Metric, + ) { self.priv_register(name, help, metric, None) } @@ -124,8 +115,8 @@ impl Registry { /// # use prometheus_client::metrics::counter::{Atomic as _, Counter}; /// # use prometheus_client::registry::{Registry, Unit}; /// # - /// let mut registry: Registry = Registry::default(); - /// let counter = Counter::default(); + /// let mut registry = Registry::default(); + /// let counter: Counter = Counter::default(); /// /// registry.register_with_unit( /// "my_counter", @@ -139,7 +130,7 @@ impl Registry { name: N, help: H, unit: Unit, - metric: M, + metric: impl Metric, ) { self.priv_register(name, help, metric, Some(unit)) } @@ -148,7 +139,7 @@ impl Registry { &mut self, name: N, help: H, - metric: M, + metric: impl Metric, unit: Option, ) { let name = name.into(); @@ -164,10 +155,9 @@ impl Registry { labels: self.labels.clone(), }; - self.metrics.push((descriptor, metric)); + 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 @@ -183,17 +173,17 @@ impl Registry { /// # use prometheus_client::metrics::counter::{Atomic as _, Counter}; /// # use prometheus_client::registry::{Registry, Unit}; /// # - /// let mut registry: Registry = Registry::default(); + /// let mut registry = Registry::default(); /// - /// let subsystem_a_counter_1 = Counter::default(); - /// let subsystem_a_counter_2 = Counter::default(); + /// let subsystem_a_counter_1: Counter = Counter::default(); + /// let subsystem_a_counter_2: Counter = Counter::default(); /// /// let subsystem_a_registry = registry.sub_registry_with_prefix("subsystem_a"); /// registry.register("counter_1", "", subsystem_a_counter_1.clone()); /// registry.register("counter_2", "", subsystem_a_counter_2.clone()); /// - /// let subsystem_b_counter_1 = Counter::default(); - /// let subsystem_b_counter_2 = Counter::default(); + /// let subsystem_b_counter_1: Counter = Counter::default(); + /// let subsystem_b_counter_2: Counter = Counter::default(); /// /// let subsystem_a_registry = registry.sub_registry_with_prefix("subsystem_b"); /// registry.register("counter_1", "", subsystem_b_counter_1.clone()); @@ -239,7 +229,7 @@ impl Registry { } /// [`Iterator`] over all metrics registered with the [`Registry`]. - pub fn iter(&self) -> RegistryIterator { + pub fn iter(&self) -> RegistryIterator { let metrics = self.metrics.iter(); let sub_registries = self.sub_registries.iter(); RegistryIterator { @@ -253,14 +243,14 @@ impl Registry { /// Iterator iterating both the metrics registered directly with the registry as /// well as all metrics registered with sub-registries. #[derive(Debug)] -pub struct RegistryIterator<'a, M> { - metrics: std::slice::Iter<'a, (Descriptor, M)>, - sub_registries: std::slice::Iter<'a, Registry>, - sub_registry: Option>>, +pub struct RegistryIterator<'a> { + metrics: std::slice::Iter<'a, (Descriptor, Box)>, + sub_registries: std::slice::Iter<'a, Registry>, + sub_registry: Option>>, } -impl<'a, M> Iterator for RegistryIterator<'a, M> { - type Item = &'a (Descriptor, M); +impl<'a> Iterator for RegistryIterator<'a> { + type Item = &'a (Descriptor, Box); fn next(&mut self) -> Option { if let Some(metric) = self.metrics.next() { @@ -365,6 +355,12 @@ impl Unit { } } +/// Super trait representing an abstract Prometheus metric. +pub trait Metric: crate::encoding::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 { use super::*; @@ -372,8 +368,8 @@ mod tests { #[test] fn register_and_iterate() { - let mut registry: Registry = Registry::default(); - let counter = Counter::default(); + let mut registry = Registry::default(); + let counter: Counter = Counter::default(); registry.register("my_counter", "My counter", counter); assert_eq!(1, registry.iter().count()) @@ -382,28 +378,29 @@ mod tests { #[test] fn sub_registry_with_prefix_and_label() { let top_level_metric_name = "my_top_level_metric"; - let mut registry = Registry::::default(); - registry.register(top_level_metric_name, "some help", Default::default()); + let mut registry = Registry::default(); + let counter: Counter = Counter::default(); + registry.register(top_level_metric_name, "some help", counter.clone()); let prefix_1 = "prefix_1"; let prefix_1_metric_name = "my_prefix_1_metric"; let sub_registry = registry.sub_registry_with_prefix(prefix_1); - sub_registry.register(prefix_1_metric_name, "some help", Default::default()); + sub_registry.register(prefix_1_metric_name, "some help", counter.clone()); let prefix_1_1 = "prefix_1_1"; let prefix_1_1_metric_name = "my_prefix_1_1_metric"; let sub_sub_registry = sub_registry.sub_registry_with_prefix(prefix_1_1); - sub_sub_registry.register(prefix_1_1_metric_name, "some help", Default::default()); + sub_sub_registry.register(prefix_1_1_metric_name, "some help", counter.clone()); let label_1_2 = (Cow::Borrowed("registry"), Cow::Borrowed("1_2")); let prefix_1_2_metric_name = "my_prefix_1_2_metric"; let sub_sub_registry = sub_registry.sub_registry_with_label(label_1_2.clone()); - sub_sub_registry.register(prefix_1_2_metric_name, "some help", Default::default()); + sub_sub_registry.register(prefix_1_2_metric_name, "some help", counter.clone()); let prefix_1_2_1 = "prefix_1_2_1"; let prefix_1_2_1_metric_name = "my_prefix_1_2_1_metric"; let sub_sub_sub_registry = sub_sub_registry.sub_registry_with_prefix(prefix_1_2_1); - sub_sub_sub_registry.register(prefix_1_2_1_metric_name, "some help", Default::default()); + sub_sub_sub_registry.register(prefix_1_2_1_metric_name, "some help", counter.clone()); let prefix_2 = "prefix_2"; let _ = registry.sub_registry_with_prefix(prefix_2); @@ -411,7 +408,7 @@ mod tests { let prefix_3 = "prefix_3"; let prefix_3_metric_name = "my_prefix_3_metric"; let sub_registry = registry.sub_registry_with_prefix(prefix_3); - sub_registry.register(prefix_3_metric_name, "some help", Default::default()); + sub_registry.register(prefix_3_metric_name, "some help", counter); let mut metric_iter = registry .iter()