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 = {
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
286pub 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
295fn 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); Ok(count)
316 }
317
318 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
319 {
320 Ok(0) }
322}
323
324fn 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) }
343}
344
345pub fn update_system_metrics() {
347 let mut sys = System::new_all();
348 sys.refresh_all();
349
350 let cpu_usage = sys.global_cpu_usage();
352 CPU_USAGE.set(cpu_usage as f64);
353
354 let total_memory = sys.total_memory();
356 TOTAL_MEMORY.set(total_memory as f64);
357
358 let available_memory = sys.available_memory();
360 AVAILABLE_MEMORY.set(available_memory as f64);
361
362 let memory_usage = sys.used_memory();
364 MEMORY_USAGE.set(memory_usage as f64);
365
366 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 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 let used_disk_space = total_disk_space.saturating_sub(total_disk_available);
385 DISK_USAGE.set(used_disk_space as f64);
386
387 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 if let Ok(fd_count) = get_fd_count() {
397 FILE_DESCRIPTORS.set(fd_count as f64);
398 }
399
400 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 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 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 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();
468
469 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 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 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 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 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 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 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 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 update_system_metrics();
543
544 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 assert!(
589 memory_usage <= total_memory,
590 "Memory usage should be <= total memory, got {memory_usage}"
591 );
592
593 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 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 #![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}