openzeppelin_monitor/utils/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 server;
7use lazy_static::lazy_static;
8use prometheus::{Encoder, Gauge, GaugeVec, Opts, Registry, TextEncoder};
9use sysinfo::{Disks, System};
10
11lazy_static! {
12	/// Global Prometheus registry.
13	///
14	/// This registry holds all metrics defined in this module and is used
15	/// to gather metrics for exposure via the metrics endpoint.
16	pub static ref REGISTRY: Registry = Registry::new();
17
18	/// Gauge for CPU usage percentage.
19	///
20	/// Tracks the current CPU usage as a percentage (0-100) across all cores.
21	pub static ref CPU_USAGE: Gauge = {
22	  let gauge = Gauge::new("cpu_usage_percentage", "Current CPU usage percentage").unwrap();
23	  REGISTRY.register(Box::new(gauge.clone())).unwrap();
24	  gauge
25	};
26
27	/// Gauge for memory usage percentage.
28	///
29	/// Tracks the percentage (0-100) of total system memory currently in use.
30	pub static ref MEMORY_USAGE_PERCENT: Gauge = {
31	  let gauge = Gauge::new("memory_usage_percentage", "Memory usage percentage").unwrap();
32	  REGISTRY.register(Box::new(gauge.clone())).unwrap();
33	  gauge
34	};
35
36	/// Gauge for memory usage in bytes.
37	///
38	/// Tracks the absolute amount of memory currently in use by the system in bytes.
39	pub static ref MEMORY_USAGE: Gauge = {
40		let gauge = Gauge::new("memory_usage_bytes", "Memory usage in bytes").unwrap();
41		REGISTRY.register(Box::new(gauge.clone())).unwrap();
42		gauge
43	};
44
45	/// Gauge for total memory in bytes.
46	///
47	/// Tracks the total amount of physical memory available on the system in bytes.
48	pub static ref TOTAL_MEMORY: Gauge = {
49	  let gauge = Gauge::new("total_memory_bytes", "Total memory in bytes").unwrap();
50	  REGISTRY.register(Box::new(gauge.clone())).unwrap();
51	  gauge
52	};
53
54	/// Gauge for available memory in bytes.
55	///
56	/// Tracks the amount of memory currently available for allocation in bytes.
57	pub static ref AVAILABLE_MEMORY: Gauge = {
58		let gauge = Gauge::new("available_memory_bytes", "Available memory in bytes").unwrap();
59		REGISTRY.register(Box::new(gauge.clone())).unwrap();
60		gauge
61	};
62
63	/// Gauge for used disk space in bytes.
64	///
65	/// Tracks the total amount of disk space currently in use across all mounted filesystems in bytes.
66	pub static ref DISK_USAGE: Gauge = {
67	  let gauge = Gauge::new("disk_usage_bytes", "Used disk space in bytes").unwrap();
68	  REGISTRY.register(Box::new(gauge.clone())).unwrap();
69	  gauge
70	};
71
72	/// Gauge for disk usage percentage.
73	///
74	/// Tracks the percentage (0-100) of total disk space currently in use across all mounted filesystems.
75	pub static ref DISK_USAGE_PERCENT: Gauge = {
76	  let gauge = Gauge::new("disk_usage_percentage", "Disk usage percentage").unwrap();
77	  REGISTRY.register(Box::new(gauge.clone())).unwrap();
78	  gauge
79	};
80
81	/// Gauge for total number of monitors (active and paused).
82	///
83	/// Tracks the total count of all configured monitors in the system, regardless of their active state.
84	pub static ref MONITORS_TOTAL: Gauge = {
85		let gauge = Gauge::new("monitors_total", "Total number of configured monitors").unwrap();
86		REGISTRY.register(Box::new(gauge.clone())).unwrap();
87		gauge
88	};
89
90	/// Gauge for number of active monitors (not paused).
91	///
92	/// Tracks the count of monitors that are currently active (not in paused state).
93	pub static ref MONITORS_ACTIVE: Gauge = {
94		let gauge = Gauge::new("monitors_active", "Number of active monitors").unwrap();
95		REGISTRY.register(Box::new(gauge.clone())).unwrap();
96		gauge
97	};
98
99	/// Gauge for total number of triggers.
100	///
101	/// Tracks the total count of all configured triggers in the system.
102	pub static ref TRIGGERS_TOTAL: Gauge = {
103		let gauge = Gauge::new("triggers_total", "Total number of configured triggers").unwrap();
104		REGISTRY.register(Box::new(gauge.clone())).unwrap();
105		gauge
106	};
107
108	/// Gauge for total number of contracts being monitored (across all monitors).
109	///
110	/// Tracks the total count of unique contracts (network + address combinations) being monitored.
111	pub static ref CONTRACTS_MONITORED: Gauge = {
112		let gauge = Gauge::new("contracts_monitored", "Total number of contracts being monitored").unwrap();
113		REGISTRY.register(Box::new(gauge.clone())).unwrap();
114		gauge
115	};
116
117	/// Gauge for total number of networks being monitored.
118	///
119	/// Tracks the count of unique blockchain networks that have at least one active monitor.
120	pub static ref NETWORKS_MONITORED: Gauge = {
121		let gauge = Gauge::new("networks_monitored", "Total number of networks being monitored").unwrap();
122		REGISTRY.register(Box::new(gauge.clone())).unwrap();
123		gauge
124	};
125
126	/// Gauge Vector for per-network metrics.
127	///
128	/// Tracks the number of active monitors for each network, with the network name as a label.
129	pub static ref NETWORK_MONITORS: GaugeVec = {
130		let gauge = GaugeVec::new(
131			Opts::new("network_monitors", "Number of monitors per network"),
132			&["network"]
133		).unwrap();
134		REGISTRY.register(Box::new(gauge.clone())).unwrap();
135		gauge
136	};
137}
138
139/// Gather all metrics and encode into the provided format.
140pub fn gather_metrics() -> Result<Vec<u8>, Box<dyn std::error::Error>> {
141	let encoder = TextEncoder::new();
142	let metric_families = REGISTRY.gather();
143	let mut buffer = Vec::new();
144	encoder.encode(&metric_families, &mut buffer)?;
145	Ok(buffer)
146}
147
148/// Updates the system metrics for CPU and memory usage.
149pub fn update_system_metrics() {
150	let mut sys = System::new_all();
151	sys.refresh_all();
152
153	// Overall CPU usage.
154	let cpu_usage = sys.global_cpu_usage();
155	CPU_USAGE.set(cpu_usage as f64);
156
157	// Total memory (in bytes).
158	let total_memory = sys.total_memory();
159	TOTAL_MEMORY.set(total_memory as f64);
160
161	// Available memory (in bytes).
162	let available_memory = sys.available_memory();
163	AVAILABLE_MEMORY.set(available_memory as f64);
164
165	// Used memory (in bytes).
166	let memory_usage = sys.used_memory();
167	MEMORY_USAGE.set(memory_usage as f64);
168
169	// Calculate memory usage percentage
170	let memory_percentage = if total_memory > 0 {
171		(memory_usage as f64 / total_memory as f64) * 100.0
172	} else {
173		0.0
174	};
175	MEMORY_USAGE_PERCENT.set(memory_percentage);
176
177	// Calculate disk usage:
178	// Sum total space and available space across all disks.
179	let disks = Disks::new_with_refreshed_list();
180	let mut total_disk_space: u64 = 0;
181	let mut total_disk_available: u64 = 0;
182	for disk in disks.list() {
183		total_disk_space += disk.total_space();
184		total_disk_available += disk.available_space();
185	}
186	// Used disk space is total minus available ( in bytes).
187	let used_disk_space = total_disk_space.saturating_sub(total_disk_available);
188	DISK_USAGE.set(used_disk_space as f64);
189
190	// Calculate disk usage percentage.
191	let disk_percentage = if total_disk_space > 0 {
192		(used_disk_space as f64 / total_disk_space as f64) * 100.0
193	} else {
194		0.0
195	};
196	DISK_USAGE_PERCENT.set(disk_percentage);
197}
198
199/// Updates metrics related to monitors, triggers, networks, and contracts.
200pub fn update_monitoring_metrics(
201	monitors: &std::collections::HashMap<String, crate::models::Monitor>,
202	triggers: &std::collections::HashMap<String, crate::models::Trigger>,
203	networks: &std::collections::HashMap<String, crate::models::Network>,
204) {
205	// Track total and active monitors
206	let total_monitors = monitors.len();
207	let active_monitors = monitors.values().filter(|m| !m.paused).count();
208
209	MONITORS_TOTAL.set(total_monitors as f64);
210	MONITORS_ACTIVE.set(active_monitors as f64);
211
212	// Track total triggers
213	TRIGGERS_TOTAL.set(triggers.len() as f64);
214
215	// Count unique contracts across all monitors
216	let mut unique_contracts = std::collections::HashSet::new();
217	for monitor in monitors.values() {
218		for address in &monitor.addresses {
219			// Create a unique identifier for each contract (network + address)
220			for network in &monitor.networks {
221				// Verify the network exists in our network repository
222				if networks.contains_key(network) {
223					unique_contracts.insert(format!("{}:{}", network, address.address));
224				}
225			}
226		}
227	}
228	CONTRACTS_MONITORED.set(unique_contracts.len() as f64);
229
230	// Count networks being monitored (those with active monitors)
231	let mut networks_with_monitors = std::collections::HashSet::new();
232	for monitor in monitors.values().filter(|m| !m.paused) {
233		for network in &monitor.networks {
234			// Only count networks that exist in our repository
235			if networks.contains_key(network) {
236				networks_with_monitors.insert(network.clone());
237			}
238		}
239	}
240	NETWORKS_MONITORED.set(networks_with_monitors.len() as f64);
241
242	// Reset all network-specific metrics
243	NETWORK_MONITORS.reset();
244
245	// Set per-network monitor counts (only for networks that exist)
246	let mut network_monitor_counts = std::collections::HashMap::<String, usize>::new();
247	for monitor in monitors.values().filter(|m| !m.paused) {
248		for network in &monitor.networks {
249			if networks.contains_key(network) {
250				*network_monitor_counts.entry(network.clone()).or_insert(0) += 1;
251			}
252		}
253	}
254
255	for (network, count) in network_monitor_counts {
256		NETWORK_MONITORS
257			.with_label_values(&[&network])
258			.set(count as f64);
259	}
260}
261
262#[cfg(test)]
263mod tests {
264	use super::*;
265	use crate::{
266		models::{BlockChainType, Monitor, Network, TransactionStatus, Trigger},
267		utils::tests::builders::{
268			evm::monitor::MonitorBuilder, network::NetworkBuilder, trigger::TriggerBuilder,
269		},
270	};
271	use std::collections::HashMap;
272	use std::sync::Mutex;
273
274	// Use a mutex to ensure tests don't run in parallel when they modify shared state
275	lazy_static! {
276		static ref TEST_MUTEX: Mutex<()> = Mutex::new(());
277	}
278
279	// Reset all metrics to a known state
280	fn reset_all_metrics() {
281		// System metrics
282		CPU_USAGE.set(0.0);
283		MEMORY_USAGE.set(0.0);
284		MEMORY_USAGE_PERCENT.set(0.0);
285		TOTAL_MEMORY.set(0.0);
286		AVAILABLE_MEMORY.set(0.0);
287		DISK_USAGE.set(0.0);
288		DISK_USAGE_PERCENT.set(0.0);
289
290		// Monitoring metrics
291		MONITORS_TOTAL.set(0.0);
292		MONITORS_ACTIVE.set(0.0);
293		TRIGGERS_TOTAL.set(0.0);
294		CONTRACTS_MONITORED.set(0.0);
295		NETWORKS_MONITORED.set(0.0);
296		NETWORK_MONITORS.reset();
297	}
298
299	// Helper function to create a test network
300	fn create_test_network(slug: &str, name: &str, chain_id: u64) -> Network {
301		NetworkBuilder::new()
302			.name(name)
303			.slug(slug)
304			.network_type(BlockChainType::EVM)
305			.chain_id(chain_id)
306			.rpc_url(&format!("https://{}.example.com", slug))
307			.block_time_ms(15000)
308			.confirmation_blocks(12)
309			.cron_schedule("*/15 * * * * *")
310			.max_past_blocks(1000)
311			.store_blocks(true)
312			.build()
313	}
314
315	// Helper function to create a test monitor
316	fn create_test_monitor(
317		name: &str,
318		networks: Vec<String>,
319		addresses: Vec<String>,
320		paused: bool,
321	) -> Monitor {
322		MonitorBuilder::new()
323			.name(name)
324			.networks(networks)
325			.paused(paused)
326			.addresses(addresses)
327			.function("transfer(address,uint256)", None)
328			.transaction(TransactionStatus::Success, None)
329			.build()
330	}
331
332	fn create_test_trigger(name: &str) -> Trigger {
333		TriggerBuilder::new()
334			.name(name)
335			.email(
336				"smtp.example.com",
337				"user@example.com",
338				"password123",
339				"alerts@example.com",
340				vec!["user@example.com"],
341			)
342			.message("Alert", "Something happened!")
343			.build()
344	}
345
346	#[test]
347	fn test_gather_metrics_contains_expected_names() {
348		let _lock = TEST_MUTEX.lock().unwrap();
349		reset_all_metrics();
350
351		// Initialize all metrics with non-zero values to ensure they appear in output
352		CPU_USAGE.set(50.0);
353		MEMORY_USAGE_PERCENT.set(60.0);
354		MEMORY_USAGE.set(1024.0);
355		TOTAL_MEMORY.set(2048.0);
356		AVAILABLE_MEMORY.set(1024.0);
357		DISK_USAGE.set(512.0);
358		DISK_USAGE_PERCENT.set(25.0);
359		MONITORS_TOTAL.set(5.0);
360		MONITORS_ACTIVE.set(3.0);
361		TRIGGERS_TOTAL.set(2.0);
362		CONTRACTS_MONITORED.set(4.0);
363		NETWORKS_MONITORED.set(2.0);
364		NETWORK_MONITORS.with_label_values(&["test"]).set(1.0);
365
366		let metrics = gather_metrics().expect("failed to gather metrics");
367		let output = String::from_utf8(metrics).expect("metrics output is not valid UTF-8");
368
369		// Check for system metrics
370		assert!(output.contains("cpu_usage_percentage"));
371		assert!(output.contains("memory_usage_percentage"));
372		assert!(output.contains("memory_usage_bytes"));
373		assert!(output.contains("total_memory_bytes"));
374		assert!(output.contains("available_memory_bytes"));
375		assert!(output.contains("disk_usage_bytes"));
376		assert!(output.contains("disk_usage_percentage"));
377
378		// Check for monitoring metrics
379		assert!(output.contains("monitors_total"));
380		assert!(output.contains("monitors_active"));
381		assert!(output.contains("triggers_total"));
382		assert!(output.contains("contracts_monitored"));
383		assert!(output.contains("networks_monitored"));
384		assert!(output.contains("network_monitors"));
385	}
386
387	#[test]
388	fn test_system_metrics_update() {
389		let _lock = TEST_MUTEX.lock().unwrap();
390		reset_all_metrics();
391
392		// Update metrics
393		update_system_metrics();
394
395		// Verify metrics were updated with reasonable values
396		let cpu_usage = CPU_USAGE.get();
397		assert!((0.0..=100.0).contains(&cpu_usage));
398
399		let memory_usage = MEMORY_USAGE.get();
400		assert!(memory_usage >= 0.0);
401
402		let memory_percent = MEMORY_USAGE_PERCENT.get();
403		assert!((0.0..=100.0).contains(&memory_percent));
404
405		let total_memory = TOTAL_MEMORY.get();
406		assert!(total_memory > 0.0);
407
408		let available_memory = AVAILABLE_MEMORY.get();
409		assert!(available_memory >= 0.0);
410
411		let disk_usage = DISK_USAGE.get();
412		assert!(disk_usage >= 0.0);
413
414		let disk_percent = DISK_USAGE_PERCENT.get();
415		assert!((0.0..=100.0).contains(&disk_percent));
416
417		// Verify that memory usage doesn't exceed total memory
418		assert!(memory_usage <= total_memory);
419
420		// Verify that available memory doesn't exceed total memory
421		assert!(available_memory <= total_memory);
422	}
423
424	#[test]
425	fn test_monitoring_metrics_update() {
426		let _lock = TEST_MUTEX.lock().unwrap();
427		reset_all_metrics();
428
429		// Create test data
430		let mut monitors = HashMap::new();
431		let mut networks = HashMap::new();
432		let triggers = HashMap::new();
433
434		// Add test networks
435		networks.insert(
436			"ethereum".to_string(),
437			create_test_network("ethereum", "Ethereum", 1),
438		);
439		networks.insert(
440			"polygon".to_string(),
441			create_test_network("polygon", "Polygon", 137),
442		);
443		networks.insert(
444			"arbitrum".to_string(),
445			create_test_network("arbitrum", "Arbitrum", 42161),
446		);
447
448		// Add test monitors
449		monitors.insert(
450			"monitor1".to_string(),
451			create_test_monitor(
452				"Test Monitor 1",
453				vec!["ethereum".to_string()],
454				vec!["0x1234567890123456789012345678901234567890".to_string()],
455				false,
456			),
457		);
458
459		monitors.insert(
460			"monitor2".to_string(),
461			create_test_monitor(
462				"Test Monitor 2",
463				vec!["polygon".to_string(), "ethereum".to_string()],
464				vec!["0x0987654321098765432109876543210987654321".to_string()],
465				true,
466			),
467		);
468
469		monitors.insert(
470			"monitor3".to_string(),
471			create_test_monitor(
472				"Test Monitor 3",
473				vec!["arbitrum".to_string()],
474				vec![
475					"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(),
476					"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(),
477				],
478				false,
479			),
480		);
481
482		// Update metrics
483		update_monitoring_metrics(&monitors, &triggers, &networks);
484
485		// Verify metrics
486		assert_eq!(MONITORS_TOTAL.get(), 3.0);
487		assert_eq!(MONITORS_ACTIVE.get(), 2.0);
488		assert_eq!(TRIGGERS_TOTAL.get(), 0.0);
489		assert_eq!(CONTRACTS_MONITORED.get(), 5.0);
490		assert_eq!(NETWORKS_MONITORED.get(), 2.0);
491
492		// Check network-specific metrics
493		let ethereum_monitors = NETWORK_MONITORS
494			.get_metric_with_label_values(&["ethereum"])
495			.unwrap();
496		assert_eq!(ethereum_monitors.get(), 1.0);
497
498		let polygon_monitors = NETWORK_MONITORS
499			.get_metric_with_label_values(&["polygon"])
500			.unwrap();
501		assert_eq!(polygon_monitors.get(), 0.0);
502
503		let arbitrum_monitors = NETWORK_MONITORS
504			.get_metric_with_label_values(&["arbitrum"])
505			.unwrap();
506		assert_eq!(arbitrum_monitors.get(), 1.0);
507	}
508
509	#[test]
510	fn test_nonexistent_networks_are_ignored() {
511		let _lock = TEST_MUTEX.lock().unwrap();
512		reset_all_metrics();
513
514		// Create test data with a monitor referencing a non-existent network
515		let mut monitors = HashMap::new();
516		let mut networks = HashMap::new();
517		let triggers = HashMap::new();
518
519		networks.insert(
520			"ethereum".to_string(),
521			create_test_network("ethereum", "Ethereum", 1),
522		);
523
524		monitors.insert(
525			"monitor1".to_string(),
526			create_test_monitor(
527				"Test Monitor 1",
528				vec!["ethereum".to_string(), "nonexistent_network".to_string()],
529				vec!["0x1234567890123456789012345678901234567890".to_string()],
530				false,
531			),
532		);
533
534		// Update metrics
535		update_monitoring_metrics(&monitors, &triggers, &networks);
536
537		// Verify metrics
538		assert_eq!(NETWORKS_MONITORED.get(), 1.0);
539		assert_eq!(CONTRACTS_MONITORED.get(), 1.0);
540
541		// The nonexistent network should not have a metric
542		let nonexistent = NETWORK_MONITORS.get_metric_with_label_values(&["nonexistent_network"]);
543		assert!(nonexistent.is_err() || nonexistent.unwrap().get() == 0.0);
544	}
545
546	#[test]
547	fn test_multiple_monitors_same_network() {
548		let _lock = TEST_MUTEX.lock().unwrap();
549		reset_all_metrics();
550
551		// Create test data with multiple monitors on the same network
552		let mut monitors = HashMap::new();
553		let mut networks = HashMap::new();
554		let triggers = HashMap::new();
555
556		networks.insert(
557			"ethereum".to_string(),
558			create_test_network("ethereum", "Ethereum", 1),
559		);
560
561		// Add three monitors all watching Ethereum
562		monitors.insert(
563			"monitor1".to_string(),
564			create_test_monitor(
565				"Test Monitor 1",
566				vec!["ethereum".to_string()],
567				vec!["0x1111111111111111111111111111111111111111".to_string()],
568				false,
569			),
570		);
571
572		monitors.insert(
573			"monitor2".to_string(),
574			create_test_monitor(
575				"Test Monitor 2",
576				vec!["ethereum".to_string()],
577				vec!["0x2222222222222222222222222222222222222222".to_string()],
578				false,
579			),
580		);
581
582		monitors.insert(
583			"monitor3".to_string(),
584			create_test_monitor(
585				"Test Monitor 3",
586				vec!["ethereum".to_string()],
587				vec!["0x3333333333333333333333333333333333333333".to_string()],
588				true, // This one is paused
589			),
590		);
591
592		// Update metrics
593		update_monitoring_metrics(&monitors, &triggers, &networks);
594
595		// Verify metrics
596		assert_eq!(MONITORS_TOTAL.get(), 3.0);
597		assert_eq!(MONITORS_ACTIVE.get(), 2.0);
598		assert_eq!(NETWORKS_MONITORED.get(), 1.0);
599
600		// Check network-specific metrics
601		let ethereum_monitors = NETWORK_MONITORS
602			.get_metric_with_label_values(&["ethereum"])
603			.unwrap();
604		assert_eq!(ethereum_monitors.get(), 2.0);
605	}
606
607	#[test]
608	fn test_multiple_contracts_per_monitor() {
609		let _lock = TEST_MUTEX.lock().unwrap();
610		reset_all_metrics();
611
612		// Create test data with a monitor watching multiple contracts
613		let mut monitors = HashMap::new();
614		let mut networks = HashMap::new();
615		let triggers = HashMap::new();
616
617		networks.insert(
618			"ethereum".to_string(),
619			create_test_network("ethereum", "Ethereum", 1),
620		);
621
622		// Add a monitor watching multiple contracts
623		monitors.insert(
624			"monitor1".to_string(),
625			create_test_monitor(
626				"Test Monitor 1",
627				vec!["ethereum".to_string()],
628				vec![
629					"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(),
630					"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(),
631					"0xcccccccccccccccccccccccccccccccccccccccc".to_string(),
632				],
633				false,
634			),
635		);
636
637		// Update metrics
638		update_monitoring_metrics(&monitors, &triggers, &networks);
639
640		// Verify metrics
641		assert_eq!(CONTRACTS_MONITORED.get(), 3.0);
642	}
643
644	#[test]
645	fn test_triggers_count() {
646		let _lock = TEST_MUTEX
647			.lock()
648			.unwrap_or_else(|poisoned| poisoned.into_inner());
649		reset_all_metrics();
650
651		// Create test data with triggers
652		let monitors = HashMap::new();
653		let networks = HashMap::new();
654		let mut triggers = HashMap::new();
655
656		// Add some triggers
657		triggers.insert("trigger1".to_string(), create_test_trigger("trigger1"));
658		triggers.insert("trigger2".to_string(), create_test_trigger("trigger2"));
659		triggers.insert("trigger3".to_string(), create_test_trigger("trigger3"));
660
661		// Update metrics
662		update_monitoring_metrics(&monitors, &triggers, &networks);
663
664		// Verify metrics
665		let total_triggers = TRIGGERS_TOTAL.get();
666		assert_eq!(total_triggers, 3.0);
667
668		// Verify other metrics are zero since we have no monitors or networks
669		assert_eq!(MONITORS_TOTAL.get(), 0.0);
670		assert_eq!(MONITORS_ACTIVE.get(), 0.0);
671		assert_eq!(CONTRACTS_MONITORED.get(), 0.0);
672		assert_eq!(NETWORKS_MONITORED.get(), 0.0);
673	}
674
675	#[test]
676	fn test_empty_collections() {
677		let _lock = TEST_MUTEX.lock().unwrap();
678
679		// Test with empty collections
680		let monitors = HashMap::new();
681		let networks = HashMap::new();
682		let triggers = HashMap::new();
683
684		// Reset metrics to non-zero values
685		MONITORS_TOTAL.set(10.0);
686		MONITORS_ACTIVE.set(5.0);
687		TRIGGERS_TOTAL.set(3.0);
688		CONTRACTS_MONITORED.set(7.0);
689		NETWORKS_MONITORED.set(2.0);
690		NETWORK_MONITORS.reset();
691
692		// Set a value for a network that doesn't exist
693		NETWORK_MONITORS.with_label_values(&["test"]).set(3.0);
694
695		// Update metrics
696		update_monitoring_metrics(&monitors, &triggers, &networks);
697
698		// Verify all metrics are reset to zero
699		assert_eq!(MONITORS_TOTAL.get(), 0.0);
700		assert_eq!(MONITORS_ACTIVE.get(), 0.0);
701		assert_eq!(TRIGGERS_TOTAL.get(), 0.0);
702		assert_eq!(CONTRACTS_MONITORED.get(), 0.0);
703		assert_eq!(NETWORKS_MONITORED.get(), 0.0);
704
705		// The test network should have been reset
706		let test_network = NETWORK_MONITORS
707			.get_metric_with_label_values(&["test"])
708			.unwrap();
709		assert_eq!(test_network.get(), 0.0);
710	}
711}