use std::collections::HashMap;
use std::sync::RwLock;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant, SystemTime};
pub struct ServerMetrics {
request_counts: RwLock<HashMap<String, AtomicU64>>,
error_counts: RwLock<HashMap<String, AtomicU64>>,
request_durations: RwLock<Vec<(String, u64)>>,
active_connections: AtomicU64,
total_connections: AtomicU64,
start_time: Instant,
start_timestamp: u64,
max_duration_entries: usize,
}
impl ServerMetrics {
pub fn new() -> Self {
Self {
request_counts: RwLock::new(HashMap::new()),
error_counts: RwLock::new(HashMap::new()),
request_durations: RwLock::new(Vec::new()),
active_connections: AtomicU64::new(0),
total_connections: AtomicU64::new(0),
start_time: Instant::now(),
start_timestamp: SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
max_duration_entries: 100_000,
}
}
pub fn record_request(&self, request_type: &str) {
let counts = self.request_counts.read().unwrap();
if let Some(counter) = counts.get(request_type) {
counter.fetch_add(1, Ordering::Relaxed);
return;
}
drop(counts);
let mut counts = self.request_counts.write().unwrap();
counts
.entry(request_type.to_string())
.or_insert_with(|| AtomicU64::new(0))
.fetch_add(1, Ordering::Relaxed);
}
pub fn record_error(&self, request_type: &str) {
let counts = self.error_counts.read().unwrap();
if let Some(counter) = counts.get(request_type) {
counter.fetch_add(1, Ordering::Relaxed);
return;
}
drop(counts);
let mut counts = self.error_counts.write().unwrap();
counts
.entry(request_type.to_string())
.or_insert_with(|| AtomicU64::new(0))
.fetch_add(1, Ordering::Relaxed);
}
pub fn record_duration(&self, request_type: &str, duration: Duration) {
let micros = duration.as_micros() as u64;
let mut durations = self.request_durations.write().unwrap();
durations.push((request_type.to_string(), micros));
if durations.len() > self.max_duration_entries {
let keep_from = durations.len() / 2;
durations.drain(..keep_from);
}
}
pub fn connection_opened(&self) {
self.active_connections.fetch_add(1, Ordering::Relaxed);
self.total_connections.fetch_add(1, Ordering::Relaxed);
}
pub fn connection_closed(&self) {
self.active_connections.fetch_sub(1, Ordering::Relaxed);
}
pub fn active_connections(&self) -> u64 {
self.active_connections.load(Ordering::Relaxed)
}
pub fn total_connections(&self) -> u64 {
self.total_connections.load(Ordering::Relaxed)
}
pub fn uptime(&self) -> Duration {
self.start_time.elapsed()
}
pub fn to_prometheus(&self) -> String {
let mut output = String::new();
output.push_str("# HELP astraea_requests_total Total number of requests by type.\n");
output.push_str("# TYPE astraea_requests_total counter\n");
let counts = self.request_counts.read().unwrap();
for (req_type, count) in counts.iter() {
let val = count.load(Ordering::Relaxed);
output.push_str(&format!(
"astraea_requests_total{{type=\"{}\"}} {}\n",
req_type, val
));
}
output.push_str("# HELP astraea_errors_total Total number of errors by type.\n");
output.push_str("# TYPE astraea_errors_total counter\n");
let errors = self.error_counts.read().unwrap();
for (req_type, count) in errors.iter() {
let val = count.load(Ordering::Relaxed);
output.push_str(&format!(
"astraea_errors_total{{type=\"{}\"}} {}\n",
req_type, val
));
}
output.push_str("# HELP astraea_active_connections Current active connections.\n");
output.push_str("# TYPE astraea_active_connections gauge\n");
output.push_str(&format!(
"astraea_active_connections {}\n",
self.active_connections()
));
output.push_str("# HELP astraea_connections_total Total connections since startup.\n");
output.push_str("# TYPE astraea_connections_total counter\n");
output.push_str(&format!(
"astraea_connections_total {}\n",
self.total_connections()
));
output.push_str("# HELP astraea_uptime_seconds Server uptime in seconds.\n");
output.push_str("# TYPE astraea_uptime_seconds gauge\n");
output.push_str(&format!(
"astraea_uptime_seconds {}\n",
self.uptime().as_secs()
));
let durations = self.request_durations.read().unwrap();
if !durations.is_empty() {
output
.push_str("# HELP astraea_request_duration_us Request duration in microseconds.\n");
output.push_str("# TYPE astraea_request_duration_us summary\n");
let mut by_type: HashMap<&str, Vec<u64>> = HashMap::new();
for (t, d) in durations.iter() {
by_type.entry(t.as_str()).or_default().push(*d);
}
for (req_type, mut vals) in by_type {
vals.sort_unstable();
let len = vals.len();
let p50 = vals[len / 2];
let p90 = vals[(len as f64 * 0.9) as usize];
let p99 = vals[((len as f64 * 0.99) as usize).min(len - 1)];
output.push_str(&format!(
"astraea_request_duration_us{{type=\"{}\",quantile=\"0.5\"}} {}\n",
req_type, p50
));
output.push_str(&format!(
"astraea_request_duration_us{{type=\"{}\",quantile=\"0.9\"}} {}\n",
req_type, p90
));
output.push_str(&format!(
"astraea_request_duration_us{{type=\"{}\",quantile=\"0.99\"}} {}\n",
req_type, p99
));
}
}
output
}
pub fn health(&self) -> serde_json::Value {
serde_json::json!({
"status": "healthy",
"uptime_seconds": self.uptime().as_secs(),
"active_connections": self.active_connections(),
"total_connections": self.total_connections(),
"start_time": self.start_timestamp,
})
}
}
impl Default for ServerMetrics {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn record_and_count_requests() {
let metrics = ServerMetrics::new();
metrics.record_request("CreateNode");
metrics.record_request("CreateNode");
metrics.record_request("GetNode");
let prom = metrics.to_prometheus();
assert!(prom.contains("astraea_requests_total{type=\"CreateNode\"} 2"));
assert!(prom.contains("astraea_requests_total{type=\"GetNode\"} 1"));
}
#[test]
fn record_errors() {
let metrics = ServerMetrics::new();
metrics.record_error("CreateNode");
let prom = metrics.to_prometheus();
assert!(prom.contains("astraea_errors_total{type=\"CreateNode\"} 1"));
}
#[test]
fn connection_tracking() {
let metrics = ServerMetrics::new();
assert_eq!(metrics.active_connections(), 0);
metrics.connection_opened();
metrics.connection_opened();
assert_eq!(metrics.active_connections(), 2);
assert_eq!(metrics.total_connections(), 2);
metrics.connection_closed();
assert_eq!(metrics.active_connections(), 1);
assert_eq!(metrics.total_connections(), 2);
}
#[test]
fn duration_recording() {
let metrics = ServerMetrics::new();
for i in 0..100 {
metrics.record_duration("Query", Duration::from_micros(i * 10));
}
let prom = metrics.to_prometheus();
assert!(prom.contains("astraea_request_duration_us{type=\"Query\",quantile=\"0.5\"}"));
assert!(prom.contains("astraea_request_duration_us{type=\"Query\",quantile=\"0.9\"}"));
assert!(prom.contains("astraea_request_duration_us{type=\"Query\",quantile=\"0.99\"}"));
}
#[test]
fn health_check() {
let metrics = ServerMetrics::new();
let health = metrics.health();
assert_eq!(health["status"], "healthy");
assert!(health["uptime_seconds"].as_u64().is_some());
}
#[test]
fn uptime_increases() {
let metrics = ServerMetrics::new();
thread::sleep(Duration::from_millis(10));
assert!(metrics.uptime() >= Duration::from_millis(10));
}
#[test]
fn prometheus_format_valid() {
let metrics = ServerMetrics::new();
metrics.record_request("Ping");
let prom = metrics.to_prometheus();
assert!(prom.contains("# HELP astraea_requests_total"));
assert!(prom.contains("# TYPE astraea_requests_total counter"));
assert!(prom.contains("# HELP astraea_active_connections"));
assert!(prom.contains("# TYPE astraea_active_connections gauge"));
}
}