Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

src/collector: Introduce Collector abstraction #82

Merged
merged 14 commits into from Dec 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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>
Comment on lines +330 to +331
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very hard to discover and doesn't feel Rust-y to me. Why do we need to depend on RefCell here?

I think it would be more idiomatic to do:

  • impl EncodeMetric for Vec<T> where T: EncodeMetric { }
  • impl EncodeMetric for (S, M) where S: EncodeLabelSet, M: EncodeMetric { }

Then, have the user provide a Vec instead of being generic over something that can be iterated. That should get rid of the RefCell and iterator dependency and would compose better with other usecases.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An Iterator::next takes &mut self in order to be able to e.g. track its position within a Vec. Unfortunately EncodeMetric::encode takes &self.

In this pull request RefCell is used for interior mutability. It allows calling an Iterator::next taking &mut self from an EncodeMetric::encode providing &self only.

An alternative approach would be to change EncodeMetric::encode to take &mut self. I deemed the change and its implications not worth it, given that Collector is an abstraction for advanced users only.

All that said, I agree that the usage of RefCell is neither simple nor intuitive. @thomaseizinger do you see other alternatives to the one above?

{
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