#![forbid(unsafe_code)]
use derive_more::Constructor;
pub use parser::{
parse_scrape,
MetricError,
ScrapeParseError,
};
use std::fmt::Display;
mod parser;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, strum::EnumString, strum::Display)]
#[strum(ascii_case_insensitive)]
#[strum(serialize_all = "snake_case")]
pub enum Type {
Counter,
Gauge,
#[default]
Untyped,
Summary,
Histogram,
}
#[derive(Debug, Clone, PartialEq, Eq, derive_more::Constructor)]
pub struct Label {
pub key: String,
pub value: String,
}
#[derive(
Debug,
Clone,
PartialEq,
Eq,
Default,
derive_more::Deref,
derive_more::DerefMut,
derive_more::From,
)]
#[repr(transparent)]
pub struct Labels(Vec<Label>);
impl Display for Labels {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.0.is_empty() {
return Ok(());
}
let last_idx = self.0.len() - 1;
f.write_str("{")?;
for (idx, label) in self.0.iter().enumerate() {
f.write_str(&label.key)?;
f.write_str("=\"")?;
f.write_str(&label.value)?;
f.write_str("\"")?;
if idx != last_idx {
f.write_str(",")?;
}
}
f.write_str("}")?;
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValueType {
Sample,
Sum,
Count,
}
#[derive(
Debug,
Clone,
PartialEq,
Eq,
derive_more::Deref,
derive_more::DerefMut,
derive_more::From,
derive_more::FromStr,
)]
#[repr(transparent)]
pub struct Float(String);
impl Float {
pub fn as_f64(&self) -> f64 {
self.0.parse().unwrap()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Constructor)]
pub struct Value {
pub value_type: ValueType,
pub value: Float,
pub timestamp: Option<i64>,
}
impl Display for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.value.as_str())?;
if let Some(ts) = self.timestamp {
write!(f, " {ts}")?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Constructor)]
pub struct Sample {
pub labels: Labels,
pub value: Value,
}
#[derive(Debug, Clone, Constructor)]
pub struct Metric {
pub kind: Type,
pub help_desc: Option<String>,
pub name: String,
pub samples: Vec<Sample>,
}
impl Metric {
pub fn add_label(&mut self, key: &str, value: &str) {
for sample in &mut self.samples {
sample.labels.0.push(Label::new(key.into(), value.into()))
}
}
pub fn remove_label(&mut self, key: &str, value: &str) {
for sample in &mut self.samples {
let maybe_idx = sample
.labels
.0
.iter()
.position(|label| label.key == key && label.value == value);
if let Some(idx) = maybe_idx {
sample.labels.0.remove(idx);
}
}
}
}
impl Display for Metric {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(help) = self.help_desc.as_deref() {
writeln!(f, "# HELP {} {help}", self.name)?;
}
if self.kind != Type::Untyped {
writeln!(f, "# TYPE {} {}", self.name, self.kind)?;
}
for sample in self.samples.iter() {
let suffix = match (self.kind, sample.value.value_type) {
(Type::Histogram, ValueType::Sample) => "_bucket",
(Type::Histogram | Type::Summary, ValueType::Sum) => "_sum",
(Type::Histogram | Type::Summary, ValueType::Count) => "_count",
_ => "",
};
writeln!(f, "{}{suffix}{} {}", self.name, sample.labels, sample.value)?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct Scrape {
pub metrics: Vec<Metric>,
}
impl Display for Scrape {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for metric in self.metrics.iter() {
write!(f, "{metric}")?;
}
Ok(())
}
}
impl Scrape {
pub fn parse(data: &str) -> Result<Self, ScrapeParseError> {
let (metrics, maybe_error) = parse_scrape(data);
match maybe_error {
Some(error) => Err(error),
None => Ok(Self { metrics }),
}
}
pub fn add_label(&mut self, key: &str, value: &str) {
for metric in &mut self.metrics {
metric.add_label(key, value);
}
}
pub fn remove_label(&mut self, key: &str, value: &str) {
for metric in &mut self.metrics {
metric.remove_label(key, value);
}
}
}
#[cfg(test)]
pub mod tests {
use super::{
Scrape,
Type,
ValueType,
};
use pretty_assertions::assert_eq;
use rstest::rstest;
use std::{
str::FromStr,
sync::Once,
};
use tracing_subscriber::EnvFilter;
static INIT_LOGGER: Once = Once::new();
pub(crate) fn init_test_logging() {
INIT_LOGGER.call_once(|| {
tracing_subscriber::fmt::fmt()
.with_env_filter(EnvFilter::new("warn,prom_text_format_parser=debug"))
.init();
});
}
pub const EXAMPLE_01: &str = include_str!("../test_data/example-01.txt");
pub const NODE_EXPORTER_01: &str = include_str!("../test_data/node-exporter-01.txt");
pub const PROMETHEUS_01: &str = include_str!("../test_data/prometheus-01.txt");
pub const FAIL_01: &str = include_str!("../test_data/fail-no-sample-01.txt");
#[test]
fn test_value_type_conversion() {
let cases = [
("untyped", Type::Untyped),
("UNTYPED", Type::Untyped),
("counter", Type::Counter),
("COUNTER", Type::Counter),
("gauge", Type::Gauge),
("GAUGE", Type::Gauge),
("histogram", Type::Histogram),
("HISTOGRAM", Type::Histogram),
("summary", Type::Summary),
("SUMMARY", Type::Summary),
];
for (expr, expected) in cases {
let found = Type::from_str(expr).unwrap();
assert_eq!(found, expected);
}
}
#[rstest]
fn test_end_to_end(#[values(NODE_EXPORTER_01, PROMETHEUS_01)] data: &str) {
init_test_logging();
let scrape = Scrape::parse(data).unwrap();
let printed = format!("{scrape}");
assert_eq!(data, printed);
}
#[rstest]
fn test_scrape_failure(#[values("", FAIL_01)] data: &str) {
init_test_logging();
let res = Scrape::parse(data);
assert!(res.is_err());
}
#[test]
fn test_complex_scrape() {
init_test_logging();
let data = EXAMPLE_01;
let metrics = Scrape::parse(data).unwrap().metrics;
assert_eq!(metrics.len(), 6);
let metric1 = metrics[0].clone();
assert_eq!(metric1.name, "http_requests_total");
assert_eq!(
metric1.help_desc.as_deref(),
Some("The total number of HTTP requests.")
);
assert_eq!(metric1.kind, Type::Counter);
assert_eq!(metric1.samples.len(), 2);
assert_eq!(metric1.samples[0].labels.len(), 2);
assert_eq!(metric1.samples[0].labels[0].key, "method");
assert_eq!(metric1.samples[0].labels[0].value, "post");
assert_eq!(metric1.samples[0].labels[1].key, "code");
assert_eq!(metric1.samples[0].labels[1].value, "200");
assert_eq!(*metric1.samples[0].value.value, "1027");
assert_eq!(metric1.samples[0].value.value_type, ValueType::Sample);
assert_eq!(metric1.samples[0].value.timestamp, Some(1395066363000));
assert_eq!(metric1.samples[1].labels.len(), 2);
assert_eq!(metric1.samples[1].labels[0].key, "method");
assert_eq!(metric1.samples[1].labels[0].value, "post");
assert_eq!(metric1.samples[1].labels[1].key, "code");
assert_eq!(metric1.samples[1].labels[1].value, "400");
assert_eq!(*metric1.samples[1].value.value, "3");
assert_eq!(metric1.samples[1].value.value_type, ValueType::Sample);
assert_eq!(metric1.samples[1].value.timestamp, Some(1395066363000));
let metric2 = metrics[1].clone();
assert_eq!(metric2.name, "msdos_file_access_time_seconds");
assert_eq!(metric2.help_desc.as_deref(), None);
assert_eq!(metric2.kind, Type::Untyped);
assert_eq!(metric2.samples.len(), 1);
assert_eq!(metric2.samples[0].labels.len(), 2);
assert_eq!(metric2.samples[0].labels[0].key, "path");
assert_eq!(metric2.samples[0].labels[0].value, r"C:\\DIR\\FILE.TXT");
assert_eq!(metric2.samples[0].labels[1].key, "error");
assert_eq!(
metric2.samples[0].labels[1].value,
r#"Cannot find file:\n\"FILE.TXT\""#
);
assert_eq!(*metric2.samples[0].value.value, "1.458255915e9");
assert_eq!(metric2.samples[0].value.value_type, ValueType::Sample);
assert_eq!(metric2.samples[0].value.timestamp, None);
let metric3 = metrics[2].clone();
assert_eq!(metric3.name, "metric_without_timestamp_and_labels");
assert_eq!(metric3.help_desc.as_deref(), None);
assert_eq!(metric3.kind, Type::Untyped);
assert_eq!(metric3.samples.len(), 1);
assert_eq!(metric3.samples[0].labels.len(), 0);
assert_eq!(*metric3.samples[0].value.value, "12.47");
assert_eq!(metric3.samples[0].value.value_type, ValueType::Sample);
assert_eq!(metric3.samples[0].value.timestamp, None);
let metric4 = metrics[3].clone();
assert_eq!(metric4.name, "something_weird");
assert_eq!(metric4.help_desc.as_deref(), None);
assert_eq!(metric4.kind, Type::Untyped);
assert_eq!(metric4.samples.len(), 1);
assert_eq!(metric4.samples[0].labels.len(), 1);
assert_eq!(metric4.samples[0].labels[0].key, "problem");
assert_eq!(metric4.samples[0].labels[0].value, "division by zero");
assert_eq!(*metric4.samples[0].value.value, "+Inf");
assert_eq!(metric4.samples[0].value.value_type, ValueType::Sample);
assert_eq!(metric4.samples[0].value.timestamp, Some(-3982045));
}
fn prepare_test_data(data: &str) -> String {
data.lines()
.map(|l| l.trim())
.filter(|l| !l.is_empty())
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn test_add_label() {
let input = r#"
# HELP http_request_duration_seconds A histogram of the request duration.
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.05"} 24054
http_request_duration_seconds_bucket{le="0.1"} 33444
http_request_duration_seconds_bucket{le="0.2"} 100392
http_request_duration_seconds_bucket{le="0.5"} 129389
http_request_duration_seconds_bucket{le="1"} 133988
http_request_duration_seconds_bucket{le="+Inf"} 144320
http_request_duration_seconds_sum 53423
http_request_duration_seconds_count 144320
"#;
let expected = r#"
# HELP http_request_duration_seconds A histogram of the request duration.
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.05",one="two",three="3"} 24054
http_request_duration_seconds_bucket{le="0.1",one="two",three="3"} 33444
http_request_duration_seconds_bucket{le="0.2",one="two",three="3"} 100392
http_request_duration_seconds_bucket{le="0.5",one="two",three="3"} 129389
http_request_duration_seconds_bucket{le="1",one="two",three="3"} 133988
http_request_duration_seconds_bucket{le="+Inf",one="two",three="3"} 144320
http_request_duration_seconds_sum{one="two",three="3"} 53423
http_request_duration_seconds_count{one="two",three="3"} 144320
"#;
let input = prepare_test_data(input);
let mut expected = prepare_test_data(expected);
expected.push('\n');
let mut scrape = Scrape::parse(&input).unwrap();
scrape.add_label("one", "two");
scrape.add_label("three", "3");
let output = format!("{scrape}");
assert_eq!(output, expected);
}
#[test]
fn test_remove_label() {
let input = r#"
# HELP http_request_duration_seconds A histogram of the request duration.
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.05",one="two",three="3"} 24054
http_request_duration_seconds_bucket{le="0.1",one="two",three="3"} 33444
http_request_duration_seconds_bucket{le="0.2",one="two",three="3"} 100392
http_request_duration_seconds_bucket{le="0.5",one="two",three="3"} 129389
http_request_duration_seconds_bucket{le="1",one="two",three="3"} 133988
http_request_duration_seconds_bucket{le="+Inf",one="two",three="3"} 144320
http_request_duration_seconds_sum{one="two",three="3"} 53423
http_request_duration_seconds_count{one="two",three="3"} 144320
"#;
let expected = r#"
# HELP http_request_duration_seconds A histogram of the request duration.
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.05"} 24054
http_request_duration_seconds_bucket{le="0.1"} 33444
http_request_duration_seconds_bucket{le="0.2"} 100392
http_request_duration_seconds_bucket{le="0.5"} 129389
http_request_duration_seconds_bucket{le="1"} 133988
http_request_duration_seconds_bucket{le="+Inf"} 144320
http_request_duration_seconds_sum 53423
http_request_duration_seconds_count 144320
"#;
let input = prepare_test_data(input);
let mut expected = prepare_test_data(expected);
expected.push('\n');
let mut scrape = Scrape::parse(&input).unwrap();
scrape.remove_label("one", "two");
scrape.remove_label("three", "3");
let output = format!("{scrape}");
assert_eq!(output, expected);
}
}