upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-01-12 09:52:36 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-01-12 09:52:36 +0000
commit93227ce02e484c1e727bfd07ceeb72fd95774170 (patch)
treed02c99355b083c4a30f2ae75f42eaaa70d213b24 /src
parenta63dc8a9e5f9cad50f4ea7c6c5d2ed544bc70656 (diff)
fix(metrics): count repositories on disk on each metrics request
Implements ngit_repositories_total metric by counting *.git directories on disk every time /metrics is requested (~15s interval by Prometheus). This approach is simpler than increment-on-create because: - No need to pass metrics through the relay builder chain - Always accurate and self-correcting - Negligible performance impact (~100-200 dir entries) Changes: - Add count_repositories_on_disk() static method to Metrics - Update Metrics::render() to count repos before encoding metrics - Pass git_data_path to Metrics::new() in main.rs - Consolidate metrics tests to avoid global Prometheus registry conflicts Fixes repository count metric issue from Phase 8 deployment plan.
Diffstat (limited to 'src')
-rw-r--r--src/main.rs7
-rw-r--r--src/metrics/mod.rs159
2 files changed, 149 insertions, 17 deletions
diff --git a/src/main.rs b/src/main.rs
index 44545b5..8b959a6 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -40,9 +40,12 @@ async fn main() -> Result<()> {
40 // Initialize metrics if enabled 40 // Initialize metrics if enabled
41 let metrics = if config.metrics_enabled { 41 let metrics = if config.metrics_enabled {
42 info!("Metrics enabled on /metrics endpoint"); 42 info!("Metrics enabled on /metrics endpoint");
43 Some(Arc::new(Metrics::new( 43 let m = Arc::new(Metrics::new(
44 config.metrics_connection_per_ip_abuse_threshold, 44 config.metrics_connection_per_ip_abuse_threshold,
45 ))) 45 Some(config.effective_git_data_path()),
46 ));
47 info!("Repository count will be updated on each metrics request");
48 Some(m)
46 } else { 49 } else {
47 info!("Metrics disabled"); 50 info!("Metrics disabled");
48 None 51 None
diff --git a/src/metrics/mod.rs b/src/metrics/mod.rs
index 5420dfd..a16b9fe 100644
--- a/src/metrics/mod.rs
+++ b/src/metrics/mod.rs
@@ -81,8 +81,10 @@ struct MetricsInner {
81 pub events_rejected_total: CounterVec, 81 pub events_rejected_total: CounterVec,
82 82
83 // === Repository Metrics === 83 // === Repository Metrics ===
84 /// Total repositories hosted 84 /// Total repositories hosted (counted from disk on each metrics request)
85 pub repositories_total: Gauge, 85 pub repositories_total: Gauge,
86 /// Git data directory path for counting repositories on disk
87 pub git_data_path: Option<String>,
86 88
87 // === System Health Metrics === 89 // === System Health Metrics ===
88 /// Server start time for uptime calculation 90 /// Server start time for uptime calculation
@@ -97,8 +99,9 @@ impl Metrics {
97 /// 99 ///
98 /// # Arguments 100 /// # Arguments
99 /// * `abuse_threshold` - Number of connections from a single IP before flagging as abuse 101 /// * `abuse_threshold` - Number of connections from a single IP before flagging as abuse
100 pub fn new(abuse_threshold: u32) -> Self { 102 /// * `git_data_path` - Optional path to git data directory for counting repositories
101 let inner = MetricsInner::new(abuse_threshold); 103 pub fn new(abuse_threshold: u32, git_data_path: Option<String>) -> Self {
104 let inner = MetricsInner::new(abuse_threshold, git_data_path);
102 Self { 105 Self {
103 inner: Arc::new(inner), 106 inner: Arc::new(inner),
104 } 107 }
@@ -214,9 +217,49 @@ impl Metrics {
214 self.inner.repositories_total.set(count as f64); 217 self.inner.repositories_total.set(count as f64);
215 } 218 }
216 219
217 /// Increment the repository count 220 /// Count all git repositories on disk.
218 pub fn inc_repositories_total(&self) { 221 ///
219 self.inner.repositories_total.inc(); 222 /// This scans the git data directory for all `*.git` directories.
223 ///
224 /// # Arguments
225 /// * `git_data_path` - Path to the git data directory (e.g., "./data/git")
226 ///
227 /// # Returns
228 /// The number of repositories found on disk
229 pub fn count_repositories_on_disk(git_data_path: &str) -> u64 {
230 use std::fs;
231 use std::path::Path;
232
233 let git_dir = Path::new(git_data_path);
234 if !git_dir.exists() {
235 return 0;
236 }
237
238 let mut count = 0u64;
239 if let Ok(entries) = fs::read_dir(git_dir) {
240 for npub_entry in entries.flatten() {
241 if let Ok(npub_meta) = npub_entry.metadata() {
242 if npub_meta.is_dir() {
243 // This is a npub directory, scan for *.git repos inside
244 if let Ok(repo_entries) = fs::read_dir(npub_entry.path()) {
245 for repo_entry in repo_entries.flatten() {
246 if let Some(name) = repo_entry.file_name().to_str() {
247 if name.ends_with(".git") {
248 if let Ok(repo_meta) = repo_entry.metadata() {
249 if repo_meta.is_dir() {
250 count += 1;
251 }
252 }
253 }
254 }
255 }
256 }
257 }
258 }
259 }
260 }
261
262 count
220 } 263 }
221 264
222 // === Rendering === 265 // === Rendering ===
@@ -225,13 +268,20 @@ impl Metrics {
225 /// 268 ///
226 /// This method: 269 /// This method:
227 /// 1. Refreshes the top-N bandwidth metrics if needed 270 /// 1. Refreshes the top-N bandwidth metrics if needed
228 /// 2. Updates uptime 271 /// 2. Counts repositories on disk (if git_data_path configured)
229 /// 3. Gathers all metrics from the registry 272 /// 3. Updates uptime
230 /// 4. Encodes them in Prometheus text format 273 /// 4. Gathers all metrics from the registry
274 /// 5. Encodes them in Prometheus text format
231 pub fn render(&self) -> String { 275 pub fn render(&self) -> String {
232 // Refresh top-N bandwidth repos if needed 276 // Refresh top-N bandwidth repos if needed
233 self.inner.bandwidth_tracker.maybe_refresh_top_n(); 277 self.inner.bandwidth_tracker.maybe_refresh_top_n();
234 278
279 // Count repositories on disk and update metric
280 if let Some(git_data_path) = &self.inner.git_data_path {
281 let count = Self::count_repositories_on_disk(git_data_path);
282 self.inner.repositories_total.set(count as f64);
283 }
284
235 // Gather and encode metrics 285 // Gather and encode metrics
236 let encoder = TextEncoder::new(); 286 let encoder = TextEncoder::new();
237 let metric_families = REGISTRY.gather(); 287 let metric_families = REGISTRY.gather();
@@ -256,7 +306,7 @@ impl Metrics {
256} 306}
257 307
258impl MetricsInner { 308impl MetricsInner {
259 fn new(abuse_threshold: u32) -> Self { 309 fn new(abuse_threshold: u32, git_data_path: Option<String>) -> Self {
260 // Create connection tracker 310 // Create connection tracker
261 let connection_tracker = ConnectionTracker::new(abuse_threshold, &REGISTRY); 311 let connection_tracker = ConnectionTracker::new(abuse_threshold, &REGISTRY);
262 312
@@ -444,6 +494,7 @@ impl MetricsInner {
444 events_stored_total, 494 events_stored_total,
445 events_rejected_total, 495 events_rejected_total,
446 repositories_total, 496 repositories_total,
497 git_data_path,
447 start_time: Instant::now(), 498 start_time: Instant::now(),
448 build_info, 499 build_info,
449 } 500 }
@@ -505,12 +556,67 @@ mod tests {
505 use super::*; 556 use super::*;
506 557
507 #[test] 558 #[test]
508 fn test_metrics_creation() { 559 fn test_count_repositories_on_disk() {
509 // Note: This test may fail if run with other tests due to global registry 560 use std::fs;
510 // In production, consider using a test-specific registry 561 use tempfile::TempDir;
511 let metrics = Metrics::new(10); 562
563 // Create temporary directory structure
564 let temp_dir = TempDir::new().unwrap();
565 let git_data_path = temp_dir.path();
566
567 // Initially should be 0
568 let count = Metrics::count_repositories_on_disk(git_data_path.to_str().unwrap());
569 assert_eq!(count, 0);
570
571 // Create some fake repositories
572 let npub1 = git_data_path.join("npub1test");
573 fs::create_dir_all(&npub1).unwrap();
574 fs::create_dir_all(npub1.join("repo1.git")).unwrap();
575 fs::create_dir_all(npub1.join("repo2.git")).unwrap();
576
577 let npub2 = git_data_path.join("npub2test");
578 fs::create_dir_all(&npub2).unwrap();
579 fs::create_dir_all(npub2.join("repo3.git")).unwrap();
580
581 // Should count 3 repositories
582 let count = Metrics::count_repositories_on_disk(git_data_path.to_str().unwrap());
583 assert_eq!(count, 3);
584
585 // Create a non-.git directory (should be ignored)
586 fs::create_dir_all(npub1.join("not-a-repo")).unwrap();
587 let count = Metrics::count_repositories_on_disk(git_data_path.to_str().unwrap());
588 assert_eq!(count, 3);
589
590 // Create a file with .git suffix (should be ignored, not a directory)
591 fs::write(npub1.join("file.git"), "content").unwrap();
592 let count = Metrics::count_repositories_on_disk(git_data_path.to_str().unwrap());
593 assert_eq!(count, 3);
594 }
595
596 /// Comprehensive test for Metrics functionality including repository counting.
597 ///
598 /// NOTE: This test creates a Metrics instance which registers with the global
599 /// Prometheus REGISTRY. Due to this global state, we cannot have multiple tests
600 /// that create Metrics instances - they would conflict. Therefore, this single
601 /// test covers:
602 /// 1. Metrics creation and basic operations
603 /// 2. Repository counting on disk via render()
604 ///
605 /// If additional Metrics tests are needed, they should either be added to this
606 /// test or use a separate test-specific Prometheus registry.
607 #[test]
608 fn test_metrics_with_repository_counting() {
609 use std::fs;
610 use tempfile::TempDir;
611
612 // Create temporary directory structure for repository counting
613 let temp_dir = TempDir::new().unwrap();
614 let git_data_path = temp_dir.path();
615
616 // Create Metrics with git_data_path for repository counting
617 let metrics = Metrics::new(10, Some(git_data_path.to_str().unwrap().to_string()));
512 618
513 // Test that we can record metrics without panicking 619 // Test basic metrics operations
514 metrics.record_websocket_connection(); 620 metrics.record_websocket_connection();
515 metrics.record_message_received("REQ"); 621 metrics.record_message_received("REQ");
516 metrics.record_message_sent("EVENT"); 622 metrics.record_message_sent("EVENT");
@@ -520,5 +626,28 @@ mod tests {
520 metrics.record_event_stored(1); 626 metrics.record_event_stored(1);
521 metrics.record_event_rejected(1, "invalid_signature"); 627 metrics.record_event_rejected(1, "invalid_signature");
522 metrics.set_repositories_total(5); 628 metrics.set_repositories_total(5);
629
630 // Test repository counting via render()
631 // Render should count 0 repos initially (even though we set it to 5 above,
632 // render() recounts from disk)
633 let output = metrics.render();
634 assert!(output.contains("ngit_repositories_total 0"));
635
636 // Create some repositories
637 let npub1 = git_data_path.join("npub1test");
638 fs::create_dir_all(&npub1).unwrap();
639 fs::create_dir_all(npub1.join("repo1.git")).unwrap();
640 fs::create_dir_all(npub1.join("repo2.git")).unwrap();
641
642 // Render should count 2 repos now
643 let output = metrics.render();
644 assert!(output.contains("ngit_repositories_total 2"));
645
646 // Add another repo
647 fs::create_dir_all(npub1.join("repo3.git")).unwrap();
648
649 // Render should count 3 repos
650 let output = metrics.render();
651 assert!(output.contains("ngit_repositories_total 3"));
523 } 652 }
524} 653}