Spatial Coverage & Extent Monitoring

Spatial coverage degrades silently. A raster mosaic loses a swath of tiles after an upstream tasking change, a vector feed quietly shrinks its bounding envelope because a partition predicate dropped a region, or a reprojection clamps geometries to the wrong hemisphere — and none of it raises an exception. The rows still load, the dashboards still render, and the gap only surfaces weeks later as a hole in a heat map or a routing engine that has no data for an entire metro area. This guide is for the data engineers, GIS platform administrators, and SREs who need to make completeness in space a measurable, alertable signal rather than a thing they discover from an angry downstream consumer. It sits directly under the Spatial Data Freshness & Quality Metrics blueprint, extending its measurement plane with the geometry-aware coverage and extent telemetry that freshness timestamps alone can never capture.

Coverage and extent monitoring control loop Each ingest batch flows into a stage that computes its spatial envelope and coverage ratio. A decision tests whether coverage or drift has been breached. If not, the batch is healthy and the baseline is updated. If it is breached, the loop raises a PagerDuty or Slack alert and then quarantines and re-fetches the affected tiles. Ingest batch Compute envelope + coverage ratio Coverage or drift breached? no Healthy update baseline yes Alert PagerDuty / Slack Quarantine + re-fetch tiles

Architecture

Spatial coverage and extent monitoring functions as the geometric telemetry layer within geospatial ETL/ELT pipelines, bridging raw ingestion with downstream analytical consumption. The architecture initiates at the ingestion boundary, where vector and raster payloads are parsed, spatially indexed, and routed through a validation gateway before landing in analytical warehouses or feature stores. A lightweight observability agent intercepts the spatial footprint of each batch, computing bounding envelopes, convex hulls, and coverage ratios without introducing blocking latency to the primary data flow. These signals are serialized into structured metadata events and routed to a centralized observability backend, where they are correlated with pipeline execution traces, infrastructure metrics, and catalog lineage.

Coverage monitoring is deliberately positioned downstream of structural checks. By the time a batch reaches this layer it has already passed through Coordinate Reference System Validation, which guarantees that every envelope is computed against a consistent datum — a reprojection error will otherwise masquerade as a catastrophic extent collapse and trigger a false page. It has also cleared Geometry Validity & Topology Checks, because self-intersecting polygons, unclosed rings, and topological slivers artificially inflate or deflate any area-based coverage calculation. Ordering these gates correctly is what keeps coverage alerts trustworthy: a single misordered stage turns the whole signal into noise.

The architecture depends on spatially aware metadata catalogs that maintain a reference extent per dataset, versioned CRS mappings, and historical coverage baselines. In production deployments, monitoring agents run as sidecar processes or embed directly inside transformation engines — Spark UDFs, dbt post-hooks, or Airflow operators — emitting OpenTelemetry spans that carry spatial transform latency, index fragmentation rate, and envelope-calculation overhead as span attributes. The attribute namespace itself follows the conventions defined in the Geospatial Metric Taxonomy for ETL, so coverage telemetry joins cleanly against traces emitted elsewhere in the platform. Wiring the agent into the collector pipeline is covered end to end in OpenTelemetry Integration for GIS Pipelines. This decoupled-yet-tightly-integrated design keeps coverage telemetry isolated from business logic while staying synchronized with orchestration events, so SREs detect spatial drift before it cascades into routing failures, analytical inaccuracies, or compliance violations.

Where the coverage and extent agent sits in the pipeline Vector and raster payloads enter at the ingest boundary and pass through CRS validation, which enforces a consistent datum, then geometry and topology checks. Only then do they reach the coverage and extent agent, which computes envelope, convex hull, coverage ratio, drift, and tile completeness. The agent runs as a non-blocking sidecar that emits OpenTelemetry spans carrying gis.spatial dot star attributes to an observability backend, where coverage telemetry is correlated with traces, metrics, and lineage. Ingest boundary vector · raster CRS validation consistent datum Geometry & topology make-valid · rings Coverage & extent agent envelope · convex hull coverage ratio · drift tile completeness OTel sidecar spans · gis.spatial.* attrs Observability backend correlate: traces · metrics · lineage non-blocking

Metric Specification

Effective coverage monitoring requires a precise set of quantitative signals that translate geometric reality into actionable observability metrics. The table below is the baseline telemetry contract: canonical OpenTelemetry instrument names in the gis.spatial.* namespace, the warehouse-side expression that computes each one, and the instrument type used for export.

