openzeppelin_relayer/metrics/
mod.rs

1//! Metrics module for the application.
2//!
3//! - This module contains the global Prometheus registry.
4//! - Defines specific metrics for the application.
5
6pub mod middleware;
7use lazy_static::lazy_static;
8use prometheus::{
9    CounterVec, Encoder, Gauge, GaugeVec, HistogramOpts, HistogramVec, Opts, Registry, TextEncoder,
10};
11use sysinfo::{Disks, System};
12
13lazy_static! {
14    // Global Prometheus registry.
15    pub static ref REGISTRY: Registry = Registry::new();
16
17    // Counter: Total HTTP requests.
18    pub static ref REQUEST_COUNTER: CounterVec = {
19        let opts = Opts::new("requests_total", "Total number of HTTP requests");
20        let counter_vec = CounterVec::new(opts, &["endpoint", "method", "status"]).unwrap();
21        REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
22        counter_vec
23    };
24
25    // Counter: Total HTTP requests by raw URI.
26    pub static ref RAW_REQUEST_COUNTER: CounterVec = {
27      let opts = Opts::new("raw_requests_total", "Total number of HTTP requests by raw URI");
28      let counter_vec = CounterVec::new(opts, &["raw_uri", "method", "status"]).unwrap();
29      REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
30      counter_vec
31    };
32
33    // Histogram for request latency in seconds.
34    pub static ref REQUEST_LATENCY: HistogramVec = {
35      let histogram_opts = HistogramOpts::new("request_latency_seconds", "Request latency in seconds")
36          .buckets(vec![0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 25.0, 50.0, 100.0]);
37      let histogram_vec = HistogramVec::new(histogram_opts, &["endpoint", "method", "status"]).unwrap();
38      REGISTRY.register(Box::new(histogram_vec.clone())).unwrap();
39      histogram_vec
40    };
41
42    // Counter for error responses.
43    pub static ref ERROR_COUNTER: CounterVec = {
44        let opts = Opts::new("error_requests_total", "Total number of error responses");
45        // Using "status" to record the HTTP status code (or a special label like "service_error")
46        let counter_vec = CounterVec::new(opts, &["endpoint", "method", "status"]).unwrap();
47        REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
48        counter_vec
49    };
50
51    // Gauge for CPU usage percentage.
52    pub static ref CPU_USAGE: Gauge = {
53      let gauge = Gauge::new("cpu_usage_percentage", "Current CPU usage percentage").unwrap();
54      REGISTRY.register(Box::new(gauge.clone())).unwrap();
55      gauge
56    };
57
58    // Gauge for memory usage percentage.
59    pub static ref MEMORY_USAGE_PERCENT: Gauge = {
60      let gauge = Gauge::new("memory_usage_percentage", "Memory usage percentage").unwrap();
61      REGISTRY.register(Box::new(gauge.clone())).unwrap();
62      gauge
63    };
64
65    // Gauge for memory usage in bytes.
66    pub static ref MEMORY_USAGE: Gauge = {
67        let gauge = Gauge::new("memory_usage_bytes", "Memory usage in bytes").unwrap();
68        REGISTRY.register(Box::new(gauge.clone())).unwrap();
69        gauge
70    };
71
72    // Gauge for total memory in bytes.
73    pub static ref TOTAL_MEMORY: Gauge = {
74      let gauge = Gauge::new("total_memory_bytes", "Total memory in bytes").unwrap();
75      REGISTRY.register(Box::new(gauge.clone())).unwrap();
76      gauge
77    };
78
79    // Gauge for available memory in bytes.
80    pub static ref AVAILABLE_MEMORY: Gauge = {
81        let gauge = Gauge::new("available_memory_bytes", "Available memory in bytes").unwrap();
82        REGISTRY.register(Box::new(gauge.clone())).unwrap();
83        gauge
84    };
85
86    // Gauge for used disk space in bytes.
87    pub static ref DISK_USAGE: Gauge = {
88      let gauge = Gauge::new("disk_usage_bytes", "Used disk space in bytes").unwrap();
89      REGISTRY.register(Box::new(gauge.clone())).unwrap();
90      gauge
91    };
92
93    // Gauge for disk usage percentage.
94    pub static ref DISK_USAGE_PERCENT: Gauge = {
95      let gauge = Gauge::new("disk_usage_percentage", "Disk usage percentage").unwrap();
96      REGISTRY.register(Box::new(gauge.clone())).unwrap();
97      gauge
98    };
99
100    // Gauge for in-flight requests.
101    pub static ref IN_FLIGHT_REQUESTS: GaugeVec = {
102        let gauge_vec = GaugeVec::new(
103            Opts::new("in_flight_requests", "Number of in-flight requests"),
104            &["endpoint"]
105        ).unwrap();
106        REGISTRY.register(Box::new(gauge_vec.clone())).unwrap();
107        gauge_vec
108    };
109
110    // Counter for request timeouts.
111    pub static ref TIMEOUT_COUNTER: CounterVec = {
112        let opts = Opts::new("request_timeouts_total", "Total number of request timeouts");
113        let counter_vec = CounterVec::new(opts, &["endpoint", "method", "timeout_type"]).unwrap();
114        REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
115        counter_vec
116    };
117
118    // Gauge for file descriptor count.
119    pub static ref FILE_DESCRIPTORS: Gauge = {
120        let gauge = Gauge::new("file_descriptors_count", "Current file descriptor count").unwrap();
121        REGISTRY.register(Box::new(gauge.clone())).unwrap();
122        gauge
123    };
124
125    // Gauge for CLOSE_WAIT socket count.
126    pub static ref CLOSE_WAIT_SOCKETS: Gauge = {
127        let gauge = Gauge::new("close_wait_sockets_count", "Number of CLOSE_WAIT sockets").unwrap();
128        REGISTRY.register(Box::new(gauge.clone())).unwrap();
129        gauge
130    };
131
132    // Counter for successful transactions (Confirmed status).
133    pub static ref TRANSACTIONS_SUCCESS: CounterVec = {
134        let opts = Opts::new("transactions_success_total", "Total number of successful transactions");
135        let counter_vec = CounterVec::new(opts, &["relayer_id", "network_type"]).unwrap();
136        REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
137        counter_vec
138    };
139
140    // Counter for failed transactions (Failed, Expired, Canceled statuses).
141    pub static ref TRANSACTIONS_FAILED: CounterVec = {
142        let opts = Opts::new("transactions_failed_total", "Total number of failed transactions");
143        let counter_vec = CounterVec::new(opts, &["relayer_id", "network_type", "failure_reason"]).unwrap();
144        REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
145        counter_vec
146    };
147
148    // Counter for RPC failures during API requests (before transaction creation).
149    // This tracks failures that occur during operations like get_status, get_balance, etc.
150    // that happen before a transaction is created.
151    pub static ref API_RPC_FAILURES: CounterVec = {
152        let opts = Opts::new("api_rpc_failures_total", "Total number of RPC failures during API requests (before transaction creation)");
153        let counter_vec = CounterVec::new(opts, &["relayer_id", "network_type", "operation_name", "error_type"]).unwrap();
154        REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
155        counter_vec
156    };
157
158    // Counter for transaction creation (when a transaction is successfully created in the repository).
159    pub static ref TRANSACTIONS_CREATED: CounterVec = {
160        let opts = Opts::new("transactions_created_total", "Total number of transactions created");
161        let counter_vec = CounterVec::new(opts, &["relayer_id", "network_type"]).unwrap();
162        REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
163        counter_vec
164    };
165
166    // Counter for transaction submissions (when status changes to Submitted).
167    pub static ref TRANSACTIONS_SUBMITTED: CounterVec = {
168        let opts = Opts::new("transactions_submitted_total", "Total number of transactions submitted to the network");
169        let counter_vec = CounterVec::new(opts, &["relayer_id", "network_type"]).unwrap();
170        REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
171        counter_vec
172    };
173
174    // Gauge for transaction status distribution (current count of transactions in each status).
175    pub static ref TRANSACTIONS_BY_STATUS: GaugeVec = {
176        let gauge_vec = GaugeVec::new(
177            Opts::new("transactions_by_status", "Current number of transactions by status"),
178            &["relayer_id", "network_type", "status"]
179        ).unwrap();
180        REGISTRY.register(Box::new(gauge_vec.clone())).unwrap();
181        gauge_vec
182    };
183
184    // Histogram for transaction processing times (creation to submission).
185    pub static ref TRANSACTION_PROCESSING_TIME: HistogramVec = {
186        let histogram_opts = HistogramOpts::new("transaction_processing_seconds", "Transaction processing time in seconds")
187            .buckets(vec![0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0, 60.0, 120.0, 300.0]);
188        let histogram_vec = HistogramVec::new(histogram_opts, &["relayer_id", "network_type", "stage"]).unwrap();
189        REGISTRY.register(Box::new(histogram_vec.clone())).unwrap();
190        histogram_vec
191    };
192
193    // Histogram for RPC call latency.
194    pub static ref RPC_CALL_LATENCY: HistogramVec = {
195        let histogram_opts = HistogramOpts::new("rpc_call_latency_seconds", "RPC call latency in seconds")
196            .buckets(vec![0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0]);
197        let histogram_vec = HistogramVec::new(histogram_opts, &["relayer_id", "network_type", "operation_name"]).unwrap();
198        REGISTRY.register(Box::new(histogram_vec.clone())).unwrap();
199        histogram_vec
200    };
201
202    // Counter for Stellar transaction submission failures with decoded result codes.
203    pub static ref STELLAR_SUBMISSION_FAILURES: CounterVec = {
204        let opts = Opts::new("stellar_submission_failures_total",
205            "Stellar transaction submission failures by status and result code");
206        let counter_vec = CounterVec::new(opts, &["submit_status", "result_code"]).unwrap();
207        REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
208        counter_vec
209    };
210
211    // Counter for plugin calls (tracks requests to /api/v1/plugins/{plugin_id}/call endpoints).
212    pub static ref PLUGIN_CALLS: CounterVec = {
213        let opts = Opts::new("plugin_calls_total", "Total number of plugin calls");
214        let counter_vec = CounterVec::new(opts, &["plugin_id", "method", "status"]).unwrap();
215        REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
216        counter_vec
217    };
218
219    // Counter for Stellar submit responses with TRY_AGAIN_LATER status.
220    pub static ref STELLAR_TRY_AGAIN_LATER: CounterVec = {
221        let opts = Opts::new(
222            "stellar_try_again_later_total",
223            "Total number of Stellar transaction submit responses with TRY_AGAIN_LATER"
224        );
225        let counter_vec = CounterVec::new(opts, &["relayer_id", "tx_status"]).unwrap();
226        REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
227        counter_vec
228    };
229
230    // Counter for transactions confirmed after experiencing TRY_AGAIN_LATER.
231    pub static ref TRANSACTIONS_TRY_AGAIN_LATER_SUCCESS: CounterVec = {
232        let opts = Opts::new(
233            "transactions_try_again_later_success_total",
234            "Total number of transactions confirmed after experiencing TRY_AGAIN_LATER"
235        );
236        let counter_vec = CounterVec::new(opts, &["relayer_id", "network_type"]).unwrap();
237        REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
238        counter_vec
239    };
240
241    // Counter for transactions that failed after experiencing TRY_AGAIN_LATER.
242    pub static ref TRANSACTIONS_TRY_AGAIN_LATER_FAILED: CounterVec = {
243        let opts = Opts::new(
244            "transactions_try_again_later_failed_total",
245            "Total number of transactions that failed after experiencing TRY_AGAIN_LATER"
246        );
247        let counter_vec = CounterVec::new(opts, &["relayer_id", "network_type"]).unwrap();
248        REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
249        counter_vec
250    };
251
252    // Counter for transactions that encountered an insufficient fee error.
253    pub static ref TRANSACTIONS_INSUFFICIENT_FEE: CounterVec = {
254        let opts = Opts::new(
255            "transactions_insufficient_fee_total",
256            "Total number of transactions that encountered an insufficient fee error"
257        );
258        let counter_vec = CounterVec::new(opts, &["relayer_id", "network_type"]).unwrap();
259        REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
260        counter_vec
261    };
262
263    // Counter for transactions confirmed after experiencing insufficient fee.
264    pub static ref TRANSACTIONS_INSUFFICIENT_FEE_SUCCESS: CounterVec = {
265        let opts = Opts::new(
266            "transactions_insufficient_fee_success_total",
267            "Total number of transactions confirmed after experiencing insufficient fee"
268        );
269        let counter_vec = CounterVec::new(opts, &["relayer_id", "network_type"]).unwrap();
270        REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
271        counter_vec
272    };
273
274    // Counter for transactions that failed after experiencing insufficient fee.
275    pub static ref TRANSACTIONS_INSUFFICIENT_FEE_FAILED: CounterVec = {
276        let opts = Opts::new(
277            "transactions_insufficient_fee_failed_total",
278            "Total number of transactions that failed after experiencing insufficient fee"
279        );
280        let counter_vec = CounterVec::new(opts, &["relayer_id", "network_type"]).unwrap();
281        REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
282        counter_vec
283    };
284}
285
286/// Gather all metrics and encode into the provided format.
287pub fn gather_metrics() -> Result<Vec<u8>, Box<dyn std::error::Error>> {
288    let encoder = TextEncoder::new();
289    let metric_families = REGISTRY.gather();
290    let mut buffer = Vec::new();
291    encoder.encode(&metric_families, &mut buffer)?;
292    Ok(buffer)
293}
294
295/// Get file descriptor count for current process.
296fn get_fd_count() -> Result<usize, std::io::Error> {
297    let pid = std::process::id();
298
299    #[cfg(target_os = "linux")]
300    {
301        let fd_dir = format!("/proc/{pid}/fd");
302        std::fs::read_dir(fd_dir).map(|entries| entries.count())
303    }
304
305    #[cfg(target_os = "macos")]
306    {
307        use std::process::Command;
308        let output = Command::new("lsof")
309            .args(["-p", &pid.to_string()])
310            .output()?;
311        let count = String::from_utf8_lossy(&output.stdout)
312            .lines()
313            .count()
314            .saturating_sub(1); // Subtract header line
315        Ok(count)
316    }
317
318    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
319    {
320        Ok(0) // Unsupported platform
321    }
322}
323
324/// Get CLOSE_WAIT socket count.
325fn get_close_wait_count() -> Result<usize, std::io::Error> {
326    #[cfg(any(target_os = "linux", target_os = "macos"))]
327    {
328        use std::process::Command;
329        let output = Command::new("sh")
330            .args(["-c", "netstat -an | grep CLOSE_WAIT | wc -l"])
331            .output()?;
332        let count = String::from_utf8_lossy(&output.stdout)
333            .trim()
334            .parse()
335            .unwrap_or(0);
336        Ok(count)
337    }
338
339    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
340    {
341        Ok(0) // Unsupported platform
342    }
343}
344
345/// Updates the system metrics for CPU and memory usage.
346pub fn update_system_metrics() {
347    let mut sys = System::new_all();
348    sys.refresh_all();
349
350    // Overall CPU usage.
351    let cpu_usage = sys.global_cpu_usage();
352    CPU_USAGE.set(cpu_usage as f64);
353
354    // Total memory (in bytes).
355    let total_memory = sys.total_memory();
356    TOTAL_MEMORY.set(total_memory as f64);
357
358    // Available memory (in bytes).
359    let available_memory = sys.available_memory();
360    AVAILABLE_MEMORY.set(available_memory as f64);
361
362    // Used memory (in bytes).
363    let memory_usage = sys.used_memory();
364    MEMORY_USAGE.set(memory_usage as f64);
365
366    // Calculate memory usage percentage
367    let memory_percentage = if total_memory > 0 {
368        (memory_usage as f64 / total_memory as f64) * 100.0
369    } else {
370        0.0
371    };
372    MEMORY_USAGE_PERCENT.set(memory_percentage);
373
374    // Calculate disk usage:
375    // Sum total space and available space across all disks.
376    let disks = Disks::new_with_refreshed_list();
377    let mut total_disk_space: u64 = 0;
378    let mut total_disk_available: u64 = 0;
379    for disk in disks.list() {
380        total_disk_space += disk.total_space();
381        total_disk_available += disk.available_space();
382    }
383    // Used disk space is total minus available ( in bytes).
384    let used_disk_space = total_disk_space.saturating_sub(total_disk_available);
385    DISK_USAGE.set(used_disk_space as f64);
386
387    // Calculate disk usage percentage.
388    let disk_percentage = if total_disk_space > 0 {
389        (used_disk_space as f64 / total_disk_space as f64) * 100.0
390    } else {
391        0.0
392    };
393    DISK_USAGE_PERCENT.set(disk_percentage);
394
395    // Update file descriptor count.
396    if let Ok(fd_count) = get_fd_count() {
397        FILE_DESCRIPTORS.set(fd_count as f64);
398    }
399
400    // Update CLOSE_WAIT socket count.
401    if let Ok(close_wait) = get_close_wait_count() {
402        CLOSE_WAIT_SOCKETS.set(close_wait as f64);
403    }
404}
405
406#[cfg(test)]
407mod actix_tests {
408    use super::*;
409    use actix_web::{
410        dev::{Service, ServiceRequest, ServiceResponse, Transform},
411        http, test, Error, HttpResponse,
412    };
413    use futures::future::{self};
414    use middleware::MetricsMiddleware;
415    use prometheus::proto::MetricFamily;
416    use std::{
417        pin::Pin,
418        task::{Context, Poll},
419    };
420
421    // Dummy service that always returns a successful response (HTTP 200 OK).
422    struct DummySuccessService;
423
424    impl Service<ServiceRequest> for DummySuccessService {
425        type Response = ServiceResponse;
426        type Error = Error;
427        type Future = Pin<Box<dyn future::Future<Output = Result<Self::Response, Self::Error>>>>;
428
429        fn poll_ready(&self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
430            Poll::Ready(Ok(()))
431        }
432
433        fn call(&self, req: ServiceRequest) -> Self::Future {
434            let resp = req.into_response(HttpResponse::Ok().finish());
435            Box::pin(async move { Ok(resp) })
436        }
437    }
438
439    // Dummy service that always returns an error.
440    struct DummyErrorService;
441
442    impl Service<ServiceRequest> for DummyErrorService {
443        type Response = ServiceResponse;
444        type Error = Error;
445        type Future = Pin<Box<dyn future::Future<Output = Result<Self::Response, Self::Error>>>>;
446
447        fn poll_ready(&self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
448            Poll::Ready(Ok(()))
449        }
450
451        fn call(&self, _req: ServiceRequest) -> Self::Future {
452            Box::pin(async move { Err(actix_web::error::ErrorInternalServerError("dummy error")) })
453        }
454    }
455
456    // Helper function to find a metric family by name.
457    fn find_metric_family<'a>(
458        name: &str,
459        families: &'a [MetricFamily],
460    ) -> Option<&'a MetricFamily> {
461        families.iter().find(|mf| mf.name() == name)
462    }
463
464    #[actix_rt::test]
465    async fn test_gather_metrics_contains_expected_names() {
466        // Update system metrics
467        update_system_metrics();
468
469        // Increment request counters to ensure they appear in output
470        REQUEST_COUNTER
471            .with_label_values(&["/test", "GET", "200"])
472            .inc();
473        RAW_REQUEST_COUNTER
474            .with_label_values(&["/test?param=value", "GET", "200"])
475            .inc();
476        REQUEST_LATENCY
477            .with_label_values(&["/test", "GET", "200"])
478            .observe(0.1);
479        ERROR_COUNTER
480            .with_label_values(&["/test", "GET", "500"])
481            .inc();
482
483        // Touch insufficient fee metrics to ensure they appear in output
484        TRANSACTIONS_INSUFFICIENT_FEE
485            .with_label_values(&["test-relayer", "stellar"])
486            .inc();
487        TRANSACTIONS_INSUFFICIENT_FEE_SUCCESS
488            .with_label_values(&["test-relayer", "stellar"])
489            .inc();
490        TRANSACTIONS_INSUFFICIENT_FEE_FAILED
491            .with_label_values(&["test-relayer", "stellar"])
492            .inc();
493
494        // Touch TRY_AGAIN_LATER metrics to ensure they appear in output
495        TRANSACTIONS_TRY_AGAIN_LATER_SUCCESS
496            .with_label_values(&["test-relayer", "stellar"])
497            .inc();
498        TRANSACTIONS_TRY_AGAIN_LATER_FAILED
499            .with_label_values(&["test-relayer", "stellar"])
500            .inc();
501
502        let metrics = gather_metrics().expect("failed to gather metrics");
503        let output = String::from_utf8(metrics).expect("metrics output is not valid UTF-8");
504
505        // System metrics
506        assert!(output.contains("cpu_usage_percentage"));
507        assert!(output.contains("memory_usage_percentage"));
508        assert!(output.contains("memory_usage_bytes"));
509        assert!(output.contains("total_memory_bytes"));
510        assert!(output.contains("available_memory_bytes"));
511        assert!(output.contains("disk_usage_bytes"));
512        assert!(output.contains("disk_usage_percentage"));
513
514        // Request metrics
515        assert!(output.contains("requests_total"));
516        assert!(output.contains("raw_requests_total"));
517        assert!(output.contains("request_latency_seconds"));
518        assert!(output.contains("error_requests_total"));
519
520        // Insufficient fee metrics
521        assert!(output.contains("transactions_insufficient_fee_total"));
522        assert!(output.contains("transactions_insufficient_fee_success_total"));
523        assert!(output.contains("transactions_insufficient_fee_failed_total"));
524
525        // TRY_AGAIN_LATER metrics
526        assert!(output.contains("transactions_try_again_later_success_total"));
527        assert!(output.contains("transactions_try_again_later_failed_total"));
528    }
529
530    #[actix_rt::test]
531    async fn test_update_system_metrics() {
532        // Reset metrics to ensure clean state
533        CPU_USAGE.set(0.0);
534        TOTAL_MEMORY.set(0.0);
535        AVAILABLE_MEMORY.set(0.0);
536        MEMORY_USAGE.set(0.0);
537        MEMORY_USAGE_PERCENT.set(0.0);
538        DISK_USAGE.set(0.0);
539        DISK_USAGE_PERCENT.set(0.0);
540
541        // Call the function we're testing
542        update_system_metrics();
543
544        // Verify that metrics have been updated with reasonable values
545        let cpu_usage = CPU_USAGE.get();
546        assert!(
547            (0.0..=100.0).contains(&cpu_usage),
548            "CPU usage should be between 0-100%, got {cpu_usage}"
549        );
550
551        let memory_usage = MEMORY_USAGE.get();
552        assert!(
553            memory_usage >= 0.0,
554            "Memory usage should be >= 0, got {memory_usage}"
555        );
556
557        let memory_percent = MEMORY_USAGE_PERCENT.get();
558        assert!(
559            (0.0..=100.0).contains(&memory_percent),
560            "Memory usage percentage should be between 0-100%, got {memory_percent}"
561        );
562
563        let total_memory = TOTAL_MEMORY.get();
564        assert!(
565            total_memory > 0.0,
566            "Total memory should be > 0, got {total_memory}"
567        );
568
569        let available_memory = AVAILABLE_MEMORY.get();
570        assert!(
571            available_memory >= 0.0,
572            "Available memory should be >= 0, got {available_memory}"
573        );
574
575        let disk_usage = DISK_USAGE.get();
576        assert!(
577            disk_usage >= 0.0,
578            "Disk usage should be >= 0, got {disk_usage}"
579        );
580
581        let disk_percent = DISK_USAGE_PERCENT.get();
582        assert!(
583            (0.0..=100.0).contains(&disk_percent),
584            "Disk usage percentage should be between 0-100%, got {disk_percent}"
585        );
586
587        // Verify that memory usage doesn't exceed total memory
588        assert!(
589            memory_usage <= total_memory,
590            "Memory usage should be <= total memory, got {memory_usage}"
591        );
592
593        // Verify that available memory plus used memory doesn't exceed total memory
594        assert!(
595            (available_memory + memory_usage) <= total_memory,
596            "Available memory plus used memory should be <= total memory {}, got {}",
597            total_memory,
598            available_memory + memory_usage
599        );
600    }
601
602    #[actix_rt::test]
603    async fn test_middleware_success() {
604        let req = test::TestRequest::with_uri("/test_success").to_srv_request();
605
606        let middleware = MetricsMiddleware;
607        let service = middleware.new_transform(DummySuccessService).await.unwrap();
608
609        let resp = service.call(req).await.unwrap();
610        assert_eq!(resp.response().status(), http::StatusCode::OK);
611
612        let families = REGISTRY.gather();
613        let counter_fam = find_metric_family("requests_total", &families)
614            .expect("requests_total metric family not found");
615
616        let mut found = false;
617        for m in counter_fam.get_metric() {
618            let labels = m.get_label();
619            if labels
620                .iter()
621                .any(|l| l.name() == "endpoint" && l.value() == "/test_success")
622            {
623                found = true;
624                assert!(m.get_counter().value() >= 1.0);
625            }
626        }
627        assert!(
628            found,
629            "Expected metric with endpoint '/test_success' not found"
630        );
631    }
632
633    #[actix_rt::test]
634    async fn test_middleware_error() {
635        let req = test::TestRequest::with_uri("/test_error").to_srv_request();
636
637        let middleware = MetricsMiddleware;
638        let service = middleware.new_transform(DummyErrorService).await.unwrap();
639
640        let result = service.call(req).await;
641        assert!(result.is_err());
642
643        let families = REGISTRY.gather();
644        let error_counter_fam = find_metric_family("error_requests_total", &families)
645            .expect("error_requests_total metric family not found");
646
647        let mut found = false;
648        for m in error_counter_fam.get_metric() {
649            let labels = m.get_label();
650            if labels
651                .iter()
652                .any(|l| l.name() == "endpoint" && l.value() == "/test_error")
653            {
654                found = true;
655                assert!(m.get_counter().value() >= 1.0);
656            }
657        }
658        assert!(
659            found,
660            "Expected error metric with endpoint '/test_error' not found"
661        );
662    }
663}
664
665#[cfg(test)]
666mod property_tests {
667    use proptest::{prelude::*, test_runner::Config};
668
669    // A helper function to compute percentage used from total.
670    fn compute_percentage(used: u64, total: u64) -> f64 {
671        if total > 0 {
672            (used as f64 / total as f64) * 100.0
673        } else {
674            0.0
675        }
676    }
677
678    proptest! {
679        // Set the number of cases to 1000
680        #![proptest_config(Config {
681          cases: 1000, ..Config::default()
682        })]
683
684        #[test]
685        fn prop_compute_percentage((total, used) in {
686            (1u64..1_000_000u64).prop_flat_map(|total| {
687                (Just(total), 0u64..=total)
688            })
689        }) {
690            let percentage = compute_percentage(used, total);
691            prop_assert!(percentage >= 0.0);
692            prop_assert!(percentage <= 100.0);
693        }
694
695        #[test]
696        fn prop_labels_are_reasonable(
697              endpoint in ".*",
698              method in prop::sample::select(vec![
699                "GET".to_string(),
700                "POST".to_string(),
701                "PUT".to_string(),
702                "DELETE".to_string()
703                ])
704            ) {
705            let endpoint_label = if endpoint.is_empty() { "/".to_string() } else { endpoint.clone() };
706            let method_label = method;
707
708            prop_assert!(endpoint_label.chars().count() <= 1024, "Endpoint label too long");
709            prop_assert!(method_label.chars().count() <= 16, "Method label too long");
710
711            let status = "200".to_string();
712            let labels = vec![endpoint_label, method_label, status];
713
714            for label in labels {
715                prop_assert!(!label.is_empty());
716                prop_assert!(label.len() < 1024);
717            }
718        }
719    }
720}