use std::sync::Arc;
use prometheus::{CounterVec, GaugeVec, HistogramVec};
use crate::metrics::registry::MetricsRegistry;
#[derive(Debug, Clone)]
pub struct ServiceMetrics {
registry: Arc<MetricsRegistry>,
service_name: String,
pub requests_total: CounterVec,
pub request_duration: HistogramVec,
pub active_requests: GaugeVec,
pub success_count: CounterVec,
pub error_count: CounterVec,
pub errors_total: CounterVec,
}
impl ServiceMetrics {
pub fn new(service_name: impl Into<String>) -> Result<Self, prometheus::Error> {
let service_name_str = service_name.into();
let registry = Arc::new(MetricsRegistry::new(&service_name_str));
let requests_total = registry.register_counter_vec(
"requests_total",
"Total number of requests",
&["service", "method"],
)?;
let request_duration = registry.register_histogram_vec(
"request_duration_seconds",
"Request duration in seconds",
None,
&["service", "method"],
)?;
let active_requests = registry.register_gauge_vec(
"active_requests",
"Number of currently active requests",
&["service", "method"],
)?;
let success_count = registry.register_counter_vec(
"success_count",
"Total number of successful requests",
&["service", "method"],
)?;
let error_count = registry.register_counter_vec(
"error_count",
"Total number of errors",
&["service", "method", "error_code"],
)?;
let errors_total = registry.register_counter_vec(
"errors_total",
"Total number of errors (legacy)",
&["method", "path", "error_type"],
)?;
Ok(Self {
registry,
service_name: service_name_str,
requests_total,
request_duration,
active_requests,
success_count,
error_count,
errors_total,
})
}
pub fn registry(&self) -> &Arc<MetricsRegistry> {
&self.registry
}
pub fn service_name(&self) -> &str {
&self.service_name
}
pub fn increment_requests(&self, method: &str, _path: &str, _status: &str) {
self.requests_total
.with_label_values(&[self.service_name.as_str(), method])
.inc();
}
pub fn record_duration(&self, method: &str, _path: &str, duration: std::time::Duration) {
self.request_duration
.with_label_values(&[self.service_name.as_str(), method])
.observe(duration.as_secs_f64());
}
pub fn increment_active(&self, method: &str) {
self.active_requests
.with_label_values(&[self.service_name.as_str(), method])
.inc();
}
pub fn decrement_active(&self, method: &str) {
self.active_requests
.with_label_values(&[self.service_name.as_str(), method])
.dec();
}
pub fn increment_errors(&self, method: &str, path: &str, error_type: &str) {
self.errors_total
.with_label_values(&[method, path, error_type])
.inc();
}
pub async fn gather_and_encode(&self) -> Result<String, prometheus::Error> {
self.registry.gather_and_encode().await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_service_metrics_new() {
let metrics = ServiceMetrics::new("test-service").unwrap();
assert_eq!(metrics.service_name(), "test-service");
}
#[test]
fn test_service_metrics_default() {
let metrics = ServiceMetrics::new("sunbeam-g2v").unwrap();
assert_eq!(metrics.service_name(), "sunbeam-g2v");
}
#[test]
fn test_service_metrics_increment_requests() {
let metrics = ServiceMetrics::new("test-service").unwrap();
metrics.increment_requests("GET", "/health", "200");
}
#[tokio::test]
async fn test_service_metrics_gather() {
let metrics = ServiceMetrics::new("test-service").unwrap();
metrics.increment_requests("GET", "/test", "200");
let encoded = metrics.gather_and_encode().await.unwrap();
assert!(!encoded.is_empty());
}
#[tokio::test]
async fn test_service_metrics_all_helpers() {
let metrics = ServiceMetrics::new("coverage-service").unwrap();
assert_eq!(metrics.service_name(), "coverage-service");
assert_eq!(metrics.registry().service_name(), "coverage-service");
metrics.increment_requests("Get", "/ignored", "200");
metrics.record_duration("Get", "/ignored", std::time::Duration::from_millis(25));
metrics.increment_active("Get");
metrics.decrement_active("Get");
metrics
.success_count
.with_label_values(&["coverage-service", "Get"])
.inc();
metrics
.error_count
.with_label_values(&["coverage-service", "Get", "Internal"])
.inc();
metrics.increment_errors("Get", "/rpc", "Internal");
let encoded = metrics.gather_and_encode().await.unwrap();
for expected in [
"requests_total",
"request_duration_seconds",
"active_requests",
"success_count",
"error_count",
"errors_total",
"coverage-service",
"Internal",
] {
assert!(encoded.contains(expected), "missing {expected}");
}
}
}