From 61d4796d84960ec9f25392635afceea3a3bd0916 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Thu, 11 Dec 2025 08:47:08 +0000 Subject: refactor: move metrics tests to tests/sync/ structure (Phase 7) --- tests/common/sync_helpers.rs | 31 ++++ tests/proactive_sync_metrics.rs | 364 ---------------------------------------- tests/sync.rs | 2 + tests/sync/metrics.rs | 334 ++++++++++++++++++++++++++++++++++++ tests/sync/mod.rs | 4 + 5 files changed, 371 insertions(+), 364 deletions(-) delete mode 100644 tests/proactive_sync_metrics.rs create mode 100644 tests/sync/metrics.rs (limited to 'tests') diff --git a/tests/common/sync_helpers.rs b/tests/common/sync_helpers.rs index 50d0d7a..7788783 100644 --- a/tests/common/sync_helpers.rs +++ b/tests/common/sync_helpers.rs @@ -561,6 +561,37 @@ pub fn repo_coord(keys: &Keys, identifier: &str) -> String { ) } +// ============================================================================ +// Metrics Helpers +// ============================================================================ + +/// Fetch Prometheus metrics from a relay's `/metrics` endpoint. +/// +/// Converts the WebSocket URL to HTTP and fetches the metrics endpoint. +/// Useful for verifying sync-related metrics in tests. +/// +/// # Arguments +/// * `relay_url` - WebSocket URL of the relay (e.g., "ws://127.0.0.1:8080") +/// +/// # Returns +/// * `Ok(String)` - The metrics text in Prometheus format +/// * `Err(reqwest::Error)` - If the request fails +/// +/// # Example +/// ```ignore +/// let metrics = fetch_metrics("ws://127.0.0.1:8080").await?; +/// assert!(metrics.contains("ngit_sync_")); +/// ``` +pub async fn fetch_metrics(relay_url: &str) -> Result { + // Convert ws:// URL to http:// for metrics endpoint + let http_url = relay_url + .replace("ws://", "http://") + .replace("/", "") + + "/metrics"; + + reqwest::get(&http_url).await?.text().await +} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/proactive_sync_metrics.rs b/tests/proactive_sync_metrics.rs deleted file mode 100644 index 32abe74..0000000 --- a/tests/proactive_sync_metrics.rs +++ /dev/null @@ -1,364 +0,0 @@ -//! GRASP-02 Phase 6: Proactive Sync Metrics Integration Tests -//! -//! Tests the Prometheus metrics integration for proactive sync: -//! - All sync metrics exposed at `/metrics` endpoint -//! - Connection metrics update correctly -//! - Health state metrics reflect actual state -//! - Gap events tracked correctly -//! - Load test with 3+ relays -//! -//! # Running Tests -//! -//! ```bash -//! cargo test --test proactive_sync_metrics -//! cargo test --test proactive_sync_metrics -- --nocapture -//! ``` - -mod common; - -use std::time::Duration; - -use common::TestRelay; -use nostr_sdk::prelude::*; - -/// Kind 30617 - Repository State (NIP-34) -const KIND_REPOSITORY_STATE: u16 = 30617; - -/// Create a valid repository announcement event for testing -fn create_valid_repo_announcement(keys: &Keys, domain: &str, identifier: &str) -> Event { - let tags = vec![ - Tag::identifier(identifier), - Tag::custom( - TagKind::custom("clone"), - vec![format!("http://{}/{}", domain, identifier)], - ), - Tag::custom(TagKind::custom("relays"), vec![format!("ws://{}", domain)]), - ]; - - EventBuilder::new(Kind::Custom(KIND_REPOSITORY_STATE), "Repository state") - .tags(tags) - .sign_with_keys(keys) - .expect("Failed to sign event") -} - -/// Helper to fetch metrics from a relay's HTTP endpoint -async fn fetch_metrics(relay: &TestRelay) -> Result { - // Extract host:port from ws:// URL - let ws_url = relay.url(); - let http_url = ws_url.replace("ws://", "http://").replace("/", "") + "/metrics"; - - reqwest::get(&http_url).await?.text().await -} - -/// Test that sync metrics are exposed at /metrics endpoint -#[tokio::test] -async fn test_sync_metrics_exposed() { - let relay = TestRelay::start().await; - - // Give time for relay to start - tokio::time::sleep(Duration::from_millis(500)).await; - - // Fetch metrics - let metrics_result = fetch_metrics(&relay).await; - - relay.stop().await; - - // Check that we got metrics (even if sync isn't configured) - let metrics = metrics_result.expect("Failed to fetch metrics"); - - // Verify basic metrics structure exists - assert!( - metrics.contains("ngit_") || metrics.contains("# HELP"), - "Metrics endpoint should return Prometheus metrics" - ); -} - -/// Test that sync metrics include expected metric names -#[tokio::test] -async fn test_sync_metric_names_present() { - // Start a relay with sync configured - let source_relay = TestRelay::start().await; - let sync_relay = TestRelay::start_with_sync(Some(source_relay.url().into())).await; - - // Give time for sync connection to attempt - tokio::time::sleep(Duration::from_secs(2)).await; - - // Fetch metrics from the syncing relay - let metrics = fetch_metrics(&sync_relay) - .await - .expect("Failed to fetch metrics"); - - sync_relay.stop().await; - source_relay.stop().await; - - // Check for expected sync metric names (they may have zero values) - // At minimum, the ngit_ prefix metrics should be present - assert!( - metrics.contains("ngit_"), - "Metrics should include ngit_ prefixed metrics" - ); -} - -/// Test connection metrics update correctly on successful connection -#[tokio::test] -async fn test_connection_metrics_on_success() { - // Start source relay - let source_relay = TestRelay::start().await; - tokio::time::sleep(Duration::from_millis(200)).await; - - // Start syncing relay - let sync_relay = TestRelay::start_with_sync(Some(source_relay.url().into())).await; - - // Wait for connection to establish - tokio::time::sleep(Duration::from_secs(2)).await; - - // Fetch metrics - we can verify the relay started and metrics endpoint works - let metrics = fetch_metrics(&sync_relay) - .await - .expect("Failed to fetch metrics"); - - sync_relay.stop().await; - source_relay.stop().await; - - // Verify metrics endpoint returned data - assert!(!metrics.is_empty(), "Metrics endpoint should return data"); -} - -/// Test that events syncing updates metrics -#[tokio::test] -async fn test_event_sync_metrics() { - // Start source relay - let source_relay = TestRelay::start().await; - tokio::time::sleep(Duration::from_millis(200)).await; - - // Start syncing relay - let sync_relay = TestRelay::start_with_sync(Some(source_relay.url().into())).await; - - // Wait for connection - tokio::time::sleep(Duration::from_secs(1)).await; - - // Create and submit an event to source relay - let keys = Keys::generate(); - let event = create_valid_repo_announcement(&keys, &source_relay.domain(), "metrics-test-repo"); - - let client = Client::default(); - client - .add_relay(source_relay.url()) - .await - .expect("Failed to add relay"); - client.connect().await; - - let _ = client.send_event(&event).await; - - // Wait for sync to occur - tokio::time::sleep(Duration::from_secs(2)).await; - - // Fetch metrics from sync relay - let metrics = fetch_metrics(&sync_relay) - .await - .expect("Failed to fetch metrics"); - - client.disconnect().await; - sync_relay.stop().await; - source_relay.stop().await; - - // Verify metrics endpoint returned data after sync activity - assert!( - !metrics.is_empty(), - "Metrics should be present after sync activity" - ); -} - -/// Test health state tracking in metrics -#[tokio::test] -async fn test_health_state_metrics() { - // Start a syncing relay pointing to a non-existent source - // This will result in connection failures and health state changes - let sync_relay = TestRelay::start_with_sync(Some("ws://127.0.0.1:19999".into())).await; - - // Wait for some connection attempts - tokio::time::sleep(Duration::from_secs(3)).await; - - // Fetch metrics - let metrics = fetch_metrics(&sync_relay) - .await - .expect("Failed to fetch metrics"); - - sync_relay.stop().await; - - // The relay should still be operational even with failed sync - assert!( - !metrics.is_empty(), - "Metrics should be present even with sync failures" - ); -} - -/// Test gap event tracking (events received during catchup) -#[tokio::test] -async fn test_gap_event_tracking() { - // Start source relay and add some events first - let source_relay = TestRelay::start().await; - tokio::time::sleep(Duration::from_millis(200)).await; - - let keys = Keys::generate(); - - // Submit event before sync relay starts - let event = create_valid_repo_announcement(&keys, &source_relay.domain(), "pre-existing-repo"); - - let client = Client::default(); - client - .add_relay(source_relay.url()) - .await - .expect("Failed to add relay"); - client.connect().await; - let _ = client.send_event(&event).await; - - // Now start syncing relay - it should catch up on existing events - let sync_relay = TestRelay::start_with_sync(Some(source_relay.url().into())).await; - - // Wait for catchup - tokio::time::sleep(Duration::from_secs(3)).await; - - // Fetch metrics - let metrics = fetch_metrics(&sync_relay) - .await - .expect("Failed to fetch metrics"); - - client.disconnect().await; - sync_relay.stop().await; - source_relay.stop().await; - - // Verify metrics exist after gap sync scenario - assert!( - !metrics.is_empty(), - "Metrics should track gap sync activity" - ); -} - -/// Load test with 3+ relays configured for sync -#[tokio::test] -async fn test_multi_relay_load() { - // Start 3 source relays - let source_relay_1 = TestRelay::start().await; - let source_relay_2 = TestRelay::start().await; - let source_relay_3 = TestRelay::start().await; - - tokio::time::sleep(Duration::from_millis(500)).await; - - // Start a syncing relay pointing to first source - // Note: The current implementation only supports single sync relay URL - // but the test demonstrates the system handles multiple relay scenarios - let sync_relay = TestRelay::start_with_sync(Some(source_relay_1.url().into())).await; - - // Wait for connections - tokio::time::sleep(Duration::from_secs(2)).await; - - // Submit events to all source relays - let keys = Keys::generate(); - - let event1 = create_valid_repo_announcement(&keys, &source_relay_1.domain(), "repo-1"); - let event2 = create_valid_repo_announcement(&keys, &source_relay_2.domain(), "repo-2"); - let event3 = create_valid_repo_announcement(&keys, &source_relay_3.domain(), "repo-3"); - - // Submit events - let client1 = Client::default(); - client1 - .add_relay(source_relay_1.url()) - .await - .expect("Failed to add relay"); - client1.connect().await; - let _ = client1.send_event(&event1).await; - - let client2 = Client::default(); - client2 - .add_relay(source_relay_2.url()) - .await - .expect("Failed to add relay"); - client2.connect().await; - let _ = client2.send_event(&event2).await; - - let client3 = Client::default(); - client3 - .add_relay(source_relay_3.url()) - .await - .expect("Failed to add relay"); - client3.connect().await; - let _ = client3.send_event(&event3).await; - - // Wait for sync - tokio::time::sleep(Duration::from_secs(3)).await; - - // Fetch metrics from sync relay - let metrics = fetch_metrics(&sync_relay) - .await - .expect("Failed to fetch metrics"); - - // Cleanup - client1.disconnect().await; - client2.disconnect().await; - client3.disconnect().await; - sync_relay.stop().await; - source_relay_1.stop().await; - source_relay_2.stop().await; - source_relay_3.stop().await; - - // Verify metrics system handled load - assert!( - !metrics.is_empty(), - "Metrics should be available under multi-relay load" - ); -} - -/// Test that Prometheus text format is valid -#[tokio::test] -async fn test_prometheus_format_valid() { - let relay = TestRelay::start().await; - tokio::time::sleep(Duration::from_millis(500)).await; - - let metrics = fetch_metrics(&relay) - .await - .expect("Failed to fetch metrics"); - - relay.stop().await; - - // Check for valid Prometheus format markers - // - Lines starting with # are comments (HELP, TYPE) - // - Metric lines have format: metric_name{labels} value - let lines: Vec<&str> = metrics.lines().collect(); - - // Should have some content - assert!(!lines.is_empty(), "Metrics should have content"); - - // Check for at least some standard Prometheus patterns - let has_help = lines.iter().any(|l| l.starts_with("# HELP")); - let has_type = lines.iter().any(|l| l.starts_with("# TYPE")); - - // At minimum we expect help/type comments for any registered metrics - assert!( - has_help || has_type || lines.iter().any(|l| l.contains("ngit_")), - "Metrics should contain Prometheus format elements" - ); -} - -/// Test metrics endpoint availability during sync operations -#[tokio::test] -async fn test_metrics_availability_during_sync() { - let source_relay = TestRelay::start().await; - let sync_relay = TestRelay::start_with_sync(Some(source_relay.url().into())).await; - - tokio::time::sleep(Duration::from_millis(500)).await; - - // Make multiple metrics requests while sync is active - for i in 0..3 { - let metrics = fetch_metrics(&sync_relay).await; - assert!( - metrics.is_ok(), - "Metrics request {} should succeed during sync", - i + 1 - ); - tokio::time::sleep(Duration::from_millis(200)).await; - } - - sync_relay.stop().await; - source_relay.stop().await; -} diff --git a/tests/sync.rs b/tests/sync.rs index 2836d8d..5b6b752 100644 --- a/tests/sync.rs +++ b/tests/sync.rs @@ -8,6 +8,7 @@ //! - `live_sync` - Tests for real-time sync after connection established //! - `tag_variations` - Tests for different Layer 2/3 tag types //! - `catchup` - Tests for catchup sync after disconnect (not yet implemented) +//! - `metrics` - Tests for Prometheus metrics integration //! //! # Running Tests //! @@ -34,5 +35,6 @@ mod sync { pub mod catchup; pub mod discovery; pub mod live_sync; + pub mod metrics; pub mod tag_variations; } \ No newline at end of file diff --git a/tests/sync/metrics.rs b/tests/sync/metrics.rs new file mode 100644 index 0000000..dbb9dc0 --- /dev/null +++ b/tests/sync/metrics.rs @@ -0,0 +1,334 @@ +//! Proactive Sync Metrics Tests +//! +//! Tests for Prometheus metrics integration with proactive sync: +//! - All sync metrics exposed at `/metrics` endpoint +//! - Connection metrics update correctly +//! - Health state metrics reflect actual state +//! - Gap events tracked correctly +//! - Load test with 3+ relays +//! +//! # Running Tests +//! +//! ```bash +//! cargo test --test sync metrics +//! cargo test --test sync metrics -- --nocapture +//! ``` + +use std::time::Duration; + +use nostr_sdk::prelude::*; + +use crate::common::{sync_helpers::*, TestRelay}; + +/// Test that sync metrics are exposed at /metrics endpoint +#[tokio::test] +async fn test_sync_metrics_exposed() { + let relay = TestRelay::start().await; + + // Give time for relay to start + tokio::time::sleep(Duration::from_millis(500)).await; + + // Fetch metrics using the shared helper + let metrics_result = fetch_metrics(&relay.url()).await; + + relay.stop().await; + + // Check that we got metrics (even if sync isn't configured) + let metrics = metrics_result.expect("Failed to fetch metrics"); + + // Verify basic metrics structure exists + assert!( + metrics.contains("ngit_") || metrics.contains("# HELP"), + "Metrics endpoint should return Prometheus metrics" + ); +} + +/// Test that sync metrics include expected metric names +#[tokio::test] +async fn test_sync_metric_names_present() { + // Start a relay with sync configured + let source_relay = TestRelay::start().await; + let sync_relay = TestRelay::start_with_sync(Some(source_relay.url().into())).await; + + // Give time for sync connection to attempt + tokio::time::sleep(Duration::from_secs(2)).await; + + // Fetch metrics from the syncing relay + let metrics = fetch_metrics(&sync_relay.url()) + .await + .expect("Failed to fetch metrics"); + + sync_relay.stop().await; + source_relay.stop().await; + + // Check for expected sync metric names (they may have zero values) + // At minimum, the ngit_ prefix metrics should be present + assert!( + metrics.contains("ngit_"), + "Metrics should include ngit_ prefixed metrics" + ); +} + +/// Test connection metrics update correctly on successful connection +#[tokio::test] +async fn test_connection_metrics_on_success() { + // Start source relay + let source_relay = TestRelay::start().await; + tokio::time::sleep(Duration::from_millis(200)).await; + + // Start syncing relay + let sync_relay = TestRelay::start_with_sync(Some(source_relay.url().into())).await; + + // Wait for connection to establish + tokio::time::sleep(Duration::from_secs(2)).await; + + // Fetch metrics - we can verify the relay started and metrics endpoint works + let metrics = fetch_metrics(&sync_relay.url()) + .await + .expect("Failed to fetch metrics"); + + sync_relay.stop().await; + source_relay.stop().await; + + // Verify metrics endpoint returned data + assert!(!metrics.is_empty(), "Metrics endpoint should return data"); +} + +/// Test that events syncing updates metrics +#[tokio::test] +async fn test_event_sync_metrics() { + // Start source relay + let source_relay = TestRelay::start().await; + tokio::time::sleep(Duration::from_millis(200)).await; + + // Start syncing relay + let sync_relay = TestRelay::start_with_sync(Some(source_relay.url().into())).await; + + // Wait for connection + tokio::time::sleep(Duration::from_secs(1)).await; + + // Create and submit an event to source relay + let keys = Keys::generate(); + let event = create_repo_announcement(&keys, &[&source_relay.domain()], "metrics-test-repo"); + + let client = Client::default(); + client + .add_relay(source_relay.url()) + .await + .expect("Failed to add relay"); + client.connect().await; + + let _ = client.send_event(&event).await; + + // Wait for sync to occur + tokio::time::sleep(Duration::from_secs(2)).await; + + // Fetch metrics from sync relay + let metrics = fetch_metrics(&sync_relay.url()) + .await + .expect("Failed to fetch metrics"); + + client.disconnect().await; + sync_relay.stop().await; + source_relay.stop().await; + + // Verify metrics endpoint returned data after sync activity + assert!( + !metrics.is_empty(), + "Metrics should be present after sync activity" + ); +} + +/// Test health state tracking in metrics +#[tokio::test] +async fn test_health_state_metrics() { + // Start a syncing relay pointing to a non-existent source + // This will result in connection failures and health state changes + let sync_relay = TestRelay::start_with_sync(Some("ws://127.0.0.1:19999".into())).await; + + // Wait for some connection attempts + tokio::time::sleep(Duration::from_secs(3)).await; + + // Fetch metrics + let metrics = fetch_metrics(&sync_relay.url()) + .await + .expect("Failed to fetch metrics"); + + sync_relay.stop().await; + + // The relay should still be operational even with failed sync + assert!( + !metrics.is_empty(), + "Metrics should be present even with sync failures" + ); +} + +/// Test gap event tracking (events received during catchup) +#[tokio::test] +async fn test_gap_event_tracking() { + // Start source relay and add some events first + let source_relay = TestRelay::start().await; + tokio::time::sleep(Duration::from_millis(200)).await; + + let keys = Keys::generate(); + + // Submit event before sync relay starts + let event = create_repo_announcement(&keys, &[&source_relay.domain()], "pre-existing-repo"); + + let client = Client::default(); + client + .add_relay(source_relay.url()) + .await + .expect("Failed to add relay"); + client.connect().await; + let _ = client.send_event(&event).await; + + // Now start syncing relay - it should catch up on existing events + let sync_relay = TestRelay::start_with_sync(Some(source_relay.url().into())).await; + + // Wait for catchup + tokio::time::sleep(Duration::from_secs(3)).await; + + // Fetch metrics + let metrics = fetch_metrics(&sync_relay.url()) + .await + .expect("Failed to fetch metrics"); + + client.disconnect().await; + sync_relay.stop().await; + source_relay.stop().await; + + // Verify metrics exist after gap sync scenario + assert!( + !metrics.is_empty(), + "Metrics should track gap sync activity" + ); +} + +/// Load test with 3+ relays configured for sync +#[tokio::test] +async fn test_multi_relay_load() { + // Start 3 source relays + let source_relay_1 = TestRelay::start().await; + let source_relay_2 = TestRelay::start().await; + let source_relay_3 = TestRelay::start().await; + + tokio::time::sleep(Duration::from_millis(500)).await; + + // Start a syncing relay pointing to first source + // Note: The current implementation only supports single sync relay URL + // but the test demonstrates the system handles multiple relay scenarios + let sync_relay = TestRelay::start_with_sync(Some(source_relay_1.url().into())).await; + + // Wait for connections + tokio::time::sleep(Duration::from_secs(2)).await; + + // Submit events to all source relays + let keys = Keys::generate(); + + let event1 = create_repo_announcement(&keys, &[&source_relay_1.domain()], "repo-1"); + let event2 = create_repo_announcement(&keys, &[&source_relay_2.domain()], "repo-2"); + let event3 = create_repo_announcement(&keys, &[&source_relay_3.domain()], "repo-3"); + + // Submit events + let client1 = Client::default(); + client1 + .add_relay(source_relay_1.url()) + .await + .expect("Failed to add relay"); + client1.connect().await; + let _ = client1.send_event(&event1).await; + + let client2 = Client::default(); + client2 + .add_relay(source_relay_2.url()) + .await + .expect("Failed to add relay"); + client2.connect().await; + let _ = client2.send_event(&event2).await; + + let client3 = Client::default(); + client3 + .add_relay(source_relay_3.url()) + .await + .expect("Failed to add relay"); + client3.connect().await; + let _ = client3.send_event(&event3).await; + + // Wait for sync + tokio::time::sleep(Duration::from_secs(3)).await; + + // Fetch metrics from sync relay + let metrics = fetch_metrics(&sync_relay.url()) + .await + .expect("Failed to fetch metrics"); + + // Cleanup + client1.disconnect().await; + client2.disconnect().await; + client3.disconnect().await; + sync_relay.stop().await; + source_relay_1.stop().await; + source_relay_2.stop().await; + source_relay_3.stop().await; + + // Verify metrics system handled load + assert!( + !metrics.is_empty(), + "Metrics should be available under multi-relay load" + ); +} + +/// Test that Prometheus text format is valid +#[tokio::test] +async fn test_prometheus_format_valid() { + let relay = TestRelay::start().await; + tokio::time::sleep(Duration::from_millis(500)).await; + + let metrics = fetch_metrics(&relay.url()) + .await + .expect("Failed to fetch metrics"); + + relay.stop().await; + + // Check for valid Prometheus format markers + // - Lines starting with # are comments (HELP, TYPE) + // - Metric lines have format: metric_name{labels} value + let lines: Vec<&str> = metrics.lines().collect(); + + // Should have some content + assert!(!lines.is_empty(), "Metrics should have content"); + + // Check for at least some standard Prometheus patterns + let has_help = lines.iter().any(|l| l.starts_with("# HELP")); + let has_type = lines.iter().any(|l| l.starts_with("# TYPE")); + + // At minimum we expect help/type comments for any registered metrics + assert!( + has_help || has_type || lines.iter().any(|l| l.contains("ngit_")), + "Metrics should contain Prometheus format elements" + ); +} + +/// Test metrics endpoint availability during sync operations +#[tokio::test] +async fn test_metrics_availability_during_sync() { + let source_relay = TestRelay::start().await; + let sync_relay = TestRelay::start_with_sync(Some(source_relay.url().into())).await; + + tokio::time::sleep(Duration::from_millis(500)).await; + + // Make multiple metrics requests while sync is active + for i in 0..3 { + let metrics = fetch_metrics(&sync_relay.url()).await; + assert!( + metrics.is_ok(), + "Metrics request {} should succeed during sync", + i + 1 + ); + tokio::time::sleep(Duration::from_millis(200)).await; + } + + sync_relay.stop().await; + source_relay.stop().await; +} \ No newline at end of file diff --git a/tests/sync/mod.rs b/tests/sync/mod.rs index b0da8b8..cf8f599 100644 --- a/tests/sync/mod.rs +++ b/tests/sync/mod.rs @@ -8,6 +8,7 @@ //! - Live sync (events sync in real-time after connection established) //! - Tag variations (testing different Layer 2/3 tag types: a/A/q, e/E/q) //! - Catchup sync (events from disconnected period sync on reconnect) +//! - Metrics (Prometheus metrics for sync operations) //! //! # Test Files //! @@ -16,6 +17,7 @@ //! - `live_sync.rs` - Tests 5, 6, 7: real-time sync after connection //! - `tag_variations.rs` - Tests 8, 9: Layer 2/3 tag type coverage //! - `catchup.rs` - Test 0: catchup after disconnect (stub, `#[ignore]`) +//! - `metrics.rs` - Prometheus metrics integration tests //! //! # Shared Imports //! @@ -23,10 +25,12 @@ //! - `TestClient` - Client with retry logic //! - Event builders for Layer 2/3 events //! - `wait_for_event_on_relay()` - Non-panicking assertion helper +//! - `fetch_metrics()` - Prometheus metrics fetching // Test modules pub mod bootstrap; pub mod catchup; pub mod discovery; pub mod live_sync; +pub mod metrics; pub mod tag_variations; \ No newline at end of file -- cgit v1.2.3