diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 7d00d45d..d9fdb9b1 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -39,7 +39,7 @@ jobs: - uses: actions-rs/cargo@v1 with: command: test - args: --benches + args: --benches --all-features test: name: Test Suite @@ -73,7 +73,7 @@ jobs: - uses: actions-rs/cargo@v1 with: command: test - args: --all + args: --all --all-features fmt: name: Rustfmt @@ -135,7 +135,7 @@ jobs: RUSTDOCFLAGS: "--deny broken_intra_doc_links" with: command: doc - args: --verbose --workspace --no-deps --document-private-items + args: --verbose --workspace --no-deps --document-private-items --all-features cross-compile: name: Cross compile @@ -159,4 +159,4 @@ jobs: with: use-cross: true command: build - args: --release --target=${{ matrix.target }} + args: --release --target=${{ matrix.target }} --all-features diff --git a/CHANGELOG.md b/CHANGELOG.md index 18229832..901b2c91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.19.0] - unreleased + +### Added +- Added support for the OpenMetrics protobuf format. See [PR 83]. + +### Changed + +- Move`Encode` trait from `prometheus_client::encoding::text` to `prometheus_client::encoding`. See [PR 83]. + +[PR 83]: https://github.com/prometheus/client_rust/pull/83 + ## [0.18.0] ### Changed diff --git a/Cargo.toml b/Cargo.toml index 647b61e1..eef8be6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "prometheus-client" -version = "0.18.0" +version = "0.19.0" authors = ["Max Inden "] edition = "2021" description = "Open Metrics client library allowing users to natively instrument applications." @@ -10,14 +10,20 @@ repository = "https://github.com/prometheus/client_rust" 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"] + [workspace] -members = ["derive-text-encode"] +members = ["derive-encode"] [dependencies] dtoa = "1.0" itoa = "1.0" parking_lot = "0.12" -prometheus-client-derive-text-encode = { version = "0.3.0", path = "derive-text-encode" } +prometheus-client-derive-encode = { version = "0.3.0", path = "derive-encode" } +prost = { version = "0.9.0", optional = true } +prost-types = { version = "0.9.0", optional = true } +void = { version = "1.0", optional = true } [dev-dependencies] async-std = { version = "1", features = ["attributes"] } @@ -29,6 +35,9 @@ rand = "0.8.4" tide = "0.16" actix-web = "4" +[build-dependencies] +prost-build = { version = "0.9.0", optional = true } + [[bench]] name = "family" harness = false @@ -37,3 +46,10 @@ harness = false name = "text" path = "benches/encoding/text.rs" harness = false +required-features = [] + +[[bench]] +name = "proto" +path = "benches/encoding/proto.rs" +harness = false +required-features = ["protobuf"] diff --git a/benches/encoding/proto.rs b/benches/encoding/proto.rs new file mode 100644 index 00000000..4de33a1a --- /dev/null +++ b/benches/encoding/proto.rs @@ -0,0 +1,87 @@ +// Benchmark inspired by +// 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::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}; + +pub fn proto(c: &mut Criterion) { + c.bench_function("encode", |b| { + #[derive(Clone, Hash, PartialEq, Eq, Encode)] + struct Labels { + path: String, + method: Method, + some_number: u64, + } + + #[derive(Clone, Hash, PartialEq, Eq, Encode)] + 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, Encode)] + enum Region { + Africa, + #[allow(dead_code)] + Asia, + } + + 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)) + }); + + registry.register( + format!("my_counter{}", i), + "My counter", + Box::new(counter_family.clone()), + ); + registry.register( + format!("my_histogram{}", i), + "My histogram", + Box::new(histogram_family.clone()), + ); + + for j in 0_u32..100 { + counter_family + .get_or_create(&Labels { + path: format!("/path/{}", i), + method: Method::Get, + some_number: j.into(), + }) + .inc(); + + histogram_family + .get_or_create(&Region::Africa) + .observe(j.into()); + } + } + + b.iter(|| { + let metric_set = encode(®istry); + black_box(metric_set); + }) + }); +} + +criterion_group!(benches, proto); +criterion_main!(benches); diff --git a/benches/encoding/text.rs b/benches/encoding/text.rs index 78c2c4f3..1ab6be52 100644 --- a/benches/encoding/text.rs +++ b/benches/encoding/text.rs @@ -1,7 +1,8 @@ // 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, Encode, EncodeMetric}; +use prometheus_client::encoding::text::{encode, EncodeMetric}; +use prometheus_client::encoding::Encode; use prometheus_client::metrics::counter::Counter; use prometheus_client::metrics::family::Family; use prometheus_client::metrics::histogram::{exponential_buckets, Histogram}; @@ -33,7 +34,7 @@ pub fn text(c: &mut Criterion) { Five, } - impl Encode for Status { + impl prometheus_client::encoding::text::Encode for Status { fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> { let status = match self { Status::Two => b"200", @@ -45,6 +46,21 @@ pub fn text(c: &mut Criterion) { } } + #[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(); for i in 0..100 { diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..d10b0197 --- /dev/null +++ b/build.rs @@ -0,0 +1,11 @@ +use std::io::Result; + +fn main() -> Result<()> { + #[cfg(feature = "protobuf")] + prost_build::compile_protos( + &["src/encoding/proto/openmetrics_data_model.proto"], + &["src/encoding/proto/"], + )?; + + Ok(()) +} diff --git a/derive-text-encode/Cargo.toml b/derive-encode/Cargo.toml similarity index 68% rename from derive-text-encode/Cargo.toml rename to derive-encode/Cargo.toml index f243c24e..9762fd3c 100644 --- a/derive-text-encode/Cargo.toml +++ b/derive-encode/Cargo.toml @@ -1,14 +1,17 @@ [package] -name = "prometheus-client-derive-text-encode" -version = "0.3.0" +name = "prometheus-client-derive-encode" +version = "0.3.1" authors = ["Max Inden "] edition = "2021" -description = "Auxiliary crate to derive text Encode trait from prometheus-client." +description = "Auxiliary crate to derive Encode trait from prometheus-client." license = "Apache-2.0 OR MIT" 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] @@ -17,7 +20,7 @@ quote = "1" syn = "1" [dev-dependencies] -prometheus-client = { path = "../" } +prometheus-client = { path = "../", features = ["protobuf"] } [lib] proc-macro = true \ No newline at end of file diff --git a/derive-text-encode/src/lib.rs b/derive-encode/src/lib.rs similarity index 53% rename from derive-text-encode/src/lib.rs rename to derive-encode/src/lib.rs index 186aae43..4612ecc9 100644 --- a/derive-text-encode/src/lib.rs +++ b/derive-encode/src/lib.rs @@ -10,7 +10,7 @@ pub fn derive_encode(input: TokenStream) -> TokenStream { let ast: DeriveInput = syn::parse(input).unwrap(); let name = &ast.ident; - let body = match ast.data { + let body = match ast.clone().data { syn::Data::Struct(s) => match s.fields { syn::Fields::Named(syn::FieldsNamed { named, .. }) => named .into_iter() @@ -70,9 +70,94 @@ 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 { + 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![]; + self.#ident.encode(&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."), + }, + 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); + } + } + }) + .collect(); + + quote! { + impl prometheus_client::encoding::proto::EncodeLabels for #name { + fn encode(&self, labels: &mut Vec) { + match self { + #match_arms + }; + } + } + } + } + syn::Data::Union(_) => panic!("Can not derive Encode for union."), + } +} + // Copied from https://github.com/djc/askama (MIT and APACHE licensed) and // modified. static KEYWORD_IDENTIFIERS: [(&str, &str); 48] = [ diff --git a/derive-encode/tests/lib.rs b/derive-encode/tests/lib.rs new file mode 100644 index 00000000..7056aa25 --- /dev/null +++ b/derive-encode/tests/lib.rs @@ -0,0 +1,130 @@ +use prometheus_client::encoding::text::encode; +use prometheus_client::encoding::Encode; +use prometheus_client::metrics::counter::Counter; +use prometheus_client::metrics::family::Family; +use prometheus_client::registry::Registry; + +#[derive(Clone, Hash, PartialEq, Eq, Encode)] +struct Labels { + method: Method, + path: String, +} + +#[derive(Clone, Hash, PartialEq, Eq, Encode)] +enum Method { + Get, + #[allow(dead_code)] + Put, +} + +#[test] +fn basic_flow() { + let mut registry = Registry::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(&Labels { + method: Method::Get, + path: "/metrics".to_string(), + }) + .inc(); + + // Encode all metrics in the registry in the text format. + let mut buffer = vec![]; + 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()); +} + +#[cfg(feature = "protobuf")] +mod protobuf { + use crate::{Labels, Method}; + use prometheus_client::encoding::proto::encode; + use prometheus_client::metrics::counter::Counter; + use prometheus_client::metrics::family::Family; + use prometheus_client::registry::Registry; + + #[test] + fn structs() { + let mut registry = Registry::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(&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 = + metric_set.metric_families.pop().unwrap(); + let metric: prometheus_client::encoding::proto::Metric = family.metrics.pop().unwrap(); + + let method = &metric.labels[0]; + assert_eq!("method", method.name); + assert_eq!("Get", method.value); + + let path = &metric.labels[1]; + assert_eq!("path", path.name); + assert_eq!("/metrics", path.value); + } + + #[test] + fn enums() { + let mut registry = Registry::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(); + + // 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 = + metric_set.metric_families.pop().unwrap(); + let metric: prometheus_client::encoding::proto::Metric = family.metrics.pop().unwrap(); + + let label = &metric.labels[0]; + assert_eq!("Method", label.name); + assert_eq!("Get", label.value); + } +} + +#[test] +fn remap_keyword_identifiers() { + #[derive(Encode, Hash, Clone, Eq, PartialEq)] + 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) + // as `type` is a keyword. + // + // Test makes sure `r#type` is replaced by `type` in the OpenMetrics + // output. + r#type: u64, + } + + let labels = Labels { r#type: 42 }; + + let mut buffer = vec![]; + + { + use prometheus_client::encoding::text::Encode; + labels.encode(&mut buffer).unwrap(); + } + + assert_eq!( + "type=\"42\"".to_string(), + String::from_utf8(buffer).unwrap() + ); +} diff --git a/derive-text-encode/tests/lib.rs b/derive-text-encode/tests/lib.rs deleted file mode 100644 index 2e4cbbfc..00000000 --- a/derive-text-encode/tests/lib.rs +++ /dev/null @@ -1,67 +0,0 @@ -use prometheus_client::encoding::text::{encode, Encode}; -use prometheus_client::metrics::counter::Counter; -use prometheus_client::metrics::family::Family; -use prometheus_client::registry::Registry; - -#[test] -fn basic_flow() { - let mut registry = Registry::default(); - #[derive(Clone, Hash, PartialEq, Eq, Encode)] - struct Labels { - method: Method, - path: String, - } - - #[derive(Clone, Hash, PartialEq, Eq, Encode)] - enum Method { - Get, - #[allow(dead_code)] - Put, - } - - let family = Family::::default(); - registry.register("my_counter", "This is my counter", family.clone()); - - // Record a single HTTP GET request. - family - .get_or_create(&Labels { - method: Method::Get, - path: "/metrics".to_string(), - }) - .inc(); - - // Encode all metrics in the registry in the text format. - let mut buffer = vec![]; - 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()); -} - -#[test] -fn remap_keyword_identifiers() { - #[derive(Encode, Hash, Clone, Eq, PartialEq)] - 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) - // as `type` is a keyword. - // - // Test makes sure `r#type` is replaced by `type` in the OpenMetrics - // output. - r#type: u64, - } - - let labels = Labels { r#type: 42 }; - - let mut buffer = vec![]; - - labels.encode(&mut buffer).unwrap(); - - assert_eq!( - "type=\"42\"".to_string(), - String::from_utf8(buffer).unwrap() - ); -} diff --git a/examples/actix-web.rs b/examples/actix-web.rs index 20fae762..80144aa2 100644 --- a/examples/actix-web.rs +++ b/examples/actix-web.rs @@ -1,7 +1,8 @@ use std::sync::Mutex; use actix_web::{web, App, HttpResponse, HttpServer, Responder, Result}; -use prometheus_client::encoding::text::{encode, Encode}; +use prometheus_client::encoding::text::encode; +use prometheus_client::encoding::Encode; use prometheus_client::metrics::counter::Counter; use prometheus_client::metrics::family::Family; use prometheus_client::registry::Registry; diff --git a/examples/tide.rs b/examples/tide.rs index dfb68125..244db06d 100644 --- a/examples/tide.rs +++ b/examples/tide.rs @@ -1,4 +1,5 @@ -use prometheus_client::encoding::text::{encode, Encode}; +use prometheus_client::encoding::text::encode; +use prometheus_client::encoding::Encode; use prometheus_client::metrics::counter::Counter; use prometheus_client::metrics::family::Family; use prometheus_client::registry::Registry; diff --git a/src/encoding.rs b/src/encoding.rs index 07149d48..8d9a0c68 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -1,3 +1,7 @@ //! Exposition format implementations. +pub use prometheus_client_derive_encode::*; + +#[cfg(feature = "protobuf")] +pub mod proto; pub mod text; diff --git a/src/encoding/proto.rs b/src/encoding/proto.rs new file mode 100644 index 00000000..1230ee4b --- /dev/null +++ b/src/encoding/proto.rs @@ -0,0 +1,980 @@ +//! Open Metrics protobuf implementation. +//! +//! ``` +//! # use prometheus_client::encoding::proto::encode; +//! # use prometheus_client::metrics::counter::Counter; +//! # use prometheus_client::registry::Registry; +//! # +//! # // Create registry and counter and register the latter with the former. +//! # let mut registry = Registry::default(); +//! # let counter: Counter = Counter::default(); +//! # registry.register( +//! # "my_counter", +//! # "This is my counter", +//! # counter.clone(), +//! # ); +//! # counter.inc(); +//! // Returns `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 family = metric_set.metric_families.first().unwrap(); +//! assert_eq!("my_counter", family.name); +//! assert_eq!("This is my counter.", family.help); +//! ``` + +// Allowing some lints here as the `openmetrics.rs` is an automatically generated file. +#[allow(missing_docs, clippy::derive_partial_eq_without_eq)] +/// Data models that are automatically generated from OpenMetrics protobuf +/// format. +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::*; + +/// 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, +{ + 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(); + metric_type as i32 + }, + unit: if let Some(unit) = desc.unit() { + unit.as_str().to_string() + } else { + String::new() + }, + help: desc.help().to_string(), + ..Default::default() + }; + + let mut labels = vec![]; + desc.labels().encode(&mut labels); + metric.encode(labels, &mut family.metrics); + + metric_set.metric_families.push(family); + } + + metric_set +} + +impl From for openmetrics_data_model::MetricType { + fn from(m: MetricType) -> Self { + match m { + MetricType::Counter => openmetrics_data_model::MetricType::Counter, + MetricType::Gauge => openmetrics_data_model::MetricType::Gauge, + MetricType::Histogram => openmetrics_data_model::MetricType::Histogram, + MetricType::Info => openmetrics_data_model::MetricType::Info, + MetricType::Unknown => openmetrics_data_model::MetricType::Unknown, + } + } +} + +/// 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; +} + +impl EncodeMetric for Box { + fn encode( + &self, + labels: Vec, + family: &mut Vec, + ) { + self.deref().encode(labels, family) + } + + fn metric_type(&self) -> MetricType { + self.deref().metric_type() + } +} + +/// Trait combining [`EncodeMetric`] and [`Send`]. +pub trait SendEncodeMetric: EncodeMetric + Send {} + +impl SendEncodeMetric for T {} + +/// 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); +} + +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 EncodeLabels for f64 { + fn encode(&self, labels: &mut Vec) { + labels.push(openmetrics_data_model::Label { + name: self.to_string(), + value: self.to_string(), + }) + } +} + +impl EncodeLabels for u64 { + fn encode(&self, labels: &mut Vec) { + labels.push(openmetrics_data_model::Label { + name: self.to_string(), + value: self.to_string(), + }) + } +} + +impl EncodeLabels for u32 { + fn encode(&self, labels: &mut Vec) { + labels.push(openmetrics_data_model::Label { + name: self.to_string(), + value: self.to_string(), + }) + } +} + +impl EncodeLabels for String { + fn encode(&self, labels: &mut Vec) { + labels.push(openmetrics_data_model::Label { + name: self.clone(), + value: self.clone(), + }) + } +} + +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())); + } +} + +impl EncodeLabels for Void { + fn encode(&self, _labels: &mut Vec) { + void::unreachable(*self); + } +} + +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 +{ + let mut exemplar_proto = openmetrics_data_model::Exemplar { + value: exemplar.value.clone().into(), + ..Default::default() + }; + exemplar.label_set.encode(&mut exemplar_proto.label); + + exemplar_proto +} + +///////////////////////////////////////////////////////////////////////////////// +// Counter + +/// Trait to implement its counter value encoding in the OpenMetrics protobuf +/// format. +pub trait EncodeCounterValue { + /// Encode the given instance into counter value in the OpenMetrics protobuf + /// encoding. + fn encode(&self) -> openmetrics_data_model::counter_value::Total; +} + +impl EncodeCounterValue for u64 { + fn encode(&self) -> openmetrics_data_model::counter_value::Total { + openmetrics_data_model::counter_value::Total::IntValue(*self) + } +} + +impl EncodeCounterValue for f64 { + fn encode(&self) -> openmetrics_data_model::counter_value::Total { + openmetrics_data_model::counter_value::Total::DoubleValue(*self) + } +} + +impl EncodeMetric for Counter +where + N: EncodeCounterValue, + A: counter::Atomic, +{ + fn encode( + &self, + labels: Vec, + family: &mut Vec, + ) { + let mut metric = encode_counter_with_maybe_exemplar(self.get(), None); + metric.labels = labels; + + family.push(metric); + } + + fn metric_type(&self) -> MetricType { + Self::TYPE + } +} + +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 + } +} + +fn encode_counter_with_maybe_exemplar( + value: N, + exemplar: Option, +) -> openmetrics_data_model::Metric +where + N: EncodeCounterValue, +{ + openmetrics_data_model::Metric { + metric_points: { + let metric_point = openmetrics_data_model::MetricPoint { + value: { + Some(openmetrics_data_model::metric_point::Value::CounterValue( + openmetrics_data_model::CounterValue { + total: Some(value.encode()), + exemplar, + ..Default::default() + }, + )) + }, + ..Default::default() + }; + + vec![metric_point] + }, + ..Default::default() + } +} + +///////////////////////////////////////////////////////////////////////////////// +// Gauge + +/// Trait to implement its gauge value encoding in the OpenMetrics protobuf +/// format. +pub trait EncodeGaugeValue { + /// Encode the given instance into gauge value in the OpenMetrics protobuf + /// encoding. + fn encode(&self) -> openmetrics_data_model::gauge_value::Value; +} + +// 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 EncodeGaugeValue for u64 { + fn encode(&self) -> openmetrics_data_model::gauge_value::Value { + openmetrics_data_model::gauge_value::Value::IntValue(*self as i64) + } +} + +impl EncodeGaugeValue for f64 { + fn encode(&self) -> openmetrics_data_model::gauge_value::Value { + openmetrics_data_model::gauge_value::Value::DoubleValue(*self) + } +} + +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 + } +} + +///////////////////////////////////////////////////////////////////////////////// +// Family + +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) + } + } + + 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; + + family.push(metric) + } + + fn metric_type(&self) -> MetricType { + Self::TYPE + } +} + +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) + } + + fn metric_type(&self) -> MetricType { + Histogram::TYPE + } +} + +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() + } +} + +///////////////////////////////////////////////////////////////////////////////// +// 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() + }; + + family.push(metric); + } + + fn metric_type(&self) -> MetricType { + Self::TYPE + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::metrics::counter::Counter; + use crate::metrics::exemplar::{CounterWithExemplar, HistogramWithExemplars}; + use crate::metrics::family::Family; + use crate::metrics::gauge::Gauge; + use crate::metrics::histogram::{exponential_buckets, Histogram}; + use crate::metrics::info::Info; + use crate::registry::Unit; + use std::borrow::Cow; + use std::sync::atomic::AtomicI64; + + #[test] + fn encode_counter_int() { + let counter: Counter = Counter::default(); + let mut registry = Registry::default(); + registry.register("my_counter", "My counter", counter.clone()); + counter.inc(); + + let metric_set = encode(®istry); + + let family = metric_set.metric_families.first().unwrap(); + assert_eq!("my_counter", family.name); + assert_eq!("My counter.", family.help); + + assert_eq!( + openmetrics_data_model::MetricType::Counter as i32, + extract_metric_type(&metric_set) + ); + + match extract_metric_point_value(metric_set) { + openmetrics_data_model::metric_point::Value::CounterValue(value) => { + let expected = openmetrics_data_model::counter_value::Total::IntValue(1); + assert_eq!(Some(expected), value.total); + assert_eq!(None, value.exemplar); + assert_eq!(None, value.created); + } + _ => panic!("wrong value type"), + } + } + + #[test] + fn encode_counter_double() { + // Using `f64` + let counter: Counter = Counter::default(); + let mut registry = Registry::default(); + registry.register("my_counter", "My counter", counter.clone()); + counter.inc(); + + let metric_set = encode(®istry); + + let family = metric_set.metric_families.first().unwrap(); + assert_eq!("my_counter", family.name); + assert_eq!("My counter.", family.help); + + assert_eq!( + openmetrics_data_model::MetricType::Counter as i32, + extract_metric_type(&metric_set) + ); + + match extract_metric_point_value(metric_set) { + openmetrics_data_model::metric_point::Value::CounterValue(value) => { + // The counter should be encoded as `DoubleValue` + let expected = openmetrics_data_model::counter_value::Total::DoubleValue(1.0); + assert_eq!(Some(expected), value.total); + assert_eq!(None, value.exemplar); + assert_eq!(None, value.created); + } + _ => panic!("wrong value type"), + } + } + + #[test] + fn encode_counter_with_unit() { + let mut registry = Registry::default(); + let counter: Counter = Counter::default(); + registry.register_with_unit("my_counter", "My counter", Unit::Seconds, counter); + + let metric_set = encode(®istry); + + let family = metric_set.metric_families.first().unwrap(); + assert_eq!("my_counter", family.name); + assert_eq!("My counter.", family.help); + assert_eq!("seconds", family.unit); + } + + #[test] + fn encode_counter_with_exemplar() { + let mut registry = Registry::default(); + + let counter_with_exemplar: CounterWithExemplar, f64> = + CounterWithExemplar::default(); + registry.register( + "my_counter_with_exemplar", + "My counter with exemplar", + counter_with_exemplar.clone(), + ); + + counter_with_exemplar.inc_by(1.0, Some(vec![("user_id".to_string(), 42.0)])); + + let metric_set = encode(®istry); + + let family = metric_set.metric_families.first().unwrap(); + assert_eq!("my_counter_with_exemplar", family.name); + assert_eq!("My counter with exemplar.", family.help); + + assert_eq!( + openmetrics_data_model::MetricType::Counter as i32, + extract_metric_type(&metric_set) + ); + + match extract_metric_point_value(metric_set) { + openmetrics_data_model::metric_point::Value::CounterValue(value) => { + // The counter should be encoded as `DoubleValue` + let expected = openmetrics_data_model::counter_value::Total::DoubleValue(1.0); + assert_eq!(Some(expected), value.total); + + let exemplar = value.exemplar.as_ref().unwrap(); + assert_eq!(1.0, exemplar.value); + + let expected_label = { + openmetrics_data_model::Label { + name: "user_id".to_string(), + value: "42".to_string(), + } + }; + assert_eq!(vec![expected_label], exemplar.label); + } + _ => panic!("wrong value type"), + } + } + + #[test] + fn encode_gauge() { + let mut registry = Registry::default(); + let gauge = Gauge::::default(); + registry.register("my_gauge", "My gauge", gauge.clone()); + gauge.inc(); + + let metric_set = encode(®istry); + let family = metric_set.metric_families.first().unwrap(); + assert_eq!("my_gauge", family.name); + assert_eq!("My gauge.", family.help); + + assert_eq!( + openmetrics_data_model::MetricType::Gauge as i32, + extract_metric_type(&metric_set) + ); + + match extract_metric_point_value(metric_set) { + openmetrics_data_model::metric_point::Value::GaugeValue(value) => { + let expected = openmetrics_data_model::gauge_value::Value::IntValue(1); + assert_eq!(Some(expected), value.value); + } + _ => panic!("wrong value type"), + } + } + + #[test] + fn encode_counter_family() { + let mut registry = Registry::default(); + let family = Family::, Counter>::default(); + registry.register("my_counter_family", "My counter family", family.clone()); + + family + .get_or_create(&vec![ + ("method".to_string(), "GET".to_string()), + ("status".to_string(), "200".to_string()), + ]) + .inc(); + + let metric_set = encode(®istry); + + let family = metric_set.metric_families.first().unwrap(); + assert_eq!("my_counter_family", family.name); + assert_eq!("My counter family.", family.help); + + assert_eq!( + openmetrics_data_model::MetricType::Counter as i32, + extract_metric_type(&metric_set) + ); + + let metric = family.metrics.first().unwrap(); + assert_eq!(2, metric.labels.len()); + assert_eq!("method", metric.labels[0].name); + assert_eq!("GET", metric.labels[0].value); + assert_eq!("status", metric.labels[1].name); + assert_eq!("200", metric.labels[1].value); + + match extract_metric_point_value(metric_set) { + openmetrics_data_model::metric_point::Value::CounterValue(value) => { + let expected = openmetrics_data_model::counter_value::Total::IntValue(1); + assert_eq!(Some(expected), value.total); + assert_eq!(None, value.exemplar); + assert_eq!(None, value.created); + } + _ => panic!("wrong value type"), + } + } + + #[test] + fn encode_counter_family_with_prefix_with_label() { + let mut registry = Registry::default(); + let sub_registry = registry.sub_registry_with_prefix("my_prefix"); + let sub_sub_registry = sub_registry + .sub_registry_with_label((Cow::Borrowed("my_key"), Cow::Borrowed("my_value"))); + let family = Family::, Counter>::default(); + sub_sub_registry.register("my_counter_family", "My counter family", family.clone()); + + family + .get_or_create(&vec![ + ("method".to_string(), "GET".to_string()), + ("status".to_string(), "200".to_string()), + ]) + .inc(); + + let metric_set = encode(®istry); + + let family = metric_set.metric_families.first().unwrap(); + assert_eq!("my_prefix_my_counter_family", family.name); + assert_eq!("My counter family.", family.help); + + assert_eq!( + openmetrics_data_model::MetricType::Counter as i32, + extract_metric_type(&metric_set) + ); + + let metric = family.metrics.first().unwrap(); + assert_eq!(3, metric.labels.len()); + assert_eq!("my_key", metric.labels[0].name); + assert_eq!("my_value", metric.labels[0].value); + assert_eq!("method", metric.labels[1].name); + assert_eq!("GET", metric.labels[1].value); + assert_eq!("status", metric.labels[2].name); + assert_eq!("200", metric.labels[2].value); + + match extract_metric_point_value(metric_set) { + openmetrics_data_model::metric_point::Value::CounterValue(value) => { + let expected = openmetrics_data_model::counter_value::Total::IntValue(1); + assert_eq!(Some(expected), value.total); + assert_eq!(None, value.exemplar); + assert_eq!(None, value.created); + } + _ => panic!("wrong value type"), + } + } + + #[test] + fn encode_histogram() { + let mut registry = Registry::default(); + let histogram = Histogram::new(exponential_buckets(1.0, 2.0, 10)); + registry.register("my_histogram", "My histogram", histogram.clone()); + histogram.observe(1.0); + + let metric_set = encode(®istry); + + let family = metric_set.metric_families.first().unwrap(); + assert_eq!("my_histogram", family.name); + assert_eq!("My histogram.", family.help); + + assert_eq!( + openmetrics_data_model::MetricType::Histogram as i32, + extract_metric_type(&metric_set) + ); + + match extract_metric_point_value(metric_set) { + openmetrics_data_model::metric_point::Value::HistogramValue(value) => { + assert_eq!( + Some(openmetrics_data_model::histogram_value::Sum::DoubleValue( + 1.0 + )), + value.sum + ); + assert_eq!(1, value.count); + assert_eq!(11, value.buckets.len()); + } + _ => panic!("wrong value type"), + } + } + + #[test] + fn encode_histogram_with_exemplars() { + let mut registry = Registry::default(); + let histogram = HistogramWithExemplars::new(exponential_buckets(1.0, 2.0, 10)); + registry.register("my_histogram", "My histogram", histogram.clone()); + histogram.observe(1.0, Some(vec![("user_id".to_string(), 42u64)])); + + let metric_set = encode(®istry); + + let family = metric_set.metric_families.first().unwrap(); + assert_eq!("my_histogram", family.name); + assert_eq!("My histogram.", family.help); + + assert_eq!( + openmetrics_data_model::MetricType::Histogram as i32, + extract_metric_type(&metric_set) + ); + + match extract_metric_point_value(metric_set) { + openmetrics_data_model::metric_point::Value::HistogramValue(value) => { + let exemplar = value.buckets.first().unwrap().exemplar.as_ref().unwrap(); + assert_eq!(1.0, exemplar.value); + + let expected_label = { + openmetrics_data_model::Label { + name: "user_id".to_string(), + value: "42".to_string(), + } + }; + assert_eq!(vec![expected_label], exemplar.label); + } + _ => panic!("wrong value type"), + } + } + + #[test] + fn encode_family_counter_histogram() { + let mut registry = Registry::>::default(); + + let counter_family = Family::, Counter>::default(); + let histogram_family = + Family::, Histogram>::new_with_constructor(|| { + Histogram::new(exponential_buckets(1.0, 2.0, 10)) + }); + + registry.register("my_counter", "My counter", Box::new(counter_family.clone())); + registry.register( + "my_histogram", + "My histogram", + Box::new(histogram_family.clone()), + ); + + counter_family + .get_or_create(&vec![("path".to_string(), "/".to_string())]) + .inc(); + + histogram_family + .get_or_create(&vec![("path".to_string(), "/".to_string())]) + .observe(1.0); + + let metric_set = encode(®istry); + 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(); + + // Family + let counter_family = Family::, Counter>::default(); + let histogram_family = + Family::, Histogram>::new_with_constructor(|| { + Histogram::new(exponential_buckets(1.0, 2.0, 10)) + }); + + registry.register( + "my_family_counter", + "My counter", + Box::new(counter_family.clone()), + ); + registry.register( + "my_family_histogram", + "My histogram", + Box::new(histogram_family.clone()), + ); + + counter_family + .get_or_create(&vec![("path".to_string(), "/".to_string())]) + .inc(); + + histogram_family + .get_or_create(&vec![("path".to_string(), "/".to_string())]) + .observe(1.0); + + // Counter + let counter: Counter = Counter::default(); + registry.register("my_counter", "My counter", Box::new(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())); + histogram.observe(1.0); + + let metric_set = encode(®istry); + assert_eq!("my_family_counter", metric_set.metric_families[0].name); + assert_eq!("my_family_histogram", metric_set.metric_families[1].name); + } + + #[test] + fn encode_info() { + let mut registry = Registry::default(); + 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 family = metric_set.metric_families.first().unwrap(); + assert_eq!("my_info_metric", family.name); + assert_eq!("My info metric.", family.help); + + assert_eq!( + openmetrics_data_model::MetricType::Info as i32, + extract_metric_type(&metric_set) + ); + + match extract_metric_point_value(metric_set) { + openmetrics_data_model::metric_point::Value::InfoValue(value) => { + assert_eq!(1, value.info.len()); + + let info = value.info.first().unwrap(); + assert_eq!("os", info.name); + assert_eq!("GNU/linux", info.value); + } + _ => panic!("wrong value type"), + } + } + + fn extract_metric_type(metric_set: &openmetrics_data_model::MetricSet) -> i32 { + let family = metric_set.metric_families.first().unwrap(); + family.r#type + } + + fn extract_metric_point_value( + metric_set: openmetrics_data_model::MetricSet, + ) -> openmetrics_data_model::metric_point::Value { + let metric = metric_set + .metric_families + .first() + .unwrap() + .metrics + .first() + .unwrap(); + + metric + .metric_points + .first() + .unwrap() + .value + .as_ref() + .unwrap() + .clone() + } +} diff --git a/src/encoding/proto/openmetrics_data_model.proto b/src/encoding/proto/openmetrics_data_model.proto new file mode 100644 index 00000000..a95942d9 --- /dev/null +++ b/src/encoding/proto/openmetrics_data_model.proto @@ -0,0 +1,214 @@ +syntax = "proto3"; + +// The OpenMetrics protobuf schema which defines the protobuf wire format. +// Ensure to interpret "required" as semantically required for a valid message. +// All string fields MUST be UTF-8 encoded strings. +package openmetrics; + +import "google/protobuf/timestamp.proto"; + +// The top-level container type that is encoded and sent over the wire. +message MetricSet { + // Each MetricFamily has one or more MetricPoints for a single Metric. + repeated MetricFamily metric_families = 1; +} + +// One or more Metrics for a single MetricFamily, where each Metric +// has one or more MetricPoints. +message MetricFamily { + // Required. + string name = 1; + + // Optional. + MetricType type = 2; + + // Optional. + string unit = 3; + + // Optional. + string help = 4; + + // Optional. + repeated Metric metrics = 5; +} + +// The type of a Metric. +enum MetricType { + // Unknown must use unknown MetricPoint values. + UNKNOWN = 0; + // Gauge must use gauge MetricPoint values. + GAUGE = 1; + // Counter must use counter MetricPoint values. + COUNTER = 2; + // State set must use state set MetricPoint values. + STATE_SET = 3; + // Info must use info MetricPoint values. + INFO = 4; + // Histogram must use histogram value MetricPoint values. + HISTOGRAM = 5; + // Gauge histogram must use histogram value MetricPoint values. + GAUGE_HISTOGRAM = 6; + // Summary quantiles must use summary value MetricPoint values. + SUMMARY = 7; +} + +// A single metric with a unique set of labels within a metric family. +message Metric { + // Optional. + repeated Label labels = 1; + + // Optional. + repeated MetricPoint metric_points = 2; +} + +// A name-value pair. These are used in multiple places: identifying +// timeseries, value of INFO metrics, and exemplars in Histograms. +message Label { + // Required. + string name = 1; + + // Required. + string value = 2; +} + +// A MetricPoint in a Metric. +message MetricPoint { + // Required. + oneof value { + UnknownValue unknown_value = 1; + GaugeValue gauge_value = 2; + CounterValue counter_value = 3; + HistogramValue histogram_value = 4; + StateSetValue state_set_value = 5; + InfoValue info_value = 6; + SummaryValue summary_value = 7; + } + + // Optional. + google.protobuf.Timestamp timestamp = 8; +} + +// Value for UNKNOWN MetricPoint. +message UnknownValue { + // Required. + oneof value { + double double_value = 1; + int64 int_value = 2; + } +} + +// Value for GAUGE MetricPoint. +message GaugeValue { + // Required. + oneof value { + double double_value = 1; + int64 int_value = 2; + } +} + +// Value for COUNTER MetricPoint. +message CounterValue { + // Required. + oneof total { + double double_value = 1; + uint64 int_value = 2; + } + + // The time values began being collected for this counter. + // Optional. + google.protobuf.Timestamp created = 3; + + // Optional. + Exemplar exemplar = 4; +} + +// Value for HISTOGRAM or GAUGE_HISTOGRAM MetricPoint. +message HistogramValue { + // Optional. + oneof sum { + double double_value = 1; + int64 int_value = 2; + } + + // Optional. + uint64 count = 3; + + // The time values began being collected for this histogram. + // Optional. + google.protobuf.Timestamp created = 4; + + // Optional. + repeated Bucket buckets = 5; + + // Bucket is the number of values for a bucket in the histogram + // with an optional exemplar. + message Bucket { + // Required. + uint64 count = 1; + + // Optional. + double upper_bound = 2; + + // Optional. + Exemplar exemplar = 3; + } +} + +message Exemplar { + // Required. + double value = 1; + + // Optional. + google.protobuf.Timestamp timestamp = 2; + + // Labels are additional information about the exemplar value (e.g. trace id). + // Optional. + repeated Label label = 3; +} + +// Value for STATE_SET MetricPoint. +message StateSetValue { + // Optional. + repeated State states = 1; + + message State { + // Required. + bool enabled = 1; + + // Required. + string name = 2; + } +} + +// Value for INFO MetricPoint. +message InfoValue { + // Optional. + repeated Label info = 1; +} + +// Value for SUMMARY MetricPoint. +message SummaryValue { + // Optional. + oneof sum { + double double_value = 1; + int64 int_value = 2; + } + + // Optional. + uint64 count = 3; + + // The time sum and count values began being collected for this summary. + // Optional. + google.protobuf.Timestamp created = 4; + + // Optional. + repeated Quantile quantile = 5; + + message Quantile { + // Required. + double quantile = 1; + + // Required. + double value = 2; + } +} diff --git a/src/encoding/text.rs b/src/encoding/text.rs index 19e5c464..754b08a7 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -38,8 +38,6 @@ use std::collections::HashMap; use std::io::Write; use std::ops::Deref; -pub use prometheus_client_derive_text_encode::*; - /// 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> @@ -197,20 +195,7 @@ impl Encode for MetricType { impl Encode for Unit { fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> { - let u = match self { - Unit::Amperes => "amperes", - Unit::Bytes => "bytes", - Unit::Celsius => "celsius", - Unit::Grams => "grams", - Unit::Joules => "joules", - Unit::Meters => "meters", - Unit::Ratios => "ratios", - Unit::Seconds => "seconds", - Unit::Volts => "volts", - Unit::Other(other) => other.as_str(), - }; - - writer.write_all(u.as_bytes())?; + writer.write_all(self.as_str().as_bytes())?; Ok(()) } } diff --git a/src/lib.rs b/src/lib.rs index dd9b6541..dbc70f31 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,7 +13,7 @@ //! # Examples //! //! ``` -//! use prometheus_client::encoding::text::Encode; +//! use prometheus_client::encoding::Encode; //! use prometheus_client::encoding::text::encode; //! use prometheus_client::metrics::counter::{Atomic, Counter}; //! use prometheus_client::metrics::family::Family; diff --git a/src/metrics/family.rs b/src/metrics/family.rs index b5f54a62..304b0e43 100644 --- a/src/metrics/family.rs +++ b/src/metrics/family.rs @@ -57,7 +57,7 @@ use std::sync::Arc; /// [`Encode`](crate::encoding::text::Encode) implementation. /// /// ``` -/// # use prometheus_client::encoding::text::Encode; +/// # use prometheus_client::encoding::Encode; /// # use prometheus_client::encoding::text::encode; /// # use prometheus_client::metrics::counter::{Atomic, Counter}; /// # use prometheus_client::metrics::family::Family; diff --git a/src/metrics/gauge.rs b/src/metrics/gauge.rs index ad49d81a..297872f3 100644 --- a/src/metrics/gauge.rs +++ b/src/metrics/gauge.rs @@ -5,7 +5,7 @@ use super::{MetricType, TypedMetric}; use std::marker::PhantomData; #[cfg(not(any(target_arch = "mips", target_arch = "powerpc")))] -use std::sync::atomic::AtomicU64; +use std::sync::atomic::{AtomicI64, AtomicU64}; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::Arc; @@ -234,6 +234,33 @@ 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) + } + + fn set(&self, v: i64) -> i64 { + self.swap(v, Ordering::Relaxed) + } + + fn get(&self) -> i64 { + self.load(Ordering::Relaxed) + } +} + impl TypedMetric for Gauge { const TYPE: MetricType = MetricType::Gauge; } diff --git a/src/registry.rs b/src/registry.rs index 8760312f..b88dc46b 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -347,6 +347,24 @@ pub enum Unit { Other(String), } +impl Unit { + /// Returns the given Unit's str representation. + pub fn as_str(&self) -> &str { + match self { + Unit::Amperes => "amperes", + Unit::Bytes => "bytes", + Unit::Celsius => "celsius", + Unit::Grams => "grams", + Unit::Joules => "joules", + Unit::Meters => "meters", + Unit::Ratios => "ratios", + Unit::Seconds => "seconds", + Unit::Volts => "volts", + Unit::Other(other) => other.as_str(), + } + } +} + #[cfg(test)] mod tests { use super::*;