Validation Telemetry and Metrics
A backup that validated successfully but left no measurable record is indistinguishable, three months later during an audit, from one that never ran at all. Within the scope of Recovery Validation Telemetry & Compliance, this guide defines how automated backup-validation and disaster-recovery drill pipelines emit structured, queryable telemetry — the numeric substrate that turns a one-off pass/fail into a time series you can alert on, trend across backup generations, and hand to an auditor as evidence. The instrumentation described here wraps the deterministic gate produced by a checksum validation pipeline, attaches a verdict and a duration to every drill, and evaluates those series against the recovery envelope your RTO and RPO mapping declares. When a series breaches, the same labels that describe the metric feed straight into your error categorization frameworks so the alert carries a severity tier rather than a raw number.
The operational gap this closes is subtle. Most teams instrument the infrastructure that runs a drill — CPU, disk, container restarts — while leaving the validation outcome itself un-metered. That inversion means a pipeline can quietly degrade (restores creeping from four minutes to nine, corrupt-page counts drifting upward across an aging storage tier) with no signal until a real recovery fails inside the window. This page fixes that by treating each validation verdict, each restore duration, and each hashing throughput sample as first-class metrics with a stable, low-cardinality label schema, then routing them through a Prometheus-compatible path built for jobs that live for ninety seconds and vanish.
Architecture and Execution Workflow
Figure. Telemetry travels from in-process instrumentation through emission and collection into aggregation, SLO evaluation, and finally the compliance evidence store.
The flow is deliberately unidirectional. Instrumentation lives inside the validator process and knows nothing about Prometheus topology; emission produces standard exposition-format samples; collection is the only stage that cares whether the job is long-lived or ephemeral. Downstream, recording rules pre-compute the expensive quantiles once so dashboards and alerts read cheap pre-aggregates, SLO evaluation compares burn rate against the error budget your recovery targets imply, and the final hop copies the labelled series into an append-only store where it becomes tamper-evident evidence rather than a transient graph.
Phase 1 — Define a Registry and a Metric Taxonomy
The instrumentation contract is three metric types, each matched to what it measures. Counters are monotonic tallies that only ever increase within a process lifetime: artifacts_validated_total, corrupt_pages_found_total, validation_failures_total. You never read a counter’s instantaneous value on a dashboard; you read its rate(). Histograms capture a distribution by bucketing observations, which is exactly what you need for latency and throughput where the tail matters more than the mean: restore_duration_seconds, hash_throughput_bytes. Gauges hold a single value that can move in either direction and are read at their last-set point: replication_lag_seconds, drill_sandboxes_active.
Use an explicit CollectorRegistry rather than the library’s global default. An explicit registry is isolated, testable, and — critically for ephemeral drill jobs — disposable, so a job can build one, fill it, ship it, and let it fall out of scope without polluting a shared namespace.
"""Phase 1: a self-contained registry and the validation metric taxonomy."""
from prometheus_client import CollectorRegistry, Counter, Histogram, Gauge
# Stable, low-cardinality label set shared by every validation metric.
LABELS = ("artifact_class", "storage_tier", "engine", "workload", "verdict")
registry = CollectorRegistry()
artifacts_validated_total = Counter(
"artifacts_validated_total",
"Backup artifacts that completed a validation attempt.",
LABELS,
registry=registry,
)
corrupt_pages_found_total = Counter(
"corrupt_pages_found_total",
"Corrupt storage-engine pages detected during validation.",
("storage_tier", "engine", "workload"),
registry=registry,
)
restore_duration_seconds = Histogram(
"restore_duration_seconds",
"Wall-clock seconds to restore an artifact into the drill sandbox.",
("engine", "workload"),
buckets=(30, 60, 120, 300, 600, 1200, 1800, 3600),
registry=registry,
)
hash_throughput_bytes = Histogram(
"hash_throughput_bytes",
"Bytes hashed per validation pass, bucketed for tail analysis.",
("engine", "storage_tier"),
buckets=(1e7, 1e8, 1e9, 1e10, 1e11, 1e12),
registry=registry,
)
replication_lag_seconds = Gauge(
"replication_lag_seconds",
"Observed replica lag against the RPO ceiling at drill start.",
("engine", "workload"),
registry=registry,
)
The verdict label carries the terminal state — valid, invalid, degraded, missing — and appears only on artifacts_validated_total, where a bounded set of outcomes keeps the series count trivial. Note that restore_duration_seconds deliberately omits verdict: you want the latency distribution of successful restores kept separate from the near-zero durations of fast-failing invalid ones, so the split happens by observing only on the success path rather than by adding a label.
Phase 2 — Observe Metrics During a Validation Pass
Emission is where instrumentation meets the actual work. Counters are incremented once per event; histograms observe a measured sample; gauges are set to a read value. The idiom that keeps latency histograms honest is to wrap the timed region in the histogram’s own context manager, which records the elapsed seconds into the correct bucket even if the block raises.
"""Phase 2: observe the taxonomy during one artifact's validation."""
import time
from prometheus_client import CollectorRegistry, Counter, Histogram
registry = CollectorRegistry()
artifacts_validated_total = Counter(
"artifacts_validated_total", "Validation attempts.",
("engine", "workload", "verdict"), registry=registry,
)
restore_duration_seconds = Histogram(
"restore_duration_seconds", "Restore wall-clock seconds.",
("engine", "workload"), buckets=(30, 60, 120, 300, 600, 1200),
registry=registry,
)
def validate_artifact(engine: str, workload: str) -> str:
"""Run one restore+verify pass; return the terminal verdict string."""
timer = restore_duration_seconds.labels(engine=engine, workload=workload)
with timer.time(): # records elapsed seconds into a bucket
time.sleep(0.01) # stand-in for restore + integrity check
verdict = "valid" # real code derives this from the gate
artifacts_validated_total.labels(
engine=engine, workload=workload, verdict=verdict,
).inc()
return verdict
if __name__ == "__main__":
print(validate_artifact("postgres", "oltp"))
Observing the histogram only on the success path — and incrementing the counter with the derived verdict regardless — gives you two independent series from one pass: a clean latency distribution and a full outcome tally. This is the pattern every downstream page builds on, and it is generalized for terabyte-scale artifacts through async batching for large datasets, where each batch worker owns its own labelled child metric.
Phase 3 — Ship the Samples: Push vs Scrape
Prometheus’s default collection model is scrape: the server periodically GETs a /metrics endpoint the process exposes. That model assumes the process is alive when the scrape fires. A DR-drill validation job is the opposite of that assumption — it may run for sixty seconds on a scheduler’s whim and be gone long before the next scrape interval. Scraping such a job either misses it entirely or, worse, records a gap that looks like a failure.
The answer for short-lived jobs is the Pushgateway: the job pushes its final metric values to a long-lived intermediary that Prometheus scrapes on its own cadence. The push is keyed by grouping labels (job name, drill id, engine) so that a re-run overwrites the prior group rather than accumulating stale series. The one discipline the Pushgateway demands is teardown: a pushed group persists until explicitly deleted, so a completed drill must delete_from_gateway to avoid a graveyard of dead series.
"""Phase 3: push an ephemeral job's registry, then clean it up."""
from prometheus_client import CollectorRegistry, Counter
from prometheus_client.exposition import (
push_to_gateway, delete_from_gateway,
)
GATEWAY = "pushgateway.dr.internal:9091"
def ship(registry: CollectorRegistry, drill_id: str, engine: str) -> None:
"""Push under idempotent grouping keys, then delete on teardown."""
grouping = {"drill_id": drill_id, "engine": engine}
push_to_gateway(GATEWAY, job="backup_validation", grouping_key=grouping,
registry=registry)
# ... Prometheus scrapes the gateway between these two calls ...
delete_from_gateway(GATEWAY, job="backup_validation", grouping_key=grouping)
if __name__ == "__main__":
reg = CollectorRegistry()
Counter("artifacts_validated_total", "Attempts.", registry=reg).inc()
# ship(reg, "drill-20260718-01", "postgres") # requires a live gateway
print("registry prepared for push")
Long-lived components — a persistent validation controller, a replication-lag watcher — keep the scrape model and expose start_http_server. The two coexist: ephemeral drill workers push, standing services are scraped, and both land in the same Prometheus. The full push-path implementation, including grouping-key idempotency and POSIX exit codes, is the subject of exporting validation metrics to Prometheus.
Phase 4 — Cardinality Discipline and Label Design
Every distinct combination of label values on a metric creates a separate time series, and each series costs memory in Prometheus and query time forever. This is the single failure mode that kills validation telemetry in production: attaching a per-artifact-id label. A backup fleet produces thousands of artifacts per day; a backup_id or snapshot_arn label multiplies your series count by that churn daily, and because artifact ids are never reused, the series are write-once and never queried again — a textbook cardinality explosion.
The stable schema keys on classes, not instances: artifact_class (full, incremental, wal, logical), storage_tier (hot, warm, glacier), engine (postgres, mysql, mongodb), workload (oltp, olap, queue), and the bounded verdict. Each dimension has a handful of possible values, so their product stays in the low thousands of series across an entire fleet. The identity of a specific artifact does not belong in a label — it belongs in an exemplar, a sampled trace/audit reference attached to a single histogram observation, which lets an operator jump from a spike on a latency graph to the exact audit record for the artifact that caused it without inflating cardinality.
"""Phase 4: attach an audit-record exemplar to a histogram observation."""
from prometheus_client import CollectorRegistry, Histogram
registry = CollectorRegistry()
restore_duration_seconds = Histogram(
"restore_duration_seconds", "Restore wall-clock seconds.",
("engine", "workload"), buckets=(30, 60, 120, 300, 600, 1200),
registry=registry,
)
def observe_with_audit(seconds: float, engine: str, workload: str,
audit_id: str) -> None:
"""Record the sample; pin the audit id as a low-cardinality exemplar."""
restore_duration_seconds.labels(engine=engine, workload=workload).observe(
seconds, exemplar={"audit_id": audit_id},
)
if __name__ == "__main__":
observe_with_audit(287.4, "postgres", "oltp", "aud-9f2b-c41a")
print("observed with exemplar")
Exemplars are the correct home for high-cardinality identity: they are stored per-bucket, not per-series, so a million distinct audit_id values cost nothing in series count while still giving every notable observation a hard link back into the audit trail.
Integration with DR Drill Orchestration
Telemetry is not a passive byproduct of a drill; it is the interface other stages read. When the sandbox provisioning automation stands up an isolated environment and the restore runs inside it, the restore_duration_seconds histogram is the objective measurement of whether that restore met its window — the orchestrator does not guess, it queries the p95 series. A replication_lag_seconds gauge read at drill start is compared directly against the RPO ceiling to decide whether the recovery point is even admissible before compute is spent. And every verdict counted on artifacts_validated_total becomes a fact that the drill scheduling & orchestration layer can gate the next run on.
Because the collection path is decoupled, the orchestrator never has to reach into a validator process. It reads pre-computed series from Prometheus after the push has landed, which means a drill running in an isolated VPC, an air-gapped region, or a partner cloud all converge on the same query surface. The recording rules in the next phase are what make those reads fast enough to gate on synchronously.
Error Classification and Thresholds
A metric breaching a threshold is not automatically an incident. The same tiering used for raw validation faults applies to their telemetry, so that a slow-but-successful restore and a corrupt-page spike escalate differently. The tolerance windows below are expressed as PromQL-evaluable conditions against the taxonomy above.
| Tier | Metric condition | Tolerance | Escalation |
|---|---|---|---|
CRITICAL |
increase(corrupt_pages_found_total[1h]) > 0 or verdict="invalid" rate > 0 |
Zero | Page on-call, quarantine artifact, halt scheduled drills |
WARNING |
restore_duration_seconds p95 exceeds 80% of the RTO budget |
Bounded, sustained 15m | Ticket, annotate audit trail, continue |
WARNING |
replication_lag_seconds between 50% and 100% of RPO ceiling |
Bounded | Ticket, flag recovery point as marginal |
INFO |
hash_throughput_bytes drift within one bucket generation-over-generation |
Unbounded | Record for capacity trend only |
The threshold is meaningful only relative to a target: an eleven-minute restore is fine for a nightly analytics warehouse and a page-worthy emergency for a payments ledger. That is why the RTO/RPO numbers are inputs to the alert expression, not constants baked into it — the same recording rule is parameterized per workload, and the mapping from a breached series to a tier is handled by the shared error-categorization layer rather than duplicated in every alert.
Telemetry and Compliance Output
The final hop copies the labelled series and their exemplars into an append-only evidence store. Where the live Prometheus optimizes for query speed and retention windows measured in weeks, the compliance store optimizes for immutability and retention measured in years. Each record carries the metric name, the full label set, the observed value, the wall-clock timestamp, and — via the exemplar — the audit_id that ties the number to the specific validation event.
| Metric | Type | Compliance purpose |
|---|---|---|
artifacts_validated_total{verdict} |
Counter | Prove that every scheduled artifact was validated and record its outcome |
restore_duration_seconds |
Histogram | Evidence that recoveries met the documented RTO across the audit period |
replication_lag_seconds |
Gauge | Evidence that recovery points stayed inside the documented RPO |
corrupt_pages_found_total |
Counter | Detection record for storage degradation, dated and attributable |
This forwarding is what connects raw instrumentation to auditable control evidence. The mapping of these series onto specific control families — and the retention and signing requirements that make them admissible — is developed in compliance evidence & control mapping, while the write-once guarantees that keep a metric record tamper-evident are the subject of immutable audit trails. The instrumentation aligns with the evidence expectations of NIST SP 800-34 for contingency-plan testing and ISO 22301 for business-continuity measurement.
Operational Best Practices
- Own the registry per job. Ephemeral drill workers build an explicit
CollectorRegistry, fill it, push it, and let it be garbage-collected. Never mutate the global default registry from a job that will be cloned across regions. - Label by class, never by instance. Freeze the label schema at
artifact_class,storage_tier,engine,workload,verdict. Any pressure to addbackup_id,hostname, or a timestamp label is a signal to reach for an exemplar instead. - Push short jobs, scrape long ones. Route sub-scrape-interval jobs through the Pushgateway with idempotent grouping keys and always
delete_from_gatewayon teardown; expose/metricsonly from processes that outlive a scrape. - Pre-compute quantiles with recording rules. Compute
histogram_quantile(0.95, ...)once in a recording rule so dashboards and burn-rate alerts read a cheap pre-aggregate rather than re-scanning buckets on every evaluation. - Alert on burn rate, not instantaneous breach. A single slow restore is noise; a sustained multi-window burn of the recovery error budget is signal. Gate
CRITICALon burn rate and reserve zero-tolerance firing for corruption andinvalidverdicts. - Bind every notable observation to an audit id. Attach an exemplar carrying the
audit_idon the success path so any spike on a graph is one click from its immutable evidence record.
Validation telemetry is the difference between a DR program you believe works and one you can prove works. By fixing the metric taxonomy, freezing a low-cardinality label schema, choosing the right collection model for ephemeral jobs, and forwarding the result into tamper-evident storage, the numbers a drill produces stop being disposable graphs and become the durable evidence that recovery is real.
Frequently Asked Questions
Why is a per-artifact-id label so dangerous in validation telemetry?
Each unique label combination is a separate time series that costs memory and query time permanently. Artifact ids are high-churn and never reused, so a backup_id label mints thousands of write-once series per day that are never queried again — a cardinality explosion that can exhaust Prometheus. Keep labels keyed to classes like engine and storage_tier, and put the specific artifact identity in an exemplar instead.
When should a drill job push to a Pushgateway instead of being scraped?
Scraping assumes the process is alive when the scrape interval fires. A validation drill job often runs for less than one interval and exits, so a scrape either misses it or records a misleading gap. Such short-lived jobs should push their final values to a Pushgateway keyed by grouping labels, then delete that group on teardown. Long-lived services keep the scrape model.
What is an exemplar and how does it link a metric to an audit record?
An exemplar is a sampled reference attached to a single histogram observation, stored per-bucket rather than per-series. Pinning an audit_id as an exemplar lets an operator jump from a latency spike straight to the immutable audit record for the artifact that caused it, giving high-cardinality traceability at zero series-count cost.
Why alert on SLO burn rate rather than a single threshold breach?
One slow restore or a momentary lag spike is normal variance and paging on it produces alert fatigue. Burn-rate alerting measures how fast a sustained condition is consuming the recovery error budget implied by your RTO and RPO, firing only when the trend threatens the objective. Zero-tolerance conditions like detected corruption or an invalid verdict are the exception and fire immediately.
Related
- Exporting validation metrics to Prometheus — a full runnable module that instruments an ephemeral drill and pushes it to a gateway.
- Grafana dashboards for DR drill observability — provisioning dashboards-as-code that render this taxonomy.
- Compliance evidence & control mapping — turning these series into auditable control evidence.
- Immutable audit trails — the write-once store that keeps a metric record tamper-evident.
- Error categorization frameworks — mapping a breached series to an actionable severity tier.
This guide is one component of the broader Recovery Validation Telemetry & Compliance area.