use anyhow::{Context, Result};
use prometheus::{
register_histogram_vec, register_int_counter_vec, register_int_gauge_vec, HistogramVec,
IntCounterVec, IntGaugeVec,
};
use std::time::Duration;
use crate::ids::Scope;
pub struct ScopedMetrics {
request_duration: HistogramVec,
request_count: IntCounterVec,
active_requests: IntGaugeVec,
upstream_attempts: IntCounterVec,
upstream_failures: IntCounterVec,
rate_limit_hits: IntCounterVec,
circuit_breaker_state: IntGaugeVec,
}
impl ScopedMetrics {
pub fn new() -> Result<Self> {
let latency_buckets = vec![
0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
];
let request_duration = register_histogram_vec!(
"zentinel_scoped_request_duration_seconds",
"Request duration in seconds with scope labels",
&["namespace", "service", "route", "method"],
latency_buckets
)
.context("Failed to register scoped_request_duration metric")?;
let request_count = register_int_counter_vec!(
"zentinel_scoped_requests_total",
"Total number of requests with scope labels",
&["namespace", "service", "route", "method", "status"]
)
.context("Failed to register scoped_requests_total metric")?;
let active_requests = register_int_gauge_vec!(
"zentinel_scoped_active_requests",
"Number of currently active requests by scope",
&["namespace", "service"]
)
.context("Failed to register scoped_active_requests metric")?;
let upstream_attempts = register_int_counter_vec!(
"zentinel_scoped_upstream_attempts_total",
"Total upstream connection attempts with scope labels",
&["namespace", "service", "upstream", "route"]
)
.context("Failed to register scoped_upstream_attempts metric")?;
let upstream_failures = register_int_counter_vec!(
"zentinel_scoped_upstream_failures_total",
"Total upstream connection failures with scope labels",
&["namespace", "service", "upstream", "route", "reason"]
)
.context("Failed to register scoped_upstream_failures metric")?;
let rate_limit_hits = register_int_counter_vec!(
"zentinel_scoped_rate_limit_hits_total",
"Total rate limit hits with scope labels",
&["namespace", "service", "route", "policy"]
)
.context("Failed to register scoped_rate_limit_hits metric")?;
let circuit_breaker_state = register_int_gauge_vec!(
"zentinel_scoped_circuit_breaker_state",
"Circuit breaker state (0=closed, 1=open) with scope labels",
&["namespace", "service", "upstream"]
)
.context("Failed to register scoped_circuit_breaker_state metric")?;
Ok(Self {
request_duration,
request_count,
active_requests,
upstream_attempts,
upstream_failures,
rate_limit_hits,
circuit_breaker_state,
})
}
#[inline]
fn scope_labels(scope: &Scope) -> (&str, &str) {
match scope {
Scope::Global => ("", ""),
Scope::Namespace(ns) => (ns.as_str(), ""),
Scope::Service { namespace, service } => (namespace.as_str(), service.as_str()),
}
}
pub fn record_request(
&self,
route: &str,
method: &str,
status: u16,
duration: Duration,
scope: &Scope,
) {
let (namespace, service) = Self::scope_labels(scope);
self.request_duration
.with_label_values(&[namespace, service, route, method])
.observe(duration.as_secs_f64());
self.request_count
.with_label_values(&[namespace, service, route, method, &status.to_string()])
.inc();
}
pub fn inc_active_requests(&self, scope: &Scope) {
let (namespace, service) = Self::scope_labels(scope);
self.active_requests
.with_label_values(&[namespace, service])
.inc();
}
pub fn dec_active_requests(&self, scope: &Scope) {
let (namespace, service) = Self::scope_labels(scope);
self.active_requests
.with_label_values(&[namespace, service])
.dec();
}
pub fn record_upstream_attempt(&self, upstream: &str, route: &str, scope: &Scope) {
let (namespace, service) = Self::scope_labels(scope);
self.upstream_attempts
.with_label_values(&[namespace, service, upstream, route])
.inc();
}
pub fn record_upstream_failure(
&self,
upstream: &str,
route: &str,
reason: &str,
scope: &Scope,
) {
let (namespace, service) = Self::scope_labels(scope);
self.upstream_failures
.with_label_values(&[namespace, service, upstream, route, reason])
.inc();
}
pub fn record_rate_limit_hit(&self, route: &str, policy: &str, scope: &Scope) {
let (namespace, service) = Self::scope_labels(scope);
self.rate_limit_hits
.with_label_values(&[namespace, service, route, policy])
.inc();
}
pub fn set_circuit_breaker_state(&self, upstream: &str, is_open: bool, scope: &Scope) {
let (namespace, service) = Self::scope_labels(scope);
let state = if is_open { 1 } else { 0 };
self.circuit_breaker_state
.with_label_values(&[namespace, service, upstream])
.set(state);
}
}
#[derive(Debug, Clone)]
pub struct ScopeLabels {
pub namespace: String,
pub service: String,
}
impl ScopeLabels {
pub fn global() -> Self {
Self {
namespace: String::new(),
service: String::new(),
}
}
pub fn from_scope(scope: &Scope) -> Self {
match scope {
Scope::Global => Self::global(),
Scope::Namespace(ns) => Self {
namespace: ns.clone(),
service: String::new(),
},
Scope::Service { namespace, service } => Self {
namespace: namespace.clone(),
service: service.clone(),
},
}
}
pub fn namespace(&self) -> &str {
&self.namespace
}
pub fn service(&self) -> &str {
&self.service
}
pub fn is_global(&self) -> bool {
self.namespace.is_empty() && self.service.is_empty()
}
pub fn is_namespace(&self) -> bool {
!self.namespace.is_empty() && self.service.is_empty()
}
pub fn is_service(&self) -> bool {
!self.namespace.is_empty() && !self.service.is_empty()
}
}
impl Default for ScopeLabels {
fn default() -> Self {
Self::global()
}
}
impl From<&Scope> for ScopeLabels {
fn from(scope: &Scope) -> Self {
Self::from_scope(scope)
}
}
impl From<Scope> for ScopeLabels {
fn from(scope: Scope) -> Self {
Self::from_scope(&scope)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scope_labels_from_global() {
let labels = ScopeLabels::from_scope(&Scope::Global);
assert!(labels.is_global());
assert!(!labels.is_namespace());
assert!(!labels.is_service());
assert_eq!(labels.namespace(), "");
assert_eq!(labels.service(), "");
}
#[test]
fn test_scope_labels_from_namespace() {
let labels = ScopeLabels::from_scope(&Scope::Namespace("api".to_string()));
assert!(!labels.is_global());
assert!(labels.is_namespace());
assert!(!labels.is_service());
assert_eq!(labels.namespace(), "api");
assert_eq!(labels.service(), "");
}
#[test]
fn test_scope_labels_from_service() {
let labels = ScopeLabels::from_scope(&Scope::Service {
namespace: "api".to_string(),
service: "payments".to_string(),
});
assert!(!labels.is_global());
assert!(!labels.is_namespace());
assert!(labels.is_service());
assert_eq!(labels.namespace(), "api");
assert_eq!(labels.service(), "payments");
}
#[test]
fn test_scope_labels_default() {
let labels = ScopeLabels::default();
assert!(labels.is_global());
}
}