use chronix_core::schema::SchemaRegistry;
pub const VALUE_FIELD: &str = "value";
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum ClassicView {
#[default]
Native,
Bucket,
Count,
Sum,
}
const CLASSIC_SUFFIXES: [(&str, ClassicView); 3] = [
("_bucket", ClassicView::Bucket),
("_count", ClassicView::Count),
("_sum", ClassicView::Sum),
];
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct MetricRef {
pub name: String,
pub measurement: String,
pub field: String,
pub view: ClassicView,
}
impl MetricRef {
#[must_use]
pub fn new(measurement: &str, field: &str) -> Self {
Self {
name: metric_name(measurement, field),
measurement: measurement.to_string(),
field: field.to_string(),
view: ClassicView::Native,
}
}
}
#[must_use]
pub fn metric_name(measurement: &str, field: &str) -> String {
if field == VALUE_FIELD {
measurement.to_string()
} else {
format!("{measurement}_{field}")
}
}
#[must_use]
pub fn all_metrics(registry: &SchemaRegistry) -> Vec<MetricRef> {
let mut out = Vec::new();
for measurement in registry.measurement_names() {
let Some(schema) = registry.lookup(&measurement) else {
continue;
};
for field in schema.field_names() {
out.push(MetricRef::new(&measurement, field));
}
}
out.sort();
out
}
#[must_use]
pub fn metrics_of(registry: &SchemaRegistry, measurement: &str) -> Vec<MetricRef> {
let Some(schema) = registry.lookup(measurement) else {
return Vec::new();
};
let mut out: Vec<MetricRef> = schema
.field_names()
.iter()
.map(|f| MetricRef::new(measurement, f))
.collect();
out.sort();
out
}
#[must_use]
pub fn resolve(registry: &SchemaRegistry, name: &str) -> Vec<MetricRef> {
let mut out = Vec::new();
if registry
.lookup(name)
.is_some_and(|s| s.field_names().contains(&VALUE_FIELD))
{
out.push(MetricRef {
name: name.to_string(),
measurement: name.to_string(),
field: VALUE_FIELD.to_string(),
view: ClassicView::Native,
});
}
for (idx, _) in name.match_indices('_') {
let (measurement, field) = (&name[..idx], &name[idx + 1..]);
if field == VALUE_FIELD {
continue;
}
if registry
.lookup(measurement)
.is_some_and(|s| s.field_names().contains(&field))
{
out.push(MetricRef {
name: name.to_string(),
measurement: measurement.to_string(),
field: field.to_string(),
view: ClassicView::Native,
});
}
}
for (suffix, view) in CLASSIC_SUFFIXES {
let Some(stem) = name.strip_suffix(suffix) else {
continue;
};
for mut base in resolve(registry, stem) {
if !is_histogram(registry, &base.measurement, &base.field) {
continue;
}
base.name = name.to_string();
base.view = view;
out.push(base);
}
}
out.sort();
out.dedup();
out
}
fn is_histogram(registry: &SchemaRegistry, measurement: &str, field: &str) -> bool {
registry.lookup(measurement).is_some_and(|s| {
s.column(field)
.is_some_and(|c| c.column_type == chronix_core::schema::ColumnType::Histogram)
})
}
#[cfg(test)]
mod tests {
use super::*;
use chronix_core::FieldValue;
use chronix_core::schema::MeasurementSchema;
fn registry(defs: &[(&str, &[&str])]) -> SchemaRegistry {
let reg = SchemaRegistry::new();
for (measurement, fields) in defs {
let mut schema = MeasurementSchema::new(*measurement);
for f in *fields {
schema.add_field(f, &FieldValue::F64(0.0)).unwrap();
}
reg.register_measurement(schema);
}
reg
}
#[test]
fn a_value_field_keeps_the_measurement_name() {
assert_eq!(metric_name("up", "value"), "up");
assert_eq!(metric_name("cpu", "usage"), "cpu_usage");
}
#[test]
fn resolve_inverts_metric_name_for_every_known_pair() {
let reg = registry(&[
("up", &["value"]),
("cpu", &["usage", "load"]),
("a_b", &["c"]),
]);
for m in all_metrics(®) {
let back = resolve(®, &m.name);
assert!(
back.contains(&m),
"{} did not resolve back to {m:?}, got {back:?}",
m.name
);
}
}
#[test]
fn an_ambiguous_name_resolves_to_every_pair_that_produces_it() {
let reg = registry(&[("a_b", &["c"]), ("a", &["b_c"])]);
let refs = resolve(®, "a_b_c");
assert_eq!(refs.len(), 2, "{refs:?}");
}
#[test]
fn a_measurement_without_a_value_field_is_not_a_metric() {
let reg = registry(&[("cpu", &["usage"])]);
assert!(resolve(®, "cpu").is_empty());
assert_eq!(resolve(®, "cpu_usage").len(), 1);
}
#[test]
fn m_value_names_a_measurement_not_a_value_field() {
let reg = registry(&[("m", &["value"]), ("m_value", &["value"])]);
let refs = resolve(®, "m_value");
assert_eq!(refs.len(), 1, "{refs:?}");
assert_eq!(refs[0].measurement, "m_value");
}
#[test]
fn adding_a_field_does_not_rename_an_existing_metric() {
let before = metric_name("cpu", "usage");
assert_eq!(before, metric_name("cpu", "usage"));
}
}