use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use parking_lot::RwLock;
use super::HookType;
pub struct PluginMetrics {
plugin_stats: RwLock<HashMap<String, PluginStatsInner>>,
global: GlobalMetrics,
hook_latencies: RwLock<HashMap<HookType, LatencyHistogram>>,
created_at: Instant,
}
impl PluginMetrics {
pub fn new() -> Self {
Self {
plugin_stats: RwLock::new(HashMap::new()),
global: GlobalMetrics::new(),
hook_latencies: RwLock::new(HashMap::new()),
created_at: Instant::now(),
}
}
pub fn record_hook_call(
&self,
plugin_name: &str,
hook: HookType,
latency: Duration,
success: bool,
) {
self.global.total_calls.fetch_add(1, Ordering::Relaxed);
if !success {
self.global.total_errors.fetch_add(1, Ordering::Relaxed);
}
{
let mut stats = self.plugin_stats.write();
let entry = stats
.entry(plugin_name.to_string())
.or_insert_with(PluginStatsInner::new);
entry.total_calls += 1;
if success {
entry.successful_calls += 1;
} else {
entry.failed_calls += 1;
}
entry.total_latency += latency;
if latency > entry.max_latency {
entry.max_latency = latency;
}
if entry.min_latency == Duration::ZERO || latency < entry.min_latency {
entry.min_latency = latency;
}
let hook_entry = entry
.hook_stats
.entry(hook)
.or_insert_with(HookStatsInner::new);
hook_entry.calls += 1;
hook_entry.latency += latency;
if !success {
hook_entry.errors += 1;
}
}
{
let mut histograms = self.hook_latencies.write();
let histogram = histograms
.entry(hook)
.or_insert_with(LatencyHistogram::new);
histogram.record(latency);
}
}
pub fn record_plugin_load(&self, plugin_name: &str) {
self.global.plugins_loaded.fetch_add(1, Ordering::Relaxed);
let mut stats = self.plugin_stats.write();
let entry = stats
.entry(plugin_name.to_string())
.or_insert_with(PluginStatsInner::new);
entry.loaded_at = Some(Instant::now());
}
pub fn record_plugin_unload(&self, plugin_name: &str) {
self.global.plugins_unloaded.fetch_add(1, Ordering::Relaxed);
let mut stats = self.plugin_stats.write();
if let Some(entry) = stats.get_mut(plugin_name) {
entry.unloaded_at = Some(Instant::now());
}
}
pub fn record_plugin_error(&self, plugin_name: &str, _error: &str) {
self.global.total_errors.fetch_add(1, Ordering::Relaxed);
let mut stats = self.plugin_stats.write();
let entry = stats
.entry(plugin_name.to_string())
.or_insert_with(PluginStatsInner::new);
entry.error_count += 1;
}
pub fn get_plugin_stats(&self, plugin_name: &str) -> PluginStats {
let stats = self.plugin_stats.read();
stats
.get(plugin_name)
.map(|s| s.to_public())
.unwrap_or_default()
}
pub fn get_all_stats(&self) -> HashMap<String, PluginStats> {
let stats = self.plugin_stats.read();
stats
.iter()
.map(|(name, s)| (name.clone(), s.to_public()))
.collect()
}
pub fn total_calls(&self) -> u64 {
self.global.total_calls.load(Ordering::Relaxed)
}
pub fn total_errors(&self) -> u64 {
self.global.total_errors.load(Ordering::Relaxed)
}
pub fn avg_latency(&self) -> Duration {
let stats = self.plugin_stats.read();
let mut total_latency = Duration::ZERO;
let mut total_calls = 0u64;
for s in stats.values() {
total_latency += s.total_latency;
total_calls += s.total_calls;
}
if total_calls == 0 {
Duration::ZERO
} else {
total_latency / total_calls as u32
}
}
pub fn get_hook_latency(&self, hook: HookType) -> HookLatency {
let histograms = self.hook_latencies.read();
histograms
.get(&hook)
.map(|h| h.to_latency())
.unwrap_or_default()
}
pub fn uptime(&self) -> Duration {
self.created_at.elapsed()
}
pub fn reset(&self) {
self.global.total_calls.store(0, Ordering::Relaxed);
self.global.total_errors.store(0, Ordering::Relaxed);
self.plugin_stats.write().clear();
self.hook_latencies.write().clear();
}
}
impl Default for PluginMetrics {
fn default() -> Self {
Self::new()
}
}
struct GlobalMetrics {
total_calls: AtomicU64,
total_errors: AtomicU64,
plugins_loaded: AtomicU64,
plugins_unloaded: AtomicU64,
}
impl GlobalMetrics {
fn new() -> Self {
Self {
total_calls: AtomicU64::new(0),
total_errors: AtomicU64::new(0),
plugins_loaded: AtomicU64::new(0),
plugins_unloaded: AtomicU64::new(0),
}
}
}
struct PluginStatsInner {
total_calls: u64,
successful_calls: u64,
failed_calls: u64,
error_count: u64,
total_latency: Duration,
min_latency: Duration,
max_latency: Duration,
hook_stats: HashMap<HookType, HookStatsInner>,
loaded_at: Option<Instant>,
unloaded_at: Option<Instant>,
}
impl PluginStatsInner {
fn new() -> Self {
Self {
total_calls: 0,
successful_calls: 0,
failed_calls: 0,
error_count: 0,
total_latency: Duration::ZERO,
min_latency: Duration::ZERO,
max_latency: Duration::ZERO,
hook_stats: HashMap::new(),
loaded_at: None,
unloaded_at: None,
}
}
fn to_public(&self) -> PluginStats {
PluginStats {
total_calls: self.total_calls,
successful_calls: self.successful_calls,
failed_calls: self.failed_calls,
error_count: self.error_count,
avg_latency: if self.total_calls > 0 {
self.total_latency / self.total_calls as u32
} else {
Duration::ZERO
},
min_latency: self.min_latency,
max_latency: self.max_latency,
uptime: self.loaded_at.map(|t| t.elapsed()),
}
}
}
struct HookStatsInner {
calls: u64,
errors: u64,
latency: Duration,
}
impl HookStatsInner {
fn new() -> Self {
Self {
calls: 0,
errors: 0,
latency: Duration::ZERO,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct PluginStats {
pub total_calls: u64,
pub successful_calls: u64,
pub failed_calls: u64,
pub error_count: u64,
pub avg_latency: Duration,
pub min_latency: Duration,
pub max_latency: Duration,
pub uptime: Option<Duration>,
}
impl PluginStats {
pub fn success_rate(&self) -> f64 {
if self.total_calls == 0 {
1.0
} else {
self.successful_calls as f64 / self.total_calls as f64
}
}
}
#[derive(Debug, Clone, Default)]
pub struct HookLatency {
pub count: u64,
pub avg: Duration,
pub p50: Duration,
pub p90: Duration,
pub p99: Duration,
pub max: Duration,
}
struct LatencyHistogram {
latencies: Vec<Duration>,
max: Duration,
sum: Duration,
}
impl LatencyHistogram {
fn new() -> Self {
Self {
latencies: Vec::new(),
max: Duration::ZERO,
sum: Duration::ZERO,
}
}
fn record(&mut self, latency: Duration) {
self.latencies.push(latency);
self.sum += latency;
if latency > self.max {
self.max = latency;
}
self.latencies.sort();
if self.latencies.len() > 10000 {
self.latencies.drain(0..5000);
}
}
fn percentile(&self, p: f64) -> Duration {
if self.latencies.is_empty() {
return Duration::ZERO;
}
let idx = ((self.latencies.len() as f64) * p / 100.0) as usize;
let idx = idx.min(self.latencies.len() - 1);
self.latencies[idx]
}
fn to_latency(&self) -> HookLatency {
HookLatency {
count: self.latencies.len() as u64,
avg: if self.latencies.is_empty() {
Duration::ZERO
} else {
self.sum / self.latencies.len() as u32
},
p50: self.percentile(50.0),
p90: self.percentile(90.0),
p99: self.percentile(99.0),
max: self.max,
}
}
}
pub struct MetricsExporter {
metrics: std::sync::Arc<PluginMetrics>,
prefix: String,
}
impl MetricsExporter {
pub fn new(metrics: std::sync::Arc<PluginMetrics>, prefix: &str) -> Self {
Self {
metrics,
prefix: prefix.to_string(),
}
}
pub fn export(&self) -> String {
let mut output = String::new();
output.push_str(&format!(
"# HELP {}_total_calls Total hook calls\n",
self.prefix
));
output.push_str(&format!(
"# TYPE {}_total_calls counter\n",
self.prefix
));
output.push_str(&format!(
"{}_total_calls {}\n",
self.prefix,
self.metrics.total_calls()
));
output.push_str(&format!(
"# HELP {}_total_errors Total errors\n",
self.prefix
));
output.push_str(&format!(
"# TYPE {}_total_errors counter\n",
self.prefix
));
output.push_str(&format!(
"{}_total_errors {}\n",
self.prefix,
self.metrics.total_errors()
));
let all_stats = self.metrics.get_all_stats();
for (name, stats) in all_stats {
let name_label = name.replace('-', "_");
output.push_str(&format!(
"{}_plugin_calls{{plugin=\"{}\"}} {}\n",
self.prefix, name_label, stats.total_calls
));
output.push_str(&format!(
"{}_plugin_errors{{plugin=\"{}\"}} {}\n",
self.prefix, name_label, stats.error_count
));
output.push_str(&format!(
"{}_plugin_latency_avg_us{{plugin=\"{}\"}} {}\n",
self.prefix,
name_label,
stats.avg_latency.as_micros()
));
}
output
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plugin_metrics_new() {
let metrics = PluginMetrics::new();
assert_eq!(metrics.total_calls(), 0);
assert_eq!(metrics.total_errors(), 0);
}
#[test]
fn test_record_hook_call() {
let metrics = PluginMetrics::new();
metrics.record_hook_call(
"test-plugin",
HookType::PreQuery,
Duration::from_micros(50),
true,
);
assert_eq!(metrics.total_calls(), 1);
assert_eq!(metrics.total_errors(), 0);
let stats = metrics.get_plugin_stats("test-plugin");
assert_eq!(stats.total_calls, 1);
assert_eq!(stats.successful_calls, 1);
}
#[test]
fn test_record_hook_call_error() {
let metrics = PluginMetrics::new();
metrics.record_hook_call(
"test-plugin",
HookType::PreQuery,
Duration::from_micros(50),
false,
);
assert_eq!(metrics.total_calls(), 1);
assert_eq!(metrics.total_errors(), 1);
let stats = metrics.get_plugin_stats("test-plugin");
assert_eq!(stats.failed_calls, 1);
}
#[test]
fn test_plugin_stats_success_rate() {
let stats = PluginStats {
total_calls: 100,
successful_calls: 90,
failed_calls: 10,
..Default::default()
};
assert!((stats.success_rate() - 0.9).abs() < 0.001);
}
#[test]
fn test_plugin_stats_default() {
let stats = PluginStats::default();
assert_eq!(stats.total_calls, 0);
assert_eq!(stats.success_rate(), 1.0);
}
#[test]
fn test_latency_histogram() {
let mut histogram = LatencyHistogram::new();
for i in 1..=100 {
histogram.record(Duration::from_micros(i));
}
let latency = histogram.to_latency();
assert_eq!(latency.count, 100);
assert!(latency.p50 >= Duration::from_micros(50));
assert!(latency.p99 >= Duration::from_micros(99));
}
#[test]
fn test_get_hook_latency() {
let metrics = PluginMetrics::new();
for i in 1..=10 {
metrics.record_hook_call(
"test",
HookType::PreQuery,
Duration::from_micros(i * 10),
true,
);
}
let latency = metrics.get_hook_latency(HookType::PreQuery);
assert_eq!(latency.count, 10);
assert!(latency.avg > Duration::ZERO);
}
#[test]
fn test_avg_latency() {
let metrics = PluginMetrics::new();
metrics.record_hook_call("p1", HookType::PreQuery, Duration::from_micros(100), true);
metrics.record_hook_call("p1", HookType::PreQuery, Duration::from_micros(200), true);
let avg = metrics.avg_latency();
assert_eq!(avg, Duration::from_micros(150));
}
#[test]
fn test_reset() {
let metrics = PluginMetrics::new();
metrics.record_hook_call("test", HookType::PreQuery, Duration::from_micros(50), true);
assert_eq!(metrics.total_calls(), 1);
metrics.reset();
assert_eq!(metrics.total_calls(), 0);
}
#[test]
fn test_metrics_exporter() {
let metrics = std::sync::Arc::new(PluginMetrics::new());
metrics.record_hook_call("test", HookType::PreQuery, Duration::from_micros(50), true);
let exporter = MetricsExporter::new(metrics, "helios_plugin");
let output = exporter.export();
assert!(output.contains("helios_plugin_total_calls"));
assert!(output.contains("helios_plugin_plugin_calls"));
}
#[test]
fn test_record_plugin_load_unload() {
let metrics = PluginMetrics::new();
metrics.record_plugin_load("test-plugin");
let stats = metrics.get_plugin_stats("test-plugin");
assert!(stats.uptime.is_some());
metrics.record_plugin_unload("test-plugin");
}
}