diff --git a/CHANGELOG.md b/CHANGELOG.md index b7ec4260..edf54b0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ 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.20.0] [unreleased] + +### Added + +- Introduce `Collector` abstraction allowing users to provide additional metrics + and their description on each scrape. See [PR 82]. + +[PR 82]: https://github.com/prometheus/client_rust/pull/82 + ## [0.19.0] This is a large release including multiple breaking changes. Major user-facing diff --git a/Cargo.toml b/Cargo.toml index 6e1d39f3..e93ffe5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "prometheus-client" -version = "0.19.0" +version = "0.20.0" authors = ["Max Inden "] edition = "2021" description = "Open Metrics client library allowing users to natively instrument applications." diff --git a/src/collector.rs b/src/collector.rs new file mode 100644 index 00000000..f55c4232 --- /dev/null +++ b/src/collector.rs @@ -0,0 +1,29 @@ +//! Metric collector implementation. +//! +//! See [`Collector`] for details. + +use std::borrow::Cow; + +use crate::{ + registry::{Descriptor, LocalMetric}, + MaybeOwned, +}; + +/// The [`Collector`] abstraction allows users to provide additional metrics and +/// their description on each scrape. +/// +/// An example use-case is an exporter that retrieves a set of operating system metrics +/// ad-hoc on each scrape. +/// +/// Register a [`Collector`] with a [`Registry`](crate::registry::Registry) via +/// [`Registry::register_collector`](crate::registry::Registry::register_collector). +pub trait Collector: std::fmt::Debug + Send + Sync + 'static { + /// Once the [`Collector`] is registered, this method is called on each scrape. + /// + /// Note that the return type allows you to either return owned (convenient) + /// or borrowed (performant) descriptions and metrics. + #[allow(clippy::type_complexity)] + fn collect<'a>( + &'a self, + ) -> Box, MaybeOwned<'a, Box>)> + 'a>; +} diff --git a/src/encoding/protobuf.rs b/src/encoding/protobuf.rs index 7fb737d2..500ded75 100644 --- a/src/encoding/protobuf.rs +++ b/src/encoding/protobuf.rs @@ -41,45 +41,56 @@ use super::{EncodeCounterValue, EncodeExemplarValue, EncodeGaugeValue, EncodeLab /// Encode the metrics registered with the provided [`Registry`] into MetricSet /// using the OpenMetrics protobuf format. 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 = - super::EncodeMetric::metric_type(metric.as_ref()).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( - LabelSetEncoder { - labels: &mut labels, - } - .into(), - )?; + Ok(openmetrics_data_model::MetricSet { + metric_families: registry + .iter_metrics() + .map(|(desc, metric)| encode_metric(desc, metric.as_ref())) + .chain( + registry + .iter_collectors() + .map(|(desc, metric)| encode_metric(desc.as_ref(), metric.as_ref())), + ) + .collect::>()?, + }) +} - let encoder = MetricEncoder { - family: &mut family.metrics, - metric_type: super::EncodeMetric::metric_type(metric.as_ref()), +fn encode_metric( + desc: &crate::registry::Descriptor, + metric: &(impl super::EncodeMetric + ?Sized), +) -> Result { + let mut family = openmetrics_data_model::MetricFamily { + name: desc.name().to_string(), + r#type: { + let metric_type: openmetrics_data_model::MetricType = + super::EncodeMetric::metric_type(metric).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( + LabelSetEncoder { labels: &mut labels, - }; + } + .into(), + )?; - super::EncodeMetric::encode(metric.as_ref(), encoder.into())?; + let encoder = MetricEncoder { + family: &mut family.metrics, + metric_type: super::EncodeMetric::metric_type(metric), + labels: &mut labels, + }; - metric_set.metric_families.push(family); - } + super::EncodeMetric::encode(metric, encoder.into())?; - Ok(metric_set) + Ok(family) } impl From for openmetrics_data_model::MetricType { diff --git a/src/encoding/text.rs b/src/encoding/text.rs index de47bbbb..dea22e05 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -26,7 +26,7 @@ use crate::encoding::{EncodeExemplarValue, EncodeLabelSet, EncodeMetric}; use crate::metrics::exemplar::Exemplar; -use crate::registry::{Registry, Unit}; +use crate::registry::{Descriptor, Registry, Unit}; use std::borrow::Cow; use std::collections::HashMap; @@ -38,50 +38,66 @@ pub fn encode(writer: &mut W, registry: &Registry) -> Result<(), std::fmt::Er where W: Write, { - for (desc, metric) in registry.iter() { - writer.write_str("# HELP ")?; - writer.write_str(desc.name())?; - if let Some(unit) = desc.unit() { - writer.write_str("_")?; - writer.write_str(unit.as_str())?; - } - writer.write_str(" ")?; - writer.write_str(desc.help())?; - writer.write_str("\n")?; + for (desc, metric) in registry.iter_metrics() { + encode_metric(writer, desc, metric.as_ref())?; + } + for (desc, metric) in registry.iter_collectors() { + encode_metric(writer, desc.as_ref(), metric.as_ref())?; + } + + writer.write_str("# EOF\n")?; + + Ok(()) +} - writer.write_str("# TYPE ")?; +fn encode_metric( + writer: &mut W, + desc: &Descriptor, + metric: &(impl EncodeMetric + ?Sized), +) -> Result<(), std::fmt::Error> +where + W: Write, +{ + writer.write_str("# HELP ")?; + writer.write_str(desc.name())?; + if let Some(unit) = desc.unit() { + writer.write_str("_")?; + writer.write_str(unit.as_str())?; + } + writer.write_str(" ")?; + writer.write_str(desc.help())?; + writer.write_str("\n")?; + + writer.write_str("# TYPE ")?; + writer.write_str(desc.name())?; + if let Some(unit) = desc.unit() { + writer.write_str("_")?; + writer.write_str(unit.as_str())?; + } + writer.write_str(" ")?; + writer.write_str(EncodeMetric::metric_type(metric).as_str())?; + writer.write_str("\n")?; + + if let Some(unit) = desc.unit() { + writer.write_str("# UNIT ")?; writer.write_str(desc.name())?; - if let Some(unit) = desc.unit() { - writer.write_str("_")?; - writer.write_str(unit.as_str())?; - } + writer.write_str("_")?; + writer.write_str(unit.as_str())?; writer.write_str(" ")?; - writer.write_str(EncodeMetric::metric_type(metric.as_ref()).as_str())?; + writer.write_str(unit.as_str())?; writer.write_str("\n")?; + } - if let Some(unit) = desc.unit() { - 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 = MetricEncoder { - writer, - name: desc.name(), - unit: desc.unit(), - const_labels: desc.labels(), - family_labels: None, - } - .into(); - - EncodeMetric::encode(metric.as_ref(), encoder)?; + let encoder = MetricEncoder { + writer, + name: desc.name(), + unit: desc.unit(), + const_labels: desc.labels(), + family_labels: None, } + .into(); - writer.write_str("# EOF\n")?; + EncodeMetric::encode(metric, encoder)?; Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index e64d60b8..69bac37a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,6 +78,32 @@ //! //! [examples]: https://github.com/prometheus/client_rust/tree/master/examples +pub mod collector; pub mod encoding; pub mod metrics; pub mod registry; + +/// Represents either borrowed or owned data. +/// +/// In contrast to [`std::borrow::Cow`] does not require +/// [`std::borrow::ToOwned`] or [`Clone`]respectively. +/// +/// Needed for [`collector::Collector`]. +#[derive(Debug)] +pub enum MaybeOwned<'a, T> { + /// Owned data + Owned(T), + /// Borrowed data + Borrowed(&'a T), +} + +impl<'a, T> std::ops::Deref for MaybeOwned<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + match self { + Self::Owned(t) => t, + Self::Borrowed(t) => t, + } + } +} diff --git a/src/metrics/counter.rs b/src/metrics/counter.rs index 4b39a1df..c1c5e511 100644 --- a/src/metrics/counter.rs +++ b/src/metrics/counter.rs @@ -187,6 +187,38 @@ where } } +/// As a [`Counter`], but constant, meaning it cannot change once created. +/// +/// Needed for advanced use-cases, e.g. in combination with [`Collector`](crate::collector::Collector). +#[derive(Debug, Default)] +pub struct ConstCounter { + value: N, +} + +impl ConstCounter { + /// Creates a new [`ConstCounter`]. + pub fn new(value: N) -> Self { + Self { value } + } +} + +impl TypedMetric for ConstCounter { + const TYPE: MetricType = MetricType::Counter; +} + +impl EncodeMetric for ConstCounter +where + N: crate::encoding::EncodeCounterValue, +{ + fn encode(&self, mut encoder: MetricEncoder) -> Result<(), std::fmt::Error> { + encoder.encode_counter::<(), _, u64>(&self.value, None) + } + + fn metric_type(&self) -> MetricType { + Self::TYPE + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/metrics/family.rs b/src/metrics/family.rs index bd490157..3b70eeb9 100644 --- a/src/metrics/family.rs +++ b/src/metrics/family.rs @@ -6,6 +6,7 @@ use crate::encoding::{EncodeLabelSet, EncodeMetric, MetricEncoder}; use super::{MetricType, TypedMetric}; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; +use std::cell::RefCell; use std::collections::HashMap; use std::sync::Arc; @@ -326,6 +327,24 @@ where } } +impl> EncodeMetric + for RefCell +{ + fn encode(&self, mut encoder: MetricEncoder<'_, '_>) -> Result<(), std::fmt::Error> { + let mut iter = self.borrow_mut(); + + for (label_set, m) in iter.by_ref() { + 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 151cad96..98671280 100644 --- a/src/metrics/gauge.rs +++ b/src/metrics/gauge.rs @@ -253,6 +253,38 @@ where } } +/// As a [`Gauge`], but constant, meaning it cannot change once created. +/// +/// Needed for advanced use-cases, e.g. in combination with [`Collector`](crate::collector::Collector). +#[derive(Debug, Default)] +pub struct ConstGauge { + value: N, +} + +impl ConstGauge { + /// Creates a new [`ConstGauge`]. + pub fn new(value: N) -> Self { + Self { value } + } +} + +impl TypedMetric for ConstGauge { + const TYPE: MetricType = MetricType::Gauge; +} + +impl EncodeMetric for ConstGauge +where + N: EncodeGaugeValue, +{ + fn encode(&self, mut encoder: MetricEncoder) -> Result<(), std::fmt::Error> { + encoder.encode_gauge(&self.value) + } + + fn metric_type(&self) -> MetricType { + Self::TYPE + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/registry.rs b/src/registry.rs index 7badf737..fabec4b0 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -4,12 +4,15 @@ use std::borrow::Cow; +use crate::collector::Collector; +use crate::MaybeOwned; + /// A metric registry. /// /// First off one registers metrics with the registry via /// [`Registry::register`]. Later on the [`Registry`] is passed to an encoder /// collecting samples of each metric by iterating all metrics in the -/// [`Registry`] via [`Registry::iter`]. +/// [`Registry`]. /// /// [`Registry`] is the core building block, generic over the metric type being /// registered. Out of convenience, the generic type parameter is set to use @@ -59,6 +62,7 @@ pub struct Registry { prefix: Option, labels: Vec<(Cow<'static, str>, Cow<'static, str>)>, metrics: Vec<(Descriptor, Box)>, + collectors: Vec>, sub_registries: Vec, } @@ -142,22 +146,48 @@ impl Registry { metric: impl Metric, unit: Option, ) { - let name = name.into(); - let help = help.into() + "."; - let descriptor = Descriptor { - name: self - .prefix - .as_ref() - .map(|p| (p.clone().0 + "_" + name.as_str())) - .unwrap_or(name), - help, - unit, - labels: self.labels.clone(), - }; + let descriptor = + Descriptor::new(name, help, unit, self.prefix.as_ref(), self.labels.clone()); self.metrics.push((descriptor, Box::new(metric))); } + /// Register a [`Collector`]. + /// + /// ``` + /// # use prometheus_client::metrics::counter::ConstCounter; + /// # use prometheus_client::registry::{Descriptor, Registry, LocalMetric}; + /// # use prometheus_client::collector::Collector; + /// # use prometheus_client::MaybeOwned; + /// # use std::borrow::Cow; + /// # + /// #[derive(Debug)] + /// struct MyCollector {} + /// + /// impl Collector for MyCollector { + /// fn collect<'a>(&'a self) -> Box, MaybeOwned<'a, Box>)> + 'a> { + /// let c: Box = Box::new(ConstCounter::new(42)); + /// let descriptor = Descriptor::new( + /// "my_counter", + /// "This is my counter", + /// None, + /// None, + /// vec![], + /// ); + /// Box::new(std::iter::once((Cow::Owned(descriptor), MaybeOwned::Owned(c)))) + /// } + /// } + /// + /// let my_collector = Box::new(MyCollector{}); + /// + /// let mut registry = Registry::default(); + /// + /// registry.register_collector(my_collector); + /// ``` + pub fn register_collector(&mut self, collector: Box) { + self.collectors.push(collector); + } + /// 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 @@ -228,41 +258,58 @@ impl Registry { .expect("sub_registries not to be empty.") } - /// [`Iterator`] over all metrics registered with the [`Registry`]. - pub fn iter(&self) -> RegistryIterator { + pub(crate) fn iter_metrics(&self) -> MetricIterator { let metrics = self.metrics.iter(); let sub_registries = self.sub_registries.iter(); - RegistryIterator { + MetricIterator { metrics, sub_registries, sub_registry: None, } } + + pub(crate) fn iter_collectors(&self) -> CollectorIterator { + let collectors = self.collectors.iter(); + let sub_registries = self.sub_registries.iter(); + CollectorIterator { + prefix: self.prefix.as_ref(), + labels: &self.labels, + + collector: None, + collectors, + + sub_collector_iter: None, + sub_registries, + } + } } -/// Iterator iterating both the metrics registered directly with the registry as -/// well as all metrics registered with sub-registries. +/// Iterator iterating both the metrics registered directly with the +/// [`Registry`] as well as all metrics registered with sub [`Registry`]s. #[derive(Debug)] -pub struct RegistryIterator<'a> { +pub struct MetricIterator<'a> { metrics: std::slice::Iter<'a, (Descriptor, Box)>, sub_registries: std::slice::Iter<'a, Registry>, - sub_registry: Option>>, + sub_registry: Option>>, } -impl<'a> Iterator for RegistryIterator<'a> { +impl<'a> Iterator for MetricIterator<'a> { type Item = &'a (Descriptor, Box); fn next(&mut self) -> Option { - if let Some(metric) = self.metrics.next() { - return Some(metric); - } - loop { + if let Some(m) = self.metrics.next() { + return Some(m); + } + if let Some(metric) = self.sub_registry.as_mut().and_then(|i| i.next()) { return Some(metric); } - self.sub_registry = self.sub_registries.next().map(|r| Box::new(r.iter())); + self.sub_registry = self + .sub_registries + .next() + .map(|r| Box::new(r.iter_metrics())); if self.sub_registry.is_none() { break; @@ -273,23 +320,99 @@ impl<'a> Iterator for RegistryIterator<'a> { } } +/// Iterator iterating metrics retrieved from [`Collector`]s registered with the [`Registry`] or sub [`Registry`]s. +pub struct CollectorIterator<'a> { + prefix: Option<&'a Prefix>, + labels: &'a [(Cow<'static, str>, Cow<'static, str>)], + + #[allow(clippy::type_complexity)] + collector: Option< + Box, MaybeOwned<'a, Box>)> + 'a>, + >, + collectors: std::slice::Iter<'a, Box>, + + sub_collector_iter: Option>>, + sub_registries: std::slice::Iter<'a, Registry>, +} + +impl<'a> std::fmt::Debug for CollectorIterator<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CollectorIterator") + .field("prefix", &self.prefix) + .field("labels", &self.labels) + .finish() + } +} + +impl<'a> Iterator for CollectorIterator<'a> { + type Item = (Cow<'a, Descriptor>, MaybeOwned<'a, Box>); + + fn next(&mut self) -> Option { + loop { + if let Some(m) = self + .collector + .as_mut() + .and_then(|c| c.next()) + .or_else(|| self.sub_collector_iter.as_mut().and_then(|i| i.next())) + .map(|(descriptor, metric)| { + if self.prefix.is_some() || !self.labels.is_empty() { + let Descriptor { + name, + help, + unit, + labels, + } = descriptor.as_ref(); + let mut labels = labels.to_vec(); + labels.extend_from_slice(self.labels); + let enriched_descriptor = + Descriptor::new(name, help, unit.to_owned(), self.prefix, labels); + + Some((Cow::Owned(enriched_descriptor), metric)) + } else { + Some((descriptor, metric)) + } + }) + { + return m; + } + + if let Some(collector) = self.collectors.next() { + self.collector = Some(collector.collect()); + continue; + } + + if let Some(collector_iter) = self + .sub_registries + .next() + .map(|r| Box::new(r.iter_collectors())) + { + self.sub_collector_iter = Some(collector_iter); + continue; + } + + return None; + } + } +} + +/// Metric prefix #[derive(Clone, Debug)] -struct Prefix(String); +pub struct Prefix(String); -impl From for Prefix { - fn from(s: String) -> Self { - Prefix(s) +impl Prefix { + fn as_str(&self) -> &str { + self.0.as_str() } } -impl From for String { - fn from(p: Prefix) -> Self { - p.0 +impl From for Prefix { + fn from(s: String) -> Self { + Prefix(s) } } /// OpenMetrics metric descriptor. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Descriptor { name: String, help: String, @@ -298,6 +421,30 @@ pub struct Descriptor { } impl Descriptor { + /// Create new [`Descriptor`]. + pub fn new, H: Into>( + name: N, + help: H, + unit: Option, + prefix: Option<&Prefix>, + labels: Vec<(Cow<'static, str>, Cow<'static, str>)>, + ) -> Self { + let mut name = name.into(); + if let Some(prefix) = prefix { + name.insert(0, '_'); + name.insert_str(0, prefix.as_str()); + } + + let help = help.into() + "."; + + Descriptor { + name, + help, + unit, + labels, + } + } + /// Returns the name of the OpenMetrics metric [`Descriptor`]. pub fn name(&self) -> &str { &self.name @@ -322,7 +469,7 @@ impl Descriptor { /// Metric units recommended by Open Metrics. /// /// See [`Unit::Other`] to specify alternative units. -#[derive(Debug)] +#[derive(Debug, Clone)] #[allow(missing_docs)] pub enum Unit { Amperes, @@ -361,6 +508,11 @@ pub trait Metric: crate::encoding::EncodeMetric + Send + Sync + std::fmt::Debug impl Metric for T where T: crate::encoding::EncodeMetric + Send + Sync + std::fmt::Debug + 'static {} +/// Similar to [`Metric`], but without the [`Send`] and [`Sync`] requirement. +pub trait LocalMetric: crate::encoding::EncodeMetric + std::fmt::Debug {} + +impl LocalMetric for T where T: crate::encoding::EncodeMetric + std::fmt::Debug {} + #[cfg(test)] mod tests { use super::*; @@ -372,7 +524,7 @@ mod tests { let counter: Counter = Counter::default(); registry.register("my_counter", "My counter", counter); - assert_eq!(1, registry.iter().count()) + assert_eq!(1, registry.iter_metrics().count()) } #[test] @@ -411,7 +563,7 @@ mod tests { sub_registry.register(prefix_3_metric_name, "some help", counter); let mut metric_iter = registry - .iter() + .iter_metrics() .map(|(desc, _)| (desc.name.clone(), desc.labels.clone())); assert_eq!( Some((top_level_metric_name.to_string(), vec![])),