use prometheus::{Gauge, Histogram, HistogramVec, IntCounter, IntCounterVec, IntGauge, Registry};
use std::time::Duration;
pub struct MetricsCollector {
registry: Registry,
request_counter: IntCounterVec,
request_duration: HistogramVec,
error_counter: IntCounterVec,
active_sessions: IntGauge,
session_creation_counter: IntCounter,
session_deletion_counter: IntCounter,
simulation_steps_counter: IntCounterVec,
simulation_duration: Histogram,
cache_hit_counter: IntCounter,
cache_miss_counter: IntCounter,
memory_usage: Gauge,
mongodb_insert_counter: IntCounterVec, mongodb_insert_duration: HistogramVec, }
impl MetricsCollector {
pub fn new() -> Result<Self, prometheus::Error> {
let registry = Registry::new();
let request_counter = IntCounterVec::new(
prometheus::opts!("api_requests_total", "Total number of API requests"),
&["endpoint", "method", "status"],
)?;
let request_duration = HistogramVec::new(
prometheus::histogram_opts!(
"api_request_duration_seconds",
"HTTP request duration in seconds",
vec![0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0]
),
&["endpoint", "method"],
)?;
let error_counter = IntCounterVec::new(
prometheus::opts!("api_errors_total", "Total number of API errors"),
&["endpoint", "error_type"],
)?;
let active_sessions = IntGauge::new(
"active_sessions",
"Number of currently active simulation sessions",
)?;
let session_creation_counter = IntCounter::new(
"session_creations_total",
"Total number of sessions created",
)?;
let session_deletion_counter = IntCounter::new(
"session_deletions_total",
"Total number of sessions deleted",
)?;
let simulation_steps_counter = IntCounterVec::new(
prometheus::opts!(
"simulation_steps_total",
"Total number of simulation steps processed"
),
&["method"],
)?;
let simulation_duration = Histogram::with_opts(prometheus::histogram_opts!(
"simulation_step_duration_seconds",
"Simulation step processing time in seconds",
vec![0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0]
))?;
let cache_hit_counter = IntCounter::new("cache_hits_total", "Total number of cache hits")?;
let cache_miss_counter =
IntCounter::new("cache_misses_total", "Total number of cache misses")?;
let memory_usage = Gauge::new("memory_usage_bytes", "Current memory usage in bytes")?;
let mongodb_insert_counter = IntCounterVec::new(
prometheus::opts!(
"mongodb_inserts_total",
"Total number of MongoDB insert operations"
),
&["collection"],
)?;
let mongodb_insert_duration = HistogramVec::new(
prometheus::histogram_opts!(
"mongodb_insert_duration_seconds",
"MongoDB insert operation duration in seconds",
vec![0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0]
),
&["collection"],
)?;
registry.register(Box::new(request_counter.clone()))?;
registry.register(Box::new(request_duration.clone()))?;
registry.register(Box::new(error_counter.clone()))?;
registry.register(Box::new(active_sessions.clone()))?;
registry.register(Box::new(session_creation_counter.clone()))?;
registry.register(Box::new(session_deletion_counter.clone()))?;
registry.register(Box::new(simulation_steps_counter.clone()))?;
registry.register(Box::new(simulation_duration.clone()))?;
registry.register(Box::new(cache_hit_counter.clone()))?;
registry.register(Box::new(cache_miss_counter.clone()))?;
registry.register(Box::new(memory_usage.clone()))?;
registry.register(Box::new(mongodb_insert_counter.clone()))?;
registry.register(Box::new(mongodb_insert_duration.clone()))?;
Ok(Self {
registry,
request_counter,
request_duration,
error_counter,
active_sessions,
session_creation_counter,
session_deletion_counter,
simulation_steps_counter,
simulation_duration,
cache_hit_counter,
cache_miss_counter,
memory_usage,
mongodb_insert_counter,
mongodb_insert_duration,
})
}
pub fn record_request(&self, endpoint: &str, method: &str, status: &str) {
self.request_counter
.with_label_values(&[endpoint, method, status])
.inc();
}
pub fn record_request_duration(&self, endpoint: &str, method: &str, duration: Duration) {
self.request_duration
.with_label_values(&[endpoint, method])
.observe(duration.as_secs_f64());
}
pub fn record_error(&self, endpoint: &str, error_type: &str) {
self.error_counter
.with_label_values(&[endpoint, error_type])
.inc();
}
pub fn increment_active_sessions(&self) {
self.active_sessions.inc();
self.session_creation_counter.inc();
}
pub fn decrement_active_sessions(&self) {
self.active_sessions.dec();
self.session_deletion_counter.inc();
}
pub fn record_simulation_step(&self, method: &str) {
self.simulation_steps_counter
.with_label_values(&[method])
.inc();
}
pub fn record_simulation_duration(&self, duration: Duration) {
self.simulation_duration.observe(duration.as_secs_f64());
}
pub fn record_cache_hit(&self) {
self.cache_hit_counter.inc();
}
pub fn record_cache_miss(&self) {
self.cache_miss_counter.inc();
}
pub fn record_memory_usage(&self, bytes: f64) {
self.memory_usage.set(bytes);
}
pub fn record_mongodb_insert(&self, collection: &str) {
self.mongodb_insert_counter
.with_label_values(&[collection])
.inc();
}
pub fn record_mongodb_insert_duration(&self, collection: &str, duration: Duration) {
self.mongodb_insert_duration
.with_label_values(&[collection])
.observe(duration.as_secs_f64());
}
pub fn registry(&self) -> &Registry {
&self.registry
}
pub fn export_metrics(&self) -> String {
use prometheus::Encoder;
let encoder = prometheus::TextEncoder::new();
let mut buffer = Vec::new();
encoder
.encode(&self.registry.gather(), &mut buffer)
.expect("Failed to encode metrics");
String::from_utf8(buffer).unwrap_or_else(|_| "Failed to export metrics".to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_new_metrics_collector() {
let collector = MetricsCollector::new();
assert!(collector.is_ok(), "Failed to create MetricsCollector");
}
#[test]
fn test_record_request() {
let collector = MetricsCollector::new().expect("Failed to create MetricsCollector");
collector.record_request("/api/v1/chain", "GET", "200");
let metrics = collector.export_metrics();
assert!(metrics.contains(
"api_requests_total{endpoint=\"/api/v1/chain\",method=\"GET\",status=\"200\"} 1"
));
}
#[test]
fn test_record_multiple_requests() {
let collector = MetricsCollector::new().expect("Failed to create MetricsCollector");
collector.record_request("/api/v1/chain", "GET", "200");
collector.record_request("/api/v1/chain", "GET", "200");
collector.record_request("/api/v1/chain", "POST", "201");
let metrics = collector.export_metrics();
assert!(metrics.contains(
"api_requests_total{endpoint=\"/api/v1/chain\",method=\"GET\",status=\"200\"} 2"
));
assert!(metrics.contains(
"api_requests_total{endpoint=\"/api/v1/chain\",method=\"POST\",status=\"201\"} 1"
));
}
#[test]
fn test_record_request_duration() {
let collector = MetricsCollector::new().expect("Failed to create MetricsCollector");
collector.record_request_duration("/api/v1/chain", "GET", Duration::from_millis(100));
let metrics = collector.export_metrics();
assert!(metrics.contains("api_request_duration_seconds_bucket{endpoint=\"/api/v1/chain\",method=\"GET\",le=\"0.1\"} 1"));
assert!(metrics.contains(
"api_request_duration_seconds_count{endpoint=\"/api/v1/chain\",method=\"GET\"} 1"
));
}
#[test]
fn test_record_error() {
let collector = MetricsCollector::new().expect("Failed to create MetricsCollector");
collector.record_error("/api/v1/chain", "not_found");
let metrics = collector.export_metrics();
assert!(
metrics.contains(
"api_errors_total{endpoint=\"/api/v1/chain\",error_type=\"not_found\"} 1"
)
);
}
#[test]
fn test_active_sessions() {
let collector = MetricsCollector::new().expect("Failed to create MetricsCollector");
collector.increment_active_sessions();
collector.increment_active_sessions();
collector.decrement_active_sessions();
let metrics = collector.export_metrics();
assert!(metrics.contains("active_sessions 1"));
assert!(metrics.contains("session_creations_total 2"));
assert!(metrics.contains("session_deletions_total 1"));
}
#[test]
fn test_simulation_metrics() {
let collector = MetricsCollector::new().expect("Failed to create MetricsCollector");
collector.record_simulation_step("GeometricBrownian");
collector.record_simulation_step("GeometricBrownian");
collector.record_simulation_duration(Duration::from_millis(50));
let metrics = collector.export_metrics();
assert!(metrics.contains("simulation_steps_total{method=\"GeometricBrownian\"} 2"));
assert!(metrics.contains("simulation_step_duration_seconds_bucket{le=\"0.05\"} 1"));
assert!(metrics.contains("simulation_step_duration_seconds_count 1"));
}
#[test]
fn test_cache_metrics() {
let collector = MetricsCollector::new().expect("Failed to create MetricsCollector");
collector.record_cache_hit();
collector.record_cache_hit();
collector.record_cache_miss();
let metrics = collector.export_metrics();
assert!(metrics.contains("cache_hits_total 2"));
assert!(metrics.contains("cache_misses_total 1"));
}
#[test]
fn test_memory_usage() {
let collector = MetricsCollector::new().expect("Failed to create MetricsCollector");
collector.record_memory_usage(1024.0 * 1024.0);
let metrics = collector.export_metrics();
assert!(metrics.contains("memory_usage_bytes 1048576"));
}
#[test]
fn test_mongodb_metrics() {
let collector = MetricsCollector::new().expect("Failed to create MetricsCollector");
collector.record_mongodb_insert("sessions");
collector.record_mongodb_insert("sessions");
collector.record_mongodb_insert("chains");
collector.record_mongodb_insert_duration("sessions", Duration::from_millis(30));
let metrics = collector.export_metrics();
assert!(metrics.contains("mongodb_inserts_total{collection=\"sessions\"} 2"));
assert!(metrics.contains("mongodb_inserts_total{collection=\"chains\"} 1"));
assert!(metrics.contains(
"mongodb_insert_duration_seconds_bucket{collection=\"sessions\",le=\"0.05\"} 1"
));
assert!(
metrics.contains("mongodb_insert_duration_seconds_count{collection=\"sessions\"} 1")
);
}
#[test]
fn test_registry() {
let collector = MetricsCollector::new().expect("Failed to create MetricsCollector");
let registry = collector.registry();
assert!(
!registry.gather().is_empty(),
"Registry should contain metrics"
);
}
#[test]
fn test_export_metrics_format() {
let collector = MetricsCollector::new().expect("Failed to create MetricsCollector");
collector.record_request("/api/v1/chain", "GET", "200");
collector.increment_active_sessions();
let metrics = collector.export_metrics();
assert!(
metrics.contains("# HELP"),
"Metrics should contain HELP comments"
);
assert!(
metrics.contains("# TYPE"),
"Metrics should contain TYPE comments"
);
assert!(
metrics.contains("api_requests_total"),
"Metrics should contain request counter"
);
assert!(
metrics.contains("active_sessions"),
"Metrics should contain active sessions gauge"
);
}
}