(feature) Show backup data totals by snapshot kind
Aggregate snapshot storage metadata by snapshot kind so operators can see scheduled, manual, incomplete, and total backup data. Surface the totals per host and across all hosts on the dashboard, using allocated snapshot size from recorded backup metadata without rescanning backup storage.
This commit is contained in:
@@ -11,6 +11,7 @@ from .models import BackupRun, GlobalConfig, HostConfig, SnapshotRecord
|
||||
|
||||
|
||||
def collect_dashboard_stats(*, hosts: Iterable[HostConfig], global_config: GlobalConfig | None) -> dict[str, Any]:
|
||||
hosts = list(hosts)
|
||||
runs = list(
|
||||
BackupRun.objects.select_related("host", "snapshot")
|
||||
.filter(status__in=_COMPLETED_BACKUP_STATUSES)
|
||||
@@ -21,6 +22,7 @@ def collect_dashboard_stats(*, hosts: Iterable[HostConfig], global_config: Globa
|
||||
|
||||
for host in hosts:
|
||||
host.stats_summary = collect_host_stats(host=host)
|
||||
backup_data = _sum_backup_data_by_kind(host.stats_summary["backup_data"] for host in hosts)
|
||||
|
||||
literal_values = [_int_at(run, "rsync", "literal_data_bytes") for run in real_runs]
|
||||
literal_values = [value for value in literal_values if value is not None]
|
||||
@@ -51,6 +53,7 @@ def collect_dashboard_stats(*, hosts: Iterable[HostConfig], global_config: Globa
|
||||
"estimated_runs_until_full": int(available / avg_literal) if available and avg_literal else None,
|
||||
"estimated_days_until_full": int(available / daily_literal) if available and daily_literal else None,
|
||||
"capacity": capacity,
|
||||
"backup_data": backup_data,
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +64,7 @@ def collect_host_stats(*, host: HostConfig, limit: int = 8) -> dict[str, Any]:
|
||||
trend_runs = [run for run in completed_real_runs if run["has_stats"]][:limit]
|
||||
latest_snapshot = host.snapshots.order_by("-started_at", "-discovered_at", "-id").first()
|
||||
latest_snapshot_stats = _snapshot_summary(latest_snapshot) if latest_snapshot else {}
|
||||
backup_data = _backup_data_by_kind(host)
|
||||
|
||||
literal_values = [_int_at(run, "rsync", "literal_data_bytes") for run in trend_runs]
|
||||
literal_values = [value for value in literal_values if value is not None]
|
||||
@@ -75,6 +79,7 @@ def collect_host_stats(*, host: HostConfig, limit: int = 8) -> dict[str, Any]:
|
||||
"latest_good_run": _first_run_with_status(real_runs, {BackupRun.Status.SUCCESS}),
|
||||
"latest_problem_run": _first_run_with_status(real_runs, {BackupRun.Status.WARNING, BackupRun.Status.FAILED}),
|
||||
"latest_snapshot": latest_snapshot_stats,
|
||||
"backup_data": backup_data,
|
||||
"avg_literal_data_bytes": _average(literal_values),
|
||||
"avg_daily_literal_data_bytes": _average_daily_literal(trend_runs),
|
||||
"total_literal_data_bytes": sum(literal_values),
|
||||
@@ -102,6 +107,60 @@ def _run_summary(run: BackupRun) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _backup_data_by_kind(host: HostConfig) -> dict[str, Any]:
|
||||
rows: dict[str, dict[str, int]] = {
|
||||
SnapshotRecord.Kind.SCHEDULED: _empty_snapshot_data_row(),
|
||||
SnapshotRecord.Kind.MANUAL: _empty_snapshot_data_row(),
|
||||
SnapshotRecord.Kind.INCOMPLETE: _empty_snapshot_data_row(),
|
||||
}
|
||||
total = _empty_snapshot_data_row()
|
||||
|
||||
for snapshot in host.snapshots.all():
|
||||
summary = _snapshot_summary(snapshot)
|
||||
row = rows.setdefault(snapshot.kind, _empty_snapshot_data_row())
|
||||
allocated = summary.get("allocated_size_bytes") or summary.get("apparent_size_bytes") or 0
|
||||
apparent = summary.get("apparent_size_bytes") or 0
|
||||
row["count"] += 1
|
||||
row["allocated_size_bytes"] += int(allocated)
|
||||
row["apparent_size_bytes"] += int(apparent)
|
||||
total["count"] += 1
|
||||
total["allocated_size_bytes"] += int(allocated)
|
||||
total["apparent_size_bytes"] += int(apparent)
|
||||
|
||||
return {
|
||||
"scheduled": rows[SnapshotRecord.Kind.SCHEDULED],
|
||||
"manual": rows[SnapshotRecord.Kind.MANUAL],
|
||||
"incomplete": rows[SnapshotRecord.Kind.INCOMPLETE],
|
||||
"total": total,
|
||||
}
|
||||
|
||||
|
||||
def _empty_snapshot_data_row() -> dict[str, int]:
|
||||
return {
|
||||
"count": 0,
|
||||
"allocated_size_bytes": 0,
|
||||
"apparent_size_bytes": 0,
|
||||
}
|
||||
|
||||
|
||||
def _sum_backup_data_by_kind(rows: Iterable[dict[str, dict[str, int]]]) -> dict[str, dict[str, int]]:
|
||||
total_rows: dict[str, dict[str, int]] = {
|
||||
"scheduled": _empty_snapshot_data_row(),
|
||||
"manual": _empty_snapshot_data_row(),
|
||||
"incomplete": _empty_snapshot_data_row(),
|
||||
"total": _empty_snapshot_data_row(),
|
||||
}
|
||||
|
||||
for row in rows:
|
||||
for kind, values in row.items():
|
||||
total_row = total_rows.setdefault(kind, _empty_snapshot_data_row())
|
||||
total_row["count"] += values.get("count", 0)
|
||||
total_row["allocated_size_bytes"] += values.get("allocated_size_bytes", 0)
|
||||
total_row["apparent_size_bytes"] += values.get("apparent_size_bytes", 0)
|
||||
|
||||
return total_rows
|
||||
|
||||
|
||||
def _snapshot_summary(snapshot: SnapshotRecord | None) -> dict[str, Any]:
|
||||
if snapshot is None:
|
||||
return {}
|
||||
|
||||
Reference in New Issue
Block a user