Monitoring Geometry Validity Rates as an SLO

The validation boundary already classifies every feature into accepted, repaired or quarantined; making that an objective costs one recording rule and one target. The SLI is the weighted share of features that reached the table without needing intervention, the budget is the small fraction that may not, and the burn rate is what tells you a publisher’s export changed before the quarantine queue is large enough to notice. Unlike freshness, this objective is about what the source delivers rather than what the pipeline does — which is exactly why it is worth promising separately.

When to Use This Pattern

  • A validation boundary exists and records outcomes per feature, per source.
  • Repairs are common enough to be a policy rather than an occasional exception.
  • Consumers care about completeness, so quarantined features are a visible loss rather than an internal detail.
  • Publishers can be influenced, since the objective’s breaches are usually theirs to fix.

Complete Working Example

The counters come from the validation boundary; the SLI aggregates them; the alert watches the burn.

from __future__ import annotations

from dataclasses import dataclass

from prometheus_client import Counter, Gauge

# Emitted once per feature at the boundary. Three outcomes, bounded labels.
VALIDATION = Counter(
    "feature_validation_total",
    "Validation outcomes at the ingest boundary",
    ("layer", "source_group", "outcome"),      # accepted | repaired | quarantined
)

VALIDITY_SLI = Gauge("geometry_validity_sli", "Share accepted without repair",
                     ("layer",))
VALIDITY_TARGET = Gauge("geometry_validity_target", "The objective", ("layer",))


@dataclass(frozen=True)
class ValidityObjective:
    layer: str
    target: float             # share accepted without repair
    repair_ceiling: float     # share that may be repaired before it is a problem
    window_days: int = 30


OBJECTIVES = [
    # A cadastral register is authoritative: repairs there are a real concern.
    ValidityObjective("parcels", target=0.999, repair_ceiling=0.0009),
    # Hand-digitised habitat polygons are messier by nature, and that is expected.
    ValidityObjective("habitats", target=0.98, repair_ceiling=0.019),
    ValidityObjective("buildings", target=0.995, repair_ceiling=0.0045),
]


def record_outcome(layer: str, source_group: str, outcome: str) -> None:
    VALIDATION.labels(layer=layer, source_group=source_group, outcome=outcome).inc()

The rules compute the SLI once and derive everything else from it, so the dashboard and the alerts cannot disagree:

groups:
  - name: validity-slo
    rules:
      - record: slo:validity_sli
        expr: |
          sum by (layer) (rate(feature_validation_total{outcome="accepted"}[1h]))
          / sum by (layer) (rate(feature_validation_total[1h]))

      - record: slo:validity_error_ratio
        expr: 1 - slo:validity_sli

      # Repairs are not failures, but a rising repair share is an early warning
      # that a publisher's export has changed. Worth a ticket well before the
      # quarantine share moves.
      - record: slo:validity_repair_ratio
        expr: |
          sum by (layer) (rate(feature_validation_total{outcome="repaired"}[1h]))
          / sum by (layer) (rate(feature_validation_total[1h]))

      - alert: ValidityBudgetBurningFast
        expr: slo:validity_error_ratio > 14.4 * (1 - geometry_validity_target)
        for: 20m
        labels: { severity: page, team: data-stewardship }
        annotations:
          summary: "{{ $labels.layer }} validity budget burning at 14x"

      - alert: RepairShareClimbing
        expr: slo:validity_repair_ratio > on (layer) geometry_validity_repair_ceiling
        for: 2h
        labels: { severity: ticket, team: data-stewardship }
        annotations:
          summary: >
            {{ $labels.layer }} is repairing {{ $value | humanizePercentage }} of features —
            check whether the publisher's export changed
Repairs move before quarantines doThe repair share begins climbing on day eight while the quarantine share stays flat until day twenty, giving nearly two weeks of warning that a source has changed.day 1day 11day 21repair share — moves on day 8quarantine share — moves on day 20Twelve days of warning, available to anyone measuring the outcome nobody thinks of as a failure.
Repairs are successes, which is why they are usually not monitored — and why their share is the earliest signal that an upstream export has changed.

The reason repairs make such a good early-warning signal is that they sit on the boundary between “fine” and “broken”, and a source drifting across that boundary crosses the repair threshold long before it crosses the quarantine one. A publisher who changes an export setting typically produces geometry that is still repairable — an unclosed ring, a repeated vertex — for many deliveries before the change produces something no repair can save. Watching only the outcome that hurts means watching the last stage of a process that has been visible from its beginning.

The target follows the source's authorityA cadastral register carries a target of 99.9 per cent. Building footprints carry 99.5. Hand-digitised habitat polygons carry 98, because messiness is inherent to how they are produced.layerhow it is producedtargetparcelssurveyed, legally authoritative99.9%buildingsderived from imagery, reviewed99.5%habitatshand-digitised in the field98%Holding the habitat layer to the parcel layer’s standard would breach every month and change nothing.
The middle column is the argument. A target that ignores how the data was produced is a target somebody will eventually have to explain away.

Parameter & Option Reference

