#10503·velero

Design: sparse per-item backup outcomes for observability

Author: JoeavaikathCreated Sep 8, 2026Updated Sep 15, 2026

Summary

Expose a stable per-item backup outcome view for one backup run: {GVR, namespace, name, outcome, message} — so integrators don't regex results.gz or stitch BSL files themselves (#9377 comment).

This is mostly merge + structure at finalize, not more logs.gz text.

Problem

PartiallyFailed backups only expose a job-level count on the CR:

yaml
status:
  phase: PartiallyFailed
  errors: 2

Per-item signal exists but is fragmented across backup storage:

File Per-item? Outcome signal Gap
resource-list.json.gz Inventory Denominator No pass/fail
results.gz Errors Sync fail/warn strings Unstructured; name: ambiguous; async holes (#9377)
volumeinfo.json.gz PVC Structured result PVC-only; can be stale
itemoperations.json.gz Async ops Structured status Partial; may disagree with results
<backup>.tar.gz Manifests Content No outcome
velero-backup.json Job Status snapshot May predate async finalize
Backup.status Job Phase, error counts No per-item rows

Same name: format, different kinds:

error executing custom action (groupResource=widgets.example.io, namespace=ns, name=app-1): ...
name: /app-1-data message: /PVC ns/app-1-data has no volume backing this claim

Proposal

One backup → one snapshot at finalize (after async work). Immutable.

Merge existing sources into one view:

  1. resource-list — scope
  2. results — sync errors (parse strings; prefer structured fields when Velero records failures)
  3. volumeinfo — PVC results
  4. itemoperations — async failures (wins over stale results/volumeinfo)

Sparse output — only non-success rows + summary; success implicit (in resource-list, not in failure sets, volumeinfo OK for PVCs):

json
{
  "summary": { "totalItems": 8420, "succeeded": 8408, "failed": 10, "warnings": 2, "skipped": 0 },
  "items": [
    {
      "group": "widgets.example.io", "resource": "widgets", "version": "v1",
      "namespace": "ns", "name": "app-1", "outcome": "failed",
      "message": "persistentvolumeclaims \"app-1-data\" not found"
    }
  ]
}

Where new capture is needed (only if merge can't cover it): per-hook outcomes, explicit skipped with reason, structured error tuples at write time instead of string-only.

Delivery (TBD): versioned pkg/ API; optional persisted file (e.g. <backup>-item-outcomes.json.gz). Summary counts on Backup.status OK; per-item rows on the CR are not.

Success criteria

  • List all failed/warned items with explicit GVR, namespace, name, message — without ad hoc results.gz parsing
  • Async failures in the same merged view (#9377)
  • Documented versioned contract
  • Sparse default: size scales with failure count

Non-goals

  • Workload rollup (VM, StatefulSet, Helm) — downstream
  • Restore outcomes — follow-up
  • Cross-backup history / diff between schedules
  • Dense per-item success list on disk or CR

Open questions

  1. v1: pkg/ API only, or persisted artifact too?
  2. skipped vs implicit success?
  3. One row per API object (PVC stays on PVC) vs collapse to parent?
  4. Gaps requiring new capture beyond merge — hooks, skips, others?

Related

  • #9377 — async metadata consistency
  • #10310 — node-agent/datamover errors in describe

Can share a multi-object PartiallyFailed reproducer and per-failure source matrix if useful.