use prometheus::{Counter, Histogram, Registry};
#[derive(Debug, Clone)]
pub struct MetricsConfig {
pub prefix: String,
pub detailed_timing: bool,
}
impl Default for MetricsConfig {
fn default() -> Self {
Self {
prefix: "fusabi_plugin".to_string(),
detailed_timing: true,
}
}
}
impl MetricsConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
self.prefix = prefix.into();
self
}
pub fn with_detailed_timing(mut self, enabled: bool) -> Self {
self.detailed_timing = enabled;
self
}
}
pub struct PluginMetrics {
config: MetricsConfig,
registry: Registry,
plugins_loaded: Counter,
plugins_unloaded: Counter,
plugin_errors: Counter,
load_duration: Histogram,
call_duration: Histogram,
}
impl PluginMetrics {
pub fn new(config: MetricsConfig) -> Self {
let registry = Registry::new();
let plugins_loaded = Counter::new(
format!("{}_loaded_total", config.prefix),
"Total number of plugins loaded",
)
.unwrap();
let plugins_unloaded = Counter::new(
format!("{}_unloaded_total", config.prefix),
"Total number of plugins unloaded",
)
.unwrap();
let plugin_errors = Counter::new(
format!("{}_errors_total", config.prefix),
"Total number of plugin errors",
)
.unwrap();
let load_duration = Histogram::with_opts(
prometheus::HistogramOpts::new(
format!("{}_load_duration_seconds", config.prefix),
"Plugin load duration in seconds",
)
.buckets(vec![0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0]),
)
.unwrap();
let call_duration = Histogram::with_opts(
prometheus::HistogramOpts::new(
format!("{}_call_duration_seconds", config.prefix),
"Plugin call duration in seconds",
)
.buckets(vec![0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.5]),
)
.unwrap();
registry.register(Box::new(plugins_loaded.clone())).ok();
registry.register(Box::new(plugins_unloaded.clone())).ok();
registry.register(Box::new(plugin_errors.clone())).ok();
registry.register(Box::new(load_duration.clone())).ok();
registry.register(Box::new(call_duration.clone())).ok();
Self {
config,
registry,
plugins_loaded,
plugins_unloaded,
plugin_errors,
load_duration,
call_duration,
}
}
pub fn config(&self) -> &MetricsConfig {
&self.config
}
pub fn registry(&self) -> &Registry {
&self.registry
}
pub fn record_load(&self, duration_secs: f64) {
self.plugins_loaded.inc();
self.load_duration.observe(duration_secs);
}
pub fn record_unload(&self) {
self.plugins_unloaded.inc();
}
pub fn record_error(&self) {
self.plugin_errors.inc();
}
pub fn record_call(&self, duration_secs: f64) {
self.call_duration.observe(duration_secs);
}
pub fn plugins_loaded_total(&self) -> u64 {
self.plugins_loaded.get() as u64
}
pub fn plugins_unloaded_total(&self) -> u64 {
self.plugins_unloaded.get() as u64
}
pub fn plugin_errors_total(&self) -> u64 {
self.plugin_errors.get() as u64
}
}
impl std::fmt::Debug for PluginMetrics {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PluginMetrics")
.field("config", &self.config)
.field("plugins_loaded", &self.plugins_loaded_total())
.field("plugins_unloaded", &self.plugins_unloaded_total())
.field("plugin_errors", &self.plugin_errors_total())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metrics_config_builder() {
let config = MetricsConfig::new()
.with_prefix("test")
.with_detailed_timing(false);
assert_eq!(config.prefix, "test");
assert!(!config.detailed_timing);
}
#[test]
fn test_metrics_recording() {
let metrics = PluginMetrics::new(MetricsConfig::default());
metrics.record_load(0.1);
metrics.record_load(0.2);
metrics.record_unload();
metrics.record_error();
metrics.record_call(0.01);
assert_eq!(metrics.plugins_loaded_total(), 2);
assert_eq!(metrics.plugins_unloaded_total(), 1);
assert_eq!(metrics.plugin_errors_total(), 1);
}
}