sunbeam-g2v 0.4.0

Sunbeam Service Framework - A ConnectRPC-based framework for building microservices
//! Service-level metrics for Sunbeam services.

use std::sync::Arc;

use prometheus::{CounterVec, GaugeVec, HistogramVec};

use crate::metrics::registry::MetricsRegistry;

/// Service metrics container.
///
/// This holds all service-level metrics in one place for easy access.
/// All metrics are automatically labeled with service name and method name.
#[derive(Debug, Clone)]
pub struct ServiceMetrics {
    /// The metrics registry.
    registry: Arc<MetricsRegistry>,
    /// Service name.
    service_name: String,
    /// Total request counter (by service, method, status).
    pub requests_total: CounterVec,
    /// Request duration histogram (by service, method).
    pub request_duration: HistogramVec,
    /// Active requests gauge (by service, method).
    pub active_requests: GaugeVec,
    /// Success counter (by service, method).
    pub success_count: CounterVec,
    /// Error counter (by service, method, error_code).
    pub error_count: CounterVec,
    /// Errors total counter (by method, path, error_type) - legacy.
    pub errors_total: CounterVec,
}

impl ServiceMetrics {
    /// Create new service metrics.
    pub fn new(service_name: impl Into<String>) -> Self {
        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"],
            )
            .unwrap();

        let request_duration = registry
            .register_histogram_vec(
                "request_duration_seconds",
                "Request duration in seconds",
                None,
                &["service", "method"],
            )
            .unwrap();

        let active_requests = registry
            .register_gauge_vec(
                "active_requests",
                "Number of currently active requests",
                &["service", "method"],
            )
            .unwrap();

        let success_count = registry
            .register_counter_vec(
                "success_count",
                "Total number of successful requests",
                &["service", "method"],
            )
            .unwrap();

        let error_count = registry
            .register_counter_vec(
                "error_count",
                "Total number of errors",
                &["service", "method", "error_code"],
            )
            .unwrap();

        let errors_total = registry
            .register_counter_vec(
                "errors_total",
                "Total number of errors (legacy)",
                &["method", "path", "error_type"],
            )
            .unwrap();

        Self {
            registry,
            service_name: service_name_str,
            requests_total,
            request_duration,
            active_requests,
            success_count,
            error_count,
            errors_total,
        }
    }

    /// Get the underlying registry.
    pub fn registry(&self) -> &Arc<MetricsRegistry> {
        &self.registry
    }

    /// Get the service name.
    pub fn service_name(&self) -> &str {
        &self.service_name
    }

    /// Increment the request counter.
    pub fn increment_requests(&self, method: &str, _path: &str, _status: &str) {
        self.requests_total
            .with_label_values(&[self.service_name.as_str(), method])
            .inc();
    }

    /// Record request duration.
    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());
    }

    /// Increment active requests.
    pub fn increment_active(&self, method: &str) {
        self.active_requests
            .with_label_values(&[self.service_name.as_str(), method])
            .inc();
    }

    /// Decrement active requests.
    pub fn decrement_active(&self, method: &str) {
        self.active_requests
            .with_label_values(&[self.service_name.as_str(), method])
            .dec();
    }

    /// Increment error counter.
    pub fn increment_errors(&self, method: &str, path: &str, error_type: &str) {
        self.errors_total
            .with_label_values(&[method, path, error_type])
            .inc();
    }

    /// Gather and encode all metrics.
    pub async fn gather_and_encode(&self) -> Result<String, prometheus::Error> {
        self.registry.gather_and_encode().await
    }
}

impl Default for ServiceMetrics {
    fn default() -> Self {
        Self::new("sunbeam-g2v")
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_service_metrics_new() {
        let metrics = ServiceMetrics::new("test-service");
        assert_eq!(metrics.service_name(), "test-service");
    }

    #[test]
    fn test_service_metrics_default() {
        let metrics = ServiceMetrics::default();
        assert_eq!(metrics.service_name(), "sunbeam-g2v");
    }

    #[test]
    fn test_service_metrics_increment_requests() {
        let metrics = ServiceMetrics::new("test-service");
        metrics.increment_requests("GET", "/health", "200");
    }

    #[tokio::test]
    async fn test_service_metrics_gather() {
        let metrics = ServiceMetrics::new("test-service");
        // Increment a metric so it gets registered and appears in the output
        metrics.increment_requests("GET", "/test", "200");
        let encoded = metrics.gather_and_encode().await.unwrap();
        // The metric should be in the output
        assert!(!encoded.is_empty());
    }
}