use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct Sample {
pub name: String,
pub labels: Vec<(String, String)>,
pub value: f64,
}
impl Sample {
pub fn label(&self, name: &str) -> Option<&str> {
self.labels
.iter()
.find(|(k, _)| k == name)
.map(|(_, v)| v.as_str())
}
pub fn series(&self) -> String {
if self.labels.is_empty() {
return self.name.clone();
}
let labels: Vec<String> = self
.labels
.iter()
.map(|(k, v)| format!("{k}=\"{v}\""))
.collect();
format!("{}{{{}}}", self.name, labels.join(","))
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Metrics {
pub samples: Vec<Sample>,
types: HashMap<String, String>,
}
impl Metrics {
pub fn parse(text: &str) -> Metrics {
let mut metrics = Metrics::default();
for line in text.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("# TYPE ") {
if let Some((name, kind)) = rest.split_once(' ') {
metrics
.types
.insert(name.to_owned(), kind.trim().to_owned());
}
} else if !line.is_empty() && !line.starts_with('#') {
if let Some(sample) = parse_sample(line) {
metrics.samples.push(sample);
}
}
}
metrics
}
pub fn kind(&self, name: &str) -> Option<&str> {
self.types.get(name).map(String::as_str)
}
pub fn is_counter(&self, name: &str) -> bool {
self.kind(name) == Some("counter")
|| name
.strip_suffix("_total")
.is_some_and(|base| self.kind(base) == Some("counter"))
}
pub fn sum(&self, name: &str) -> Option<f64> {
let mut found = false;
let mut total = 0.0;
for sample in self.samples.iter().filter(|s| s.name == name) {
found = true;
total += sample.value;
}
found.then_some(total)
}
}
pub fn rate(previous: f64, now: f64, seconds: f64) -> Option<f64> {
(seconds > 0.0 && now >= previous).then(|| (now - previous) / seconds)
}
fn parse_sample(line: &str) -> Option<Sample> {
let (head, rest) = match line.find('{') {
Some(open) => {
let close = closing_brace(line, open)?;
(&line[..=close], &line[close + 1..])
}
None => line.split_once(' ')?,
};
let (name, labels) = match head.find('{') {
Some(open) => (&head[..open], parse_labels(&head[open + 1..head.len() - 1])),
None => (head, Vec::new()),
};
let value = rest.split_whitespace().next()?;
Some(Sample {
name: name.to_owned(),
labels,
value: parse_value(value)?,
})
}
fn parse_value(text: &str) -> Option<f64> {
match text {
"+Inf" | "Inf" => Some(f64::INFINITY),
"-Inf" => Some(f64::NEG_INFINITY),
"NaN" => Some(f64::NAN),
other => other.parse().ok(),
}
}
fn closing_brace(line: &str, open: usize) -> Option<usize> {
let mut quoted = false;
let mut escaped = false;
for (i, c) in line.char_indices().skip_while(|(i, _)| *i < open) {
match c {
_ if escaped => escaped = false,
'\\' if quoted => escaped = true,
'"' => quoted = !quoted,
'}' if !quoted => return Some(i),
_ => {}
}
}
None
}
fn parse_labels(text: &str) -> Vec<(String, String)> {
let mut labels = Vec::new();
let mut rest = text;
while let Some((name, after)) = rest.split_once("=\"") {
let mut value = String::new();
let mut chars = after.char_indices();
let mut end = after.len();
while let Some((i, c)) = chars.next() {
match c {
'\\' => match chars.next() {
Some((_, 'n')) => value.push('\n'),
Some((_, other)) => value.push(other),
None => break,
},
'"' => {
end = i + 1;
break;
}
other => value.push(other),
}
}
labels.push((name.trim_start_matches(',').trim().to_owned(), value));
rest = after.get(end..).unwrap_or("");
}
labels
}
#[cfg(test)]
mod tests;