use crate::core::DbCore;
use crate::error::DbError;
use crate::prometheus_api::{eval_matrix, eval_vector};
use crate::query_surface::{parse_eval_expr, parse_instant_selector, series_matches_selector};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug)]
pub enum PromqlError {
Parse(String),
BadParameter(String),
Execution(DbError),
}
impl std::fmt::Display for PromqlError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PromqlError::Parse(s) => write!(f, "parse error: {}", s),
PromqlError::BadParameter(s) => write!(f, "bad parameter: {}", s),
PromqlError::Execution(e) => write!(f, "execution error: {}", e),
}
}
}
impl std::error::Error for PromqlError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
PromqlError::Execution(e) => Some(e),
_ => None,
}
}
}
impl From<DbError> for PromqlError {
fn from(e: DbError) -> Self {
PromqlError::Execution(e)
}
}
#[derive(Debug, Clone, PartialEq)]
#[must_use]
pub struct InstantSample {
pub metric: HashMap<String, String>,
pub ts_ns: u64,
pub value: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[must_use]
pub struct RangeSeries {
pub metric: HashMap<String, String>,
pub steps: Vec<(u64, f64)>,
}
pub type MetricLabels = HashMap<String, String>;
pub fn query_instant(
db: &Arc<DbCore>,
query: &str,
time_ns: u64,
) -> Result<Vec<InstantSample>, PromqlError> {
let q = query.trim();
if q.is_empty() {
return Err(PromqlError::BadParameter("query string is empty".to_string()));
}
let expr = parse_eval_expr(q).map_err(PromqlError::Parse)?;
let internal = eval_vector(&expr, time_ns, db).map_err(PromqlError::Execution)?;
Ok(internal
.into_iter()
.map(|s| InstantSample {
metric: s.metric,
ts_ns: s.ts_ns,
value: s.value,
})
.collect())
}
pub fn query_range(
db: &Arc<DbCore>,
query: &str,
start_ns: u64,
end_ns: u64,
step_ns: u64,
) -> Result<Vec<RangeSeries>, PromqlError> {
let q = query.trim();
if q.is_empty() {
return Err(PromqlError::BadParameter("query string is empty".to_string()));
}
if start_ns >= end_ns {
return Err(PromqlError::BadParameter(
"start must be before end".to_string(),
));
}
if step_ns == 0 {
return Err(PromqlError::BadParameter(
"step must be positive".to_string(),
));
}
let expr = parse_eval_expr(q).map_err(PromqlError::Parse)?;
let internal = eval_matrix(&expr, start_ns, end_ns, step_ns, db).map_err(PromqlError::Execution)?;
Ok(internal
.into_iter()
.map(|sd| RangeSeries {
metric: sd.metric,
steps: sd.steps,
})
.collect())
}
fn now_ns() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64
}
pub fn parse_eval_time(s: Option<&str>) -> Result<u64, String> {
let s = match s {
Some(t) => t.trim(),
None => return Ok(now_ns()),
};
if s.is_empty() {
return Ok(now_ns());
}
if let Ok(secs) = s.parse::<f64>() {
if secs.is_finite() && secs >= 0.0 {
return Ok((secs * 1e9) as u64);
}
}
if let Ok(secs) = s.parse::<u64>() {
return Ok(secs * 1_000_000_000);
}
Err(format!(
"invalid time parameter (use Unix seconds): {:?}",
s
))
}
pub fn parse_step(s: &str) -> Result<u64, String> {
let s = s.trim();
if s.is_empty() {
return Err("empty step parameter".to_string());
}
let s = ascii_lower(s);
let (num_str, mult) = if s.ends_with('s') {
(&s[..s.len() - 1], 1u64)
} else if s.ends_with('m') {
(&s[..s.len() - 1], 60)
} else if s.ends_with('h') {
(&s[..s.len() - 1], 3600)
} else if s.ends_with('d') {
(&s[..s.len() - 1], 86400)
} else if let Ok(n) = s.parse::<f64>() {
if !n.is_finite() || n < 0.0 {
return Err("step must be non-negative".to_string());
}
return Ok((n as u64) * 1_000_000_000);
} else {
return Err(format!("invalid step: {:?}", s));
};
let n: f64 = num_str
.parse()
.map_err(|_| format!("invalid step number: {:?}", num_str))?;
if !n.is_finite() || n < 0.0 {
return Err("step must be non-negative".to_string());
}
Ok((n * mult as f64) as u64 * 1_000_000_000)
}
fn ascii_lower(s: &str) -> String {
s.chars().map(|c| c.to_ascii_lowercase()).collect()
}
pub fn labels(
db: &Arc<DbCore>,
match_selectors: Option<&[impl AsRef<str>]>,
start_ns: u64,
end_ns: u64,
) -> Result<Vec<String>, PromqlError> {
if start_ns >= end_ns {
return Err(PromqlError::BadParameter(
"start must be before end".to_string(),
));
}
let query_end = end_ns.saturating_add(1);
let keys = db.list_series_keys();
let keys_to_consider: Vec<(String, HashMap<String, String>)> = match match_selectors {
None => keys.clone(),
Some(s) if s.is_empty() => keys.clone(),
Some(selectors) => {
let parsed: Vec<_> = selectors
.iter()
.map(|s| parse_instant_selector(s.as_ref().trim()).map_err(PromqlError::Parse))
.collect::<Result<Vec<_>, _>>()?;
keys.into_iter()
.filter(|(name, tags)| {
parsed.iter().any(|inst| {
series_matches_selector(name, tags, &inst.selector)
})
})
.collect()
}
};
let mut names: HashSet<String> = HashSet::new();
names.insert("__name__".to_string());
for (series_name, tags) in keys_to_consider {
let range = start_ns..query_end;
match db.query(&series_name, range, Some(&tags)) {
Ok(points) if !points.is_empty() => {
for k in tags.keys() {
names.insert(k.clone());
}
}
Ok(_) | Err(_) => {}
}
}
let mut data: Vec<String> = names.into_iter().collect();
data.sort();
Ok(data)
}
pub fn label_values(
db: &Arc<DbCore>,
label_name: &str,
match_selectors: Option<&[impl AsRef<str>]>,
start_ns: u64,
end_ns: u64,
) -> Result<Vec<String>, PromqlError> {
if start_ns >= end_ns {
return Err(PromqlError::BadParameter(
"start must be before end".to_string(),
));
}
let query_end = end_ns.saturating_add(1);
let keys = db.list_series_keys();
let keys_to_consider: Vec<(String, HashMap<String, String>)> = match match_selectors {
None => keys.clone(),
Some(s) if s.is_empty() => keys.clone(),
Some(selectors) => {
let parsed: Vec<_> = selectors
.iter()
.map(|s| parse_instant_selector(s.as_ref().trim()).map_err(PromqlError::Parse))
.collect::<Result<Vec<_>, _>>()?;
keys.into_iter()
.filter(|(name, tags)| {
parsed.iter().any(|inst| {
series_matches_selector(name, tags, &inst.selector)
})
})
.collect()
}
};
let mut values: HashSet<String> = HashSet::new();
for (series_name, tags) in keys_to_consider {
let range = start_ns..query_end;
match db.query(&series_name, range, Some(&tags)) {
Ok(points) if !points.is_empty() => {
if label_name == "__name__" {
values.insert(series_name);
} else if let Some(v) = tags.get(label_name) {
values.insert(v.clone());
}
}
Ok(_) | Err(_) => {}
}
}
let mut data: Vec<String> = values.into_iter().collect();
data.sort();
Ok(data)
}
pub fn series(
db: &Arc<DbCore>,
match_selectors: &[impl AsRef<str>],
start_ns: u64,
end_ns: u64,
) -> Result<Vec<MetricLabels>, PromqlError> {
let selectors_ref: Vec<&str> = match_selectors.iter().map(AsRef::as_ref).collect();
let non_empty: Vec<&str> = selectors_ref.iter().map(|s| s.trim()).filter(|s| !s.is_empty()).collect();
if non_empty.is_empty() {
return Err(PromqlError::BadParameter(
"at least one match[] parameter is required".to_string(),
));
}
if start_ns >= end_ns {
return Err(PromqlError::BadParameter(
"start must be before end".to_string(),
));
}
let query_end = end_ns.saturating_add(1);
let keys = db.list_series_keys();
let parsed: Vec<_> = non_empty
.iter()
.map(|s| parse_instant_selector(s).map_err(PromqlError::Parse))
.collect::<Result<Vec<_>, _>>()?;
let keys_to_consider: Vec<(String, HashMap<String, String>)> = keys
.into_iter()
.filter(|(name, tags)| {
parsed.iter().any(|inst| series_matches_selector(name, tags, &inst.selector))
})
.collect();
let mut seen: HashSet<Vec<(String, String)>> = HashSet::new();
let mut out: Vec<MetricLabels> = Vec::new();
for (series_name, tags) in keys_to_consider {
let range = start_ns..query_end;
match db.query(&series_name, range, Some(&tags)) {
Ok(points) if !points.is_empty() => {
let metric = metric_from_series_and_tags(&series_name, &tags);
let key: Vec<(String, String)> = {
let mut k: Vec<_> = metric.iter().map(|(a, b)| (a.clone(), b.clone())).collect();
k.sort_by(|a, b| a.0.cmp(&b.0));
k
};
if seen.insert(key) {
out.push(metric);
}
}
Ok(_) | Err(_) => {}
}
}
out.sort_by(|a, b| {
let mut ka: Vec<_> = a.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
let mut kb: Vec<_> = b.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
ka.sort_by(|x, y| x.0.cmp(y.0));
kb.sort_by(|x, y| x.0.cmp(y.0));
ka.cmp(&kb)
});
Ok(out)
}
fn metric_from_series_and_tags(
series: &str,
tags: &HashMap<String, String>,
) -> HashMap<String, String> {
let mut m = HashMap::new();
m.insert("__name__".to_string(), series.to_string());
for (k, v) in tags {
m.insert(k.clone(), v.clone());
}
m
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::error::Error;
fn make_db_with_series() -> (Arc<DbCore>, tempfile::TempDir) {
let dir = tempfile::tempdir().unwrap();
let config = crate::DbConfig {
data_dir: dir.path().to_path_buf(),
max_series_cardinality: Some(1000),
..Default::default()
};
let mut db = DbCore::with_config(config).unwrap();
db.recover().unwrap();
let db = Arc::new(db);
db.insert(
"http_requests_total",
1_000_000_000,
10.0,
[("job".to_string(), "api".to_string())]
.into_iter()
.collect(),
)
.unwrap();
db.insert(
"http_requests_total",
2_000_000_000,
20.0,
[("job".to_string(), "api".to_string())]
.into_iter()
.collect(),
)
.unwrap();
db.insert(
"http_requests_total",
1_500_000_000,
15.0,
[("job".to_string(), "web".to_string())]
.into_iter()
.collect(),
)
.unwrap();
db.flush().unwrap();
(db, dir)
}
fn make_db_for_aggregation() -> (Arc<DbCore>, tempfile::TempDir) {
let dir = tempfile::tempdir().unwrap();
let config = crate::DbConfig {
data_dir: dir.path().to_path_buf(),
max_series_cardinality: Some(1000),
..Default::default()
};
let mut db = DbCore::with_config(config).unwrap();
db.recover().unwrap();
let db = Arc::new(db);
for i in 0..5 {
db.insert(
"http_requests_total",
(i + 1) * 1_000_000_000,
(i as f64 + 1.0) * 10.0,
[
("job".to_string(), "api".to_string()),
("instance".to_string(), "a".to_string()),
]
.into_iter()
.collect(),
)
.unwrap();
}
for i in 0..5 {
db.insert(
"http_requests_total",
(i + 1) * 1_000_000_000,
(i as f64 + 1.0) * 5.0,
[
("job".to_string(), "api".to_string()),
("instance".to_string(), "b".to_string()),
]
.into_iter()
.collect(),
)
.unwrap();
}
for i in 0..5 {
db.insert(
"http_requests_total",
(i + 1) * 1_000_000_000,
(i as f64 + 1.0) * 3.0,
[
("job".to_string(), "web".to_string()),
("instance".to_string(), "c".to_string()),
]
.into_iter()
.collect(),
)
.unwrap();
}
db.flush().unwrap();
(db, dir)
}
fn metric_sort_key(m: &HashMap<String, String>) -> String {
let mut pairs: Vec<_> = m.iter().collect();
pairs.sort_by_key(|(k, _)| *k);
pairs
.into_iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join(",")
}
fn sort_samples(samples: &mut [InstantSample]) {
samples.sort_by_key(|a| metric_sort_key(&a.metric));
}
#[test]
fn parse_eval_time_none_or_empty_means_now() {
let t_none = parse_eval_time(None).unwrap();
let t_empty = parse_eval_time(Some("")).unwrap();
let t_whitespace = parse_eval_time(Some(" \t ")).unwrap();
assert!(t_none > 0, "now should be positive");
assert!(t_empty > 0);
assert!(t_whitespace > 0);
}
#[test]
fn parse_eval_time_unix_seconds_integer() {
assert_eq!(parse_eval_time(Some("0")).unwrap(), 0);
assert_eq!(parse_eval_time(Some("1")).unwrap(), 1_000_000_000);
assert_eq!(parse_eval_time(Some("123")).unwrap(), 123_000_000_000);
assert_eq!(parse_eval_time(Some(" 2 ")).unwrap(), 2_000_000_000);
}
#[test]
fn parse_eval_time_unix_seconds_float() {
assert_eq!(parse_eval_time(Some("1.5")).unwrap(), 1_500_000_000);
assert_eq!(parse_eval_time(Some("2.5")).unwrap(), 2_500_000_000);
assert_eq!(parse_eval_time(Some("0.001")).unwrap(), 1_000_000);
}
#[test]
fn parse_eval_time_invalid_returns_error() {
let err = parse_eval_time(Some("-1")).unwrap_err();
assert!(err.contains("invalid time parameter"));
let err = parse_eval_time(Some("x")).unwrap_err();
assert!(err.contains("invalid time parameter"));
let err = parse_eval_time(Some("inf")).unwrap_err();
assert!(err.contains("invalid time parameter"));
let err = parse_eval_time(Some("nan")).unwrap_err();
assert!(err.contains("invalid time parameter"), "NaN must be rejected");
}
#[test]
fn parse_step_seconds_unit() {
assert_eq!(parse_step("15s").unwrap(), 15_000_000_000);
assert_eq!(parse_step("0s").unwrap(), 0);
assert_eq!(parse_step(" 30s ").unwrap(), 30_000_000_000);
assert_eq!(parse_step("15S").unwrap(), 15_000_000_000);
}
#[test]
fn parse_step_minutes_hours_days() {
assert_eq!(parse_step("1m").unwrap(), 60_000_000_000);
assert_eq!(parse_step("2m").unwrap(), 120_000_000_000);
assert_eq!(parse_step("1h").unwrap(), 3_600_000_000_000);
assert_eq!(parse_step("1d").unwrap(), 86_400_000_000_000);
assert_eq!(parse_step("1M").unwrap(), 60_000_000_000);
assert_eq!(parse_step("1H").unwrap(), 3_600_000_000_000);
}
#[test]
fn parse_step_bare_number_is_seconds() {
assert_eq!(parse_step("30").unwrap(), 30_000_000_000);
assert_eq!(parse_step("1").unwrap(), 1_000_000_000);
assert_eq!(parse_step("0").unwrap(), 0);
}
#[test]
fn parse_step_fractional_duration() {
assert_eq!(parse_step("1.5m").unwrap(), 90_000_000_000);
assert_eq!(parse_step("0.5h").unwrap(), 1_800_000_000_000);
}
#[test]
fn parse_step_empty_or_invalid_returns_error() {
assert!(parse_step("").unwrap_err().contains("empty step"));
assert!(parse_step(" ").unwrap_err().contains("empty step"));
assert!(parse_step("x").unwrap_err().contains("invalid step"));
assert!(
parse_step("1x").unwrap_err().contains("invalid step"),
"unknown unit or invalid number"
);
assert!(parse_step("-1s").unwrap_err().contains("non-negative"));
assert!(
parse_step("-1").unwrap_err().contains("non-negative"),
"bare negative step must be rejected (parity with -1s)"
);
}
#[test]
fn parse_step_equivalent_durations_same_ns() {
assert_eq!(
parse_step("60s").unwrap(),
parse_step("1m").unwrap(),
"60s and 1m must yield same step_ns"
);
assert_eq!(
parse_step("3600s").unwrap(),
parse_step("1h").unwrap(),
"3600s and 1h must yield same step_ns"
);
assert_eq!(
parse_step("86400s").unwrap(),
parse_step("1d").unwrap(),
"86400s and 1d must yield same step_ns"
);
assert_eq!(parse_step("60s").unwrap(), 60_000_000_000);
assert_eq!(parse_step("1m").unwrap(), 60_000_000_000);
}
#[test]
fn parse_step_zero_accepted_parser_rejected_by_query_range() {
assert_eq!(parse_step("0").unwrap(), 0);
assert_eq!(parse_step("0s").unwrap(), 0);
let (db, _guard) = make_db_with_series();
let err = query_range(
&db,
"http_requests_total",
1_000_000_000,
3_000_000_000,
parse_step("0").unwrap(),
)
.unwrap_err();
assert!(
matches!(&err, PromqlError::BadParameter(m) if m.contains("step")),
"query_range must reject step_ns 0: {:?}",
err
);
}
#[test]
fn parse_eval_time_and_step_same_values_as_http_layer() {
assert_eq!(
parse_eval_time(Some("2")).unwrap(),
2_000_000_000,
"time param \"2\" must yield 2s in ns for HTTP and library"
);
assert_eq!(
parse_eval_time(Some("0")).unwrap(),
0,
"time param \"0\" must yield 0 ns"
);
assert_eq!(
parse_step("1").unwrap(),
1_000_000_000,
"step param \"1\" must yield 1s in ns for HTTP and library"
);
assert_eq!(
parse_step("15s").unwrap(),
15_000_000_000,
"step param \"15s\" must yield 15s in ns"
);
}
#[test]
fn parse_eval_time_used_in_query_instant_matches_direct_time_ns() {
let (db, _guard) = make_db_with_series();
let time_ns_from_parser = parse_eval_time(Some("2")).unwrap();
let from_parser = query_instant(&db, "http_requests_total", time_ns_from_parser).unwrap();
let direct = query_instant(&db, "http_requests_total", 2_000_000_000).unwrap();
assert_eq!(from_parser.len(), direct.len());
assert_eq!(from_parser.len(), 2, "expect two series at t=2");
let mut a = from_parser;
let mut b = direct;
sort_samples(&mut a);
sort_samples(&mut b);
assert_eq!(a[0].value, b[0].value);
assert_eq!(a[1].value, b[1].value);
}
#[test]
fn parse_step_used_in_query_range_matches_direct_step_ns() {
let (db, _guard) = make_db_with_series();
let step_ns_from_parser = parse_step("1").unwrap();
let from_parser = query_range(
&db,
"http_requests_total",
1_000_000_000,
3_000_000_000,
step_ns_from_parser,
)
.unwrap();
let direct = query_range(
&db,
"http_requests_total",
1_000_000_000,
3_000_000_000,
1_000_000_000,
)
.unwrap();
assert_eq!(from_parser.len(), direct.len());
if let (Some(a), Some(b)) = (from_parser.first(), direct.first()) {
assert_eq!(a.steps.len(), b.steps.len(), "same step count");
assert_eq!(
a.steps.first().map(|(t, _)| *t),
b.steps.first().map(|(t, _)| *t),
"same first step time"
);
}
}
#[test]
fn instant_selector_returns_matching_series() {
let (db, _guard) = make_db_with_series();
let time_ns = 2_000_000_000u64;
let result = query_instant(&db, "http_requests_total", time_ns).unwrap();
let mut samples = result;
sort_samples(&mut samples);
assert_eq!(samples.len(), 2, "api and web series");
let names: Vec<_> = samples
.iter()
.map(|s| s.metric.get("__name__").map(|x| x.as_str()))
.collect();
assert!(names.iter().all(|n| *n == Some("http_requests_total")));
let jobs: Vec<_> = samples
.iter()
.map(|s| s.metric.get("job").map(|x| x.as_str()))
.collect();
assert!(jobs.contains(&Some("api")));
assert!(jobs.contains(&Some("web")));
let api_sample = samples.iter().find(|s| s.metric.get("job") == Some(&"api".to_string()));
let web_sample = samples.iter().find(|s| s.metric.get("job") == Some(&"web".to_string()));
assert_eq!(api_sample.map(|s| s.value), Some(20.0));
assert_eq!(web_sample.map(|s| s.value), Some(15.0));
assert_eq!(api_sample.map(|s| s.ts_ns), Some(2_000_000_000));
assert_eq!(web_sample.map(|s| s.ts_ns), Some(1_500_000_000));
}
#[test]
fn instant_selector_with_label_matcher_equality() {
let (db, _guard) = make_db_with_series();
let result = query_instant(&db, r#"http_requests_total{job="api"}"#, 2_000_000_000).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].metric.get("job"), Some(&"api".to_string()));
assert_eq!(result[0].value, 20.0);
}
#[test]
fn instant_selector_with_label_matcher_not_equal() {
let (db, _guard) = make_db_with_series();
let result =
query_instant(&db, r#"http_requests_total{job!="web"}"#, 2_000_000_000).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].metric.get("job"), Some(&"api".to_string()));
}
#[test]
fn instant_selector_with_regex_matcher() {
let (db, _guard) = make_db_with_series();
let result =
query_instant(&db, r#"http_requests_total{job=~"api|web"}"#, 2_000_000_000).unwrap();
assert_eq!(result.len(), 2);
}
#[test]
fn instant_empty_query_returns_bad_parameter() {
let (db, _guard) = make_db_with_series();
let err = query_instant(&db, "", 2_000_000_000).unwrap_err();
match &err {
PromqlError::BadParameter(msg) => assert!(msg.contains("empty")),
_ => panic!("expected BadParameter, got {:?}", err),
}
}
#[test]
fn instant_whitespace_only_query_returns_bad_parameter() {
let (db, _guard) = make_db_with_series();
let err = query_instant(&db, " ", 2_000_000_000).unwrap_err();
match &err {
PromqlError::BadParameter(msg) => assert!(!msg.is_empty()),
_ => panic!("expected BadParameter, got {:?}", err),
}
}
#[test]
fn instant_parse_error_binary_expr() {
let (db, _guard) = make_db_with_series();
let err = query_instant(&db, "metric_a + metric_b", 2_000_000_000).unwrap_err();
match &err {
PromqlError::Parse(_) => {}
_ => panic!("expected Parse, got {:?}", err),
}
}
#[test]
fn instant_parse_error_bare_range_vector() {
let (db, _guard) = make_db_with_series();
let err = query_instant(&db, "http_requests_total[5m]", 2_000_000_000).unwrap_err();
match &err {
PromqlError::Parse(_) => {}
_ => panic!("expected Parse, got {:?}", err),
}
}
#[test]
fn instant_parse_error_unsupported_function() {
let (db, _guard) = make_db_with_series();
let err = query_instant(
&db,
"histogram_quantile(0.9, rate(http_duration_bucket[5m]))",
2_000_000_000,
)
.unwrap_err();
match &err {
PromqlError::Parse(_) => {}
_ => panic!("expected Parse, got {:?}", err),
}
}
#[test]
fn instant_nonexistent_metric_returns_empty_vector() {
let (db, _guard) = make_db_with_series();
let result =
query_instant(&db, "nonexistent_metric", 2_000_000_000).unwrap();
assert!(result.is_empty());
}
#[test]
fn instant_nonexistent_selector_returns_empty_vector() {
let (db, _guard) = make_db_with_series();
let result =
query_instant(&db, r#"nonexistent{job="x"}"#, 2_000_000_000).unwrap();
assert!(result.is_empty());
}
#[test]
fn instant_rate_returns_vector_same_semantics_as_http() {
let (db, _guard) = make_db_with_series();
let result =
query_instant(&db, "rate(http_requests_total[5m])", 2_000_000_000).unwrap();
assert!(!result.is_empty());
for s in &result {
assert!(s.metric.contains_key("__name__"));
assert!(s.metric.contains_key("job"));
assert!(s.ts_ns == 2_000_000_000);
}
}
#[test]
fn instant_increase_returns_vector() {
let (db, _guard) = make_db_with_series();
let result =
query_instant(&db, "increase(http_requests_total[5m])", 2_000_000_000).unwrap();
assert!(!result.is_empty());
}
#[test]
fn instant_avg_over_time_returns_vector() {
let (db, _guard) = make_db_with_series();
let result =
query_instant(&db, "avg_over_time(http_requests_total[5m])", 2_000_000_000).unwrap();
assert!(!result.is_empty());
}
#[test]
fn instant_sum_by_returns_grouped_vector() {
let (db, _guard) = make_db_for_aggregation();
let result = query_instant(
&db,
r#"sum by (job) (http_requests_total)"#,
5_000_000_000,
)
.unwrap();
let mut samples = result;
sort_samples(&mut samples);
assert_eq!(samples.len(), 2, "one group per job");
let api = samples.iter().find(|s| s.metric.get("job") == Some(&"api".to_string()));
let web = samples.iter().find(|s| s.metric.get("job") == Some(&"web".to_string()));
assert!(api.is_some());
assert!(web.is_some());
assert_eq!(api.map(|s| s.value), Some(75.0));
assert_eq!(web.map(|s| s.value), Some(15.0));
}
#[test]
fn instant_count_returns_series_count() {
let (db, _guard) = make_db_for_aggregation();
let result = query_instant(&db, "count(http_requests_total)", 5_000_000_000).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].value, 3.0, "three series");
}
#[test]
fn instant_sum_of_empty_set_returns_empty_vector() {
let (db, _guard) = make_db_with_series();
let result =
query_instant(&db, r#"sum by (job) (nonexistent)"#, 2_000_000_000).unwrap();
assert!(result.is_empty());
}
#[test]
fn instant_error_variants_distinguishable() {
let (db, _guard) = make_db_with_series();
let parse_err = query_instant(&db, "invalid {", 2_000_000_000).unwrap_err();
assert!(matches!(parse_err, PromqlError::Parse(_)));
let param_err = query_instant(&db, "", 2_000_000_000).unwrap_err();
assert!(matches!(param_err, PromqlError::BadParameter(_)));
}
#[test]
fn promql_error_parse_bad_parameter_execution_distinct() {
let (db, _guard) = make_db_with_series();
let parse_err = query_instant(&db, "metric_a + metric_b", 2_000_000_000).unwrap_err();
let param_err = query_instant(&db, "", 2_000_000_000).unwrap_err();
let exec_err =
PromqlError::Execution(crate::error::DbError::SeriesNotFound("test".to_string()));
assert!(matches!(parse_err, PromqlError::Parse(_)), "Parse must match Parse only");
assert!(!matches!(parse_err, PromqlError::BadParameter(_)));
assert!(!matches!(parse_err, PromqlError::Execution(_)));
assert!(matches!(param_err, PromqlError::BadParameter(_)), "BadParameter must match only");
assert!(!matches!(param_err, PromqlError::Parse(_)));
assert!(!matches!(param_err, PromqlError::Execution(_)));
assert!(matches!(exec_err, PromqlError::Execution(_)), "Execution must match Execution only");
assert!(!matches!(exec_err, PromqlError::Parse(_)));
assert!(!matches!(exec_err, PromqlError::BadParameter(_)));
}
#[test]
fn promql_error_caller_can_match_exhaustively() {
fn variant_discriminant(e: &PromqlError) -> u8 {
match e {
PromqlError::Parse(_) => 0,
PromqlError::BadParameter(_) => 1,
PromqlError::Execution(_) => 2,
}
}
let (db, _guard) = make_db_with_series();
let parse_err = query_instant(&db, "invalid {", 2_000_000_000).unwrap_err();
let param_err = query_range(&db, "", 1_000_000_000, 3_000_000_000, 0).unwrap_err();
let exec_err =
PromqlError::Execution(crate::error::DbError::Internal("test".to_string()));
assert_eq!(variant_discriminant(&parse_err), 0);
assert_eq!(variant_discriminant(¶m_err), 1);
assert_eq!(variant_discriminant(&exec_err), 2);
}
#[test]
fn promql_error_execution_variant_wraps_db_error_and_source() {
let (db, _guard) = make_db_with_series();
let parse_err = query_instant(&db, "syntax [", 2_000_000_000).unwrap_err();
let param_err = query_instant(&db, "", 2_000_000_000).unwrap_err();
let db_err = crate::error::DbError::InvalidTimeRange {
start: 2,
end: 1,
};
let exec_err = PromqlError::Execution(db_err);
assert!(parse_err.source().is_none());
assert!(param_err.source().is_none());
assert!(exec_err.source().is_some());
assert!(
exec_err.source().unwrap().downcast_ref::<crate::error::DbError>().is_some(),
"Execution source must be DbError"
);
}
#[test]
fn promql_error_variants_from_labels_label_values_series_range() {
let (db, _guard) = make_db_with_series();
let err_bad = labels(&db, None::<&[String]>, 3_000_000_000, 1_000_000_000).unwrap_err();
let err_parse = labels(
&db,
Some(&["sum(rate(x[5m]))".to_string()]),
0,
3_000_000_000,
)
.unwrap_err();
assert!(matches!(err_bad, PromqlError::BadParameter(_)));
assert!(matches!(err_parse, PromqlError::Parse(_)));
let err_bad_lv =
label_values(&db, "job", None::<&[String]>, 3_000_000_000, 1_000_000_000).unwrap_err();
assert!(matches!(err_bad_lv, PromqlError::BadParameter(_)));
let err_series_empty = series(&db, &[] as &[&str], 0, 3_000_000_000).unwrap_err();
let err_series_parse = series(
&db,
&["sum(metric)".to_string()],
0,
3_000_000_000,
)
.unwrap_err();
assert!(matches!(err_series_empty, PromqlError::BadParameter(_)));
assert!(matches!(err_series_parse, PromqlError::Parse(_)));
let err_range_param = query_range(&db, "x", 3_000_000_000, 1_000_000_000, 1_000_000_000)
.unwrap_err();
assert!(matches!(err_range_param, PromqlError::BadParameter(_)));
let err_range_parse =
query_range(&db, "a + b", 1_000_000_000, 3_000_000_000, 1_000_000_000).unwrap_err();
assert!(matches!(err_range_parse, PromqlError::Parse(_)));
}
#[test]
fn labels_returns_sorted_unique_names_including_metric_name() {
let (db, _guard) = make_db_with_series();
let names = labels(&db, None::<&[String]>, 0, 3_000_000_000).unwrap();
assert!(
names.contains(&"__name__".to_string()),
"labels must include __name__"
);
assert!(names.contains(&"job".to_string()), "labels must include job");
let mut sorted = names.clone();
sorted.sort();
assert_eq!(names, sorted, "labels must be returned sorted");
}
#[test]
fn labels_with_match_selector_restricts_to_matching_series() {
let (db, _guard) = make_db_with_series();
let names = labels(
&db,
Some(&["http_requests_total{job=\"api\"}".to_string()]),
0,
3_000_000_000,
)
.unwrap();
assert!(names.contains(&"__name__".to_string()));
assert!(names.contains(&"job".to_string()));
assert_eq!(names.len(), 2);
}
#[test]
fn labels_time_range_excludes_series_with_no_data_in_range() {
let (db, _guard) = make_db_with_series();
let names = labels(&db, None::<&[String]>, 2_500_000_000, 3_000_000_000).unwrap();
assert_eq!(names, ["__name__"]);
}
#[test]
fn labels_time_range_includes_only_series_with_points_in_range() {
let (db, _guard) = make_db_with_series();
let names = labels(&db, None::<&[String]>, 1_000_000_000, 1_200_000_000).unwrap();
assert!(names.contains(&"__name__".to_string()));
assert!(names.contains(&"job".to_string()));
}
#[test]
fn labels_start_ge_end_returns_bad_parameter() {
let (db, _guard) = make_db_with_series();
let err = labels(&db, None::<&[String]>, 2_000_000_000, 1_000_000_000).unwrap_err();
match &err {
PromqlError::BadParameter(msg) => assert!(msg.contains("start") && msg.contains("end")),
_ => panic!("expected BadParameter, got {:?}", err),
}
}
#[test]
fn labels_invalid_match_selector_returns_parse_error() {
let (db, _guard) = make_db_with_series();
let err = labels(
&db,
Some(&["sum(rate(x[5m]))".to_string()]),
0,
3_000_000_000,
)
.unwrap_err();
assert!(matches!(err, PromqlError::Parse(_)));
}
#[test]
fn labels_library_parity_with_instant_and_range() {
let (db, _guard) = make_db_for_aggregation();
let time_ns = 3_000_000_000u64;
let _samples = query_instant(&db, "http_requests_total", time_ns).unwrap();
let _series = query_range(
&db,
"http_requests_total",
1_000_000_000,
5_000_000_000,
1_000_000_000,
)
.unwrap();
let names = labels(
&db,
Some(&["http_requests_total".to_string()]),
1_000_000_000,
5_000_000_000,
)
.unwrap();
assert!(names.contains(&"__name__".to_string()));
assert!(names.contains(&"job".to_string()));
assert!(names.contains(&"instance".to_string()));
}
#[test]
fn label_values_returns_sorted_unique_values_for_label() {
let (db, _guard) = make_db_with_series();
let values = label_values(&db, "job", None::<&[String]>, 0, 3_000_000_000).unwrap();
assert!(values.contains(&"api".to_string()));
assert!(values.contains(&"web".to_string()));
let mut sorted = values.clone();
sorted.sort();
assert_eq!(values, sorted, "label values must be returned sorted");
}
#[test]
fn label_values_for_name_returns_metric_names() {
let (db, _guard) = make_db_with_series();
let names = label_values(&db, "__name__", None::<&[String]>, 0, 3_000_000_000).unwrap();
assert_eq!(names, ["http_requests_total"]);
}
#[test]
fn label_values_with_match_selector_restricts_to_matching_series() {
let (db, _guard) = make_db_with_series();
let values = label_values(
&db,
"job",
Some(&["http_requests_total{job=\"api\"}".to_string()]),
0,
3_000_000_000,
)
.unwrap();
assert_eq!(values, ["api"]);
}
#[test]
fn label_values_time_range_excludes_series_with_no_data_in_range() {
let (db, _guard) = make_db_with_series();
let values = label_values(&db, "job", None::<&[String]>, 2_500_000_000, 3_000_000_000).unwrap();
assert!(values.is_empty());
let names = label_values(&db, "__name__", None::<&[String]>, 2_500_000_000, 3_000_000_000).unwrap();
assert!(names.is_empty());
}
#[test]
fn label_values_time_range_includes_only_series_with_points_in_range() {
let (db, _guard) = make_db_with_series();
let values = label_values(&db, "job", None::<&[String]>, 1_000_000_000, 1_200_000_000).unwrap();
assert_eq!(values, ["api"]);
}
#[test]
fn label_values_start_ge_end_returns_bad_parameter() {
let (db, _guard) = make_db_with_series();
let err = label_values(&db, "job", None::<&[String]>, 2_000_000_000, 1_000_000_000).unwrap_err();
match &err {
PromqlError::BadParameter(msg) => assert!(msg.contains("start") && msg.contains("end")),
_ => panic!("expected BadParameter, got {:?}", err),
}
}
#[test]
fn label_values_invalid_match_selector_returns_parse_error() {
let (db, _guard) = make_db_with_series();
let err = label_values(
&db,
"job",
Some(&["sum(rate(x[5m]))".to_string()]),
0,
3_000_000_000,
)
.unwrap_err();
assert!(matches!(err, PromqlError::Parse(_)));
}
#[test]
fn label_values_unknown_label_returns_empty() {
let (db, _guard) = make_db_with_series();
let values = label_values(&db, "nonexistent_label", None::<&[String]>, 0, 3_000_000_000).unwrap();
assert!(values.is_empty());
}
#[test]
fn label_values_library_parity_with_instant_and_labels() {
let (db, _guard) = make_db_for_aggregation();
let _samples = query_instant(&db, "http_requests_total", 3_000_000_000).unwrap();
let _names = labels(
&db,
Some(&["http_requests_total".to_string()]),
1_000_000_000,
5_000_000_000,
)
.unwrap();
let job_values = label_values(
&db,
"job",
Some(&["http_requests_total".to_string()]),
1_000_000_000,
5_000_000_000,
)
.unwrap();
assert!(job_values.contains(&"api".to_string()));
assert!(job_values.contains(&"web".to_string()));
let instance_values = label_values(
&db,
"instance",
None::<&[String]>,
1_000_000_000,
5_000_000_000,
)
.unwrap();
assert!(instance_values.contains(&"a".to_string()));
assert!(instance_values.contains(&"b".to_string()));
assert!(instance_values.contains(&"c".to_string()));
}
#[test]
fn series_empty_match_returns_bad_parameter() {
let (db, _guard) = make_db_with_series();
let err = series(&db, &[] as &[String], 0, 3_000_000_000).unwrap_err();
match &err {
PromqlError::BadParameter(msg) => assert!(msg.contains("match")),
_ => panic!("expected BadParameter, got {:?}", err),
}
}
#[test]
fn series_start_ge_end_returns_bad_parameter() {
let (db, _guard) = make_db_with_series();
let err = series(
&db,
&["http_requests_total".to_string()],
2_000_000_000,
1_000_000_000,
)
.unwrap_err();
match &err {
PromqlError::BadParameter(msg) => assert!(msg.contains("start") && msg.contains("end")),
_ => panic!("expected BadParameter, got {:?}", err),
}
}
#[test]
fn series_invalid_selector_returns_parse_error() {
let (db, _guard) = make_db_with_series();
let err = series(
&db,
&["sum(rate(x[5m]))".to_string()],
0,
3_000_000_000,
)
.unwrap_err();
assert!(matches!(err, PromqlError::Parse(_)));
}
#[test]
fn series_with_one_match_returns_all_matching_series_in_range() {
let (db, _guard) = make_db_with_series();
let metrics = series(
&db,
&["http_requests_total".to_string()],
0,
3_000_000_000,
)
.unwrap();
assert_eq!(metrics.len(), 2, "api and web series");
let has_api = metrics.iter().any(|m| m.get("job") == Some(&"api".to_string()));
let has_web = metrics.iter().any(|m| m.get("job") == Some(&"web".to_string()));
assert!(has_api && has_web);
for m in &metrics {
assert_eq!(m.get("__name__"), Some(&"http_requests_total".to_string()));
}
}
#[test]
fn series_time_range_restricts_to_series_with_data_in_range() {
let (db, _guard) = make_db_with_series();
let metrics = series(
&db,
&["http_requests_total".to_string()],
2_000_000_000,
2_500_000_000,
)
.unwrap();
assert_eq!(metrics.len(), 1);
assert_eq!(metrics[0].get("job"), Some(&"api".to_string()));
let metrics_web = series(
&db,
&["http_requests_total".to_string()],
1_400_000_000,
1_600_000_000,
)
.unwrap();
assert_eq!(metrics_web.len(), 1);
assert_eq!(metrics_web[0].get("job"), Some(&"web".to_string()));
}
#[test]
fn series_match_selector_restricts_to_matching_series() {
let (db, _guard) = make_db_with_series();
let metrics = series(
&db,
&["http_requests_total{job=\"api\"}".to_string()],
0,
3_000_000_000,
)
.unwrap();
assert_eq!(metrics.len(), 1);
assert_eq!(metrics[0].get("job"), Some(&"api".to_string()));
}
#[test]
fn series_multiple_match_dedupes() {
let (db, _guard) = make_db_with_series();
let metrics = series(
&db,
&[
"http_requests_total".to_string(),
"http_requests_total{job=\"api\"}".to_string(),
],
0,
3_000_000_000,
)
.unwrap();
assert_eq!(metrics.len(), 2, "api and web; api matched by both selectors once");
let api_count = metrics.iter().filter(|m| m.get("job") == Some(&"api".to_string())).count();
assert_eq!(api_count, 1);
}
#[test]
fn series_library_parity_with_instant_and_labels() {
let (db, _guard) = make_db_for_aggregation();
let _samples = query_instant(&db, "http_requests_total", 3_000_000_000).unwrap();
let _names = labels(
&db,
Some(&["http_requests_total".to_string()]),
1_000_000_000,
5_000_000_000,
)
.unwrap();
let metrics = series(
&db,
&["http_requests_total".to_string()],
1_000_000_000,
5_000_000_000,
)
.unwrap();
assert!(!metrics.is_empty());
assert!(metrics.iter().all(|m| m.get("__name__") == Some(&"http_requests_total".to_string())));
}
fn sort_range_series(series: &mut [super::RangeSeries]) {
series.sort_by_key(|a| metric_sort_key(&a.metric));
}
fn assert_range_series_invariant(
series: &[super::RangeSeries],
start_ns: u64,
end_ns: u64,
step_ns: u64,
) {
for s in series {
assert!(s.metric.contains_key("__name__"));
assert!(!s.steps.is_empty());
let mut prev_ts = 0u64;
for (ts, _) in &s.steps {
assert!(
*ts >= start_ns && *ts <= end_ns && (ts - start_ns) % step_ns == 0,
"steps must be on query grid"
);
assert!(*ts > prev_ts, "steps must be strictly increasing");
prev_ts = *ts;
}
}
}
#[test]
fn range_instant_selector_returns_step_aligned_matrix() {
let (db, _guard) = make_db_with_series();
let start_ns = 1_000_000_000;
let end_ns = 3_000_000_000;
let step_ns = 1_000_000_000;
let result = query_range(&db, "http_requests_total", start_ns, end_ns, step_ns).unwrap();
let mut series = result;
sort_range_series(&mut series);
assert!(!series.is_empty(), "at least one series");
assert_range_series_invariant(&series, start_ns, end_ns, step_ns);
let api_series = series.iter().find(|s| s.metric.get("job") == Some(&"api".to_string()));
let web_series = series.iter().find(|s| s.metric.get("job") == Some(&"web".to_string()));
assert!(api_series.is_some(), "api series must be present");
assert!(web_series.is_some(), "web series must be present");
assert_eq!(
api_series.unwrap().steps.len(),
3,
"api has points at 1s and 2s so has a value at every grid step 1s,2s,3s"
);
assert_eq!(
web_series.unwrap().steps.len(),
2,
"web has point only at 1.5s; no point <= 1s so step 1s omitted; steps at 2s and 3s only"
);
}
#[test]
fn range_start_after_end_returns_bad_parameter() {
let (db, _guard) = make_db_with_series();
let err = query_range(
&db,
"http_requests_total",
3_000_000_000,
1_000_000_000,
1_000_000_000,
)
.unwrap_err();
match &err {
PromqlError::BadParameter(msg) => assert!(msg.contains("start") && msg.contains("end")),
_ => panic!("expected BadParameter, got {:?}", err),
}
}
#[test]
fn range_zero_step_returns_bad_parameter() {
let (db, _guard) = make_db_with_series();
let err = query_range(
&db,
"http_requests_total",
1_000_000_000,
3_000_000_000,
0,
)
.unwrap_err();
match &err {
PromqlError::BadParameter(msg) => assert!(msg.contains("step")),
_ => panic!("expected BadParameter, got {:?}", err),
}
}
#[test]
fn range_empty_query_returns_bad_parameter() {
let (db, _guard) = make_db_with_series();
let err = query_range(&db, "", 1_000_000_000, 3_000_000_000, 1_000_000_000).unwrap_err();
match &err {
PromqlError::BadParameter(msg) => assert!(msg.contains("empty")),
_ => panic!("expected BadParameter, got {:?}", err),
}
}
#[test]
fn range_parse_error_returns_parse() {
let (db, _guard) = make_db_with_series();
let err = query_range(
&db,
"metric_a + metric_b",
1_000_000_000,
3_000_000_000,
1_000_000_000,
)
.unwrap_err();
assert!(matches!(err, PromqlError::Parse(_)));
}
#[test]
fn range_rate_returns_matrix_with_steps() {
let (db, _guard) = make_db_with_series();
let result = query_range(
&db,
"rate(http_requests_total[2s])",
1_000_000_000,
3_000_000_000,
1_000_000_000,
)
.unwrap();
assert!(!result.is_empty());
for s in &result {
assert!(s.metric.contains_key("__name__"));
assert!(!s.steps.is_empty());
for (_, v) in &s.steps {
assert!(!v.is_nan(), "rate should not produce NaN for this data");
}
}
}
#[test]
fn range_sum_by_returns_grouped_matrix() {
let (db, _guard) = make_db_for_aggregation();
let result = query_range(
&db,
r#"sum by (job) (http_requests_total)"#,
1_000_000_000,
5_000_000_000,
1_000_000_000,
)
.unwrap();
let mut series = result;
sort_range_series(&mut series);
assert_eq!(series.len(), 2, "one series per job group");
let api = series.iter().find(|s| s.metric.get("job") == Some(&"api".to_string()));
let web = series.iter().find(|s| s.metric.get("job") == Some(&"web".to_string()));
assert!(api.is_some());
assert!(web.is_some());
assert_eq!(api.unwrap().steps.len(), 5);
assert_eq!(web.unwrap().steps.len(), 5);
}
#[test]
fn range_nonexistent_metric_returns_empty_matrix() {
let (db, _guard) = make_db_with_series();
let result = query_range(
&db,
"nonexistent_metric",
1_000_000_000,
3_000_000_000,
1_000_000_000,
)
.unwrap();
assert!(result.is_empty());
}
#[test]
fn range_library_and_http_same_result_shape() {
let (db, _guard) = make_db_with_series();
let start_ns = 1_000_000_000;
let end_ns = 3_000_000_000;
let step_ns = 1_000_000_000;
let lib_series = query_range(&db, "http_requests_total", start_ns, end_ns, step_ns).unwrap();
let r = crate::prometheus_api::handle_query_range(
Some("http_requests_total"),
Some("1"),
Some("3"),
Some("1s"),
&db,
);
assert_eq!(r.status, http::StatusCode::OK);
let body: crate::prometheus_api::ApiEnvelope<crate::prometheus_api::QueryData> =
serde_json::from_slice(&r.body).unwrap();
let data = body.data.unwrap();
let crate::prometheus_api::QueryResult::Matrix(http_matrix) = data.result else {
panic!("expected matrix")
};
assert_eq!(
lib_series.len(),
http_matrix.len(),
"library and HTTP must return same number of series"
);
let mut lib_sorted = lib_series;
sort_range_series(&mut lib_sorted);
let mut http_sorted = http_matrix;
http_sorted.sort_by_key(|a| metric_sort_key(&a.metric));
for (lib_s, http_s) in lib_sorted.iter().zip(http_sorted.iter()) {
assert_eq!(lib_s.metric, http_s.metric);
assert_eq!(
lib_s.steps.len(),
http_s.values.len(),
"same step count per series"
);
}
}
#[test]
fn result_types_expose_numeric_timestamps_and_values() {
let (db, _guard) = make_db_with_series();
let samples: Vec<InstantSample> =
query_instant(&db, "http_requests_total", 2_000_000_000).unwrap();
assert!(!samples.is_empty());
for s in &samples {
let _ts_ns: u64 = s.ts_ns;
let _value: f64 = s.value;
let _labels: &HashMap<String, String> = &s.metric;
}
let range_result: Vec<RangeSeries> = query_range(
&db,
"http_requests_total",
1_000_000_000,
3_000_000_000,
1_000_000_000,
)
.unwrap();
assert!(!range_result.is_empty());
for rs in &range_result {
let _steps: &[(u64, f64)] = &rs.steps;
let _metric: &HashMap<String, String> = &rs.metric;
}
let metrics: Vec<MetricLabels> =
super::series(&db, &["http_requests_total".to_string()], 0, 3_000_000_000).unwrap();
assert!(!metrics.is_empty());
for m in &metrics {
let _name: Option<&String> = m.get("__name__");
let _job: Option<&String> = m.get("job");
}
}
#[test]
fn instant_sample_result_type_contract() {
let (db, _guard) = make_db_with_series();
let samples = query_instant(&db, "http_requests_total", 2_000_000_000).unwrap();
for s in &samples {
assert!(s.metric.contains_key("__name__"), "instant sample metric must include __name__");
assert!(s.ts_ns > 0, "ts_ns must be nonzero for real data");
let _: f64 = s.value;
}
let empty = query_instant(&db, "nonexistent_metric", 2_000_000_000).unwrap();
assert_eq!(empty.len(), 0);
}
#[test]
fn range_series_result_type_contract() {
let (db, _guard) = make_db_with_series();
let start_ns = 1_000_000_000;
let end_ns = 3_000_000_000;
let step_ns = 1_000_000_000;
let series = query_range(&db, "http_requests_total", start_ns, end_ns, step_ns).unwrap();
for rs in &series {
assert!(rs.metric.contains_key("__name__"), "range series metric must include __name__");
assert!(!rs.steps.is_empty(), "at least one step for this query");
let mut prev = 0u64;
for (ts, val) in &rs.steps {
assert!(ts >= &start_ns && ts <= &end_ns, "step on range");
assert!((ts - start_ns) % step_ns == 0, "step on grid");
assert!(*ts > prev, "steps strictly increasing");
prev = *ts;
let _: f64 = *val;
}
}
}
#[test]
fn metric_labels_result_type_contract() {
let (db, _guard) = make_db_with_series();
let metrics: Vec<MetricLabels> =
series(&db, &["http_requests_total".to_string()], 0, 3_000_000_000).unwrap();
assert_eq!(metrics.len(), 2, "api and web");
for m in &metrics {
assert!(m.contains_key("__name__"));
assert!(m.get("__name__") == Some(&"http_requests_total".to_string()));
let job = m.get("job").map(String::as_str);
assert!(job == Some("api") || job == Some("web"));
}
}
#[test]
fn result_types_programmatic_construction() {
let mut metric = HashMap::new();
metric.insert("__name__".to_string(), "http_requests_total".to_string());
metric.insert("job".to_string(), "api".to_string());
let sample = InstantSample {
metric: metric.clone(),
ts_ns: 2_000_000_000,
value: 20.0,
};
assert_eq!(sample.ts_ns, 2_000_000_000);
assert_eq!(sample.value, 20.0);
assert_eq!(sample.metric.get("job"), Some(&"api".to_string()));
let steps = vec![(1_000_000_000, 10.0), (2_000_000_000, 20.0)];
let range_series = RangeSeries {
metric: metric.clone(),
steps: steps.clone(),
};
assert_eq!(range_series.steps.len(), 2);
assert_eq!(range_series.steps[0], (1_000_000_000, 10.0));
let labels: MetricLabels = metric;
assert_eq!(labels.get("__name__"), Some(&"http_requests_total".to_string()));
let (db, _guard) = make_db_with_series();
let from_lib = query_instant(&db, r#"http_requests_total{job="api"}"#, 2_000_000_000).unwrap();
assert_eq!(from_lib.len(), 1);
assert_eq!(from_lib[0].ts_ns, sample.ts_ns);
assert_eq!(from_lib[0].value, sample.value);
assert_eq!(from_lib[0].metric.get("job"), sample.metric.get("job"));
}
}