Parameter Example Spatial notes
target 0.999 (parcels) Share accepted without repair. Authoritative registers deserve a tighter number than digitised surveys.
repair_ceiling 0.0009 The early-warning threshold. Repairs are not failures and a rising share is still a signal.
outcome label 3 values accepted, repaired, quarantined. Bounded, and the three are genuinely different states.
source_group bounded A dozen values. It is what turns “validity fell” into “this publisher changed something”.
window 30 days The same as the freshness objective, so the two budgets are comparable.
fast burn 14.4× Pages. A sudden collapse in validity usually means a wholesale export change.
repair alert for: 2h A ticket. This is a trend, and a two-hour hold keeps a single odd delivery quiet.

Verification & Testing

The tests worth having assert that the three outcomes are exhaustive and that the alert fires on the right one.

def test_outcomes_are_exhaustive(sample_delivery) -> None:
    """Every feature must land in exactly one bucket, or the SLI is wrong."""
    counts = validate_delivery(sample_delivery)
    assert counts["accepted"] + counts["repaired"] + counts["quarantined"] == \
        sample_delivery.feature_count


def test_sli_excludes_repairs_from_the_numerator() -> None:
    """A repaired feature loaded successfully and still counts against validity."""
    sli = compute_sli(accepted=980, repaired=15, quarantined=5)
    assert sli == pytest.approx(0.98)          # not 0.995


def test_repair_alert_fires_before_the_quarantine_alert(promtool) -> None:
    series = ramp_repair_share(start=0.0005, end=0.004, days=21)
    fired = promtool.evaluate(series)
    repair_at = fired["RepairShareClimbing"]
    validity_at = fired.get("ValidityBudgetBurningFast")
    assert repair_at is not None
    assert validity_at is None or repair_at < validity_at

Once the counters exist, the query that turns a breach into a conversation groups by source and by defect class, which is what a publisher can actually act on:

SELECT source_group, defect_class,
       count(*) FILTER (WHERE outcome = 'repaired')    AS repaired,
       count(*) FILTER (WHERE outcome = 'quarantined') AS quarantined,
       min(checked_at) AS first_seen
FROM   validation_log
WHERE  checked_at > now() - interval '14 days' AND outcome <> 'accepted'
GROUP  BY source_group, defect_class
ORDER  BY repaired + quarantined DESC
LIMIT  15;
Three outcomes, three different meaningsAccepted features count towards the objective. Repaired features load successfully but count against it and are the early warning. Quarantined features are absent from the output entirely.outcomein the output?counts for the SLI?signalacceptedyes, unchangedyeshealthyrepairedyes, alteredno — against itearly warningquarantinednono — against itvisible lossThe middle row is the one most pipelines never count, and it is where the warning lives.
Treating a repair as a success for monitoring purposes discards the signal that arrives twelve days before anything visible goes wrong.

Grouping by defect class as well as by source is what makes the resulting message specific enough to act on. “Your export is producing invalid geometry” invites a shrug; “since the eleventh, 4% of your polygons have an unclosed outer ring, which our repair step closes — here are three examples” identifies a setting somebody can change. The columns needed for that message are already in the validation log, which is another reason to record the defect class at the boundary rather than only the outcome.

Common Pitfalls

  • Counting repairs as successes. They are successes for the load and failures for the source. Excluding them from the SLI’s numerator is what makes the objective an early-warning system rather than a lagging one.
  • A target copied between layers. A cadastral register and a set of hand-digitised habitat polygons have genuinely different validity expectations. One number across both either excuses the register or condemns the survey.
  • No source_group label. Without it a breach says “validity fell” and nothing more. With it the alert names the publisher, which is who has to change something.
  • Alerting only on quarantines. By the time features are being dropped, the export has been wrong for a fortnight. The repair share moves first and costs nothing extra to watch.
  • Outcomes that are not exhaustive. A feature that fails in a way the boundary does not classify vanishes from the denominator, and the SLI silently improves. Assert that the three counts sum to the delivery’s feature count.
  • Treating the objective as the pipeline’s problem. The pipeline is reporting what arrived. The remedy is nearly always upstream, which is why the alert routes to whoever owns the relationship with the publisher.

Frequently Asked Questions

Should repaired features count against the objective?

Yes. The objective is about what the source delivers, and a repaired feature is one the source got wrong. Counting repairs as successes produces an objective that only moves when things are already bad, which forfeits the main advantage of measuring at the boundary at all. Where a repair is genuinely routine and expected — a format that always needs ring closure — raise the target rather than reclassifying the outcome.

How do I set the initial target?

Measure for a month per layer and set the target just below what is currently achieved, exactly as with freshness. Validity rates are usually very stable per source, so the measurement converges quickly, and a target set this way will move when a publisher changes something — which is the only time you want it to move.

What about validity failures we cause ourselves?

They exist — a reprojection near a projection’s edge can produce an invalid polygon from a valid input — and they should be separated by labelling the outcome with the stage that produced it. Mixing them with source defects makes the objective unactionable, because the two have different owners and different fixes.

Does this need a separate log table?

Not for the SLI, which is computed from the counters. The table earns its place at triage time, when the question becomes “which source, which defect, since when” — and those are exactly the columns the dead-letter queue already stores for quarantined features. Extending it to record repairs as well is usually cheaper than a second table.

How does this relate to the coverage objective?

Quarantined features are missing output, so a validity breach is also a coverage breach, and the two budgets burn together. That overlap is acceptable and worth being aware of: an incident that quarantines a percentage of a delivery consumes both allowances, which is a fair reflection of the fact that it hurt consumers twice.

Data-Quality SLOs for Spatial Pipelines