use lazy_static::lazy_static;
use prometheus::{
register_counter_vec, register_gauge, register_histogram_vec, CounterVec, Gauge, HistogramVec,
Registry,
};
use std::sync::Arc;
lazy_static! {
static ref METRICS_REGISTRY: Registry = Registry::new();
static ref SEARCH_METRICS: Arc<SearchMetrics> = Arc::new(
SearchMetrics::new(&METRICS_REGISTRY)
.expect("Failed to initialize search metrics")
);
}
pub fn get_metrics() -> Arc<SearchMetrics> {
SEARCH_METRICS.clone()
}
pub fn get_registry() -> &'static Registry {
&METRICS_REGISTRY
}
pub struct SearchMetrics {
query_latency: HistogramVec,
fusion_time: HistogramVec,
cache_hit_rate: Gauge,
result_count: HistogramVec,
errors_total: CounterVec,
queries_total: CounterVec,
}
impl SearchMetrics {
pub fn new(registry: &Registry) -> Result<Self, prometheus::Error> {
let query_latency = register_histogram_vec!(
prometheus::HistogramOpts::new(
"maproom_search_query_latency_seconds",
"End-to-end search query latency in seconds"
)
.buckets(vec![
0.001, 0.005, 0.010, 0.025, 0.050, 0.075, 0.100, 0.250, 0.500, 1.0, 2.5, 5.0
]),
&["mode", "status"]
)?;
registry.register(Box::new(query_latency.clone()))?;
let fusion_time = register_histogram_vec!(
prometheus::HistogramOpts::new(
"maproom_search_fusion_time_seconds",
"Score fusion computation time in seconds"
)
.buckets(vec![0.0005, 0.001, 0.002, 0.005, 0.010, 0.025, 0.050]),
&["strategy"]
)?;
registry.register(Box::new(fusion_time.clone()))?;
let cache_hit_rate = register_gauge!(
"maproom_search_cache_hit_rate",
"Cache effectiveness as a ratio of hits to total requests (0.0-1.0)"
)?;
registry.register(Box::new(cache_hit_rate.clone()))?;
let result_count = register_histogram_vec!(
prometheus::HistogramOpts::new(
"maproom_search_result_count",
"Number of results returned per search query"
)
.buckets(vec![0.0, 1.0, 5.0, 10.0, 20.0, 50.0, 100.0]),
&["mode"]
)?;
registry.register(Box::new(result_count.clone()))?;
let errors_total = register_counter_vec!(
"maproom_search_errors_total",
"Total number of search errors by type",
&["error_type"]
)?;
registry.register(Box::new(errors_total.clone()))?;
let queries_total = register_counter_vec!(
"maproom_search_queries_total",
"Total number of search queries executed",
&["mode", "status"]
)?;
registry.register(Box::new(queries_total.clone()))?;
Ok(Self {
query_latency,
fusion_time,
cache_hit_rate,
result_count,
errors_total,
queries_total,
})
}
pub fn record_query_latency(&self, duration_seconds: f64, mode: &str, success: bool) {
let status = if success { "success" } else { "error" };
self.query_latency
.with_label_values(&[mode, status])
.observe(duration_seconds);
}
pub fn record_fusion_time(&self, duration_seconds: f64, strategy: &str) {
self.fusion_time
.with_label_values(&[strategy])
.observe(duration_seconds);
}
pub fn update_cache_hit_rate(&self, hit_rate: f64) {
self.cache_hit_rate.set(hit_rate);
}
pub fn record_result_count(&self, count: usize, mode: &str) {
self.result_count
.with_label_values(&[mode])
.observe(count as f64);
}
pub fn record_error(&self, error_type: &str) {
self.errors_total.with_label_values(&[error_type]).inc();
}
pub fn increment_queries(&self, mode: &str, success: bool) {
let status = if success { "success" } else { "error" };
self.queries_total.with_label_values(&[mode, status]).inc();
}
pub fn update_cache_hit_rate_from_stats(&self, cache_stats: &crate::search::CacheStats) {
let hit_rate = cache_stats.hit_rate();
self.update_cache_hit_rate(hit_rate);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metrics_recording() {
let metrics = get_metrics();
metrics.record_query_latency(0.025, "code_test", true);
metrics.record_fusion_time(0.003, "basic_weighted");
metrics.update_cache_hit_rate(0.75);
metrics.record_result_count(10, "text_test");
metrics.record_error("query_processing");
metrics.increment_queries("auto_test", true);
}
#[test]
fn test_global_metrics_instance() {
let metrics1 = get_metrics();
let metrics2 = get_metrics();
assert!(Arc::ptr_eq(&metrics1, &metrics2));
}
#[test]
fn test_cache_hit_rate_bounds() {
let metrics = get_metrics();
metrics.update_cache_hit_rate(0.0);
metrics.update_cache_hit_rate(0.5);
metrics.update_cache_hit_rate(1.0);
}
#[test]
fn test_error_types() {
let metrics = get_metrics();
metrics.record_error("query_processing_test");
metrics.record_error("search_execution_test");
metrics.record_error("fusion_test");
metrics.record_error("database_test");
}
#[test]
fn test_search_modes() {
let metrics = get_metrics();
metrics.record_query_latency(0.025, "code_mode_test", true);
metrics.record_query_latency(0.030, "text_mode_test", true);
metrics.record_query_latency(0.028, "auto_mode_test", true);
metrics.record_query_latency(0.100, "code_mode_error_test", false);
}
#[test]
fn test_result_count_histogram() {
let metrics = get_metrics();
metrics.record_result_count(0, "code_hist_test");
metrics.record_result_count(1, "text_hist_test");
metrics.record_result_count(10, "auto_hist_test");
metrics.record_result_count(50, "code_hist_test2");
metrics.record_result_count(100, "text_hist_test2");
}
}