1pub 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 pub static ref REGISTRY: Registry = Registry::new();
16
17 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 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 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 pub static ref ERROR_COUNTER: CounterVec = {
44 let opts = Opts::new("error_requests_total", "Total number of error responses");
45 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 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 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 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 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 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 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 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 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 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 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 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 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 pub static ref TRANSACTIONS_FAILED: CounterVec = {
145 let opts = Opts::new("transactions_failed_total", "Total number of failed transactions");
146 let counter_vec = CounterVec::new(opts, &["relayer_id", "network_type", "failure_reason", "previous_status"]).unwrap();
147 REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
148 counter_vec
149 };
150
151 pub static ref API_RPC_FAILURES: CounterVec = {
155 let opts = Opts::new("api_rpc_failures_total", "Total number of RPC failures during API requests (before transaction creation)");
156 let counter_vec = CounterVec::new(opts, &["relayer_id", "network_type", "operation_name", "error_type"]).unwrap();
157 REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
158 counter_vec
159 };
160
161 pub static ref TRANSACTIONS_CREATED: CounterVec = {
163 let opts = Opts::new("transactions_created_total", "Total number of transactions created");
164 let counter_vec = CounterVec::new(opts, &["relayer_id", "network_type"]).unwrap();
165 REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
166 counter_vec
167 };
168
169 pub static ref TRANSACTIONS_SUBMITTED: CounterVec = {
171 let opts = Opts::new("transactions_submitted_total", "Total number of transactions submitted to the network");
172 let counter_vec = CounterVec::new(opts, &["relayer_id", "network_type"]).unwrap();
173 REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
174 counter_vec
175 };
176
177 pub static ref TRANSACTIONS_BY_STATUS: GaugeVec = {
179 let gauge_vec = GaugeVec::new(
180 Opts::new("transactions_by_status", "Current number of transactions by status"),
181 &["relayer_id", "network_type", "status"]
182 ).unwrap();
183 REGISTRY.register(Box::new(gauge_vec.clone())).unwrap();
184 gauge_vec
185 };
186
187 pub static ref TRANSACTION_PROCESSING_TIME: HistogramVec = {
189 let histogram_opts = HistogramOpts::new("transaction_processing_seconds", "Transaction processing time in seconds")
190 .buckets(vec![0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0, 60.0, 120.0, 300.0]);
191 let histogram_vec = HistogramVec::new(histogram_opts, &["relayer_id", "network_type", "stage"]).unwrap();
192 REGISTRY.register(Box::new(histogram_vec.clone())).unwrap();
193 histogram_vec
194 };
195
196 pub static ref RPC_CALL_LATENCY: HistogramVec = {
198 let histogram_opts = HistogramOpts::new("rpc_call_latency_seconds", "RPC call latency in seconds")
199 .buckets(vec![0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0]);
200 let histogram_vec = HistogramVec::new(histogram_opts, &["relayer_id", "network_type", "operation_name"]).unwrap();
201 REGISTRY.register(Box::new(histogram_vec.clone())).unwrap();
202 histogram_vec
203 };
204
205 pub static ref STELLAR_SUBMISSION_FAILURES: CounterVec = {
207 let opts = Opts::new("stellar_submission_failures_total",
208 "Stellar transaction submission failures by status and result code");
209 let counter_vec = CounterVec::new(opts, &["submit_status", "result_code"]).unwrap();
210 REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
211 counter_vec
212 };
213
214 pub static ref PLUGIN_CALLS: CounterVec = {
216 let opts = Opts::new("plugin_calls_total", "Total number of plugin calls");
217 let counter_vec = CounterVec::new(opts, &["plugin_id", "method", "status"]).unwrap();
218 REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
219 counter_vec
220 };
221
222 pub static ref STELLAR_TRY_AGAIN_LATER: CounterVec = {
224 let opts = Opts::new(
225 "stellar_try_again_later_total",
226 "Total number of Stellar transaction submit responses with TRY_AGAIN_LATER"
227 );
228 let counter_vec = CounterVec::new(opts, &["relayer_id", "tx_status"]).unwrap();
229 REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
230 counter_vec
231 };
232
233 pub static ref TRANSACTIONS_TRY_AGAIN_LATER_SUCCESS: CounterVec = {
235 let opts = Opts::new(
236 "transactions_try_again_later_success_total",
237 "Total number of transactions confirmed after experiencing TRY_AGAIN_LATER"
238 );
239 let counter_vec = CounterVec::new(opts, &["relayer_id", "network_type"]).unwrap();
240 REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
241 counter_vec
242 };
243
244 pub static ref TRANSACTIONS_TRY_AGAIN_LATER_FAILED: CounterVec = {
246 let opts = Opts::new(
247 "transactions_try_again_later_failed_total",
248 "Total number of transactions that failed after experiencing TRY_AGAIN_LATER"
249 );
250 let counter_vec = CounterVec::new(opts, &["relayer_id", "network_type"]).unwrap();
251 REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
252 counter_vec
253 };
254
255 pub static ref TRANSACTIONS_INSUFFICIENT_FEE: CounterVec = {
257 let opts = Opts::new(
258 "transactions_insufficient_fee_total",
259 "Total number of transactions that encountered an insufficient fee error"
260 );
261 let counter_vec = CounterVec::new(opts, &["relayer_id", "network_type"]).unwrap();
262 REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
263 counter_vec
264 };
265
266 pub static ref TRANSACTIONS_INSUFFICIENT_FEE_SUCCESS: CounterVec = {
268 let opts = Opts::new(
269 "transactions_insufficient_fee_success_total",
270 "Total number of transactions confirmed after experiencing insufficient fee"
271 );
272 let counter_vec = CounterVec::new(opts, &["relayer_id", "network_type"]).unwrap();
273 REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
274 counter_vec
275 };
276
277 pub static ref TRANSACTIONS_INSUFFICIENT_FEE_FAILED: CounterVec = {
279 let opts = Opts::new(
280 "transactions_insufficient_fee_failed_total",
281 "Total number of transactions that failed after experiencing insufficient fee"
282 );
283 let counter_vec = CounterVec::new(opts, &["relayer_id", "network_type"]).unwrap();
284 REGISTRY.register(Box::new(counter_vec.clone())).unwrap();
285 counter_vec
286 };
287}
288
289pub fn gather_metrics() -> Result<Vec<u8>, Box<dyn std::error::Error>> {
291 let encoder = TextEncoder::new();
292 let metric_families = REGISTRY.gather();
293 let mut buffer = Vec::new();
294 encoder.encode(&metric_families, &mut buffer)?;
295 Ok(buffer)
296}
297
298fn get_fd_count() -> Result<usize, std::io::Error> {
300 let pid = std::process::id();
301
302 #[cfg(target_os = "linux")]
303 {
304 let fd_dir = format!("/proc/{pid}/fd");
305 std::fs::read_dir(fd_dir).map(|entries| entries.count())
306 }
307
308 #[cfg(target_os = "macos")]
309 {
310 use std::process::Command;
311 let output = Command::new("lsof")
312 .args(["-p", &pid.to_string()])
313 .output()?;
314 let count = String::from_utf8_lossy(&output.stdout)
315 .lines()
316 .count()
317 .saturating_sub(1); Ok(count)
319 }
320
321 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
322 {
323 Ok(0) }
325}
326
327fn get_close_wait_count() -> Result<usize, std::io::Error> {
329 #[cfg(any(target_os = "linux", target_os = "macos"))]
330 {
331 use std::process::Command;
332 let output = Command::new("sh")
333 .args(["-c", "netstat -an | grep CLOSE_WAIT | wc -l"])
334 .output()?;
335 let count = String::from_utf8_lossy(&output.stdout)
336 .trim()
337 .parse()
338 .unwrap_or(0);
339 Ok(count)
340 }
341
342 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
343 {
344 Ok(0) }
346}
347
348pub fn update_system_metrics() {
350 let mut sys = System::new_all();
351 sys.refresh_all();
352
353 let cpu_usage = sys.global_cpu_usage();
355 CPU_USAGE.set(cpu_usage as f64);
356
357 let total_memory = sys.total_memory();
359 TOTAL_MEMORY.set(total_memory as f64);
360
361 let available_memory = sys.available_memory();
363 AVAILABLE_MEMORY.set(available_memory as f64);
364
365 let memory_usage = sys.used_memory();
367 MEMORY_USAGE.set(memory_usage as f64);
368
369 let memory_percentage = if total_memory > 0 {
371 (memory_usage as f64 / total_memory as f64) * 100.0
372 } else {
373 0.0
374 };
375 MEMORY_USAGE_PERCENT.set(memory_percentage);
376
377 let disks = Disks::new_with_refreshed_list();
380 let mut total_disk_space: u64 = 0;
381 let mut total_disk_available: u64 = 0;
382 for disk in disks.list() {
383 total_disk_space += disk.total_space();
384 total_disk_available += disk.available_space();
385 }
386 let used_disk_space = total_disk_space.saturating_sub(total_disk_available);
388 DISK_USAGE.set(used_disk_space as f64);
389
390 let disk_percentage = if total_disk_space > 0 {
392 (used_disk_space as f64 / total_disk_space as f64) * 100.0
393 } else {
394 0.0
395 };
396 DISK_USAGE_PERCENT.set(disk_percentage);
397
398 if let Ok(fd_count) = get_fd_count() {
400 FILE_DESCRIPTORS.set(fd_count as f64);
401 }
402
403 if let Ok(close_wait) = get_close_wait_count() {
405 CLOSE_WAIT_SOCKETS.set(close_wait as f64);
406 }
407}
408
409#[cfg(test)]
410mod actix_tests {
411 use super::*;
412 use actix_web::{
413 dev::{Service, ServiceRequest, ServiceResponse, Transform},
414 http, test, Error, HttpResponse,
415 };
416 use futures::future::{self};
417 use middleware::MetricsMiddleware;
418 use prometheus::proto::MetricFamily;
419 use std::{
420 pin::Pin,
421 task::{Context, Poll},
422 };
423
424 struct DummySuccessService;
426
427 impl Service<ServiceRequest> for DummySuccessService {
428 type Response = ServiceResponse;
429 type Error = Error;
430 type Future = Pin<Box<dyn future::Future<Output = Result<Self::Response, Self::Error>>>>;
431
432 fn poll_ready(&self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
433 Poll::Ready(Ok(()))
434 }
435
436 fn call(&self, req: ServiceRequest) -> Self::Future {
437 let resp = req.into_response(HttpResponse::Ok().finish());
438 Box::pin(async move { Ok(resp) })
439 }
440 }
441
442 struct DummyErrorService;
444
445 impl Service<ServiceRequest> for DummyErrorService {
446 type Response = ServiceResponse;
447 type Error = Error;
448 type Future = Pin<Box<dyn future::Future<Output = Result<Self::Response, Self::Error>>>>;
449
450 fn poll_ready(&self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
451 Poll::Ready(Ok(()))
452 }
453
454 fn call(&self, _req: ServiceRequest) -> Self::Future {
455 Box::pin(async move { Err(actix_web::error::ErrorInternalServerError("dummy error")) })
456 }
457 }
458
459 fn find_metric_family<'a>(
461 name: &str,
462 families: &'a [MetricFamily],
463 ) -> Option<&'a MetricFamily> {
464 families.iter().find(|mf| mf.name() == name)
465 }
466
467 #[actix_rt::test]
468 async fn test_gather_metrics_contains_expected_names() {
469 update_system_metrics();
471
472 REQUEST_COUNTER
474 .with_label_values(&["/test", "GET", "200"])
475 .inc();
476 RAW_REQUEST_COUNTER
477 .with_label_values(&["/test?param=value", "GET", "200"])
478 .inc();
479 REQUEST_LATENCY
480 .with_label_values(&["/test", "GET", "200"])
481 .observe(0.1);
482 ERROR_COUNTER
483 .with_label_values(&["/test", "GET", "500"])
484 .inc();
485
486 TRANSACTIONS_INSUFFICIENT_FEE
488 .with_label_values(&["test-relayer", "stellar"])
489 .inc();
490 TRANSACTIONS_INSUFFICIENT_FEE_SUCCESS
491 .with_label_values(&["test-relayer", "stellar"])
492 .inc();
493 TRANSACTIONS_INSUFFICIENT_FEE_FAILED
494 .with_label_values(&["test-relayer", "stellar"])
495 .inc();
496
497 TRANSACTIONS_TRY_AGAIN_LATER_SUCCESS
499 .with_label_values(&["test-relayer", "stellar"])
500 .inc();
501 TRANSACTIONS_TRY_AGAIN_LATER_FAILED
502 .with_label_values(&["test-relayer", "stellar"])
503 .inc();
504
505 let metrics = gather_metrics().expect("failed to gather metrics");
506 let output = String::from_utf8(metrics).expect("metrics output is not valid UTF-8");
507
508 assert!(output.contains("cpu_usage_percentage"));
510 assert!(output.contains("memory_usage_percentage"));
511 assert!(output.contains("memory_usage_bytes"));
512 assert!(output.contains("total_memory_bytes"));
513 assert!(output.contains("available_memory_bytes"));
514 assert!(output.contains("disk_usage_bytes"));
515 assert!(output.contains("disk_usage_percentage"));
516
517 assert!(output.contains("requests_total"));
519 assert!(output.contains("raw_requests_total"));
520 assert!(output.contains("request_latency_seconds"));
521 assert!(output.contains("error_requests_total"));
522
523 assert!(output.contains("transactions_insufficient_fee_total"));
525 assert!(output.contains("transactions_insufficient_fee_success_total"));
526 assert!(output.contains("transactions_insufficient_fee_failed_total"));
527
528 assert!(output.contains("transactions_try_again_later_success_total"));
530 assert!(output.contains("transactions_try_again_later_failed_total"));
531 }
532
533 #[actix_rt::test]
534 async fn test_update_system_metrics() {
535 CPU_USAGE.set(0.0);
537 TOTAL_MEMORY.set(0.0);
538 AVAILABLE_MEMORY.set(0.0);
539 MEMORY_USAGE.set(0.0);
540 MEMORY_USAGE_PERCENT.set(0.0);
541 DISK_USAGE.set(0.0);
542 DISK_USAGE_PERCENT.set(0.0);
543
544 update_system_metrics();
546
547 let cpu_usage = CPU_USAGE.get();
549 assert!(
550 (0.0..=100.0).contains(&cpu_usage),
551 "CPU usage should be between 0-100%, got {cpu_usage}"
552 );
553
554 let memory_usage = MEMORY_USAGE.get();
555 assert!(
556 memory_usage >= 0.0,
557 "Memory usage should be >= 0, got {memory_usage}"
558 );
559
560 let memory_percent = MEMORY_USAGE_PERCENT.get();
561 assert!(
562 (0.0..=100.0).contains(&memory_percent),
563 "Memory usage percentage should be between 0-100%, got {memory_percent}"
564 );
565
566 let total_memory = TOTAL_MEMORY.get();
567 assert!(
568 total_memory > 0.0,
569 "Total memory should be > 0, got {total_memory}"
570 );
571
572 let available_memory = AVAILABLE_MEMORY.get();
573 assert!(
574 available_memory >= 0.0,
575 "Available memory should be >= 0, got {available_memory}"
576 );
577
578 let disk_usage = DISK_USAGE.get();
579 assert!(
580 disk_usage >= 0.0,
581 "Disk usage should be >= 0, got {disk_usage}"
582 );
583
584 let disk_percent = DISK_USAGE_PERCENT.get();
585 assert!(
586 (0.0..=100.0).contains(&disk_percent),
587 "Disk usage percentage should be between 0-100%, got {disk_percent}"
588 );
589
590 assert!(
592 memory_usage <= total_memory,
593 "Memory usage should be <= total memory, got {memory_usage}"
594 );
595
596 assert!(
598 (available_memory + memory_usage) <= total_memory,
599 "Available memory plus used memory should be <= total memory {}, got {}",
600 total_memory,
601 available_memory + memory_usage
602 );
603 }
604
605 #[actix_rt::test]
606 async fn test_middleware_success() {
607 let req = test::TestRequest::with_uri("/test_success").to_srv_request();
608
609 let middleware = MetricsMiddleware;
610 let service = middleware.new_transform(DummySuccessService).await.unwrap();
611
612 let resp = service.call(req).await.unwrap();
613 assert_eq!(resp.response().status(), http::StatusCode::OK);
614
615 let families = REGISTRY.gather();
616 let counter_fam = find_metric_family("requests_total", &families)
617 .expect("requests_total metric family not found");
618
619 let mut found = false;
620 for m in counter_fam.get_metric() {
621 let labels = m.get_label();
622 if labels
623 .iter()
624 .any(|l| l.name() == "endpoint" && l.value() == "/test_success")
625 {
626 found = true;
627 assert!(m.get_counter().value() >= 1.0);
628 }
629 }
630 assert!(
631 found,
632 "Expected metric with endpoint '/test_success' not found"
633 );
634 }
635
636 #[actix_rt::test]
637 async fn test_middleware_error() {
638 let req = test::TestRequest::with_uri("/test_error").to_srv_request();
639
640 let middleware = MetricsMiddleware;
641 let service = middleware.new_transform(DummyErrorService).await.unwrap();
642
643 let result = service.call(req).await;
644 assert!(result.is_err());
645
646 let families = REGISTRY.gather();
647 let error_counter_fam = find_metric_family("error_requests_total", &families)
648 .expect("error_requests_total metric family not found");
649
650 let mut found = false;
651 for m in error_counter_fam.get_metric() {
652 let labels = m.get_label();
653 if labels
654 .iter()
655 .any(|l| l.name() == "endpoint" && l.value() == "/test_error")
656 {
657 found = true;
658 assert!(m.get_counter().value() >= 1.0);
659 }
660 }
661 assert!(
662 found,
663 "Expected error metric with endpoint '/test_error' not found"
664 );
665 }
666}
667
668#[cfg(test)]
669mod property_tests {
670 use proptest::{prelude::*, test_runner::Config};
671
672 fn compute_percentage(used: u64, total: u64) -> f64 {
674 if total > 0 {
675 (used as f64 / total as f64) * 100.0
676 } else {
677 0.0
678 }
679 }
680
681 proptest! {
682 #![proptest_config(Config {
684 cases: 1000, ..Config::default()
685 })]
686
687 #[test]
688 fn prop_compute_percentage((total, used) in {
689 (1u64..1_000_000u64).prop_flat_map(|total| {
690 (Just(total), 0u64..=total)
691 })
692 }) {
693 let percentage = compute_percentage(used, total);
694 prop_assert!(percentage >= 0.0);
695 prop_assert!(percentage <= 100.0);
696 }
697
698 #[test]
699 fn prop_labels_are_reasonable(
700 endpoint in ".*",
701 method in prop::sample::select(vec![
702 "GET".to_string(),
703 "POST".to_string(),
704 "PUT".to_string(),
705 "DELETE".to_string()
706 ])
707 ) {
708 let endpoint_label = if endpoint.is_empty() { "/".to_string() } else { endpoint.clone() };
709 let method_label = method;
710
711 prop_assert!(endpoint_label.chars().count() <= 1024, "Endpoint label too long");
712 prop_assert!(method_label.chars().count() <= 16, "Method label too long");
713
714 let status = "200".to_string();
715 let labels = vec![endpoint_label, method_label, status];
716
717 for label in labels {
718 prop_assert!(!label.is_empty());
719 prop_assert!(label.len() < 1024);
720 }
721 }
722 }
723}