use super::{Metric, MetricKey, MetricsError, Result, Session, SqliteConnection};
use diesel::prelude::*;
use std::collections::HashMap;
pub(crate) fn metric_key_for_name<'a>(
db: &'a mut SqliteConnection,
key_name: &str,
) -> Result<MetricKey<'a>> {
use crate::schema::metric_keys::dsl::*;
let query = metric_keys.filter(key.eq(key_name));
let keys = query.load::<MetricKey>(db)?;
keys.into_iter()
.next()
.ok_or_else(|| MetricsError::KeyNotFound(key_name.to_string()))
}
pub(crate) fn session_from_signpost(db: &mut SqliteConnection, metric: &str) -> Result<Session> {
use crate::schema::metrics::dsl::*;
let metric_key = metric_key_for_name(db, metric)?;
let start_query = metrics
.order(timestamp.desc())
.filter(metric_key_id.eq(metric_key.id))
.limit(1);
let start = start_query.first::<Metric>(db)?;
let end_query = metrics.order(timestamp.desc()).limit(1);
let end = end_query.first::<Metric>(db)?;
if end.timestamp <= start.timestamp {
return Err(MetricsError::ZeroLengthSession(metric.to_string()));
}
Ok(Session::new(start.timestamp, end.timestamp))
}
pub(crate) fn metrics_for_key(
db: &mut SqliteConnection,
key_name: &str,
session: Option<&Session>,
) -> Result<Vec<Metric>> {
use crate::schema::metrics::dsl::*;
let metric_key = metric_key_for_name(db, key_name)?;
let query = metrics
.order(timestamp.asc())
.filter(metric_key_id.eq(metric_key.id));
let rows = match session {
Some(session) => query
.filter(timestamp.ge(session.start_time))
.filter(timestamp.le(session.end_time))
.load::<Metric>(db)?,
None => query.load::<Metric>(db)?,
};
Ok(rows)
}
pub(crate) fn average_for_session(
db: &mut SqliteConnection,
key_name: &str,
session: &Session,
) -> Result<f64> {
use crate::schema::metrics::dsl::*;
use diesel::dsl::{avg, count_star};
let metric_key = metric_key_for_name(db, key_name)?;
let (average, count): (Option<f64>, i64) = metrics
.filter(metric_key_id.eq(metric_key.id))
.filter(timestamp.ge(session.start_time))
.filter(timestamp.le(session.end_time))
.select((avg(value), count_star()))
.first(db)?;
if count == 0 {
Err(MetricsError::NoMetricsForKey(key_name.to_string()))
} else {
Ok(average.unwrap_or(0.0))
}
}
pub(crate) fn metrics_summary_for_signpost_and_keys(
db: &mut SqliteConnection,
signpost: &str,
keys: Vec<String>,
) -> Result<HashMap<String, f64>> {
let session = session_from_signpost(db, signpost)?;
let mut results = HashMap::new();
for key in keys {
let value = average_for_session(db, &key, &session)?;
results.insert(key, value);
}
let duration_secs = session.end_time - session.start_time;
results.insert("session.duration".to_string(), duration_secs);
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{MetricKey, NewMetric};
fn setup_test_db() -> (SqliteConnection, tempfile::TempDir) {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("test.db");
let db = crate::setup_db(&db_path).unwrap();
(db, dir)
}
fn insert_key(db: &mut SqliteConnection, name: &str) -> i64 {
let key = MetricKey::key_by_name(name, db).unwrap();
key.id
}
fn insert_metric(db: &mut SqliteConnection, key_id: i64, ts: f64, val: f64) {
use crate::schema::metrics::dsl::*;
diesel::insert_into(metrics)
.values(&NewMetric {
timestamp: ts,
metric_key_id: key_id,
value: val,
})
.execute(db)
.unwrap();
}
fn populate_test_db(db: &mut SqliteConnection) {
let signpost_id = insert_key(db, "app.started");
let cpu_id = insert_key(db, "cpu");
insert_metric(db, signpost_id, 100.0, 1.0);
insert_metric(db, cpu_id, 100.0, 10.0);
insert_metric(db, cpu_id, 110.0, 20.0);
insert_metric(db, signpost_id, 500.0, 1.0);
insert_metric(db, cpu_id, 500.0, 50.0);
insert_metric(db, cpu_id, 510.0, 60.0);
insert_metric(db, cpu_id, 520.0, 70.0);
}
#[test]
fn test_session_from_signpost_uses_latest_signpost() {
let (mut db, _dir) = setup_test_db();
populate_test_db(&mut db);
let session = session_from_signpost(&mut db, "app.started").unwrap();
assert_eq!(
session.start_time, 500.0,
"start should be the most recent signpost (t=500), not the stale one (t=100)"
);
assert_eq!(session.end_time, 520.0);
}
#[test]
fn test_summary_duration_not_inflated_by_stale_signpost() {
let (mut db, _dir) = setup_test_db();
populate_test_db(&mut db);
let results =
metrics_summary_for_signpost_and_keys(&mut db, "app.started", vec!["cpu".to_string()])
.unwrap();
assert!(
(results["cpu"] - 60.0).abs() < f64::EPSILON,
"cpu average should only include current session metrics"
);
assert_eq!(
results["session.duration"], 20.0,
"duration should be 20s (t=500..520), not inflated to 420s by stale signpost"
);
}
#[test]
fn test_average_for_session_with_explicit_range() {
let (mut db, _dir) = setup_test_db();
populate_test_db(&mut db);
let session = Session::new(500.0, 510.0);
let avg = average_for_session(&mut db, "cpu", &session).unwrap();
assert!((avg - 55.0).abs() < f64::EPSILON);
}
}