feat: Phase 2 data aggregation pipeline (Strike 4A)
Backend: - Stats ingestion consumer subscribing to corrosion.*.stats NATS subject - Hourly aggregation scheduler (runs :05 past every hour) - Daily cleanup job (03:00 UTC) with 7-day raw / 90-day hourly retention - Analytics API (summary, timeseries, CSV export) - Complete stats DB queries with aggregation and cleanup Frontend: - Analytics dashboard with ECharts integration - Player count and server performance charts - Time range selector (24h/7d/30d) - CSV export functionality - Real-time data loading Infrastructure: - Exposed NatsBridge.jetstream for consumer access - Background service initialization in main.rs Data flow: Plugin → NATS → Consumer → DB → Aggregation → API → Charts Unblocks Strike 4B (dashboards) and 4C (alerting). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -242,4 +242,99 @@ impl SchedulerService {
|
||||
|
||||
Ok(next_times)
|
||||
}
|
||||
|
||||
/// Register hourly stats aggregation job (runs at :05 past every hour).
|
||||
pub async fn register_stats_aggregation(&self) -> Result<()> {
|
||||
let db = self.db.clone();
|
||||
|
||||
let job = Job::new_async("0 5 * * * *", move |_uuid, _l| {
|
||||
let db = db.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
tracing::info!("Running hourly stats aggregation");
|
||||
|
||||
// Get all active licenses
|
||||
let licenses: Vec<(Uuid,)> = match sqlx::query_as(
|
||||
"SELECT id FROM licenses WHERE status = 'active'",
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await
|
||||
{
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to query active licenses: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
tracing::info!("Aggregating stats for {} licenses", licenses.len());
|
||||
|
||||
for (license_id,) in licenses {
|
||||
if let Err(e) = crate::db::stats::aggregate_hourly_stats(&db, license_id).await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to aggregate stats for license {}: {}",
|
||||
license_id,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("Hourly stats aggregation complete");
|
||||
})
|
||||
})
|
||||
.context("Failed to create stats aggregation job")?;
|
||||
|
||||
self.scheduler
|
||||
.add(job)
|
||||
.await
|
||||
.context("Failed to add stats aggregation job to scheduler")?;
|
||||
|
||||
tracing::info!("Registered hourly stats aggregation job (cron: 0 5 * * * *)");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Register daily stats cleanup job (runs at 03:00 UTC).
|
||||
pub async fn register_stats_cleanup(&self) -> Result<()> {
|
||||
let db = self.db.clone();
|
||||
|
||||
let job = Job::new_async("0 0 3 * * *", move |_uuid, _l| {
|
||||
let db = db.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
tracing::info!("Running daily stats cleanup");
|
||||
|
||||
// Delete raw stats older than 7 days
|
||||
match crate::db::stats::cleanup_old_stats(&db, 7).await {
|
||||
Ok(deleted) => {
|
||||
tracing::info!("Deleted {} old raw stats records", deleted);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to cleanup old raw stats: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete hourly stats older than 90 days
|
||||
match crate::db::stats::cleanup_old_hourly_stats(&db, 90).await {
|
||||
Ok(deleted) => {
|
||||
tracing::info!("Deleted {} old hourly stats records", deleted);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to cleanup old hourly stats: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("Daily stats cleanup complete");
|
||||
})
|
||||
})
|
||||
.context("Failed to create stats cleanup job")?;
|
||||
|
||||
self.scheduler
|
||||
.add(job)
|
||||
.await
|
||||
.context("Failed to add stats cleanup job to scheduler")?;
|
||||
|
||||
tracing::info!("Registered daily stats cleanup job (cron: 0 0 3 * * *)");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user