Metric (gis.spatial.*) Description Instrument Calculation Method Unit
gis.spatial.coverage_ratio Ingested feature area against a canonical reference extent Gauge ST_Area(ST_Union(ingested_geom)) / ST_Area(reference_extent) ratio (0–1)
gis.spatial.extent_drift_m Centroid displacement between successive runs Histogram ST_Distance(ST_Centroid(current_extent), ST_Centroid(previous_extent)) metres
gis.spatial.bbox_null_ratio Proportion of features with missing or empty geometry Gauge COUNT(*) FILTER (WHERE geom IS NULL OR ST_IsEmpty(geom)) / COUNT(*) ratio (0–1)
gis.spatial.tile_completeness Present tiles against the expected mosaic grid Gauge COUNT(DISTINCT present_tile_id) / expected_tile_count ratio (0–1)
gis.spatial.crs_extent_mismatch Count of features whose reprojected extent falls outside expected bounds Counter ST_Transform(geom, target_crs) outside expected_bounds count

All five are exported as Prometheus-compatible series so they can be aggregated as time series and fed to statistical baselines. Each carries the same dimension set — dataset_id, crs (EPSG code), region, and pipeline_run_id — which lets an alert pivot instantly from “coverage dropped” to “coverage dropped for cadastral parcels in EPSG:2154 in the eu-west region on run 8842.”

Composite coverage health score

No single ratio is sufficient on its own: a feed can hold a healthy area ratio while drifting badly, or stay perfectly centred while shedding null geometries. Collapse the three orthogonal signals into one bounded health score for SLO reporting, where the drift term is normalised against a dataset-specific maximum tolerable displacement dmaxd_{max}:

Hcov=w1AingestedAref+w2(1min ⁣(ddriftdmax,1))+w3(1rnull)H_{\text{cov}} = w_1 \cdot \frac{A_{\text{ingested}}}{A_{\text{ref}}} + w_2 \cdot \left(1 - \min\!\left(\frac{d_{\text{drift}}}{d_{\max}},\, 1\right)\right) + w_3 \cdot (1 - r_{\text{null}})

with w1+w2+w3=1w_1 + w_2 + w_3 = 1. Typical production weights bias toward area completeness (w1=0.5w_1 = 0.5, w2=0.3w_2 = 0.3, w3=0.2w_3 = 0.2), but high-mobility feeds such as fleet telemetry invert this to emphasise drift. Thresholding on HcovH_{\text{cov}} rather than on any single metric prevents the “one green light hides two red ones” failure that plagues naive coverage dashboards.

Pipeline Integration & Configuration

Integrating coverage telemetry requires a minimal code footprint and strict adherence to observability standards. The three snippets below cover the full path: in-pipeline extraction, warehouse-side validation, and collector routing.

1. Spatial envelope extraction (Python / GeoPandas)

import geopandas as gpd
from opentelemetry import metrics

meter = metrics.get_meter("gis.spatial.coverage_monitor")
# Gauges via async callbacks would be ideal; counters/histograms shown for clarity.
coverage_gauge = meter.create_gauge("gis.spatial.coverage_ratio", unit="1")
drift_histogram = meter.create_histogram("gis.spatial.extent_drift_m", unit="m")

def compute_coverage_telemetry(df: gpd.GeoDataFrame, reference_geom, previous_centroid=None):
    attrs = {"dataset_id": "parcel_updates_v3", "crs": str(df.crs.to_epsg())}

    if df.empty:                                    # empty batch is itself a coverage event
        coverage_gauge.set(0.0, attributes=attrs)
        return

    ingested_union = df.geometry.union_all()        # dissolve to avoid double-counting overlaps
    ref_area = reference_geom.area
    coverage_ratio = ingested_union.area / ref_area if ref_area > 0 else 0.0
    coverage_gauge.set(coverage_ratio, attributes=attrs)

    if previous_centroid is not None:               # drift is meaningless on the first run
        drift = ingested_union.centroid.distance(previous_centroid)
        drift_histogram.record(drift, attributes=attrs)

2. Warehouse validation query (PostGIS)

