fix: persist dashboard metrics and count versions instead of repos

Metrics (downloads, uploads, cache hits) were stored in-memory only
and reset to zero on every restart. Now they persist to metrics.json
in the storage directory with:
- Load on startup from {storage_path}/metrics.json
- Background save every 30 seconds
- Final save on graceful shutdown
- Atomic writes (tmp + rename) to prevent corruption

Artifact count on dashboard now shows total tags/versions across
all registries instead of just counting unique repository names.
This matches user expectations when pushing multiple tags to the
same image (e.g. myapp:v1, myapp:v2 now shows 2, not 1).
This commit is contained in:
2026-03-13 15:43:03 +00:00
parent b80c7c5160
commit 61de6c6ddd
3 changed files with 119 additions and 11 deletions

View File

@@ -1,8 +1,29 @@
// Copyright (c) 2026 Volkov Pavel | DevITWay
// SPDX-License-Identifier: MIT
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
use tracing::{info, warn};
/// Serializable snapshot of metrics for persistence
#[derive(Serialize, Deserialize, Default)]
struct MetricsSnapshot {
downloads: u64,
uploads: u64,
cache_hits: u64,
cache_misses: u64,
docker_downloads: u64,
docker_uploads: u64,
npm_downloads: u64,
maven_downloads: u64,
maven_uploads: u64,
cargo_downloads: u64,
pypi_downloads: u64,
raw_downloads: u64,
raw_uploads: u64,
}
/// Dashboard metrics for tracking registry activity
/// Uses atomic counters for thread-safe access without locks
@@ -25,6 +46,9 @@ pub struct DashboardMetrics {
pub raw_uploads: AtomicU64,
pub start_time: Instant,
/// Path to metrics.json for persistence
persist_path: Option<PathBuf>,
}
impl DashboardMetrics {
@@ -44,6 +68,75 @@ impl DashboardMetrics {
raw_downloads: AtomicU64::new(0),
raw_uploads: AtomicU64::new(0),
start_time: Instant::now(),
persist_path: None,
}
}
/// Create metrics with persistence — loads existing data from metrics.json
pub fn with_persistence(storage_path: &str) -> Self {
let path = Path::new(storage_path).join("metrics.json");
let mut metrics = Self::new();
metrics.persist_path = Some(path.clone());
// Load existing metrics if file exists
if path.exists() {
match std::fs::read_to_string(&path) {
Ok(data) => match serde_json::from_str::<MetricsSnapshot>(&data) {
Ok(snap) => {
metrics.downloads = AtomicU64::new(snap.downloads);
metrics.uploads = AtomicU64::new(snap.uploads);
metrics.cache_hits = AtomicU64::new(snap.cache_hits);
metrics.cache_misses = AtomicU64::new(snap.cache_misses);
metrics.docker_downloads = AtomicU64::new(snap.docker_downloads);
metrics.docker_uploads = AtomicU64::new(snap.docker_uploads);
metrics.npm_downloads = AtomicU64::new(snap.npm_downloads);
metrics.maven_downloads = AtomicU64::new(snap.maven_downloads);
metrics.maven_uploads = AtomicU64::new(snap.maven_uploads);
metrics.cargo_downloads = AtomicU64::new(snap.cargo_downloads);
metrics.pypi_downloads = AtomicU64::new(snap.pypi_downloads);
metrics.raw_downloads = AtomicU64::new(snap.raw_downloads);
metrics.raw_uploads = AtomicU64::new(snap.raw_uploads);
info!(
downloads = snap.downloads,
uploads = snap.uploads,
"Loaded persisted metrics"
);
}
Err(e) => warn!("Failed to parse metrics.json: {}", e),
},
Err(e) => warn!("Failed to read metrics.json: {}", e),
}
}
metrics
}
/// Save current metrics to disk
pub fn save(&self) {
let Some(path) = &self.persist_path else {
return;
};
let snap = MetricsSnapshot {
downloads: self.downloads.load(Ordering::Relaxed),
uploads: self.uploads.load(Ordering::Relaxed),
cache_hits: self.cache_hits.load(Ordering::Relaxed),
cache_misses: self.cache_misses.load(Ordering::Relaxed),
docker_downloads: self.docker_downloads.load(Ordering::Relaxed),
docker_uploads: self.docker_uploads.load(Ordering::Relaxed),
npm_downloads: self.npm_downloads.load(Ordering::Relaxed),
maven_downloads: self.maven_downloads.load(Ordering::Relaxed),
maven_uploads: self.maven_uploads.load(Ordering::Relaxed),
cargo_downloads: self.cargo_downloads.load(Ordering::Relaxed),
pypi_downloads: self.pypi_downloads.load(Ordering::Relaxed),
raw_downloads: self.raw_downloads.load(Ordering::Relaxed),
raw_uploads: self.raw_uploads.load(Ordering::Relaxed),
};
// Atomic write: write to tmp then rename
let tmp = path.with_extension("json.tmp");
if let Ok(data) = serde_json::to_string_pretty(&snap) {
if std::fs::write(&tmp, &data).is_ok() {
let _ = std::fs::rename(&tmp, path);
}
}
}