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
3 changes: 3 additions & 0 deletions foundations-metrics-registry/src/encode_metric.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@ use crate::proto::MetricFamily;
/// 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.
///
/// Every returned [`MetricFamily`] must set `name` to a complete, non-empty
/// producer-level name.
fn encode(&self) -> Vec<MetricFamily>;
}
2 changes: 1 addition & 1 deletion foundations-metrics-registry/src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
/// [`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)]
#[derive(Clone, Debug, Default)]
pub struct RegistrationMetadata {
/// Whether the metric is exported only when optional metrics are requested.
pub optional: bool,
Expand Down
3 changes: 2 additions & 1 deletion foundations-metrics-registry/src/proto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
mod model;

pub use model::{
Bucket, BucketSpan, Counter, Gauge, Histogram, LabelPair, Metric, MetricFamily, MetricType,
Bucket, BucketSpan, Counter, Exemplar, Gauge, Histogram, LabelPair, Metric, MetricFamily,
MetricType, Quantile, Summary,
};

#[cfg(test)]
Expand Down
1 change: 1 addition & 0 deletions foundations-metrics/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ prometheus-client = { version = "0.25.0", features = [
serde = { workspace = true, features = ["derive"] }
ryu = "1.0.23"
parking_lot = { workspace = true }
prost = { workspace = true }

[lints]
workspace = true
164 changes: 164 additions & 0 deletions foundations-metrics/src/collect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
use foundations_metrics_registry::{iter, proto::LabelPair};

use crate::MetricFamily;

/// Options that control which registered metrics are collected and how the
/// service name is represented.
#[derive(Copy, Clone, Debug)]
pub struct CollectionOptions<'a> {
/// Whether metrics registered as optional are included.
pub include_optional: bool,

/// Service name to add to collected metrics, if any.
pub service_name: Option<&'a str>,

/// How `service_name` is represented in collected metrics.
pub service_name_format: ServiceNameFormat<'a>,
}

/// How a service name is represented in collected metrics.
#[derive(Copy, Clone, Debug)]
pub enum ServiceNameFormat<'a> {
/// Prefix metric family names with the service name.
MetricPrefix,

/// Add the service name to every metric row under the given label name.
LabelWithName(&'a str),
}

/// Collects the currently registered metrics into the canonical protobuf model.
pub fn collect(options: CollectionOptions) -> Vec<MetricFamily> {
let mut collected = Vec::new();

for registered in iter() {
let metadata = registered.metadata();

if metadata.optional && !options.include_optional {
continue;
}

let mut families = registered.metric().encode();

if let Some(service_name) = options.service_name {
match options.service_name_format {
ServiceNameFormat::MetricPrefix if !metadata.unprefixed => {
for family in &mut families {
if let Some(name) = &mut family.name {
name.insert(0, '_');
name.insert_str(0, service_name);
}
}
}
ServiceNameFormat::LabelWithName(label_name) => {
let service_label = LabelPair {
name: Some(label_name.to_owned()),
value: Some(service_name.to_owned()),
};

for family in &mut families {
for metric in &mut family.metric {
metric.label.insert(0, service_label.clone());
}
}
}
ServiceNameFormat::MetricPrefix => {}
}
}

collected.extend(families);
}

collected
}

#[cfg(test)]
mod tests {
use foundations_metrics_registry::proto::{Metric, MetricType};

use super::*;
use crate::{EncodeMetric, RegistrationMetadata, register};

struct TestMetric(&'static str);

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

fn register_test_metric(name: &'static str, metadata: RegistrationMetadata) {
register(
Box::new(TestMetric(name)) as Box<dyn EncodeMetric>,
metadata,
);
}

#[test]
fn filters_optional_metrics_and_applies_service_prefix() {
register_test_metric("collect_required_metric", RegistrationMetadata::default());
register_test_metric(
"collect_optional_metric",
RegistrationMetadata::default().optional(true),
);
register_test_metric(
"collect_unprefixed_metric",
RegistrationMetadata::default().unprefixed(true),
);

let required = collect(CollectionOptions {
include_optional: false,
service_name: Some("test_service"),
service_name_format: ServiceNameFormat::MetricPrefix,
});
let required_names: Vec<_> = required
.iter()
.filter_map(|family| family.name.as_deref())
.collect();

assert!(required_names.contains(&"test_service_collect_required_metric"));
assert!(!required_names.contains(&"test_service_collect_optional_metric"));
assert!(required_names.contains(&"collect_unprefixed_metric"));

let with_optional = collect(CollectionOptions {
include_optional: true,
service_name: Some("test_service"),
service_name_format: ServiceNameFormat::MetricPrefix,
});

assert!(with_optional.iter().any(|family| {
family.name.as_deref() == Some("test_service_collect_optional_metric")
}));
}

#[test]
fn service_label_is_added_to_prefixed_and_unprefixed_metrics() {
register_test_metric("collect_label_metric", RegistrationMetadata::default());
register_test_metric(
"collect_label_unprefixed_metric",
RegistrationMetadata::default().unprefixed(true),
);

let families = collect(CollectionOptions {
include_optional: false,
service_name: Some("test_service"),
service_name_format: ServiceNameFormat::LabelWithName("service"),
});

for name in ["collect_label_metric", "collect_label_unprefixed_metric"] {
let family = families
.iter()
.find(|family| family.name.as_deref() == Some(name))
.expect("registered metric should be collected");
let label = &family.metric[0].label[0];

assert_eq!(label.name.as_deref(), Some("service"));
assert_eq!(label.value.as_deref(), Some("test_service"));
}
}
}
111 changes: 111 additions & 0 deletions foundations-metrics/src/encoding/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
mod text;

use prost::Message;

use crate::MetricFamily;

pub use text::encode_to_text;

/// Encodes metric families as length-delimited Prometheus protobuf messages.
pub fn encode_to_protobuf(families: &[MetricFamily]) -> Vec<u8> {
families
.iter()
.flat_map(Message::encode_length_delimited_to_vec)
.collect()
}

#[cfg(test)]
mod tests {
use foundations_metrics_registry::proto::{
Bucket, Gauge, Histogram, LabelPair, Metric, MetricType, Quantile, Summary,
};

use super::*;

#[test]
fn preserves_summary_and_gauge_histogram_families() {
let families = [
MetricFamily {
name: Some("empty_summary".to_owned()),
help: None,
r#type: Some(MetricType::Summary as i32),
metric: Vec::new(),
unit: None,
},
MetricFamily {
name: Some("request_size".to_owned()),
help: Some("Request size.".to_owned()),
r#type: Some(MetricType::Summary as i32),
metric: vec![Metric {
summary: Some(Summary {
sample_count: Some(2),
sample_sum: Some(6.0),
quantile: vec![Quantile {
quantile: Some(0.5),
value: Some(3.0),
}],
created_timestamp: None,
}),
..Default::default()
}],
unit: None,
},
MetricFamily {
name: Some("empty_gauge_histogram".to_owned()),
help: None,
r#type: Some(MetricType::GaugeHistogram as i32),
metric: Vec::new(),
unit: None,
},
MetricFamily {
name: Some("queue_item_age".to_owned()),
help: Some("Current age distribution of queued items.".to_owned()),
r#type: Some(MetricType::GaugeHistogram as i32),
metric: vec![Metric {
histogram: Some(Histogram {
sample_count: Some(3),
sample_sum: Some(8.0),
bucket: vec![Bucket {
cumulative_count: Some(1),
upper_bound: Some(1.0),
..Default::default()
}],
..Default::default()
}),
..Default::default()
}],
unit: None,
},
];

let expected: Vec<_> = families
.iter()
.flat_map(Message::encode_length_delimited_to_vec)
.collect();

assert_eq!(encode_to_protobuf(&families), expected);
}

#[test]
fn preserves_legacy_info_gauge_representation() {
let families = [MetricFamily {
name: Some("build_info".to_owned()),
help: Some("Build information.".to_owned()),
r#type: Some(MetricType::Gauge as i32),
metric: vec![Metric {
label: vec![LabelPair {
name: Some("version".to_owned()),
value: Some("1.2.3".to_owned()),
}],
gauge: Some(Gauge { value: Some(1.0) }),
..Default::default()
}],
unit: None,
}];

let encoded = encode_to_protobuf(&families);
let decoded = MetricFamily::decode_length_delimited(encoded.as_slice()).unwrap();

assert_eq!(decoded, families[0]);
}
}
Loading
Loading