-- Coverage + drift in a single pass against the registered reference extent.
WITH batch_extent AS (
  SELECT
    ST_Union(geom)        AS footprint,           -- dissolve, not ST_Envelope, for true area
    ST_Extent(geom)::geometry AS bbox,
    COUNT(*)              AS row_count
  FROM raw_ingest_batch
  WHERE geom IS NOT NULL AND ST_IsValid(geom)      -- never area-sum invalid geometry
),
coverage_calc AS (
  SELECT
    ST_Area(b.footprint) / NULLIF(ST_Area(r.ref_extent), 0) AS coverage_ratio,
    ST_Distance(ST_Centroid(b.footprint), ST_Centroid(r.ref_extent)) AS extent_drift_m,
    b.row_count
  FROM batch_extent b
  JOIN reference_extents r ON r.dataset_id = 'parcel_updates_v3'
  -- reference_extents and raw_ingest_batch MUST share an SRID; assert it upstream.
)
SELECT * FROM coverage_calc;

3. OpenTelemetry Collector routing (contrib build, YAML)

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  batch:
    timeout: 5s
    send_batch_size: 1000
  filter:                                    # keep only spatial coverage series on this pipe
    metrics:
      include:
        match_type: strict
        metric_names:
          - "gis.spatial.coverage_ratio"
          - "gis.spatial.extent_drift_m"
          - "gis.spatial.bbox_null_ratio"
          - "gis.spatial.tile_completeness"

exporters:
  prometheus:
    endpoint: "0.0.0.0:8889"
    namespace: "geospatial_pipeline"

service:
  pipelines:
    metrics:
      receivers: [otlp]
      processors: [batch, filter]
      exporters: [prometheus]

This wiring lets coverage series flow into Grafana and the alerting stack while staying aligned with the SLA-bound thresholds defined in Tracking Spatial Data Freshness SLAs through shared metric naming. When coverage collapses because an upstream tile service is down rather than because the data genuinely shrank, the degradation strategy in Fallback Chains for Spatial API Failures keeps the pipeline serving a bounding-box approximation instead of a hole.

Threshold Design & Alerting Logic

Coverage thresholds are not universal constants — they are a function of how volatile each dataset’s footprint legitimately is. A cadastral boundary set should barely move between runs, so even 150 m of centroid drift is suspicious; a fleet of moving assets relocates by kilometres every minute by design. Encode this with a tiered severity model rather than a single global cutoff.

Severity Condition Spatial-workload rationale
WARNING coverage_ratio < 0.92 for 2 consecutive runs A small dip is often a legitimate seasonal gap; warn, don’t page.
CRITICAL coverage_ratio < 0.85 or extent_drift_m > 500 Area loss or displacement this large breaks spatial joins and routing.
CRITICAL bbox_null_ratio > 0.08 Mass null geometry indicates a parser or schema break upstream.
DYNAMIC_BASELINE drift exceeds mean + 3σ of the trailing 30-run window High-mobility feeds where no static cutoff is meaningful.

The corresponding PromQL keeps alerts scoped per dataset so a single noisy feed cannot mask a quiet one:

# CRITICAL: coverage collapse, evaluated per dataset over a 15m window
- alert: SpatialCoverageCollapse
  expr: |
    min by (dataset_id, region) (
      geospatial_pipeline_gis_spatial_coverage_ratio
    ) < 0.85
  for: 15m
  labels: { severity: critical }
  annotations:
    summary: "Coverage for {{ $labels.dataset_id }} ({{ $labels.region }}) below 85%"

# DYNAMIC_BASELINE: drift beyond 3σ of its own trailing baseline
- alert: SpatialExtentDriftAnomaly
  expr: |
    histogram_quantile(0.95,
      sum by (dataset_id, le) (rate(geospatial_pipeline_gis_spatial_extent_drift_m_bucket[15m]))
    )
    >
    (avg_over_time(spatial_drift_baseline_mean[30d])
     + 3 * avg_over_time(spatial_drift_baseline_stddev[30d]))
  for: 10m
  labels: { severity: critical }

Static thresholds belong to stable datasets; DYNAMIC_BASELINE rules belong to anything whose footprint moves on purpose. Mixing the two — applying a static drift cap to a GPS feed — is the single most common cause of alert fatigue in coverage monitoring. The temporal weighting behind those trailing baselines is handled in Temporal Baseline Alignment for Time-Series GIS, which keeps event-time and ingestion-time windows from contaminating the statistics.

Failure Modes & Edge Cases

