use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime};
#[derive(Clone)]
pub struct MetricsCollector {
request_counts: Arc<dashmap::DashMap<String, AtomicU64>>,
error_counts: Arc<dashmap::DashMap<String, AtomicU64>>,
latency_sums: Arc<dashmap::DashMap<String, AtomicU64>>,
start_time: Arc<Instant>,
}
impl MetricsCollector {
pub fn new() -> Self {
Self {
request_counts: Arc::new(dashmap::DashMap::new()),
error_counts: Arc::new(dashmap::DashMap::new()),
latency_sums: Arc::new(dashmap::DashMap::new()),
start_time: Arc::new(Instant::now()),
}
}
pub fn record_request(&self, tool: &str, latency: Duration, success: bool) {
self.request_counts
.entry(tool.to_string())
.or_insert_with(|| AtomicU64::new(0))
.fetch_add(1, Ordering::Relaxed);
let micros = latency.as_micros() as u64;
self.latency_sums
.entry(tool.to_string())
.or_insert_with(|| AtomicU64::new(0))
.fetch_add(micros, Ordering::Relaxed);
if !success {
self.error_counts
.entry(tool.to_string())
.or_insert_with(|| AtomicU64::new(0))
.fetch_add(1, Ordering::Relaxed);
}
}
pub fn get_request_count(&self, tool: &str) -> u64 {
self.request_counts
.get(tool)
.map(|v| v.load(Ordering::Relaxed))
.unwrap_or(0)
}
pub fn get_error_count(&self, tool: &str) -> u64 {
self.error_counts
.get(tool)
.map(|v| v.load(Ordering::Relaxed))
.unwrap_or(0)
}
pub fn get_avg_latency_micros(&self, tool: &str) -> Option<f64> {
let count = self.get_request_count(tool);
if count == 0 {
return None;
}
let sum = self
.latency_sums
.get(tool)
.map(|v| v.load(Ordering::Relaxed))
.unwrap_or(0);
Some(sum as f64 / count as f64)
}
pub fn get_error_rate(&self, tool: &str) -> f64 {
let total = self.get_request_count(tool);
if total == 0 {
return 0.0;
}
let errors = self.get_error_count(tool);
errors as f64 / total as f64
}
pub fn uptime_seconds(&self) -> u64 {
self.start_time.elapsed().as_secs()
}
pub fn export_prometheus(&self) -> String {
let mut output = String::new();
output.push_str("# HELP pforge_requests_total Total number of requests\n");
output.push_str("# TYPE pforge_requests_total counter\n");
for entry in self.request_counts.iter() {
let count = entry.value().load(Ordering::Relaxed);
output.push_str(&format!(
"pforge_requests_total{{tool=\"{}\"}} {}\n",
entry.key(),
count
));
}
output.push_str("# HELP pforge_errors_total Total number of errors\n");
output.push_str("# TYPE pforge_errors_total counter\n");
for entry in self.error_counts.iter() {
let count = entry.value().load(Ordering::Relaxed);
output.push_str(&format!(
"pforge_errors_total{{tool=\"{}\"}} {}\n",
entry.key(),
count
));
}
output.push_str("# HELP pforge_latency_microseconds_sum Sum of request latencies\n");
output.push_str("# TYPE pforge_latency_microseconds_sum counter\n");
for entry in self.latency_sums.iter() {
let sum = entry.value().load(Ordering::Relaxed);
output.push_str(&format!(
"pforge_latency_microseconds_sum{{tool=\"{}\"}} {}\n",
entry.key(),
sum
));
}
output.push_str("# HELP pforge_uptime_seconds Server uptime in seconds\n");
output.push_str("# TYPE pforge_uptime_seconds gauge\n");
output.push_str(&format!(
"pforge_uptime_seconds {}\n",
self.uptime_seconds()
));
output
}
pub fn export_json(&self) -> serde_json::Value {
let mut tools = serde_json::Map::new();
for entry in self.request_counts.iter() {
let tool = entry.key();
let requests = entry.value().load(Ordering::Relaxed);
let errors = self.get_error_count(tool);
let avg_latency = self.get_avg_latency_micros(tool);
let mut tool_data = serde_json::Map::new();
tool_data.insert("requests".to_string(), serde_json::json!(requests));
tool_data.insert("errors".to_string(), serde_json::json!(errors));
tool_data.insert(
"error_rate".to_string(),
serde_json::json!(self.get_error_rate(tool)),
);
if let Some(latency) = avg_latency {
tool_data.insert("avg_latency_micros".to_string(), serde_json::json!(latency));
}
tools.insert(tool.clone(), serde_json::Value::Object(tool_data));
}
serde_json::json!({
"uptime_seconds": self.uptime_seconds(),
"tools": tools
})
}
}
impl Default for MetricsCollector {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HealthStatus {
Healthy,
Degraded,
Unhealthy,
}
impl HealthStatus {
pub fn is_healthy(&self) -> bool {
matches!(self, HealthStatus::Healthy)
}
pub fn http_status(&self) -> u16 {
match self {
HealthStatus::Healthy => 200,
HealthStatus::Degraded => 200,
HealthStatus::Unhealthy => 503,
}
}
}
#[derive(Debug, Clone)]
pub struct ComponentHealth {
pub name: String,
pub status: HealthStatus,
pub message: Option<String>,
pub timestamp: SystemTime,
}
#[derive(Clone)]
pub struct HealthCheck {
components: Arc<dashmap::DashMap<String, ComponentHealth>>,
start_time: Arc<SystemTime>,
}
impl HealthCheck {
pub fn new() -> Self {
Self {
components: Arc::new(dashmap::DashMap::new()),
start_time: Arc::new(SystemTime::now()),
}
}
pub fn register_component(&self, name: impl Into<String>, status: HealthStatus) {
self.register_component_with_message(name, status, None);
}
pub fn register_component_with_message(
&self,
name: impl Into<String>,
status: HealthStatus,
message: Option<String>,
) {
let name = name.into();
self.components.insert(
name.clone(),
ComponentHealth {
name,
status,
message,
timestamp: SystemTime::now(),
},
);
}
pub fn get_status(&self) -> HealthStatus {
if self.components.is_empty() {
return HealthStatus::Healthy;
}
let mut has_degraded = false;
for component in self.components.iter() {
match component.status {
HealthStatus::Unhealthy => return HealthStatus::Unhealthy,
HealthStatus::Degraded => has_degraded = true,
HealthStatus::Healthy => {}
}
}
if has_degraded {
HealthStatus::Degraded
} else {
HealthStatus::Healthy
}
}
pub fn is_healthy(&self) -> bool {
self.get_status().is_healthy()
}
pub fn export_json(&self) -> serde_json::Value {
let overall_status = self.get_status();
let mut components = Vec::new();
for entry in self.components.iter() {
let health = entry.value();
components.push(serde_json::json!({
"name": health.name,
"status": format!("{:?}", health.status),
"message": health.message,
"timestamp": health.timestamp
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}));
}
serde_json::json!({
"status": format!("{:?}", overall_status),
"uptime_seconds": SystemTime::now()
.duration_since(*self.start_time)
.unwrap_or_default()
.as_secs(),
"components": components
})
}
pub fn get_component(&self, name: &str) -> Option<ComponentHealth> {
self.components.get(name).map(|c| c.clone())
}
pub fn remove_component(&self, name: &str) {
self.components.remove(name);
}
}
impl Default for HealthCheck {
fn default() -> Self {
Self::new()
}
}
pub struct TelemetryMiddleware {
collector: MetricsCollector,
}
impl TelemetryMiddleware {
pub fn new(collector: MetricsCollector) -> Self {
Self { collector }
}
pub fn collector(&self) -> &MetricsCollector {
&self.collector
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metrics_collector() {
let collector = MetricsCollector::new();
collector.record_request("greet", Duration::from_micros(100), true);
assert_eq!(collector.get_request_count("greet"), 1);
assert_eq!(collector.get_error_count("greet"), 0);
assert_eq!(collector.get_avg_latency_micros("greet"), Some(100.0));
collector.record_request("greet", Duration::from_micros(200), false);
assert_eq!(collector.get_request_count("greet"), 2);
assert_eq!(collector.get_error_count("greet"), 1);
assert_eq!(collector.get_avg_latency_micros("greet"), Some(150.0));
assert_eq!(collector.get_error_rate("greet"), 0.5);
}
#[test]
fn test_prometheus_export() {
let collector = MetricsCollector::new();
collector.record_request("greet", Duration::from_micros(100), true);
let output = collector.export_prometheus();
assert!(output.contains("pforge_requests_total"));
assert!(output.contains("pforge_errors_total"));
assert!(output.contains("pforge_latency_microseconds_sum"));
assert!(output.contains("pforge_uptime_seconds"));
}
#[test]
fn test_json_export() {
let collector = MetricsCollector::new();
collector.record_request("greet", Duration::from_micros(100), true);
let json = collector.export_json();
assert!(json["uptime_seconds"].is_u64());
assert!(json["tools"]["greet"]["requests"].is_u64());
assert_eq!(json["tools"]["greet"]["requests"], 1);
}
#[test]
fn test_health_check() {
let health = HealthCheck::new();
assert!(health.is_healthy());
health.register_component("database", HealthStatus::Healthy);
assert_eq!(health.get_status(), HealthStatus::Healthy);
health.register_component("cache", HealthStatus::Degraded);
assert_eq!(health.get_status(), HealthStatus::Degraded);
health.register_component("storage", HealthStatus::Unhealthy);
assert_eq!(health.get_status(), HealthStatus::Unhealthy);
}
#[test]
fn test_health_json_export() {
let health = HealthCheck::new();
health.register_component_with_message(
"service",
HealthStatus::Healthy,
Some("All systems operational".to_string()),
);
let json = health.export_json();
assert_eq!(json["status"], "Healthy");
assert!(json["uptime_seconds"].is_u64());
assert_eq!(json["components"].as_array().unwrap().len(), 1);
}
#[test]
fn test_component_management() {
let health = HealthCheck::new();
health.register_component("test", HealthStatus::Healthy);
let component = health.get_component("test");
assert!(component.is_some());
assert_eq!(component.unwrap().name, "test");
health.remove_component("test");
assert!(health.get_component("test").is_none());
}
}