1use pingora_http::ResponseHeader;
10use std::collections::HashMap;
11use std::sync::Arc;
12use tokio::sync::RwLock;
13use zentinel_agent_protocol::v2::{MetricsCollector, UnifiedMetricsAggregator};
14
15pub struct MetricsManager {
20 aggregator: Arc<UnifiedMetricsAggregator>,
22 enabled: bool,
24 path: String,
26 allowed_ips: Vec<String>,
28 pool_metrics: RwLock<HashMap<String, Arc<MetricsCollector>>>,
30}
31
32impl MetricsManager {
33 pub fn new(service_name: impl Into<String>, instance_id: impl Into<String>) -> Self {
35 Self {
36 aggregator: Arc::new(UnifiedMetricsAggregator::new(service_name, instance_id)),
37 enabled: true,
38 path: "/metrics".to_string(),
39 allowed_ips: Vec::new(),
40 pool_metrics: RwLock::new(HashMap::new()),
41 }
42 }
43
44 pub fn with_aggregator(aggregator: Arc<UnifiedMetricsAggregator>) -> Self {
46 Self {
47 aggregator,
48 enabled: true,
49 path: "/metrics".to_string(),
50 allowed_ips: Vec::new(),
51 pool_metrics: RwLock::new(HashMap::new()),
52 }
53 }
54
55 pub fn from_config(
61 config: &zentinel_config::MetricsConfig,
62 service_name: impl Into<String>,
63 instance_id: impl Into<String>,
64 ) -> Self {
65 let mut manager = Self::new(service_name, instance_id);
66 manager.enabled = config.enabled;
67 manager.path = config.path.clone();
68 manager
69 }
70
71 pub fn path(mut self, path: impl Into<String>) -> Self {
73 self.path = path.into();
74 self
75 }
76
77 pub fn allowed_ips(mut self, ips: Vec<String>) -> Self {
79 self.allowed_ips = ips;
80 self
81 }
82
83 pub fn disable(mut self) -> Self {
85 self.enabled = false;
86 self
87 }
88
89 pub fn is_enabled(&self) -> bool {
91 self.enabled
92 }
93
94 pub fn metrics_path(&self) -> &str {
96 &self.path
97 }
98
99 pub fn aggregator(&self) -> &UnifiedMetricsAggregator {
101 &self.aggregator
102 }
103
104 pub fn aggregator_arc(&self) -> Arc<UnifiedMetricsAggregator> {
106 Arc::clone(&self.aggregator)
107 }
108
109 pub fn is_ip_allowed(&self, ip: &str) -> bool {
111 if self.allowed_ips.is_empty() {
112 return true;
113 }
114 self.allowed_ips.iter().any(|allowed| allowed == ip)
115 }
116
117 pub async fn register_pool_metrics(
121 &self,
122 agent_id: impl Into<String>,
123 collector: Arc<MetricsCollector>,
124 ) {
125 self.pool_metrics
126 .write()
127 .await
128 .insert(agent_id.into(), collector);
129 }
130
131 pub async fn unregister_pool_metrics(&self, agent_id: &str) {
133 self.pool_metrics.write().await.remove(agent_id);
134 }
135
136 pub fn handle_metrics_request(&self) -> MetricsResponse {
142 if !self.enabled {
143 return MetricsResponse::not_found();
144 }
145
146 let mut body = self.aggregator.export_prometheus();
148
149 if let Ok(pool_metrics) = self.pool_metrics.try_read() {
152 for (agent_id, collector) in pool_metrics.iter() {
153 let pool_output = collector.export_prometheus();
154 if !pool_output.is_empty() {
155 body.push_str(&format!("\n# Agent pool metrics: {}\n", agent_id));
157 body.push_str(&pool_output);
158 }
159 }
160 }
161
162 MetricsResponse::ok(body)
163 }
164
165 pub fn inc_requests_total(&self, method: &str, status: u16, route: &str) {
171 let mut labels = HashMap::new();
172 labels.insert("method".to_string(), method.to_string());
173 labels.insert("status".to_string(), status.to_string());
174 labels.insert("route".to_string(), route.to_string());
175
176 self.aggregator.increment_counter(
177 "zentinel_requests_total",
178 "Total HTTP requests handled by the proxy",
179 labels,
180 1,
181 );
182 }
183
184 pub fn observe_request_duration(&self, method: &str, route: &str, duration_secs: f64) {
186 let mut labels = HashMap::new();
187 labels.insert("method".to_string(), method.to_string());
188 labels.insert("route".to_string(), route.to_string());
189
190 let buckets = vec![
192 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
193 ];
194
195 self.aggregator.observe_histogram(
196 "zentinel_request_duration_seconds",
197 "HTTP request duration in seconds",
198 labels,
199 &buckets,
200 duration_secs,
201 );
202 }
203
204 pub fn set_active_connections(&self, count: f64) {
206 self.aggregator.set_gauge(
207 "zentinel_active_connections",
208 "Number of active client connections",
209 HashMap::new(),
210 count,
211 );
212 }
213
214 pub fn set_active_requests(&self, count: f64) {
216 self.aggregator.set_gauge(
217 "zentinel_active_requests",
218 "Number of requests currently being processed",
219 HashMap::new(),
220 count,
221 );
222 }
223
224 pub fn inc_upstream_requests(&self, upstream: &str, status: u16, success: bool) {
226 let mut labels = HashMap::new();
227 labels.insert("upstream".to_string(), upstream.to_string());
228 labels.insert("status".to_string(), status.to_string());
229 labels.insert("success".to_string(), success.to_string());
230
231 self.aggregator.increment_counter(
232 "zentinel_upstream_requests_total",
233 "Total requests to upstream servers",
234 labels,
235 1,
236 );
237 }
238
239 pub fn observe_upstream_duration(&self, upstream: &str, duration_secs: f64) {
241 let mut labels = HashMap::new();
242 labels.insert("upstream".to_string(), upstream.to_string());
243
244 let buckets = vec![
245 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
246 ];
247
248 self.aggregator.observe_histogram(
249 "zentinel_upstream_duration_seconds",
250 "Time spent waiting for upstream response",
251 labels,
252 &buckets,
253 duration_secs,
254 );
255 }
256
257 pub fn observe_upstream_write_pending(&self, upstream: &str, duration_secs: f64) {
259 let mut labels = HashMap::new();
260 labels.insert("upstream".to_string(), upstream.to_string());
261
262 let buckets = vec![
263 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
264 ];
265
266 self.aggregator.observe_histogram(
267 "zentinel_upstream_write_pending_seconds",
268 "Time spent waiting to write request to upstream",
269 labels,
270 &buckets,
271 duration_secs,
272 );
273 }
274
275 pub fn inc_agent_requests(&self, agent: &str, decision: &str) {
277 let mut labels = HashMap::new();
278 labels.insert("agent".to_string(), agent.to_string());
279 labels.insert("decision".to_string(), decision.to_string());
280
281 self.aggregator.increment_counter(
282 "zentinel_agent_requests_total",
283 "Total requests processed by agents",
284 labels,
285 1,
286 );
287 }
288
289 pub fn observe_agent_duration(&self, agent: &str, duration_secs: f64) {
291 let mut labels = HashMap::new();
292 labels.insert("agent".to_string(), agent.to_string());
293
294 let buckets = vec![0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0];
295
296 self.aggregator.observe_histogram(
297 "zentinel_agent_duration_seconds",
298 "Time spent processing request in agent",
299 labels,
300 &buckets,
301 duration_secs,
302 );
303 }
304
305 pub fn inc_circuit_breaker_trips(&self, upstream: &str) {
307 let mut labels = HashMap::new();
308 labels.insert("upstream".to_string(), upstream.to_string());
309
310 self.aggregator.increment_counter(
311 "zentinel_circuit_breaker_trips_total",
312 "Number of times circuit breaker has tripped",
313 labels,
314 1,
315 );
316 }
317
318 pub fn set_circuit_breaker_state(&self, upstream: &str, open: bool) {
320 let mut labels = HashMap::new();
321 labels.insert("upstream".to_string(), upstream.to_string());
322
323 self.aggregator.set_gauge(
324 "zentinel_circuit_breaker_open",
325 "Whether circuit breaker is open (1) or closed (0)",
326 labels,
327 if open { 1.0 } else { 0.0 },
328 );
329 }
330
331 pub fn inc_rate_limited(&self, route: &str) {
333 let mut labels = HashMap::new();
334 labels.insert("route".to_string(), route.to_string());
335
336 self.aggregator.increment_counter(
337 "zentinel_rate_limited_total",
338 "Total requests rate limited",
339 labels,
340 1,
341 );
342 }
343
344 pub fn inc_cache_access(&self, hit: bool) {
346 let mut labels = HashMap::new();
347 labels.insert(
348 "result".to_string(),
349 if hit { "hit" } else { "miss" }.to_string(),
350 );
351
352 self.aggregator.increment_counter(
353 "zentinel_cache_accesses_total",
354 "Total cache accesses",
355 labels,
356 1,
357 );
358 }
359
360 pub fn set_cache_size(&self, size_bytes: f64) {
362 self.aggregator.set_gauge(
363 "zentinel_cache_size_bytes",
364 "Current cache size in bytes",
365 HashMap::new(),
366 size_bytes,
367 );
368 }
369}
370
371#[derive(Debug)]
373pub struct MetricsResponse {
374 pub status: u16,
376 pub content_type: String,
378 pub body: String,
380}
381
382impl MetricsResponse {
383 pub fn ok(body: String) -> Self {
385 Self {
386 status: 200,
387 content_type: "text/plain; version=0.0.4; charset=utf-8".to_string(),
388 body,
389 }
390 }
391
392 pub fn not_found() -> Self {
394 Self {
395 status: 404,
396 content_type: "text/plain".to_string(),
397 body: "Metrics not found".to_string(),
398 }
399 }
400
401 pub fn forbidden() -> Self {
403 Self {
404 status: 403,
405 content_type: "text/plain".to_string(),
406 body: "Forbidden".to_string(),
407 }
408 }
409
410 pub fn to_header(&self) -> ResponseHeader {
412 let mut header = ResponseHeader::build(self.status, Some(2)).unwrap();
413 header
414 .append_header("Content-Type", &self.content_type)
415 .ok();
416 header
417 .append_header("Content-Length", self.body.len().to_string())
418 .ok();
419 header
420 }
421}
422
423pub mod standard {
425 pub const REQUESTS_TOTAL: &str = "zentinel_requests_total";
427 pub const REQUEST_DURATION: &str = "zentinel_request_duration_seconds";
429 pub const ACTIVE_CONNECTIONS: &str = "zentinel_active_connections";
431 pub const ACTIVE_REQUESTS: &str = "zentinel_active_requests";
433 pub const UPSTREAM_REQUESTS: &str = "zentinel_upstream_requests_total";
435 pub const UPSTREAM_DURATION: &str = "zentinel_upstream_duration_seconds";
437 pub const AGENT_REQUESTS: &str = "zentinel_agent_requests_total";
439 pub const AGENT_DURATION: &str = "zentinel_agent_duration_seconds";
441 pub const CIRCUIT_BREAKER_TRIPS: &str = "zentinel_circuit_breaker_trips_total";
443 pub const CIRCUIT_BREAKER_OPEN: &str = "zentinel_circuit_breaker_open";
445 pub const RATE_LIMITED: &str = "zentinel_rate_limited_total";
447 pub const CACHE_ACCESSES: &str = "zentinel_cache_accesses_total";
449 pub const CACHE_SIZE: &str = "zentinel_cache_size_bytes";
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456
457 #[test]
458 fn test_metrics_manager_creation() {
459 let manager = MetricsManager::new("test-service", "node-1");
460 assert!(manager.is_enabled());
461 assert_eq!(manager.metrics_path(), "/metrics");
462 }
463
464 #[test]
465 fn test_metrics_manager_disabled() {
466 let manager = MetricsManager::new("test", "1").disable();
467 assert!(!manager.is_enabled());
468
469 let response = manager.handle_metrics_request();
470 assert_eq!(response.status, 404);
471 }
472
473 #[test]
474 fn test_metrics_manager_ip_filtering() {
475 let manager = MetricsManager::new("test", "1")
476 .allowed_ips(vec!["127.0.0.1".to_string(), "10.0.0.1".to_string()]);
477
478 assert!(manager.is_ip_allowed("127.0.0.1"));
479 assert!(manager.is_ip_allowed("10.0.0.1"));
480 assert!(!manager.is_ip_allowed("192.168.1.1"));
481 }
482
483 #[test]
484 fn test_metrics_manager_all_ips_allowed() {
485 let manager = MetricsManager::new("test", "1");
486
487 assert!(manager.is_ip_allowed("127.0.0.1"));
489 assert!(manager.is_ip_allowed("192.168.1.1"));
490 assert!(manager.is_ip_allowed("any-ip"));
491 }
492
493 #[test]
494 fn test_metrics_response() {
495 let manager = MetricsManager::new("test", "node-1");
496
497 manager.inc_requests_total("GET", 200, "/api/users");
499 manager.set_active_connections(42.0);
500
501 let response = manager.handle_metrics_request();
502 assert_eq!(response.status, 200);
503 assert!(response.content_type.contains("text/plain"));
504 assert!(response.body.contains("zentinel_requests_total"));
505 assert!(response.body.contains("zentinel_active_connections"));
506 assert!(response.body.contains("zentinel_info"));
507 }
508
509 #[test]
510 fn test_request_duration_histogram() {
511 let manager = MetricsManager::new("test", "1");
512
513 manager.observe_request_duration("GET", "/api", 0.05);
514 manager.observe_request_duration("GET", "/api", 0.15);
515 manager.observe_request_duration("GET", "/api", 0.5);
516
517 let response = manager.handle_metrics_request();
518 assert!(response
519 .body
520 .contains("zentinel_request_duration_seconds_bucket"));
521 assert!(response
522 .body
523 .contains("zentinel_request_duration_seconds_sum"));
524 assert!(response
525 .body
526 .contains("zentinel_request_duration_seconds_count"));
527 assert!(response.body.contains("} 3\n") || response.body.contains(" 3\n"));
529 }
530
531 #[test]
532 fn test_custom_path() {
533 let manager = MetricsManager::new("test", "1").path("/internal/metrics");
534 assert_eq!(manager.metrics_path(), "/internal/metrics");
535 }
536
537 #[test]
538 fn test_upstream_metrics() {
539 let manager = MetricsManager::new("test", "1");
540
541 manager.inc_upstream_requests("backend-1", 200, true);
542 manager.observe_upstream_duration("backend-1", 0.1);
543
544 let response = manager.handle_metrics_request();
545 assert!(response.body.contains("zentinel_upstream_requests_total"));
546 assert!(response.body.contains("zentinel_upstream_duration_seconds"));
547 }
548
549 #[test]
550 fn test_agent_metrics() {
551 let manager = MetricsManager::new("test", "1");
552
553 manager.inc_agent_requests("waf", "allow");
554 manager.observe_agent_duration("waf", 0.005);
555
556 let response = manager.handle_metrics_request();
557 assert!(response.body.contains("zentinel_agent_requests_total"));
558 assert!(response.body.contains("zentinel_agent_duration_seconds"));
559 }
560}