-
Notifications
You must be signed in to change notification settings - Fork 122
feat(metrics-registry): add core registration API #229
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
Merged
TheJokr
merged 4 commits into
cloudflare:main
from
ethanolchik:metrics/create-registry-core
Jul 10, 2026
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
1dd8793
feat(metrics-registry): add core registration API
ethanolchik c54670a
Apply suggestions from code review
ethanolchik 7babfa5
refactor(metrics-registry): simplify snapshots with static metric ref…
ethanolchik 5c1e72f
chore(metrics-registry): remove subsystem comment
ethanolchik File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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>; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| } | ||
|
|
||
| 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. | ||
|
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())))); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.