Skip to content

Commit 1f88b84

Browse files
committed
Add per-table write divergence detection to health check script
Compare origin vs target successful write counts per keyspace.table using the new zdm_proxy_write_success_total metric. Alerts when: - Target has zero writes for a table that origin is writing to (critical) - Origin and target counts differ by more than the threshold (warning) During normal dual-write operation, counts should be identical. Any divergence indicates the target is missing writes and will need repair. New config: --write-divergence-threshold / ZDM_WRITE_DIVERGENCE_THRESHOLD (default: 0, meaning any divergence triggers an alert).
1 parent 7363ee1 commit 1f88b84

2 files changed

Lines changed: 59 additions & 2 deletions

File tree

scripts/README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,23 @@ Tracks failed TCP connection attempts from the proxy to cluster nodes. A spike m
4747

4848
*Alert triggers when:* delta > 3 per interval.
4949

50+
**5. Per-table write divergence — `zdm_proxy_write_success_total`**
51+
52+
This counter tracks successful writes per cluster, keyspace, and table with labels `{cluster="origin|target", keyspace="...", table="..."}`. The script compares origin and target counts for each table.
53+
54+
- If origin has writes but target has zero for a table, the target may be completely down for that table — this is **critical**.
55+
- If origin and target counts differ by more than the threshold, writes are succeeding on origin but failing on target for that table — data is diverging and will need repair after migration.
56+
57+
During normal dual-write operation, origin and target counts should be identical. Any divergence means the target is missing writes.
58+
59+
*Alert triggers when:* origin and target counts differ by more than `--write-divergence-threshold` (default: 0).
60+
5061
### Alert severity levels
5162

5263
| Severity | When | What it means |
5364
|----------|------|---------------|
54-
| **critical** | Origin failures, metrics endpoint unreachable | Production is impacted right now |
55-
| **warning** | Target failures, connection problems | Migration data flow is degraded but origin (source of truth) is fine |
65+
| **critical** | Origin failures, metrics endpoint unreachable, target has zero writes for a table that origin is writing to | Production is impacted right now |
66+
| **warning** | Target failures, connection problems, origin/target write count divergence | Migration data flow is degraded but origin (source of truth) is fine |
5667

5768
### Why target failures matter even though origin is fine
5869

@@ -96,6 +107,7 @@ Everything can be set via CLI flags or environment variables. CLI flags take pre
96107
| `--interval` | `ZDM_CHECK_INTERVAL` | `0` (one-shot) | Seconds between checks |
97108
| `--failed-writes-threshold` | `ZDM_FAILED_WRITES_THRESHOLD` | `5` | Alert if failed writes increase by more than this per interval |
98109
| `--write-timeout-threshold` | `ZDM_WRITE_TIMEOUT_THRESHOLD` | `5` | Alert if target timeouts increase by more than this per interval |
110+
| `--write-divergence-threshold` | `ZDM_WRITE_DIVERGENCE_THRESHOLD` | `0` | Alert if origin/target per-table write counts differ by more than this |
99111
| `--slack-webhook-url` | `ZDM_SLACK_WEBHOOK_URL` | *(none)* | Slack incoming webhook URL |
100112
| `--pagerduty-routing-key` | `ZDM_PAGERDUTY_ROUTING_KEY` | *(none)* | PagerDuty Events API v2 integration/routing key |
101113
| `--pagerduty-source` | `ZDM_PAGERDUTY_SOURCE` | `zdm-proxy` | Source field in PagerDuty events |

scripts/zdm-health-check.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
ZDM_CHECK_INTERVAL - seconds between checks when running in loop mode
3030
ZDM_FAILED_WRITES_THRESHOLD - alert if failed writes increase by more than this per interval (default: 5)
3131
ZDM_WRITE_TIMEOUT_THRESHOLD - alert if target write timeouts increase by more than this per interval (default: 5)
32+
ZDM_WRITE_DIVERGENCE_THRESHOLD - alert if origin/target per-table write counts differ by more than this (default: 0)
3233
ZDM_SLACK_WEBHOOK_URL - Slack incoming webhook URL
3334
ZDM_PAGERDUTY_ROUTING_KEY - PagerDuty Events API v2 integration/routing key
3435
ZDM_PAGERDUTY_SOURCE - source field for PagerDuty events (default: zdm-proxy)
@@ -139,9 +140,49 @@ def delta(current, previous):
139140
if d > 3:
140141
problems.append(("warning", f"{cluster.title()} connection failures: +{int(d)} in last interval"))
141142

143+
# Per-table write divergence: compare origin vs target successful writes
144+
# If origin and target counts differ for a table, data may be diverging
145+
write_counts = parse_per_table_writes(metrics)
146+
for table_key, counts in write_counts.items():
147+
origin_count = counts.get("origin", 0)
148+
target_count = counts.get("target", 0)
149+
if origin_count > 0 and target_count == 0:
150+
problems.append(("critical",
151+
f"Write divergence on {table_key}: origin={int(origin_count)} target=0 — target may be down"))
152+
elif origin_count != target_count:
153+
diff = abs(origin_count - target_count)
154+
if diff > config["write_divergence_threshold"]:
155+
problems.append(("warning",
156+
f"Write divergence on {table_key}: origin={int(origin_count)} target={int(target_count)} (diff={int(diff)})"))
157+
142158
return problems
143159

144160

161+
def parse_per_table_writes(metrics):
162+
"""Parse zdm_proxy_write_success_total metrics into {keyspace.table: {origin: N, target: N}}."""
163+
result = {}
164+
prefix = "zdm_proxy_write_success_total{"
165+
for key, value in metrics.items():
166+
if not key.startswith(prefix):
167+
continue
168+
# Extract labels from key like: zdm_proxy_write_success_total{cluster="origin",keyspace="ks",table="t"}
169+
labels_str = key[len(prefix):-1] # strip prefix and trailing }
170+
labels = {}
171+
for part in labels_str.split(","):
172+
if "=" in part:
173+
k, v = part.split("=", 1)
174+
labels[k.strip()] = v.strip().strip('"')
175+
cluster = labels.get("cluster", "")
176+
keyspace = labels.get("keyspace", "")
177+
table = labels.get("table", "")
178+
if cluster and (keyspace or table):
179+
table_key = f"{keyspace}.{table}" if keyspace else table
180+
if table_key not in result:
181+
result[table_key] = {}
182+
result[table_key][cluster] = value
183+
return result
184+
185+
145186
# ---------------------------------------------------------------------------
146187
# Alerting — Slack
147188
# ---------------------------------------------------------------------------
@@ -307,6 +348,9 @@ def main():
307348
parser.add_argument("--write-timeout-threshold", type=int,
308349
default=int(os.environ.get("ZDM_WRITE_TIMEOUT_THRESHOLD", "5")),
309350
help="Alert if target timeouts increase by more than this per interval")
351+
parser.add_argument("--write-divergence-threshold", type=int,
352+
default=int(os.environ.get("ZDM_WRITE_DIVERGENCE_THRESHOLD", "0")),
353+
help="Alert if origin/target per-table write counts differ by more than this")
310354

311355
# Slack
312356
parser.add_argument("--slack-webhook-url",
@@ -326,6 +370,7 @@ def main():
326370
config = {
327371
"failed_writes_threshold": args.failed_writes_threshold,
328372
"write_timeout_threshold": args.write_timeout_threshold,
373+
"write_divergence_threshold": args.write_divergence_threshold,
329374
}
330375

331376
has_alerting = bool(args.slack_webhook_url or args.pagerduty_routing_key)

0 commit comments

Comments
 (0)