Skip to content

Commit

Permalink
feat!: Introduce Collector abstraction (#82)
Browse files Browse the repository at this point in the history
The `Collector` abstraction allows users to provide additional metrics
and their description on each scrape.

See also:

- https://pkg.go.dev/github.com/prometheus/client_golang/prometheus#hdr-Custom_Collectors_and_constant_Metrics
- #49
- #29

Signed-off-by: Max Inden <mail@max-inden.de>
  • Loading branch information
mxinden committed Dec 29, 2022
1 parent 2f72a1b commit c619ad5
Show file tree
Hide file tree
Showing 10 changed files with 436 additions and 110 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "prometheus-client"
version = "0.19.0"
version = "0.20.0"
authors = ["Max Inden <mail@max-inden.de>"]
edition = "2021"
description = "Open Metrics client library allowing users to natively instrument applications."
Expand Down
29 changes: 29 additions & 0 deletions 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<dyn Iterator<Item = (Cow<'a, Descriptor>, MaybeOwned<'a, Box<dyn LocalMetric>>)> + 'a>;
}
79 changes: 45 additions & 34 deletions src/encoding/protobuf.rs
Expand Up @@ -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<openmetrics_data_model::MetricSet, std::fmt::Error> {
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::<Result<_, std::fmt::Error>>()?,
})
}

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<openmetrics_data_model::MetricFamily, std::fmt::Error> {
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<MetricType> for openmetrics_data_model::MetricType {
Expand Down
92 changes: 54 additions & 38 deletions src/encoding/text.rs
Expand Up @@ -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;
Expand All @@ -38,50 +38,66 @@ pub fn encode<W>(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<W>(
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(())
}
Expand Down
26 changes: 26 additions & 0 deletions src/lib.rs
Expand Up @@ -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,
}
}
}
32 changes: 32 additions & 0 deletions src/metrics/counter.rs
Expand Up @@ -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<N = u64> {
value: N,
}

impl<N> ConstCounter<N> {
/// Creates a new [`ConstCounter`].
pub fn new(value: N) -> Self {
Self { value }
}
}

impl<N> TypedMetric for ConstCounter<N> {
const TYPE: MetricType = MetricType::Counter;
}

impl<N> EncodeMetric for ConstCounter<N>
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::*;
Expand Down
19 changes: 19 additions & 0 deletions src/metrics/family.rs
Expand Up @@ -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;

Expand Down Expand Up @@ -326,6 +327,24 @@ where
}
}

impl<S: EncodeLabelSet, M: EncodeMetric + TypedMetric, T: Iterator<Item = (S, M)>> EncodeMetric
for RefCell<T>
{
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::*;
Expand Down

0 comments on commit c619ad5

Please sign in to comment.