Coverage telemetry has its own failure surface — situations where the metric lies, or where a real defect slips past a naively configured gate. These are the patterns worth instrumenting against explicitly.

  • CRS mismatch inflates an apparent extent collapse. A batch silently delivered in EPSG:3857 instead of EPSG:4326 produces a footprint area thousands of times larger or smaller than the reference, firing a coverage alarm that has nothing to do with completeness. Diagnosis: SELECT ST_SRID(geom), COUNT(*) FROM raw_ingest_batch GROUP BY 1 — more than one SRID, or an unexpected one, is the smoking gun. Reject implicit conversions and enforce ST_Transform with explicit EPSG codes upstream of the coverage gate.

  • Antimeridian-crossing envelopes wrap the wrong way. A dataset spanning the ±180° line computes a bounding box that girdles the entire globe, so coverage_ratio reads near 1.0 even when half the tiles are missing. Diagnosis: check for features with longitude extremes near both +180 and −180; switch to ST_Union-based footprints over geography type, or split geometries at the antimeridian before measuring.

  • Convex hull masks interior holes. Coverage computed from a convex hull or bounding envelope reports full completeness even when a doughnut-shaped gap sits in the middle of the dataset. Diagnosis: compare hull area against dissolved ST_Union area; a large divergence means the interior is hollow. Measure against the dissolved footprint, not the hull, for completeness-sensitive feeds.

  • Index fragmentation degrades the query that computes coverage. GiST index bloat turns the envelope query from logarithmic to near-linear, so the coverage check itself times out and emits no metric — and missing data reads as healthy. Diagnosis: SELECT pg_size_pretty(pg_relation_size('idx_geom')) trending upward against stable row counts. Schedule REINDEX INDEX CONCURRENTLY.

  • Empty batch silently clears the alert. A zero-row batch produces no envelope, and a monitor that only evaluates non-empty batches never fires — the most complete-looking outage there is. Diagnosis: treat an empty batch as coverage_ratio = 0, as the extraction snippet above does, and alert on absence with absent_over_time.

Extent drift, shrinkage, and the interior hole a convex hull masks A dashed reference extent rectangle is overlaid by a smaller solid current-run extent that has shifted down and right. The reference centroid and the current footprint centroid are marked, connected by a drift vector labelled d_drift. The ingested footprint is drawn as a filled blob with a transparent doughnut hole in its interior, representing real coverage loss. A dashed convex hull is drawn around the footprint to show that hull-based coverage would span the gap and read full completeness, while the dissolved footprint correctly reveals the missing interior. Reference extent · A_ref Current-run extent ref centroid d_drift Reference extent (A_ref) Current-run extent Convex hull (masks gap) Ingested footprint Interior gap = real loss d_drift centroid shift

Troubleshooting Checklist

When a coverage or drift alert fires, work the signal from data plane outward rather than guessing:

  1. Confirm the CRS first. Run SELECT ST_SRID(geom), COUNT(*) FROM raw_ingest_batch GROUP BY 1. A rogue SRID explains most sudden extent swings — fix the projection contract before touching anything else.
  2. Distinguish shrinkage from displacement. Compare coverage_ratio and extent_drift_m side by side: low ratio with low drift means missing data; healthy ratio with high drift means the footprint moved, which points at a different region selection upstream.
  3. Check the null ratio. A spiking bbox_null_ratio means malformed ingestion — run ogrinfo -al -so input.shp on the source and route survivors through ST_MakeValid before re-measuring.
  4. Audit the partition predicate. SELECT partition_key, COUNT(*) FROM ingest_logs WHERE status='filtered' reveals whether an upstream filter quietly dropped a geographic shard. Restore the missing shards and re-run.
  5. Inspect index health. If the coverage query is slow or timing out, check pg_relation_size('idx_geom') for bloat and REINDEX INDEX CONCURRENTLY during a low-traffic window.
  6. Validate against the golden reference. Reprocess a 1% sample against the registered reference extent; if computed envelopes deviate beyond ±0.001% from the stored golden file, the regression is real and the baseline should not be updated.
  7. Reconcile and reset the baseline. Once telemetry clears, update the historical coverage baseline so the next DYNAMIC_BASELINE evaluation reflects the corrected state — never let an incident’s bad data poison the trailing statistics.

For streaming feeds where assets move continuously, this checklist compresses into a sub-minute loop; the dedicated procedure for that case lives in Setting Up Freshness Alerts for Real-Time GPS Feeds. Coverage anomalies that span regions also need topology-aware correlation across collectors, which is the focus of Monitoring Topology for Multi-Region GIS.