Automated Row Count & Attribute Sync for Geospatial Observability
Spatial pipelines lose records silently. A reprojection drops features that fall outside a clipping envelope, an inner join against a stale reference table discards rows with no matching key, and a schema migration coerces a GEOMETRY column into text without raising a single error. By the time a basemap looks wrong in production, the cause has scrolled out of the logs. Automated row count and attribute synchronization closes that gap by treating cardinality and schema state as measured, alertable signals at every pipeline hop. This guide is written for the data engineers, GIS platform administrators, and SREs who own ingestion integrity, and it sits under the broader Spatial Data Freshness & Quality Metrics program, where it provides the baseline counts that freshness and coverage checks build on top of.
Architecture
Automated row count and attribute synchronization operates as the foundational telemetry layer within spatial ETL/ELT pipelines. The architecture initiates at the ingestion boundary, where lightweight, partition-aware collectors intercept transaction logs (CDC), API payloads, or batch dumps prior to any spatial transformation. These collectors materialize a deterministic pre-transform snapshot capturing raw row cardinality, column presence flags, and data type signatures. As records traverse staging and enter the transformation layer, a parallel metadata stream is emitted to a centralized observability store. This dual-path design guarantees that heavy spatial operations — ST_Transform, ST_Union, or spatial joins — never silently drop records or coerce attributes without leaving an auditable trail.
The collector framework is engineered for idempotency and temporal baseline alignment, anchoring each snapshot to a fixed UTC ingestion window to prevent timezone-induced phantom drops. Because the alignment logic is shared, the same window definitions that govern these snapshots should be reused by Temporal Baseline Alignment for Time-Series GIS, so that a row-delta alert and a freshness-lag alert agree on what “the same batch” means. By decoupling telemetry from compute, the system scales horizontally: observability agents run as dedicated sidecar processes, eliminating resource contention with spatial indexing or raster workloads.
This structural separation feeds directly into adjacent quality gates. Counts that pass the sync layer flow into Spatial Coverage & Extent Monitoring, which interprets a sudden drop in row count against the area it should cover, and into Geometry Validity & Topology Checks, which catches malformed polygons before they propagate to analytical stores. The collector itself attaches spatial attributes to its spans using the conventions defined in the Geospatial Metric Taxonomy for ETL, guaranteeing that a gis.etl.row_count.delta emitted from a Python operator and one emitted from a PostGIS trigger land in the same namespace. For streaming telemetry, the OpenTelemetry Specification defines the metric emission and context-propagation patterns these collectors follow.
Metric Specification
The core metrics are engineered to capture both absolute state and relative drift across pipeline hops. Row count delta is computed as a signed percentage deviation between source and target partitions, normalized against a 30-day rolling average to absorb seasonal ingestion variance. Attribute synchronization is quantified via a composite sync score that tracks column presence, null-ratio shifts, and implicit type-coercion events. For geospatial tables this extends to spatial attribute parity: verifying that geometry columns, declared SRIDs, bounding boxes, and vertex counts remain structurally consistent across the transform. All metrics are anchored to UTC ingestion timestamps to eliminate phantom drops caused by distributed clock skew.
The composite attribute sync score gis.etl.attribute.sync_score is a weighted product of three sub-signals, bounded to [0, 1], where 1.0 is perfect parity:
where is the fraction of expected columns present, is the null-ratio shift of column against its baseline, is the count of implicit type coercions, is the column count, and the weights sum to 1 (defaults 0.5 / 0.3 / 0.2, weighting column loss most heavily). The row-delta ratio is the simpler .
| Metric | Instrument | Unit | Key dimensions |
|---|---|---|---|
gis.etl.row_count.delta |
gauge | ratio | partition_id, stage, src_srid |
gis.etl.attribute.sync_score |
gauge | score [0,1] |
partition_id, table, schema_version |
gis.etl.attribute.null_ratio_shift |
histogram | ratio | column, dtype |
gis.etl.attribute.type_coercion |
counter | events | column, from_type, to_type |
gis.spatial.geometry.parity |
gauge | bool→{0,1} |
geom_column, srid, geom_type |
gis.etl.join.cardinality_mismatch |
gauge | ratio | left_table, right_table, join_key |
These measurements aggregate into hourly and daily baselines, which in turn feed Tracking Spatial Data Freshness SLAs by establishing the acceptable variance thresholds that an SLA is defined against. Compliance teams rely on the same series to certify that attribute lineage remains unbroken across regulatory boundaries.
Pipeline Integration & Configuration
Deploying the sync layer means embedding collectors directly into the ingestion workflow and exporting their measurements through the OpenTelemetry Collector contrib build, whose filter processor keeps high-cardinality per-column series from overwhelming the metrics backend.
# otel-collector-contrib.yaml — spatial sync telemetry
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
# contrib-only: drop noisy per-column null-shift series below the alert floor
filter/spatial_sampling:
metrics:
datapoint:
- 'metric.name == "gis.etl.attribute.null_ratio_shift" and value_double < 0.01'
batch:
timeout: 10s
exporters:
prometheusremotewrite:
endpoint: "http://observability-store:9090/api/v1/write"
service:
pipelines:
metrics:
receivers: [otlp]
processors: [filter/spatial_sampling, batch]
exporters: [prometheusremotewrite]
The pre-transform snapshot is generated by a partition-aware, idempotent collector that inspects schema with pyarrow and emits metrics through the OpenTelemetry SDK. The checksum sorts columns for determinism so that schema-compatible row reorderings do not register as spurious drift.
# collector_snapshot.py
import hashlib
from datetime import datetime, timezone
import pyarrow.parquet as pq
from opentelemetry import metrics
meter = metrics.get_meter("gis.etl.row_count_attribute_sync")
row_delta = meter.create_gauge("gis.etl.row_count.delta", unit="ratio")
sync_score = meter.create_gauge("gis.etl.attribute.sync_score", unit="1")
def generate_pre_transform_snapshot(source_path: str, batch_id: str, partition_id: str) -> dict:
table = pq.read_table(source_path)
schema = table.schema
null_ratios = {}
for col in schema.names:
arr = table.column(col)
null_ratios[col] = arr.null_count / len(arr) if len(arr) else 0.0
# Stable checksum: sort columns so schema-compatible reorderings don't read as drift
csv_bytes = table.to_pandas().sort_index(axis=1).to_csv(index=False).encode()
snapshot = {
"batch_id": batch_id,
"partition_id": partition_id,
"timestamp_utc": datetime.now(timezone.utc).isoformat(),
"row_count": table.num_rows,
"columns_present": schema.names,
"dtypes": {f.name: str(f.type) for f in schema},
"null_ratios": null_ratios,
"checksum": hashlib.sha256(csv_bytes).hexdigest(),
}
return snapshot
For pipelines that run inside PostGIS, the equivalent baseline is captured in SQL, where geometry-aware aggregates make spatial parity a first-class part of the snapshot rather than an afterthought:
-- Pre-transform spatial snapshot for a single partition
SELECT
count(*) AS row_count,
count(*) FILTER (WHERE geom IS NULL) AS null_geom_count,
ST_SRID(ST_Collect(geom)) AS partition_srid,
sum(ST_NPoints(geom)) AS total_vertices,
ST_Extent(geom) AS bbox
FROM staging.parcels
WHERE ingest_window = date_trunc('hour', now() AT TIME ZONE 'UTC');
Wrap the collector in a pre-execution hook so the schema contract is validated before any spatial processing begins. The same hook is the natural place to short-circuit a run into quarantine using the degradation tiers from Fallback Chains for Spatial API Failures. Operators integrating directly within PostgreSQL can consult the PostGIS Reference Manual for native equivalents of each aggregate above.
Threshold Design & Alerting Logic
Thresholds are tiered so that ordinary ingestion variance never pages a human, while a genuine structural break halts propagation immediately. Spatial workloads are non-linear — a 2% row drop concentrated in one bounding box is far more serious than 2% scattered uniformly — so the dynamic baseline tier compares against a per-partition rolling average rather than a global constant.
| Signal | WARNING | CRITICAL | Action |
|---|---|---|---|
gis.etl.row_count.delta |
> 1.5% |
> 3.0% |
Critical halts downstream publish + opens lineage trace |
gis.etl.attribute.null_ratio_shift |
> 5% |
> 12% |
Schema reconciliation; freeze schema_version pin |
gis.etl.attribute.type_coercion |
any cast | GEOMETRY→VARCHAR / FLOAT→INT |
ERROR severity, block merge |
gis.etl.join.cardinality_mismatch |
> 2% |
> 5% |
Auto lineage trace against reference table |
gis.spatial.geometry.parity |
— | parity == 0 |
Halt; route partition to quarantine |
These translate into PromQL alert rules that fire against the remote-write store:
groups:
- name: spatial-sync.rules
rules:
- alert: RowCountDeltaCritical
expr: abs(gis_etl_row_count_delta) > 0.03
for: 5m
labels: { severity: critical }
annotations:
summary: "Row delta {{ $value | humanizePercentage }} on {{ $labels.partition_id }}"
- alert: AttributeSyncDegraded
expr: gis_etl_attribute_sync_score < 0.95
for: 10m
labels: { severity: warning }
- alert: GeometryParityLost
# dynamic baseline: parity must hold for every emitting partition
expr: min by (partition_id) (gis_spatial_geometry_parity) == 0
labels: { severity: critical }
annotations:
summary: "Geometry parity lost — geom column dropped or recast"
Failure Modes & Edge Cases
-
Reprojection clip drop masked as steady-state.
ST_Transforminto a projected CRS can push features outside a downstream clipping envelope. Row count falls by a constant fraction each cycle, so a global threshold normalized against the 30-day average slowly “learns” the loss as normal. Diagnose by alerting on per-bounding-box delta rather than partition-wide delta, and cross-reference Validating Coordinate Reference System Drift Over Time to confirm the projection, not the data, changed. -
Silent geometry recast. A migration that rewrites
geomfromGEOMETRYtoTEXT(WKT) keeps row count identical, so cardinality checks pass cleanly. Onlygis.spatial.geometry.paritycatches it, which is why parity is a hard CRITICAL independent of row delta. -
Inner-join cardinality erosion. Enriching a spatial table against a stale non-spatial reference drops rows with no matching key. The loss looks like a data problem but is a reference-data problem. Quantify by switching to
LEFT JOINand counting NULL right-side keys; reconcile the reference table before reverting. -
Phantom drop from clock skew. Source and target partitions queried with different local timezones appear to disagree on row count when they are in fact identical batches offset by hours. Always anchor both queries to
now() AT TIME ZONE 'UTC'boundaries. -
Null-ratio shift hidden by averaging. A column that flips from 0% to 40% null in one partition can be diluted below the 5% floor when scored across many partitions. Score null-ratio shift per partition, not per table, and keep the histogram dimensioned by
column.
Troubleshooting Checklist
- Verify partition alignment. Confirm source and target temporal windows match exactly using deterministic UTC boundaries; misaligned ingestion windows are the most common false-positive row delta.
- Audit schema coercion. If the sync score drops, inspect the transformation DAG for implicit casts with
pg_typeof()orpyarrowschema inspection to find silent downcasts that truncate precision or drop geometry metadata. - Isolate spatial drops. A delta over 3% during a spatial join usually means invalid geometries or a projection mismatch. Run
ST_IsValid()andST_IsValidReason()on the source partition and checkST_SRID()against the expected EPSG code for axis-order swaps. - Validate join cardinality. When enriching with non-spatial reference tables, verify foreign-key distributions; switch to
LEFT JOINtemporarily to quantify unmatched records and reconcile reference gaps. - Escalate recurring drift. If the same drift pattern repeats across ingestion cycles, route the partition to quarantine, pin the last known-good
schema_version, and feed the trend into capacity planning before it becomes an SLA breach.
By keeping compute and telemetry strictly separate, geospatial platforms achieve deterministic observability: automated row count and attribute synchronization gives data engineers, SREs, and compliance teams a single, auditable source of truth for what entered the pipeline versus what came out.
Related
- Spatial Data Freshness & Quality Metrics — the parent guide this sync layer reports into.
- Coordinate Reference System Validation — catches the projection mismatches that surface here as row drops.
- Geometry Validity & Topology Checks — the validation gate that runs immediately after sync.
- Tracking Spatial Data Freshness SLAs — turns these baselines into enforceable variance thresholds.
- Validating Coordinate Reference System Drift Over Time — the deep dive on CRS drift behind masked cardinality loss.