1use anyhow::{Context, Result};
28use prometheus::{
29 register_histogram_vec, register_int_counter_vec, register_int_gauge_vec, HistogramVec,
30 IntCounterVec, IntGaugeVec,
31};
32use std::time::Duration;
33
34use crate::ids::Scope;
35
36pub struct ScopedMetrics {
41 request_duration: HistogramVec,
43
44 request_count: IntCounterVec,
46
47 active_requests: IntGaugeVec,
49
50 upstream_attempts: IntCounterVec,
52
53 upstream_failures: IntCounterVec,
55
56 rate_limit_hits: IntCounterVec,
58
59 circuit_breaker_state: IntGaugeVec,
61}
62
63impl ScopedMetrics {
64 pub fn new() -> Result<Self> {
66 let latency_buckets = vec![
68 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
69 ];
70
71 let request_duration = register_histogram_vec!(
72 "sentinel_scoped_request_duration_seconds",
73 "Request duration in seconds with scope labels",
74 &["namespace", "service", "route", "method"],
75 latency_buckets
76 )
77 .context("Failed to register scoped_request_duration metric")?;
78
79 let request_count = register_int_counter_vec!(
80 "sentinel_scoped_requests_total",
81 "Total number of requests with scope labels",
82 &["namespace", "service", "route", "method", "status"]
83 )
84 .context("Failed to register scoped_requests_total metric")?;
85
86 let active_requests = register_int_gauge_vec!(
87 "sentinel_scoped_active_requests",
88 "Number of currently active requests by scope",
89 &["namespace", "service"]
90 )
91 .context("Failed to register scoped_active_requests metric")?;
92
93 let upstream_attempts = register_int_counter_vec!(
94 "sentinel_scoped_upstream_attempts_total",
95 "Total upstream connection attempts with scope labels",
96 &["namespace", "service", "upstream", "route"]
97 )
98 .context("Failed to register scoped_upstream_attempts metric")?;
99
100 let upstream_failures = register_int_counter_vec!(
101 "sentinel_scoped_upstream_failures_total",
102 "Total upstream connection failures with scope labels",
103 &["namespace", "service", "upstream", "route", "reason"]
104 )
105 .context("Failed to register scoped_upstream_failures metric")?;
106
107 let rate_limit_hits = register_int_counter_vec!(
108 "sentinel_scoped_rate_limit_hits_total",
109 "Total rate limit hits with scope labels",
110 &["namespace", "service", "route", "policy"]
111 )
112 .context("Failed to register scoped_rate_limit_hits metric")?;
113
114 let circuit_breaker_state = register_int_gauge_vec!(
115 "sentinel_scoped_circuit_breaker_state",
116 "Circuit breaker state (0=closed, 1=open) with scope labels",
117 &["namespace", "service", "upstream"]
118 )
119 .context("Failed to register scoped_circuit_breaker_state metric")?;
120
121 Ok(Self {
122 request_duration,
123 request_count,
124 active_requests,
125 upstream_attempts,
126 upstream_failures,
127 rate_limit_hits,
128 circuit_breaker_state,
129 })
130 }
131
132 #[inline]
134 fn scope_labels(scope: &Scope) -> (&str, &str) {
135 match scope {
136 Scope::Global => ("", ""),
137 Scope::Namespace(ns) => (ns.as_str(), ""),
138 Scope::Service { namespace, service } => (namespace.as_str(), service.as_str()),
139 }
140 }
141
142 pub fn record_request(
144 &self,
145 route: &str,
146 method: &str,
147 status: u16,
148 duration: Duration,
149 scope: &Scope,
150 ) {
151 let (namespace, service) = Self::scope_labels(scope);
152
153 self.request_duration
154 .with_label_values(&[namespace, service, route, method])
155 .observe(duration.as_secs_f64());
156
157 self.request_count
158 .with_label_values(&[namespace, service, route, method, &status.to_string()])
159 .inc();
160 }
161
162 pub fn inc_active_requests(&self, scope: &Scope) {
164 let (namespace, service) = Self::scope_labels(scope);
165 self.active_requests
166 .with_label_values(&[namespace, service])
167 .inc();
168 }
169
170 pub fn dec_active_requests(&self, scope: &Scope) {
172 let (namespace, service) = Self::scope_labels(scope);
173 self.active_requests
174 .with_label_values(&[namespace, service])
175 .dec();
176 }
177
178 pub fn record_upstream_attempt(&self, upstream: &str, route: &str, scope: &Scope) {
180 let (namespace, service) = Self::scope_labels(scope);
181 self.upstream_attempts
182 .with_label_values(&[namespace, service, upstream, route])
183 .inc();
184 }
185
186 pub fn record_upstream_failure(
188 &self,
189 upstream: &str,
190 route: &str,
191 reason: &str,
192 scope: &Scope,
193 ) {
194 let (namespace, service) = Self::scope_labels(scope);
195 self.upstream_failures
196 .with_label_values(&[namespace, service, upstream, route, reason])
197 .inc();
198 }
199
200 pub fn record_rate_limit_hit(&self, route: &str, policy: &str, scope: &Scope) {
202 let (namespace, service) = Self::scope_labels(scope);
203 self.rate_limit_hits
204 .with_label_values(&[namespace, service, route, policy])
205 .inc();
206 }
207
208 pub fn set_circuit_breaker_state(&self, upstream: &str, is_open: bool, scope: &Scope) {
210 let (namespace, service) = Self::scope_labels(scope);
211 let state = if is_open { 1 } else { 0 };
212 self.circuit_breaker_state
213 .with_label_values(&[namespace, service, upstream])
214 .set(state);
215 }
216}
217
218#[derive(Debug, Clone)]
223pub struct ScopeLabels {
224 pub namespace: String,
225 pub service: String,
226}
227
228impl ScopeLabels {
229 pub fn global() -> Self {
231 Self {
232 namespace: String::new(),
233 service: String::new(),
234 }
235 }
236
237 pub fn from_scope(scope: &Scope) -> Self {
239 match scope {
240 Scope::Global => Self::global(),
241 Scope::Namespace(ns) => Self {
242 namespace: ns.clone(),
243 service: String::new(),
244 },
245 Scope::Service { namespace, service } => Self {
246 namespace: namespace.clone(),
247 service: service.clone(),
248 },
249 }
250 }
251
252 pub fn namespace(&self) -> &str {
254 &self.namespace
255 }
256
257 pub fn service(&self) -> &str {
259 &self.service
260 }
261
262 pub fn is_global(&self) -> bool {
264 self.namespace.is_empty() && self.service.is_empty()
265 }
266
267 pub fn is_namespace(&self) -> bool {
269 !self.namespace.is_empty() && self.service.is_empty()
270 }
271
272 pub fn is_service(&self) -> bool {
274 !self.namespace.is_empty() && !self.service.is_empty()
275 }
276}
277
278impl Default for ScopeLabels {
279 fn default() -> Self {
280 Self::global()
281 }
282}
283
284impl From<&Scope> for ScopeLabels {
285 fn from(scope: &Scope) -> Self {
286 Self::from_scope(scope)
287 }
288}
289
290impl From<Scope> for ScopeLabels {
291 fn from(scope: Scope) -> Self {
292 Self::from_scope(&scope)
293 }
294}
295
296#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn test_scope_labels_from_global() {
306 let labels = ScopeLabels::from_scope(&Scope::Global);
307 assert!(labels.is_global());
308 assert!(!labels.is_namespace());
309 assert!(!labels.is_service());
310 assert_eq!(labels.namespace(), "");
311 assert_eq!(labels.service(), "");
312 }
313
314 #[test]
315 fn test_scope_labels_from_namespace() {
316 let labels = ScopeLabels::from_scope(&Scope::Namespace("api".to_string()));
317 assert!(!labels.is_global());
318 assert!(labels.is_namespace());
319 assert!(!labels.is_service());
320 assert_eq!(labels.namespace(), "api");
321 assert_eq!(labels.service(), "");
322 }
323
324 #[test]
325 fn test_scope_labels_from_service() {
326 let labels = ScopeLabels::from_scope(&Scope::Service {
327 namespace: "api".to_string(),
328 service: "payments".to_string(),
329 });
330 assert!(!labels.is_global());
331 assert!(!labels.is_namespace());
332 assert!(labels.is_service());
333 assert_eq!(labels.namespace(), "api");
334 assert_eq!(labels.service(), "payments");
335 }
336
337 #[test]
338 fn test_scope_labels_default() {
339 let labels = ScopeLabels::default();
340 assert!(labels.is_global());
341 }
342
343 }