Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions foundations-metrics-registry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ license = { workspace = true }
workspace = true

[dependencies]
parking_lot = { workspace = true }
prost = { workspace = true }
prost-types = { workspace = true }

Expand Down
10 changes: 10 additions & 0 deletions foundations-metrics-registry/src/encode_metric.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use crate::proto::MetricFamily;

/// A metric that can encode itself into the protobuf data model.
///
/// Encoding is best-effort: implementations skip (and internally report) any
/// metric or series that fails, so an empty `Vec` is a valid result.
pub trait EncodeMetric: Send + Sync + 'static {
/// Encodes this metric into zero or more [`MetricFamily`] messages.
fn encode(&self) -> Vec<MetricFamily>;
}
53 changes: 53 additions & 0 deletions foundations-metrics-registry/src/iter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use crate::EncodeMetric;
use crate::RegistrationMetadata;
use crate::registry::Entry;

/// A registered metric together with its metadata.
///
/// Yielded by [`MetricsIter`].
pub struct RegisteredMetric {
entry: Entry,
}

impl RegisteredMetric {
/// The metadata supplied to [`register`](crate::register).
pub fn metadata(&self) -> &RegistrationMetadata {
&self.entry.metadata
}
/// The registered metric.
pub fn metric(&self) -> &dyn EncodeMetric {
self.entry.metric
}
}

/// A point-in-time snapshot iterator over the registered metrics.
///
/// Metrics registered after [`iter`](crate::iter()) was called are not observed,
/// and the registry lock is not held while iterating.
pub struct MetricsIter {
entries: std::vec::IntoIter<Entry>,
}

impl MetricsIter {
pub(crate) fn new(entries: Vec<Entry>) -> Self {
Self {
entries: entries.into_iter(),
}
}
}

impl Iterator for MetricsIter {
type Item = RegisteredMetric;

fn next(&mut self) -> Option<Self::Item> {
let entry = self.entries.next()?;

Some(RegisteredMetric { entry })
}

fn size_hint(&self) -> (usize, Option<usize>) {
self.entries.size_hint()
}
}

impl ExactSizeIterator for MetricsIter {}
11 changes: 11 additions & 0 deletions foundations-metrics-registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,15 @@
//! [`prometheus/client_model`]: https://github.com/prometheus/client_model
#![warn(missing_docs)]

mod encode_metric;
mod iter;
mod metadata;
mod registry;

pub mod proto;

pub use encode_metric::EncodeMetric;
pub use iter::{MetricsIter, RegisteredMetric};
pub use metadata::RegistrationMetadata;
pub use proto::MetricFamily;
pub use registry::{IntoMetrics, iter, register};
30 changes: 30 additions & 0 deletions foundations-metrics-registry/src/metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/// Metadata attached to a metric at registration time.
///
/// `#[non_exhaustive]` so fields can be added later on without breaking
/// [`register`](crate::register). Build it from [`default`](Self::default) plus
/// the setters, since downstream crates can't use a struct literal.
#[non_exhaustive]
#[derive(Clone, Default)]
pub struct RegistrationMetadata {
/// Whether the metric is exported only when optional metrics are requested.
pub optional: bool,

/// Whether to suppress the service-name prefix for this metric.
pub unprefixed: bool,
}

impl RegistrationMetadata {
/// Sets [`optional`](Self::optional)
#[must_use]
pub fn optional(mut self, optional: bool) -> Self {
self.optional = optional;
self
}

/// Sets [`unprefixed`](Self::unprefixed)
#[must_use]
pub fn unprefixed(mut self, unprefixed: bool) -> Self {
self.unprefixed = unprefixed;
self
}
}
115 changes: 115 additions & 0 deletions foundations-metrics-registry/src/registry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use std::sync::OnceLock;

use parking_lot::RwLock;

use crate::EncodeMetric;
use crate::RegistrationMetadata;
use crate::iter::MetricsIter;

/// A registered metric paired with its registration metadata
#[derive(Clone)]
pub(crate) struct Entry {
pub(crate) metadata: RegistrationMetadata,
pub(crate) metric: &'static dyn EncodeMetric,
}
Comment thread
ethanolchik marked this conversation as resolved.

static REGISTRY: OnceLock<RwLock<Vec<Entry>>> = OnceLock::new();

fn registry() -> &'static RwLock<Vec<Entry>> {
REGISTRY.get_or_init(|| RwLock::new(Vec::new()))
}

/// Registers one or more metrics with the given metadata.
///
/// Registration is append-only; there is no unregister. Service-name handling
/// deliberately happens at encode time, not here, so metrics can be registered
/// before the service name is known.
pub fn register(metrics: impl IntoMetrics, metadata: RegistrationMetadata) {
let mut guard = registry().write();
for metric in metrics.into_metrics() {
// The registry is append-only, so registered metrics live for the
// process lifetime and can be shared without reference counting.
let metric: &'static dyn EncodeMetric = Box::leak(metric);
let entry = Entry {
metadata: metadata.clone(),
metric,
};
guard.push(entry);
}
}

/// Returns a snapshot iterator over the registered metrics and their metadata.
pub fn iter() -> MetricsIter {
let entries = registry().read().clone();
MetricsIter::new(entries)
}

mod private {
pub trait Sealed {}
}

/// Converts a value into the metrics it contributes to the registry.
Comment thread
ethanolchik marked this conversation as resolved.
///
/// This allows passing both `Box<dyn EncodeMetric>` and `Vec<Box<dyn EncodeMetric>>` to [`register`].
pub trait IntoMetrics: private::Sealed {
/// Consumes `self`, yielding the metrics to be registered.
fn into_metrics(self) -> Vec<Box<dyn EncodeMetric>>;
}

impl private::Sealed for Box<dyn EncodeMetric> {}
impl IntoMetrics for Box<dyn EncodeMetric> {
fn into_metrics(self) -> Vec<Box<dyn EncodeMetric>> {
vec![self]
}
}

impl private::Sealed for Vec<Box<dyn EncodeMetric>> {}
impl IntoMetrics for Vec<Box<dyn EncodeMetric>> {
fn into_metrics(self) -> Vec<Box<dyn EncodeMetric>> {
self
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::proto::{Metric, MetricFamily, MetricType};

struct Named(&'static str);

impl EncodeMetric for Named {
fn encode(&self) -> Vec<MetricFamily> {
vec![MetricFamily {
name: Some(self.0.to_owned()),
help: Some("help.".to_owned()),
r#type: Some(MetricType::Counter as i32),
metric: vec![Metric::default()],
unit: Some("seconds".to_owned()),
}]
}
}

#[test]
fn register_and_iter_with_metadata() {
register(
Box::new(Named("required_metric")) as Box<dyn EncodeMetric>,
RegistrationMetadata::default(),
);
register(
vec![Box::new(Named("optional_metric")) as Box<dyn EncodeMetric>],
RegistrationMetadata::default().optional(true),
);

let observed: Vec<(bool, Option<String>)> = iter()
.map(|reg| {
(
reg.metadata().optional,
reg.metric().encode()[0].name.clone(),
)
})
.collect();

assert!(observed.contains(&(false, Some("required_metric".to_owned()))));
assert!(observed.contains(&(true, Some("optional_metric".to_owned()))));
}
}
Loading