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 expected_percentage = if total_memory > 0.0 {
409			(memory_usage / total_memory) * 100.0
410		} else {
411			0.0
412		};
413		assert_eq!(memory_percent, expected_percentage);
414
415		let available_memory = AVAILABLE_MEMORY.get();
416		assert!(available_memory >= 0.0);
417
418		let disk_usage = DISK_USAGE.get();
419		assert!(disk_usage >= 0.0);
420
421		let disk_percent = DISK_USAGE_PERCENT.get();
422		assert!((0.0..=100.0).contains(&disk_percent));
423
424		// Verify that memory usage doesn't exceed total memory
425		assert!(memory_usage <= total_memory);
426
427		// Verify that available memory doesn't exceed total memory
428		assert!(available_memory <= total_memory);
429	}
430
431	#[test]
432	fn test_monitoring_metrics_update() {
433		let _lock = TEST_MUTEX.lock().unwrap();
434		reset_all_metrics();
435
436		// Create test data
437		let mut monitors = HashMap::new();
438		let mut networks = HashMap::new();
439		let triggers = HashMap::new();
440
441		// Add test networks
442		networks.insert(
443			"ethereum".to_string(),
444			create_test_network("ethereum", "Ethereum", 1),
445		);
446		networks.insert(
447			"polygon".to_string(),
448			create_test_network("polygon", "Polygon", 137),
449		);
450		networks.insert(
451			"arbitrum".to_string(),
452			create_test_network("arbitrum", "Arbitrum", 42161),
453		);
454
455		// Add test monitors
456		monitors.insert(
457			"monitor1".to_string(),
458			create_test_monitor(
459				"Test Monitor 1",
460				vec!["ethereum".to_string()],
461				vec!["0x1234567890123456789012345678901234567890".to_string()],
462				false,
463			),
464		);
465
466		monitors.insert(
467			"monitor2".to_string(),
468			create_test_monitor(
469				"Test Monitor 2",
470				vec!["polygon".to_string(), "ethereum".to_string()],
471				vec!["0x0987654321098765432109876543210987654321".to_string()],
472				true,
473			),
474		);
475
476		monitors.insert(
477			"monitor3".to_string(),
478			create_test_monitor(
479				"Test Monitor 3",
480				vec!["arbitrum".to_string()],
481				vec![
482					"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(),
483					"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(),
484				],
485				false,
486			),
487		);
488
489		// Update metrics
490		update_monitoring_metrics(&monitors, &triggers, &networks);
491
492		// Verify metrics
493		assert_eq!(MONITORS_TOTAL.get(), 3.0);
494		assert_eq!(MONITORS_ACTIVE.get(), 2.0);
495		assert_eq!(TRIGGERS_TOTAL.get(), 0.0);
496		assert_eq!(CONTRACTS_MONITORED.get(), 5.0);
497		assert_eq!(NETWORKS_MONITORED.get(), 2.0);
498
499		// Check network-specific metrics
500		let ethereum_monitors = NETWORK_MONITORS
501			.get_metric_with_label_values(&["ethereum"])
502			.unwrap();
503		assert_eq!(ethereum_monitors.get(), 1.0);
504
505		let polygon_monitors = NETWORK_MONITORS
506			.get_metric_with_label_values(&["polygon"])
507			.unwrap();
508		assert_eq!(polygon_monitors.get(), 0.0);
509
510		let arbitrum_monitors = NETWORK_MONITORS
511			.get_metric_with_label_values(&["arbitrum"])
512			.unwrap();
513		assert_eq!(arbitrum_monitors.get(), 1.0);
514	}
515
516	#[test]
517	fn test_nonexistent_networks_are_ignored() {
518		let _lock = TEST_MUTEX.lock().unwrap();
519		reset_all_metrics();
520
521		// Create test data with a monitor referencing a non-existent network
522		let mut monitors = HashMap::new();
523		let mut networks = HashMap::new();
524		let triggers = HashMap::new();
525
526		networks.insert(
527			"ethereum".to_string(),
528			create_test_network("ethereum", "Ethereum", 1),
529		);
530
531		monitors.insert(
532			"monitor1".to_string(),
533			create_test_monitor(
534				"Test Monitor 1",
535				vec!["ethereum".to_string(), "nonexistent_network".to_string()],
536				vec!["0x1234567890123456789012345678901234567890".to_string()],
537				false,
538			),
539		);
540
541		// Update metrics
542		update_monitoring_metrics(&monitors, &triggers, &networks);
543
544		// Verify metrics
545		assert_eq!(NETWORKS_MONITORED.get(), 1.0);
546		assert_eq!(CONTRACTS_MONITORED.get(), 1.0);
547
548		// The nonexistent network should not have a metric
549		let nonexistent = NETWORK_MONITORS.get_metric_with_label_values(&["nonexistent_network"]);
550		assert!(nonexistent.is_err() || nonexistent.unwrap().get() == 0.0);
551	}
552
553	#[test]
554	fn test_multiple_monitors_same_network() {
555		let _lock = TEST_MUTEX.lock().unwrap();
556		reset_all_metrics();
557
558		// Create test data with multiple monitors on the same network
559		let mut monitors = HashMap::new();
560		let mut networks = HashMap::new();
561		let triggers = HashMap::new();
562
563		networks.insert(
564			"ethereum".to_string(),
565			create_test_network("ethereum", "Ethereum", 1),
566		);
567
568		// Add three monitors all watching Ethereum
569		monitors.insert(
570			"monitor1".to_string(),
571			create_test_monitor(
572				"Test Monitor 1",
573				vec!["ethereum".to_string()],
574				vec!["0x1111111111111111111111111111111111111111".to_string()],
575				false,
576			),
577		);
578
579		monitors.insert(
580			"monitor2".to_string(),
581			create_test_monitor(
582				"Test Monitor 2",
583				vec!["ethereum".to_string()],
584				vec!["0x2222222222222222222222222222222222222222".to_string()],
585				false,
586			),
587		);
588
589		monitors.insert(
590			"monitor3".to_string(),
591			create_test_monitor(
592				"Test Monitor 3",
593				vec!["ethereum".to_string()],
594				vec!["0x3333333333333333333333333333333333333333".to_string()],
595				true, // This one is paused
596			),
597		);
598
599		// Update metrics
600		update_monitoring_metrics(&monitors, &triggers, &networks);
601
602		// Verify metrics
603		assert_eq!(MONITORS_TOTAL.get(), 3.0);
604		assert_eq!(MONITORS_ACTIVE.get(), 2.0);
605		assert_eq!(NETWORKS_MONITORED.get(), 1.0);
606
607		// Check network-specific metrics
608		let ethereum_monitors = NETWORK_MONITORS
609			.get_metric_with_label_values(&["ethereum"])
610			.unwrap();
611		assert_eq!(ethereum_monitors.get(), 2.0);
612	}
613
614	#[test]
615	fn test_multiple_contracts_per_monitor() {
616		let _lock = TEST_MUTEX.lock().unwrap();
617		reset_all_metrics();
618
619		// Create test data with a monitor watching multiple contracts
620		let mut monitors = HashMap::new();
621		let mut networks = HashMap::new();
622		let triggers = HashMap::new();
623
624		networks.insert(
625			"ethereum".to_string(),
626			create_test_network("ethereum", "Ethereum", 1),
627		);
628
629		// Add a monitor watching multiple contracts
630		monitors.insert(
631			"monitor1".to_string(),
632			create_test_monitor(
633				"Test Monitor 1",
634				vec!["ethereum".to_string()],
635				vec![
636					"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(),
637					"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(),
638					"0xcccccccccccccccccccccccccccccccccccccccc".to_string(),
639				],
640				false,
641			),
642		);
643
644		// Update metrics
645		update_monitoring_metrics(&monitors, &triggers, &networks);
646
647		// Verify metrics
648		assert_eq!(CONTRACTS_MONITORED.get(), 3.0);
649	}
650
651	#[test]
652	fn test_triggers_count() {
653		let _lock = TEST_MUTEX
654			.lock()
655			.unwrap_or_else(|poisoned| poisoned.into_inner());
656		reset_all_metrics();
657
658		// Create test data with triggers
659		let monitors = HashMap::new();
660		let networks = HashMap::new();
661		let mut triggers = HashMap::new();
662
663		// Add some triggers
664		triggers.insert("trigger1".to_string(), create_test_trigger("trigger1"));
665		triggers.insert("trigger2".to_string(), create_test_trigger("trigger2"));
666		triggers.insert("trigger3".to_string(), create_test_trigger("trigger3"));
667
668		// Update metrics
669		update_monitoring_metrics(&monitors, &triggers, &networks);
670
671		// Verify metrics
672		let total_triggers = TRIGGERS_TOTAL.get();
673		assert_eq!(total_triggers, 3.0);
674
675		// Verify other metrics are zero since we have no monitors or networks
676		assert_eq!(MONITORS_TOTAL.get(), 0.0);
677		assert_eq!(MONITORS_ACTIVE.get(), 0.0);
678		assert_eq!(CONTRACTS_MONITORED.get(), 0.0);
679		assert_eq!(NETWORKS_MONITORED.get(), 0.0);
680	}
681
682	#[test]
683	fn test_empty_collections() {
684		let _lock = TEST_MUTEX.lock().unwrap();
685
686		// Test with empty collections
687		let monitors = HashMap::new();
688		let networks = HashMap::new();
689		let triggers = HashMap::new();
690
691		// Reset metrics to non-zero values
692		MONITORS_TOTAL.set(10.0);
693		MONITORS_ACTIVE.set(5.0);
694		TRIGGERS_TOTAL.set(3.0);
695		CONTRACTS_MONITORED.set(7.0);
696		NETWORKS_MONITORED.set(2.0);
697		NETWORK_MONITORS.reset();
698
699		// Set a value for a network that doesn't exist
700		NETWORK_MONITORS.with_label_values(&["test"]).set(3.0);
701
702		// Update metrics
703		update_monitoring_metrics(&monitors, &triggers, &networks);
704
705		// Verify all metrics are reset to zero
706		assert_eq!(MONITORS_TOTAL.get(), 0.0);
707		assert_eq!(MONITORS_ACTIVE.get(), 0.0);
708		assert_eq!(TRIGGERS_TOTAL.get(), 0.0);
709		assert_eq!(CONTRACTS_MONITORED.get(), 0.0);
710		assert_eq!(NETWORKS_MONITORED.get(), 0.0);
711
712		// The test network should have been reset
713		let test_network = NETWORK_MONITORS
714			.get_metric_with_label_values(&["test"])
715			.unwrap();
716		assert_eq!(test_network.get(), 0.0);
717	}
718}