photoncloud-monorepo/nightlight/crates/nightlight-server/src/query.rs

2153 lines
67 KiB
Rust

//! PromQL query engine
//!
//! Implements a subset of PromQL for querying time-series data.
//! Provides instant and range query execution with basic aggregations.
use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::{IntoResponse, Json},
routing::get,
Router,
};
use nightlight_types::{Error, Label, Result, Sample, SeriesId, TimeSeries};
use parking_lot::Mutex;
use promql_parser::{
label::{MatchOp, Matchers},
parser::{
AggregateExpr, BinModifier, BinaryExpr, Call, Expr, LabelModifier, MatrixSelector,
NumberLiteral, UnaryExpr, VectorMatchCardinality, VectorSelector,
},
};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap, VecDeque};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::RwLock;
#[cfg(test)]
use tracing::info;
use tracing::{debug, error};
const QUERY_DURATION_HISTORY_LIMIT: usize = 512;
/// Query service state
#[derive(Clone)]
pub struct QueryService {
// Reference to queryable storage (shared with ingestion)
storage: Arc<RwLock<QueryableStorage>>,
metrics: Arc<QueryMetrics>,
}
/// In-memory queryable storage (reads from ingestion buffer)
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct QueryableStorage {
// Series metadata indexed by SeriesId
pub series: HashMap<SeriesId, TimeSeries>,
// Inverted index: label name -> label value -> [SeriesId]
pub label_index: HashMap<String, HashMap<String, Vec<SeriesId>>>,
}
#[derive(Debug)]
pub struct QueryMetrics {
queries_total: AtomicU64,
queries_failed: AtomicU64,
queries_active: AtomicU64,
durations_ms: Mutex<VecDeque<u64>>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct QueryMetricsSnapshot {
pub queries_total: u64,
pub queries_failed: u64,
pub queries_active: u64,
pub query_duration_p50: f64,
pub query_duration_p95: f64,
pub query_duration_p99: f64,
}
#[derive(Debug, Clone)]
enum EvalValue {
Vector(Vec<TimeSeries>),
Scalar(f64),
}
impl QueryService {
pub fn new() -> Self {
Self {
storage: Arc::new(RwLock::new(QueryableStorage {
series: HashMap::new(),
label_index: HashMap::new(),
})),
metrics: Arc::new(QueryMetrics::new()),
}
}
/// Create QueryService from existing shared storage
pub fn from_storage(storage: Arc<RwLock<QueryableStorage>>) -> Self {
Self {
storage,
metrics: Arc::new(QueryMetrics::new()),
}
}
/// Create QueryService and load persistent state from disk if it exists
#[cfg(test)]
pub fn new_with_persistence(data_path: &std::path::Path) -> Result<Self> {
let storage = QueryableStorage::load_from_file(data_path)?;
info!(
"Loaded {} series from persistent storage",
storage.series.len()
);
Ok(Self {
storage: Arc::new(RwLock::new(storage)),
metrics: Arc::new(QueryMetrics::new()),
})
}
/// Save current storage state to disk
#[cfg(test)]
pub async fn save_to_disk(&self, data_path: &std::path::Path) -> Result<()> {
let storage = self.storage.read().await;
storage.save_to_file(data_path)?;
debug!(
"Saved {} series to persistent storage",
storage.series.len()
);
Ok(())
}
/// Create Axum router for query endpoints
pub fn router(self) -> Router {
Router::new()
.route("/api/v1/query", get(handle_instant_query))
.route("/api/v1/query_range", get(handle_range_query))
.route("/api/v1/label/:label_name/values", get(handle_label_values))
.route("/api/v1/series", get(handle_series))
.with_state(self)
}
pub fn metrics(&self) -> Arc<QueryMetrics> {
Arc::clone(&self.metrics)
}
/// Execute an instant query at a specific timestamp
pub async fn execute_instant_query(&self, query: &str, time: i64) -> Result<QueryResult> {
debug!("Executing instant query: {} at time {}", query, time);
let started = self.metrics.begin_query();
// Parse PromQL expression
let expr = promql_parser::parser::parse(query)
.map_err(|e| Error::Query(format!("Parse error: {:?}", e)));
let expr = match expr {
Ok(expr) => expr,
Err(error) => {
self.metrics.finish_query(started, false);
return Err(error);
}
};
// Execute the expression
let storage = self.storage.read().await;
let result = self.evaluate_value(&expr, time, time, 0, &storage).await;
let success = result.is_ok();
self.metrics.finish_query(started, success);
let result = result?;
Ok(instant_query_from_eval_value(result, time))
}
/// Execute a range query over a time range with step
pub async fn execute_range_query(
&self,
query: &str,
start: i64,
end: i64,
step: i64,
) -> Result<RangeQueryResult> {
debug!(
"Executing range query: {} from {} to {} step {}",
query, start, end, step
);
let started = self.metrics.begin_query();
if step <= 0 {
self.metrics.finish_query(started, false);
return Err(Error::InvalidTimeRange(
"range query step must be greater than zero".to_string(),
));
}
if end < start {
self.metrics.finish_query(started, false);
return Err(Error::InvalidTimeRange(
"range query end must be greater than or equal to start".to_string(),
));
}
// Parse PromQL expression
let expr = promql_parser::parser::parse(query)
.map_err(|e| Error::Query(format!("Parse error: {:?}", e)));
let expr = match expr {
Ok(expr) => expr,
Err(error) => {
self.metrics.finish_query(started, false);
return Err(error);
}
};
let storage = self.storage.read().await;
let mut results: HashMap<String, RangeResult> = HashMap::new();
// Evaluate at each step
let mut current_time = start;
while current_time <= end {
let step_result = self
.evaluate_value(&expr, current_time, end, step, &storage)
.await;
let step_result = match step_result {
Ok(step_result) => step_result,
Err(error) => {
self.metrics.finish_query(started, false);
return Err(error);
}
};
append_range_step_result(&mut results, step_result, current_time);
current_time += step;
}
let result = RangeQueryResult {
result_type: "matrix".to_string(),
result: results.into_values().collect(),
};
self.metrics.finish_query(started, true);
Ok(result)
}
/// Evaluate a PromQL expression (recursive with boxing for async)
fn evaluate_value<'a>(
&'a self,
expr: &'a Expr,
time: i64,
end_time: i64,
step: i64,
storage: &'a QueryableStorage,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<EvalValue>> + Send + 'a>> {
Box::pin(async move {
match expr {
Expr::VectorSelector(selector) => {
// Vector selector: metric_name{label="value"}
self.evaluate_vector_selector(selector, time, storage)
.map(EvalValue::Vector)
}
Expr::MatrixSelector(selector) => {
// Range selector: metric_name[5m]
self.evaluate_matrix_selector(selector, time, storage)
.map(EvalValue::Vector)
}
Expr::Aggregate(agg) => {
// Aggregation: sum(metric), avg(metric), etc.
self.evaluate_aggregation(agg, time, end_time, step, storage)
.await
.map(EvalValue::Vector)
}
Expr::Call(call) => {
// Function call: rate(metric[5m]), etc.
self.evaluate_function(call, time, end_time, step, storage)
.await
}
Expr::Binary(binary) => {
// Binary operation: metric1 + metric2
self.evaluate_binary(binary, time, end_time, step, storage)
.await
}
Expr::NumberLiteral(NumberLiteral { val }) => Ok(EvalValue::Scalar(*val)),
Expr::Paren(paren) => {
self.evaluate_value(&paren.expr, time, end_time, step, storage)
.await
}
Expr::Unary(unary) => {
self.evaluate_unary(unary, time, end_time, step, storage)
.await
}
_ => Err(Error::Query(format!(
"Unsupported expression type: {:?}",
expr
))),
}
})
}
/// Evaluate vector selector
fn evaluate_vector_selector(
&self,
selector: &VectorSelector,
time: i64,
storage: &QueryableStorage,
) -> Result<Vec<TimeSeries>> {
// Find all series matching the label matchers
let matching_series: Vec<TimeSeries> = storage
.series
.values()
.filter(|ts| self.matches_selector(ts, selector))
.cloned()
.map(|mut ts| {
// Filter to get sample closest to query time
ts.samples.retain(|s| s.timestamp <= time);
// Keep only the most recent sample
if let Some(last) = ts.samples.last().cloned() {
ts.samples = vec![last];
}
ts
})
.filter(|ts| !ts.samples.is_empty())
.collect();
Ok(matching_series)
}
/// Evaluate matrix selector (range selector)
fn evaluate_matrix_selector(
&self,
selector: &MatrixSelector,
time: i64,
storage: &QueryableStorage,
) -> Result<Vec<TimeSeries>> {
// Get the time range in milliseconds
let range_ms = selector.range.as_millis() as i64;
let start_time = time - range_ms;
// Evaluate underlying vector selector
let mut series = storage
.series
.values()
.filter(|ts| self.matches_selector(ts, &selector.vs))
.cloned()
.collect::<Vec<_>>();
// Filter samples to the time range
for ts in &mut series {
ts.samples
.retain(|s| s.timestamp >= start_time && s.timestamp <= time);
}
// Remove empty series
series.retain(|ts| !ts.samples.is_empty());
Ok(series)
}
/// Evaluate aggregation (sum, avg, min, max, count)
async fn evaluate_aggregation(
&self,
agg: &AggregateExpr,
time: i64,
end_time: i64,
step: i64,
storage: &QueryableStorage,
) -> Result<Vec<TimeSeries>> {
let input_series = expect_vector(
self.evaluate_value(&agg.expr, time, end_time, step, storage)
.await?,
"aggregation input must be a vector",
)?;
if input_series.is_empty() {
return Ok(vec![]);
}
let mut groups: BTreeMap<String, (Vec<Label>, Vec<f64>)> = BTreeMap::new();
for series in input_series {
let Some(sample) = series.samples.last() else {
continue;
};
let labels = aggregation_output_labels(&series.labels, agg.modifier.as_ref());
let key = labels_key(&labels);
groups
.entry(key)
.or_insert_with(|| (labels, Vec::new()))
.1
.push(sample.value);
}
let op = agg.op.to_string().to_lowercase();
let mut results = Vec::new();
for (labels, values) in groups.into_values() {
if values.is_empty() {
continue;
}
let aggregated_value = aggregate_values(&op, &values)?;
results.push(TimeSeries {
id: SeriesId(results.len() as u64),
labels,
samples: vec![Sample::new(time, aggregated_value)],
});
}
Ok(results)
}
/// Evaluate function call (rate, irate, increase, histogram_quantile)
async fn evaluate_function(
&self,
call: &Call,
time: i64,
end_time: i64,
step: i64,
storage: &QueryableStorage,
) -> Result<EvalValue> {
let func_name = &call.func.name;
match func_name.to_string().as_str() {
"rate" => self
.evaluate_rate(call, time, end_time, step, storage)
.await
.map(EvalValue::Vector),
"irate" => self
.evaluate_irate(call, time, end_time, step, storage)
.await
.map(EvalValue::Vector),
"increase" => self
.evaluate_increase(call, time, end_time, step, storage)
.await
.map(EvalValue::Vector),
"histogram_quantile" => self
.evaluate_histogram_quantile(call, time, end_time, step, storage)
.await
.map(EvalValue::Vector),
_ => Err(Error::Query(format!("Unsupported function: {}", func_name))),
}
}
/// Evaluate rate() function - calculates per-second average rate of increase
async fn evaluate_rate(
&self,
call: &Call,
time: i64,
end_time: i64,
step: i64,
storage: &QueryableStorage,
) -> Result<Vec<TimeSeries>> {
// rate() expects a range vector (MatrixSelector) as argument
if call.args.args.is_empty() {
return Err(Error::Query(
"rate() requires a range vector argument".into(),
));
}
let arg = &call.args.args[0];
let series_list = expect_vector(
self.evaluate_value(arg, time, end_time, step, storage)
.await?,
"rate() requires a range vector argument",
)?;
// Apply rate calculation to each series
let mut result = Vec::new();
for series in series_list {
if series.samples.len() < 2 {
continue; // Need at least 2 samples for rate calculation
}
// Get first and last samples
let first = &series.samples[0];
let last = &series.samples[series.samples.len() - 1];
// Calculate time range in seconds
let duration_seconds = (last.timestamp - first.timestamp) as f64 / 1000.0;
if duration_seconds <= 0.0 {
continue;
}
// Calculate rate (per-second average)
// For counter metrics, we should handle resets, but simplified here
let value_diff = last.value - first.value;
let rate = value_diff / duration_seconds;
// Create result series with single sample at query time
result.push(TimeSeries {
id: series.id,
labels: series.labels.clone(),
samples: vec![Sample {
timestamp: time,
value: rate.max(0.0), // Rates can't be negative for counters
}],
});
}
Ok(result)
}
/// Evaluate irate() function - calculates instant rate using last two samples
async fn evaluate_irate(
&self,
call: &Call,
time: i64,
end_time: i64,
step: i64,
storage: &QueryableStorage,
) -> Result<Vec<TimeSeries>> {
// irate() expects a range vector (MatrixSelector) as argument
if call.args.args.is_empty() {
return Err(Error::Query(
"irate() requires a range vector argument".into(),
));
}
let arg = &call.args.args[0];
let series_list = expect_vector(
self.evaluate_value(arg, time, end_time, step, storage)
.await?,
"irate() requires a range vector argument",
)?;
// Apply irate calculation to each series
let mut result = Vec::new();
for series in series_list {
if series.samples.len() < 2 {
continue; // Need at least 2 samples for irate calculation
}
// Get last two samples
let second_last = &series.samples[series.samples.len() - 2];
let last = &series.samples[series.samples.len() - 1];
// Calculate time difference in seconds
let duration_seconds = (last.timestamp - second_last.timestamp) as f64 / 1000.0;
if duration_seconds <= 0.0 {
continue;
}
// Calculate instant rate
let value_diff = last.value - second_last.value;
let rate = value_diff / duration_seconds;
// Create result series with single sample at query time
result.push(TimeSeries {
id: series.id,
labels: series.labels.clone(),
samples: vec![Sample {
timestamp: time,
value: rate.max(0.0), // Rates can't be negative for counters
}],
});
}
Ok(result)
}
/// Evaluate increase() function - calculates total increase over time range
async fn evaluate_increase(
&self,
call: &Call,
time: i64,
end_time: i64,
step: i64,
storage: &QueryableStorage,
) -> Result<Vec<TimeSeries>> {
// increase() expects a range vector (MatrixSelector) as argument
if call.args.args.is_empty() {
return Err(Error::Query(
"increase() requires a range vector argument".into(),
));
}
let arg = &call.args.args[0];
let series_list = expect_vector(
self.evaluate_value(arg, time, end_time, step, storage)
.await?,
"increase() requires a range vector argument",
)?;
// Apply increase calculation to each series
let mut result = Vec::new();
for series in series_list {
if series.samples.len() < 2 {
continue; // Need at least 2 samples for increase calculation
}
// Get first and last samples
let first = &series.samples[0];
let last = &series.samples[series.samples.len() - 1];
// Calculate total increase
// For counter metrics, we should handle resets, but simplified here
let increase = last.value - first.value;
// Create result series with single sample at query time
result.push(TimeSeries {
id: series.id,
labels: series.labels.clone(),
samples: vec![Sample {
timestamp: time,
value: increase.max(0.0), // Increase can't be negative for counters
}],
});
}
Ok(result)
}
/// Evaluate histogram_quantile() from Prometheus-style cumulative buckets.
async fn evaluate_histogram_quantile(
&self,
call: &Call,
time: i64,
end_time: i64,
step: i64,
storage: &QueryableStorage,
) -> Result<Vec<TimeSeries>> {
if call.args.args.len() != 2 {
return Err(Error::Query(
"histogram_quantile() requires a scalar quantile and a bucket vector".into(),
));
}
let quantile = expect_scalar(
self.evaluate_value(&call.args.args[0], time, end_time, step, storage)
.await?,
"histogram_quantile() requires a scalar quantile",
)?;
if !(0.0..=1.0).contains(&quantile) {
return Err(Error::Query(format!(
"histogram_quantile() quantile must be within [0, 1], got {quantile}"
)));
}
let buckets = expect_vector(
self.evaluate_value(&call.args.args[1], time, end_time, step, storage)
.await?,
"histogram_quantile() requires a bucket vector",
)?;
let mut grouped: BTreeMap<String, (Vec<Label>, Vec<(f64, f64)>)> = BTreeMap::new();
for series in buckets {
let Some(sample) = series.samples.last() else {
continue;
};
let Some(le) = series.get_label("le") else {
continue;
};
let upper_bound = parse_histogram_bound(le)?;
let labels = histogram_output_labels(&series.labels);
let key = labels_key(&labels);
grouped
.entry(key)
.or_insert_with(|| (labels, Vec::new()))
.1
.push((upper_bound, sample.value));
}
let mut results = Vec::new();
for (labels, mut buckets) in grouped.into_values() {
buckets.sort_by(|(lhs, _), (rhs, _)| lhs.total_cmp(rhs));
let value = histogram_quantile_value(quantile, &buckets)?;
results.push(TimeSeries {
id: SeriesId(results.len() as u64),
labels,
samples: vec![Sample::new(time, value)],
});
}
Ok(results)
}
/// Evaluate binary operations for scalar/vector arithmetic and one-to-one vector matching.
async fn evaluate_binary(
&self,
binary: &BinaryExpr,
time: i64,
end_time: i64,
step: i64,
storage: &QueryableStorage,
) -> Result<EvalValue> {
if let Some(modifier) = &binary.modifier {
if !matches!(modifier.card, VectorMatchCardinality::OneToOne) {
return Err(Error::Query(
"many-to-one and many-to-many vector matching are not supported yet".into(),
));
}
}
let lhs = self
.evaluate_value(&binary.lhs, time, end_time, step, storage)
.await?;
let rhs = self
.evaluate_value(&binary.rhs, time, end_time, step, storage)
.await?;
match (lhs, rhs) {
(EvalValue::Scalar(lhs), EvalValue::Scalar(rhs)) => {
evaluate_scalar_binary(binary, lhs, rhs).map(EvalValue::Scalar)
}
(EvalValue::Vector(lhs), EvalValue::Scalar(rhs)) => {
evaluate_vector_scalar_binary(binary, lhs, rhs, true).map(EvalValue::Vector)
}
(EvalValue::Scalar(lhs), EvalValue::Vector(rhs)) => {
evaluate_vector_scalar_binary(binary, rhs, lhs, false).map(EvalValue::Vector)
}
(EvalValue::Vector(lhs), EvalValue::Vector(rhs)) => {
evaluate_vector_vector_binary(binary, lhs, rhs).map(EvalValue::Vector)
}
}
}
async fn evaluate_unary(
&self,
unary: &UnaryExpr,
time: i64,
end_time: i64,
step: i64,
storage: &QueryableStorage,
) -> Result<EvalValue> {
match self
.evaluate_value(&unary.expr, time, end_time, step, storage)
.await?
{
EvalValue::Scalar(value) => Ok(EvalValue::Scalar(-value)),
EvalValue::Vector(mut series) => {
for entry in &mut series {
for sample in &mut entry.samples {
sample.value = -sample.value;
}
}
Ok(EvalValue::Vector(series))
}
}
}
fn matches_selector(&self, ts: &TimeSeries, selector: &VectorSelector) -> bool {
if let Some(name) = selector.name.as_deref() {
if ts.name() != Some(name) {
return false;
}
}
self.matches_matchers(ts, &selector.matchers)
}
fn matches_matchers(&self, ts: &TimeSeries, matchers: &Matchers) -> bool {
if !matchers
.matchers
.iter()
.all(|matcher| self.matcher_matches(ts, matcher))
{
return false;
}
if matchers.or_matchers.is_empty() {
return true;
}
matchers.or_matchers.iter().any(|group| {
group
.iter()
.all(|matcher| self.matcher_matches(ts, matcher))
})
}
fn matcher_matches(&self, ts: &TimeSeries, matcher: &promql_parser::label::Matcher) -> bool {
let label_value = ts.get_label(&matcher.name);
match &matcher.op {
MatchOp::Equal => label_value == Some(matcher.value.as_str()),
MatchOp::NotEqual => match label_value {
Some(value) => value != matcher.value,
None => true,
},
MatchOp::Re(regex) => label_value.is_some_and(|value| regex.is_match(value)),
MatchOp::NotRe(regex) => match label_value {
Some(value) => !regex.is_match(value),
None => true,
},
}
}
pub async fn series_metadata(
&self,
matchers: &[String],
start: Option<i64>,
end: Option<i64>,
) -> Result<Vec<HashMap<String, String>>> {
let started = self.metrics.begin_query();
let storage = self.storage.read().await;
let series = self.matching_series(&storage, matchers, start, end);
let result = Ok(series
.into_iter()
.map(|ts| {
ts.labels
.iter()
.map(|label| (label.name.clone(), label.value.clone()))
.collect()
})
.collect());
self.metrics.finish_query(started, true);
result
}
pub async fn label_values_for_matchers(
&self,
label_name: &str,
matchers: &[String],
start: Option<i64>,
end: Option<i64>,
) -> Result<Vec<String>> {
let started = self.metrics.begin_query();
let storage = self.storage.read().await;
let mut values: Vec<String> = self
.matching_series(&storage, matchers, start, end)
.into_iter()
.filter_map(|series| series.get_label(label_name).map(str::to_string))
.collect();
values.sort();
values.dedup();
self.metrics.finish_query(started, true);
Ok(values)
}
fn matching_series(
&self,
storage: &QueryableStorage,
matchers: &[String],
start: Option<i64>,
end: Option<i64>,
) -> Vec<TimeSeries> {
let parsed_matchers = parse_label_matchers(matchers);
storage
.series
.values()
.filter(|series| series_matches(series, &parsed_matchers))
.filter(|series| series_in_time_range(series, start, end))
.cloned()
.collect()
}
}
impl QueryMetrics {
fn new() -> Self {
Self {
queries_total: AtomicU64::new(0),
queries_failed: AtomicU64::new(0),
queries_active: AtomicU64::new(0),
durations_ms: Mutex::new(VecDeque::with_capacity(QUERY_DURATION_HISTORY_LIMIT)),
}
}
fn begin_query(&self) -> Instant {
self.queries_total.fetch_add(1, Ordering::Relaxed);
self.queries_active.fetch_add(1, Ordering::Relaxed);
Instant::now()
}
fn finish_query(&self, started: Instant, success: bool) {
if !success {
self.queries_failed.fetch_add(1, Ordering::Relaxed);
}
self.queries_active.fetch_sub(1, Ordering::Relaxed);
let elapsed_ms = started.elapsed().as_millis() as u64;
let mut durations = self.durations_ms.lock();
if durations.len() >= QUERY_DURATION_HISTORY_LIMIT {
durations.pop_front();
}
durations.push_back(elapsed_ms);
}
pub fn snapshot(&self) -> QueryMetricsSnapshot {
let mut sorted_durations: Vec<u64> = self.durations_ms.lock().iter().copied().collect();
sorted_durations.sort_unstable();
QueryMetricsSnapshot {
queries_total: self.queries_total.load(Ordering::Relaxed),
queries_failed: self.queries_failed.load(Ordering::Relaxed),
queries_active: self.queries_active.load(Ordering::Relaxed),
query_duration_p50: percentile(&sorted_durations, 0.50),
query_duration_p95: percentile(&sorted_durations, 0.95),
query_duration_p99: percentile(&sorted_durations, 0.99),
}
}
}
impl QueryableStorage {
/// Add or update a time series in storage
pub fn upsert_series(&mut self, series: TimeSeries) {
// Update label index
for label in &series.labels {
let series_ids = self
.label_index
.entry(label.name.clone())
.or_default()
.entry(label.value.clone())
.or_default();
if !series_ids.contains(&series.id) {
series_ids.push(series.id);
}
}
// Upsert series
self.series
.entry(series.id)
.and_modify(|existing| {
// Merge samples (append new samples)
existing.samples.extend(series.samples.clone());
// Sort by timestamp
existing.samples.sort_by_key(|s| s.timestamp);
// Deduplicate
existing.samples.dedup_by_key(|s| s.timestamp);
})
.or_insert(series);
}
/// Get label values for a specific label name
#[cfg(test)]
pub fn label_values(&self, label_name: &str) -> Vec<String> {
let mut values: Vec<String> = self
.label_index
.get(label_name)
.map(|values| values.keys().cloned().collect())
.unwrap_or_default();
values.sort();
values
}
pub fn rebuild_index(&mut self) {
self.label_index.clear();
let series: Vec<TimeSeries> = self.series.values().cloned().collect();
for series in series {
for label in &series.labels {
self.label_index
.entry(label.name.clone())
.or_default()
.entry(label.value.clone())
.or_default()
.push(series.id);
}
}
}
pub fn prune_before(&mut self, cutoff: i64) -> usize {
let mut removed_samples = 0usize;
self.series.retain(|_, series| {
let before = series.samples.len();
series.samples.retain(|sample| sample.timestamp >= cutoff);
removed_samples += before.saturating_sub(series.samples.len());
!series.samples.is_empty()
});
self.rebuild_index();
removed_samples
}
}
fn expect_vector(value: EvalValue, message: &str) -> Result<Vec<TimeSeries>> {
match value {
EvalValue::Vector(series) => Ok(series),
EvalValue::Scalar(_) => Err(Error::Query(message.to_string())),
}
}
fn expect_scalar(value: EvalValue, message: &str) -> Result<f64> {
match value {
EvalValue::Scalar(value) => Ok(value),
EvalValue::Vector(_) => Err(Error::Query(message.to_string())),
}
}
fn instant_query_from_eval_value(value: EvalValue, time: i64) -> QueryResult {
match value {
EvalValue::Scalar(value) => QueryResult {
result_type: "scalar".to_string(),
result: vec![InstantQueryResult {
metric: HashMap::new(),
value: Some((time, value)),
}],
},
EvalValue::Vector(series) => QueryResult {
result_type: "vector".to_string(),
result: series
.into_iter()
.map(|ts| InstantQueryResult {
metric: labels_to_map(&ts.labels),
value: ts
.samples
.last()
.map(|sample| (sample.timestamp, sample.value)),
})
.collect(),
},
}
}
fn append_range_step_result(
results: &mut HashMap<String, RangeResult>,
step_result: EvalValue,
time: i64,
) {
match step_result {
EvalValue::Scalar(value) => {
let entry = results
.entry("__scalar__".to_string())
.or_insert_with(|| RangeResult {
metric: HashMap::new(),
values: Vec::new(),
});
entry.values.push((time, value));
}
EvalValue::Vector(series) => {
for ts in series {
let key = labels_key(&ts.labels);
let metric = labels_to_map(&ts.labels);
let entry = results.entry(key).or_insert_with(|| RangeResult {
metric,
values: Vec::new(),
});
for sample in ts.samples {
entry.values.push((sample.timestamp, sample.value));
}
}
}
}
}
fn labels_to_map(labels: &[Label]) -> HashMap<String, String> {
labels
.iter()
.map(|label| (label.name.clone(), label.value.clone()))
.collect()
}
fn normalize_labels(mut labels: Vec<Label>) -> Vec<Label> {
labels.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name).then(lhs.value.cmp(&rhs.value)));
labels
}
fn labels_key(labels: &[Label]) -> String {
let mut pairs: Vec<(String, String)> = labels
.iter()
.map(|label| (label.name.clone(), label.value.clone()))
.collect();
pairs.sort();
pairs
.into_iter()
.map(|(name, value)| format!("{name}={value}"))
.collect::<Vec<_>>()
.join(",")
}
fn aggregate_values(op: &str, values: &[f64]) -> Result<f64> {
match op {
"sum" => Ok(values.iter().sum()),
"avg" => Ok(values.iter().sum::<f64>() / values.len() as f64),
"min" => Ok(values.iter().copied().fold(f64::INFINITY, f64::min)),
"max" => Ok(values.iter().copied().fold(f64::NEG_INFINITY, f64::max)),
"count" => Ok(values.len() as f64),
other => Err(Error::Query(format!("Unsupported aggregation: {other}"))),
}
}
fn aggregation_output_labels(labels: &[Label], modifier: Option<&LabelModifier>) -> Vec<Label> {
match modifier {
None => Vec::new(),
Some(LabelModifier::Include(group)) => normalize_labels(
labels
.iter()
.filter(|label| group.labels.iter().any(|name| name == &label.name))
.cloned()
.collect(),
),
Some(LabelModifier::Exclude(group)) => normalize_labels(
labels
.iter()
.filter(|label| label.name != "__name__")
.filter(|label| !group.labels.iter().any(|name| name == &label.name))
.cloned()
.collect(),
),
}
}
fn histogram_output_labels(labels: &[Label]) -> Vec<Label> {
normalize_labels(
labels
.iter()
.filter(|label| label.name != "__name__" && label.name != "le")
.cloned()
.collect(),
)
}
fn parse_histogram_bound(value: &str) -> Result<f64> {
match value {
"+Inf" | "Inf" | "+inf" | "inf" => Ok(f64::INFINITY),
_ => value.parse::<f64>().map_err(|error| {
Error::Query(format!("invalid histogram bucket bound {value:?}: {error}"))
}),
}
}
fn histogram_quantile_value(quantile: f64, buckets: &[(f64, f64)]) -> Result<f64> {
let Some((_, total_count)) = buckets.last() else {
return Err(Error::Query(
"histogram_quantile() requires at least one bucket".into(),
));
};
if *total_count <= 0.0 {
return Ok(0.0);
}
let target = quantile * total_count;
let mut lower_bound = 0.0;
let mut previous_count = 0.0;
for (upper_bound, count) in buckets {
if *count >= target {
if upper_bound.is_infinite() {
return Ok(lower_bound);
}
let bucket_count = (*count - previous_count).max(0.0);
if bucket_count == 0.0 {
return Ok(*upper_bound);
}
let fraction = ((target - previous_count) / bucket_count).clamp(0.0, 1.0);
return Ok(lower_bound + (upper_bound - lower_bound) * fraction);
}
lower_bound = *upper_bound;
previous_count = *count;
}
Ok(lower_bound)
}
fn evaluate_scalar_binary(binary: &BinaryExpr, lhs: f64, rhs: f64) -> Result<f64> {
let op = binary.op.to_string();
if is_set_operator(&op) {
return Err(Error::Query(format!("Unsupported set operator: {op}")));
}
if is_comparison_operator(&op) {
if !binary.return_bool() {
return Err(Error::Query(
"scalar comparisons require the bool modifier".into(),
));
}
return Ok(if compare_values(&op, lhs, rhs)? {
1.0
} else {
0.0
});
}
arithmetic_value(&op, lhs, rhs)
}
fn evaluate_vector_scalar_binary(
binary: &BinaryExpr,
series_list: Vec<TimeSeries>,
scalar: f64,
vector_on_lhs: bool,
) -> Result<Vec<TimeSeries>> {
let mut results = Vec::new();
for mut series in series_list {
let labels = binary_result_labels(&series.labels, binary.modifier.as_ref());
let mut samples = Vec::new();
for sample in &mut series.samples {
let (lhs, rhs) = if vector_on_lhs {
(sample.value, scalar)
} else {
(scalar, sample.value)
};
let retained = sample.value;
if let Some(value) = apply_binary_sample(binary, lhs, rhs, retained)? {
samples.push(Sample::new(sample.timestamp, value));
}
}
if !samples.is_empty() {
results.push(TimeSeries {
id: series.id,
labels,
samples,
});
}
}
Ok(results)
}
fn evaluate_vector_vector_binary(
binary: &BinaryExpr,
lhs_series: Vec<TimeSeries>,
rhs_series: Vec<TimeSeries>,
) -> Result<Vec<TimeSeries>> {
let lhs_index = build_vector_match_index(&lhs_series, binary.modifier.as_ref())?;
let rhs_index = build_vector_match_index(&rhs_series, binary.modifier.as_ref())?;
let mut results = Vec::new();
for (key, lhs) in lhs_index {
let Some(rhs) = rhs_index.get(&key) else {
continue;
};
let Some(lhs_sample) = lhs.samples.last() else {
continue;
};
let Some(rhs_sample) = rhs.samples.last() else {
continue;
};
if let Some(value) =
apply_binary_sample(binary, lhs_sample.value, rhs_sample.value, lhs_sample.value)?
{
results.push(TimeSeries {
id: lhs.id,
labels: binary_result_labels(&lhs.labels, binary.modifier.as_ref()),
samples: vec![Sample::new(
lhs_sample.timestamp.max(rhs_sample.timestamp),
value,
)],
});
}
}
Ok(results)
}
fn build_vector_match_index<'a>(
series_list: &'a [TimeSeries],
modifier: Option<&BinModifier>,
) -> Result<HashMap<String, &'a TimeSeries>> {
let mut index = HashMap::new();
for series in series_list {
let key = vector_match_key(series, modifier);
if index.insert(key.clone(), series).is_some() {
return Err(Error::Query(format!(
"duplicate vector match key {key:?}; many-to-one matching is not supported"
)));
}
}
Ok(index)
}
fn vector_match_key(series: &TimeSeries, modifier: Option<&BinModifier>) -> String {
labels_key(&binary_result_labels(&series.labels, modifier))
}
fn binary_result_labels(labels: &[Label], modifier: Option<&BinModifier>) -> Vec<Label> {
let selected = match modifier.and_then(|modifier| modifier.matching.as_ref()) {
Some(LabelModifier::Include(group)) => labels
.iter()
.filter(|label| group.labels.iter().any(|name| name == &label.name))
.cloned()
.collect(),
Some(LabelModifier::Exclude(group)) => labels
.iter()
.filter(|label| label.name != "__name__")
.filter(|label| !group.labels.iter().any(|name| name == &label.name))
.cloned()
.collect(),
None => labels
.iter()
.filter(|label| label.name != "__name__")
.cloned()
.collect(),
};
normalize_labels(selected)
}
fn apply_binary_sample(
binary: &BinaryExpr,
lhs: f64,
rhs: f64,
retained: f64,
) -> Result<Option<f64>> {
let op = binary.op.to_string();
if is_set_operator(&op) {
return Err(Error::Query(format!("Unsupported set operator: {op}")));
}
if is_comparison_operator(&op) {
let matched = compare_values(&op, lhs, rhs)?;
if binary.return_bool() {
return Ok(Some(if matched { 1.0 } else { 0.0 }));
}
return Ok(matched.then_some(retained));
}
Ok(Some(arithmetic_value(&op, lhs, rhs)?))
}
fn compare_values(op: &str, lhs: f64, rhs: f64) -> Result<bool> {
match op {
"==" => Ok(lhs == rhs),
"!=" => Ok(lhs != rhs),
">" => Ok(lhs > rhs),
">=" => Ok(lhs >= rhs),
"<" => Ok(lhs < rhs),
"<=" => Ok(lhs <= rhs),
other => Err(Error::Query(format!(
"Unsupported comparison operator: {other}"
))),
}
}
fn arithmetic_value(op: &str, lhs: f64, rhs: f64) -> Result<f64> {
match op {
"+" => Ok(lhs + rhs),
"-" => Ok(lhs - rhs),
"*" => Ok(lhs * rhs),
"/" => Ok(lhs / rhs),
"%" => Ok(lhs % rhs),
"^" => Ok(lhs.powf(rhs)),
"atan2" => Ok(lhs.atan2(rhs)),
other => Err(Error::Query(format!(
"Unsupported arithmetic operator: {other}"
))),
}
}
fn is_comparison_operator(op: &str) -> bool {
matches!(op, "==" | "!=" | ">" | ">=" | "<" | "<=")
}
fn is_set_operator(op: &str) -> bool {
matches!(op, "and" | "or" | "unless")
}
fn percentile(values: &[u64], quantile: f64) -> f64 {
if values.is_empty() {
return 0.0;
}
let index = ((values.len() - 1) as f64 * quantile).round() as usize;
values[index.min(values.len() - 1)] as f64
}
fn parse_label_matchers(matchers: &[String]) -> Vec<(String, String)> {
matchers
.iter()
.filter_map(|matcher| matcher.split_once('='))
.map(|(key, value)| {
(
key.trim().to_string(),
value.trim().trim_matches('"').to_string(),
)
})
.collect()
}
fn series_matches(series: &TimeSeries, matchers: &[(String, String)]) -> bool {
matchers.iter().all(|(key, value)| {
series
.labels
.iter()
.any(|label| &label.name == key && &label.value == value)
})
}
fn series_in_time_range(series: &TimeSeries, start: Option<i64>, end: Option<i64>) -> bool {
let Some((series_start, series_end)) = series.time_range() else {
return true;
};
if let Some(start) = start {
if series_end < start {
return false;
}
}
if let Some(end) = end {
if series_start > end {
return false;
}
}
true
}
/// HTTP handler for instant queries
#[axum::debug_handler]
async fn handle_instant_query(
State(service): State<QueryService>,
Query(params): Query<InstantQueryParams>,
) -> (StatusCode, Json<QueryResponse>) {
let time = params
.time
.unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
let response = match service.execute_instant_query(&params.query, time).await {
Ok(result) => QueryResponse {
status: "success".to_string(),
data: Some(serde_json::to_value(result).unwrap()),
error: None,
error_type: None,
},
Err(e) => {
error!("Query failed: {}", e);
QueryResponse {
status: "error".to_string(),
data: None,
error: Some(e.to_string()),
error_type: Some("execution".to_string()),
}
}
};
(StatusCode::OK, Json(response))
}
/// HTTP handler for range queries
#[axum::debug_handler]
async fn handle_range_query(
State(service): State<QueryService>,
Query(params): Query<RangeQueryParams>,
) -> (StatusCode, Json<QueryResponse>) {
let response = match service
.execute_range_query(&params.query, params.start, params.end, params.step)
.await
{
Ok(result) => QueryResponse {
status: "success".to_string(),
data: Some(serde_json::to_value(result).unwrap()),
error: None,
error_type: None,
},
Err(e) => {
error!("Range query failed: {}", e);
QueryResponse {
status: "error".to_string(),
data: None,
error: Some(e.to_string()),
error_type: Some("execution".to_string()),
}
}
};
(StatusCode::OK, Json(response))
}
/// HTTP handler for label values
async fn handle_label_values(
State(service): State<QueryService>,
Path(label_name): Path<String>,
Query(params): Query<SeriesQueryParams>,
) -> impl IntoResponse {
match service
.label_values_for_matchers(&label_name, &params.matchers, params.start, params.end)
.await
{
Ok(values) => (
StatusCode::OK,
Json(LabelValuesResponse {
status: "success".to_string(),
data: values,
}),
)
.into_response(),
Err(error) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"status": "error",
"error": error.to_string(),
})),
)
.into_response(),
}
}
/// HTTP handler for series metadata
async fn handle_series(
State(service): State<QueryService>,
Query(params): Query<SeriesQueryParams>,
) -> impl IntoResponse {
match service
.series_metadata(&params.matchers, params.start, params.end)
.await
{
Ok(series) => (
StatusCode::OK,
Json(SeriesResponse {
status: "success".to_string(),
data: series,
}),
)
.into_response(),
Err(error) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"status": "error",
"error": error.to_string(),
})),
)
.into_response(),
}
}
// Request/Response Types
#[derive(Debug, Deserialize)]
struct InstantQueryParams {
query: String,
#[serde(default)]
time: Option<i64>,
}
#[derive(Debug, Deserialize)]
struct RangeQueryParams {
query: String,
start: i64,
end: i64,
step: i64,
}
#[derive(Debug, Deserialize)]
struct SeriesQueryParams {
#[serde(default)]
#[serde(rename = "match[]")]
matchers: Vec<String>,
#[serde(default)]
start: Option<i64>,
#[serde(default)]
end: Option<i64>,
}
#[derive(Debug, Serialize)]
struct QueryResponse {
status: String,
data: Option<serde_json::Value>,
error: Option<String>,
error_type: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct QueryResult {
#[serde(rename = "resultType")]
pub result_type: String,
pub result: Vec<InstantQueryResult>,
}
#[derive(Debug, Clone, Serialize)]
pub struct InstantQueryResult {
pub metric: HashMap<String, String>,
pub value: Option<(i64, f64)>,
}
#[derive(Debug, Clone, Serialize)]
pub struct RangeQueryResult {
#[serde(rename = "resultType")]
pub result_type: String,
pub result: Vec<RangeResult>,
}
#[derive(Debug, Clone, Serialize)]
pub struct RangeResult {
pub metric: HashMap<String, String>,
pub values: Vec<(i64, f64)>,
}
#[derive(Debug, Serialize)]
struct LabelValuesResponse {
status: String,
data: Vec<String>,
}
#[derive(Debug, Serialize)]
struct SeriesResponse {
status: String,
data: Vec<HashMap<String, String>>,
}
impl Default for QueryService {
fn default() -> Self {
Self::new()
}
}
impl QueryableStorage {
/// Save storage state to disk using bincode serialization
pub fn save_to_file(&self, path: &std::path::Path) -> Result<()> {
use std::fs::File;
use std::io::Write;
// Serialize to bincode
let encoded = bincode::serialize(self)
.map_err(|e| Error::Storage(format!("Serialization failed: {}", e)))?;
// Create parent directory if needed
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| Error::Storage(format!("Failed to create directory: {}", e)))?;
}
// Write to file atomically (write to temp, then rename)
let temp_path = path.with_extension("tmp");
let mut file = File::create(&temp_path)
.map_err(|e| Error::Storage(format!("Failed to create file: {}", e)))?;
file.write_all(&encoded)
.map_err(|e| Error::Storage(format!("Failed to write file: {}", e)))?;
file.sync_all()
.map_err(|e| Error::Storage(format!("Failed to sync file: {}", e)))?;
std::fs::rename(&temp_path, path)
.map_err(|e| Error::Storage(format!("Failed to rename file: {}", e)))?;
Ok(())
}
/// Load storage state from disk using bincode deserialization
pub fn load_from_file(path: &std::path::Path) -> Result<Self> {
use std::fs::File;
use std::io::Read;
// Check if file exists
if !path.exists() {
return Ok(Self {
series: HashMap::new(),
label_index: HashMap::new(),
});
}
// Read file
let mut file =
File::open(path).map_err(|e| Error::Storage(format!("Failed to open file: {}", e)))?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)
.map_err(|e| Error::Storage(format!("Failed to read file: {}", e)))?;
// Deserialize from bincode
let mut storage: Self = bincode::deserialize(&buffer)
.map_err(|e| Error::Storage(format!("Deserialization failed: {}", e)))?;
storage.rebuild_index();
Ok(storage)
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::{
body::{to_bytes, Body},
http::{Method, Request, StatusCode},
};
use tower::ServiceExt;
fn test_series(id: u64, labels: &[(&str, &str)], samples: &[(i64, f64)]) -> TimeSeries {
TimeSeries {
id: SeriesId(id),
labels: labels
.iter()
.map(|(name, value)| Label::new(*name, *value))
.collect(),
samples: samples
.iter()
.map(|(timestamp, value)| Sample::new(*timestamp, *value))
.collect(),
}
}
async fn seed_series(service: &QueryService, series: Vec<TimeSeries>) {
let mut storage = service.storage.write().await;
for entry in series {
storage.upsert_series(entry);
}
}
#[tokio::test]
async fn test_query_service_creation() {
let service = QueryService::new();
// Verify service can be created
assert!(service.storage.read().await.series.is_empty());
}
#[test]
fn test_simple_selector_parsing() {
// Test that we can parse a simple PromQL query
let query = "http_requests_total";
let result = promql_parser::parser::parse(query);
assert!(result.is_ok());
}
#[test]
fn test_label_selector_parsing() {
let query = "http_requests_total{method=\"GET\"}";
let result = promql_parser::parser::parse(query);
assert!(result.is_ok());
}
#[test]
fn test_aggregation_parsing() {
let query = "sum(http_requests_total)";
let result = promql_parser::parser::parse(query);
assert!(result.is_ok());
}
#[test]
fn test_rate_function_parsing() {
let query = "rate(http_requests_total[5m])";
let result = promql_parser::parser::parse(query);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_instant_query_empty_storage() {
let service = QueryService::new();
let result = service.execute_instant_query("up", 1000).await;
assert!(result.is_ok());
let query_result = result.unwrap();
assert_eq!(query_result.result_type, "vector");
}
#[tokio::test]
async fn test_range_query_empty_storage() {
let service = QueryService::new();
let result = service.execute_range_query("up", 1000, 2000, 100).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_scalar_instant_query_returns_scalar_result() {
let service = QueryService::new();
let result = service.execute_instant_query("1", 1000).await.unwrap();
assert_eq!(result.result_type, "scalar");
assert_eq!(result.result.len(), 1);
assert_eq!(result.result[0].value, Some((1000, 1.0)));
}
#[tokio::test]
async fn test_label_matchers_support_regex_and_negative_match() {
let service = QueryService::new();
seed_series(
&service,
vec![
test_series(
1,
&[
("__name__", "http_requests_total"),
("method", "GET"),
("status", "200"),
],
&[(1000, 10.0)],
),
test_series(
2,
&[
("__name__", "http_requests_total"),
("method", "POST"),
("status", "500"),
],
&[(1000, 20.0)],
),
],
)
.await;
let result = service
.execute_instant_query("http_requests_total{method=~\"G.*\",status!=\"500\"}", 1000)
.await
.unwrap();
assert_eq!(result.result_type, "vector");
assert_eq!(result.result.len(), 1);
assert_eq!(
result.result[0].metric.get("method"),
Some(&"GET".to_string())
);
}
#[tokio::test]
async fn test_aggregation_groups_by_labels() {
let service = QueryService::new();
seed_series(
&service,
vec![
test_series(
1,
&[("__name__", "cpu_usage"), ("job", "api"), ("instance", "a")],
&[(1000, 2.0)],
),
test_series(
2,
&[("__name__", "cpu_usage"), ("job", "api"), ("instance", "b")],
&[(1000, 3.0)],
),
],
)
.await;
let result = service
.execute_instant_query("sum by (job)(cpu_usage)", 1000)
.await
.unwrap();
assert_eq!(result.result.len(), 1);
assert_eq!(result.result[0].metric.get("job"), Some(&"api".to_string()));
assert_eq!(result.result[0].value, Some((1000, 5.0)));
}
#[tokio::test]
async fn test_binary_queries_support_vector_scalar_and_vector_vector() {
let service = QueryService::new();
seed_series(
&service,
vec![
test_series(
1,
&[("__name__", "requests_total"), ("service", "api")],
&[(1000, 20.0)],
),
test_series(
2,
&[("__name__", "errors_total"), ("service", "api")],
&[(1000, 5.0)],
),
],
)
.await;
let vector_scalar = service
.execute_instant_query("requests_total / 2", 1000)
.await
.unwrap();
assert_eq!(vector_scalar.result.len(), 1);
assert_eq!(
vector_scalar.result[0].metric.get("service"),
Some(&"api".to_string())
);
assert_eq!(vector_scalar.result[0].value, Some((1000, 10.0)));
assert!(!vector_scalar.result[0].metric.contains_key("__name__"));
let ratio = service
.execute_instant_query("errors_total / requests_total", 1000)
.await
.unwrap();
assert_eq!(ratio.result.len(), 1);
assert_eq!(ratio.result[0].value, Some((1000, 0.25)));
assert_eq!(ratio.result[0].metric.len(), 1);
assert_eq!(
ratio.result[0].metric.get("service"),
Some(&"api".to_string())
);
}
#[tokio::test]
async fn test_histogram_quantile_with_grouped_buckets() {
let service = QueryService::new();
seed_series(
&service,
vec![
test_series(
1,
&[
("__name__", "request_duration_seconds_bucket"),
("job", "api"),
("instance", "a"),
("le", "0.1"),
],
&[(1000, 5.0)],
),
test_series(
2,
&[
("__name__", "request_duration_seconds_bucket"),
("job", "api"),
("instance", "a"),
("le", "0.2"),
],
&[(1000, 10.0)],
),
test_series(
3,
&[
("__name__", "request_duration_seconds_bucket"),
("job", "api"),
("instance", "a"),
("le", "+Inf"),
],
&[(1000, 20.0)],
),
test_series(
4,
&[
("__name__", "request_duration_seconds_bucket"),
("job", "api"),
("instance", "b"),
("le", "0.1"),
],
&[(1000, 5.0)],
),
test_series(
5,
&[
("__name__", "request_duration_seconds_bucket"),
("job", "api"),
("instance", "b"),
("le", "0.2"),
],
&[(1000, 10.0)],
),
test_series(
6,
&[
("__name__", "request_duration_seconds_bucket"),
("job", "api"),
("instance", "b"),
("le", "+Inf"),
],
&[(1000, 20.0)],
),
],
)
.await;
let result = service
.execute_instant_query(
"histogram_quantile(0.5, sum by (job, le)(request_duration_seconds_bucket{job=\"api\"}))",
1000,
)
.await
.unwrap();
assert_eq!(result.result.len(), 1);
assert_eq!(result.result[0].metric.get("job"), Some(&"api".to_string()));
assert!(!result.result[0].metric.contains_key("le"));
assert_eq!(result.result[0].value, Some((1000, 0.2)));
}
#[tokio::test]
async fn test_query_range_route_supports_binary_expression() {
let service = QueryService::new();
seed_series(
&service,
vec![test_series(
1,
&[("__name__", "requests_total"), ("service", "api")],
&[(1000, 10.0), (2000, 20.0)],
)],
)
.await;
let app: axum::Router = service.router();
let request = Request::builder()
.method(Method::GET)
.uri("/api/v1/query_range?query=requests_total%20/%202&start=1000&end=2000&step=1000")
.body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = to_bytes(response.into_body(), 1024 * 1024).await.unwrap();
let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(payload["status"], "success");
assert_eq!(payload["data"]["resultType"], "matrix");
assert_eq!(payload["data"]["result"][0]["metric"]["service"], "api");
assert_eq!(
payload["data"]["result"][0]["values"][0],
serde_json::json!([1000, 5.0])
);
assert_eq!(
payload["data"]["result"][0]["values"][1],
serde_json::json!([2000, 10.0])
);
}
#[tokio::test]
async fn test_storage_upsert() {
let service = QueryService::new();
let mut storage = service.storage.write().await;
let series = TimeSeries {
id: SeriesId(1),
labels: vec![
Label::new("__name__", "test_metric"),
Label::new("job", "test"),
],
samples: vec![Sample::new(1000, 42.0)],
};
storage.upsert_series(series);
assert_eq!(storage.series.len(), 1);
}
#[tokio::test]
async fn test_label_values() {
let service = QueryService::new();
let mut storage = service.storage.write().await;
let series = TimeSeries {
id: SeriesId(1),
labels: vec![
Label::new("__name__", "test_metric"),
Label::new("job", "test_job"),
],
samples: vec![],
};
storage.upsert_series(series);
let values = storage.label_values("job");
assert_eq!(values.len(), 1);
assert!(values.contains(&"test_job".to_string()));
}
#[tokio::test]
async fn test_label_values_route() {
let service = QueryService::new();
{
let mut storage = service.storage.write().await;
storage.upsert_series(TimeSeries {
id: SeriesId(1),
labels: vec![
Label::new("__name__", "test_metric"),
Label::new("job", "test_job"),
],
samples: vec![],
});
}
let app: axum::Router = service.router();
let request = Request::builder()
.method(Method::GET)
.uri("/api/v1/label/job/values")
.body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = to_bytes(response.into_body(), 1024 * 1024).await.unwrap();
let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(payload["status"], "success");
assert!(payload["data"]
.as_array()
.unwrap()
.iter()
.any(|value| value == "test_job"));
}
#[test]
fn test_persistence_save_load_empty() {
use tempfile::tempdir;
// Create temporary directory
let dir = tempdir().unwrap();
let path = dir.path().join("test.db");
// Create empty storage and save
let storage = QueryableStorage {
series: HashMap::new(),
label_index: HashMap::new(),
};
storage.save_to_file(&path).unwrap();
assert!(path.exists());
// Load back
let loaded = QueryableStorage::load_from_file(&path).unwrap();
assert_eq!(loaded.series.len(), 0);
assert_eq!(loaded.label_index.len(), 0);
}
#[test]
fn test_persistence_save_load_with_data() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("test.db");
// Create storage with data
let mut storage = QueryableStorage {
series: HashMap::new(),
label_index: HashMap::new(),
};
let series1 = TimeSeries {
id: SeriesId(1),
labels: vec![Label::new("__name__", "metric1"), Label::new("job", "test")],
samples: vec![Sample::new(1000, 10.0), Sample::new(2000, 20.0)],
};
let series2 = TimeSeries {
id: SeriesId(2),
labels: vec![Label::new("__name__", "metric2"), Label::new("job", "prod")],
samples: vec![Sample::new(1000, 30.0)],
};
storage.upsert_series(series1.clone());
storage.upsert_series(series2.clone());
// Save to disk
storage.save_to_file(&path).unwrap();
assert!(path.exists());
// Load back
let loaded = QueryableStorage::load_from_file(&path).unwrap();
assert_eq!(loaded.series.len(), 2);
assert_eq!(loaded.label_index.len(), 2); // __name__ and job
// Verify series data
let loaded_series1 = loaded.series.get(&SeriesId(1)).unwrap();
assert_eq!(loaded_series1.labels.len(), 2);
assert_eq!(loaded_series1.samples.len(), 2);
assert_eq!(loaded_series1.samples[0].value, 10.0);
// Verify label index
let job_values = loaded.label_values("job");
assert_eq!(job_values.len(), 2);
assert!(job_values.contains(&"test".to_string()));
assert!(job_values.contains(&"prod".to_string()));
}
#[test]
fn test_persistence_load_nonexistent_file() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("nonexistent.db");
// Loading non-existent file should return empty storage
let loaded = QueryableStorage::load_from_file(&path).unwrap();
assert_eq!(loaded.series.len(), 0);
assert_eq!(loaded.label_index.len(), 0);
}
#[tokio::test]
async fn test_query_service_persistence() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("service_test.db");
// Create service and add some data
let service = QueryService::new();
{
let mut storage = service.storage.write().await;
let series = TimeSeries {
id: SeriesId(42),
labels: vec![Label::new("__name__", "test_metric")],
samples: vec![Sample::new(1000, 99.5)],
};
storage.upsert_series(series);
}
// Save to disk
service.save_to_disk(&path).await.unwrap();
// Create new service loading from disk
let new_service = QueryService::new_with_persistence(&path).unwrap();
let storage = new_service.storage.read().await;
// Verify data was persisted
assert_eq!(storage.series.len(), 1);
let loaded_series = storage.series.get(&SeriesId(42)).unwrap();
assert_eq!(loaded_series.samples[0].value, 99.5);
}
}