diff --git a/crates/proxy/src/admin/db.rs b/crates/proxy/src/admin/db.rs index 111f8e6..ef43c2b 100644 --- a/crates/proxy/src/admin/db.rs +++ b/crates/proxy/src/admin/db.rs @@ -373,6 +373,18 @@ pub fn purge_old_logs(conn: &Connection, retention_days: u32) -> rusqlite::Resul Ok(changed) } +/// Count request log entries with a timestamp >= `since_epoch` (Unix seconds). +/// Used to compute requests-per-second for the metrics dashboard. +pub fn count_requests_since(conn: &Connection, since_epoch: u64) -> rusqlite::Result { + let since_iso = epoch_to_iso8601(since_epoch); + let count: i64 = conn.query_row( + "SELECT COUNT(*) FROM request_log WHERE timestamp >= ?1", + rusqlite::params![since_iso], + |row| row.get(0), + )?; + Ok(count.max(0) as u64) +} + /// Spawn the write buffer background task. Returns the sender for proxy handlers. /// Flushes every 100ms or 100 rows, whichever comes first. pub fn spawn_write_buffer(db: Arc>) -> mpsc::Sender { @@ -1052,4 +1064,31 @@ mod tests { query_request_log(&conn, 10, 0, None, None, Some("garbage"), None).unwrap(); assert_eq!(results.len(), 1); } + + #[test] + fn count_requests_since_returns_zero_on_empty_log() { + let conn = in_memory_db(); + let count = count_requests_since(&conn, 0).unwrap(); + assert_eq!(count, 0); + } + + #[test] + fn count_requests_since_counts_recent_entries() { + let conn = in_memory_db(); + + // Insert a recent entry (sample_entry uses current time). + let recent = sample_entry(); + insert_request_log(&conn, &recent).unwrap(); + + // Insert an old entry. + let mut old = sample_entry(); + old.request_id = "old-req".to_string(); + old.timestamp = "2020-01-01T00:00:00Z".to_string(); + insert_request_log(&conn, &old).unwrap(); + + // Count since 2025-01-01 should include only the recent entry. + let since_2025: u64 = 1735689600; // 2025-01-01T00:00:00Z + let count = count_requests_since(&conn, since_2025).unwrap(); + assert_eq!(count, 1); + } } diff --git a/crates/proxy/src/main.rs b/crates/proxy/src/main.rs index 8214aa1..a0f27ce 100644 --- a/crates/proxy/src/main.rs +++ b/crates/proxy/src/main.rs @@ -400,12 +400,28 @@ async fn main() { backends.insert(name.clone(), snap); } let error_rate = aggregate.error_rate(); + // Count requests in the last 60 seconds for RPS. + let rps = { + let db = snapshot_shared.db.clone(); + let now_secs = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + let since = now_secs.saturating_sub(60); + tokio::task::spawn_blocking(move || { + let conn = db.lock().unwrap_or_else(|e| e.into_inner()); + admin::db::count_requests_since(&conn, since).unwrap_or(0) + }) + .await + .unwrap_or(0) as f64 + / 60.0 + }; let snapshot = admin::state::MetricsSnapshotData { backends, latency_p50_ms: None, // Computed on demand by REST endpoint latency_p95_ms: None, latency_p99_ms: None, - requests_per_second: 0.0, // TODO: compute from recent request log + requests_per_second: rps, error_rate, }; let _ = snapshot_shared