Skip to main content

A canary for your CDC pipeline โ€” catches silent replication drift before your analysts do.

Project description

๐Ÿค CDCanary

CI License: MIT Python

A canary for your CDC pipeline โ€” catches silent replication drift before your analysts do.

CDCanary demo โ€” one scan catches five kinds of silent drift

CDC pipelines fail loudly when they crash โ€” and silently when they don't. A column added mid-stream lands as NULL in your warehouse. A backfill quietly skips rows. Replication lag creeps from minutes to days. No error, no alert. Someone finds out three months later, in a business meeting, with a wrong number on the slide.

CDCanary is a lightweight monitor that runs aggregate consistency checks between your source and replica on a schedule, and pings Slack when they disagree.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   CDC (Datastream / Debezium / Fivetran / ...)   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  MySQL   โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ โ”‚ BigQuery  โ”‚
โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜                                                  โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜
     โ”‚                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                             โ”‚
     โ””โ”€โ”€โ”€โ”€โ”€ aggregate โ”€โ”€โ”€โ–ถโ”‚  CDCanary โ”‚โ—€โ”€โ”€โ”€โ”€ aggregate queries -โ”€โ”€โ”€โ”€โ”˜
            queries       โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜
                                โ”‚  row delta ยท freshness ยท null-rate ยท schema drift
                                โ–ผ
                            Slack alert

Approach

CDCanary deliberately does not do row-level diffing. Aggregate signals (counts, timestamps, null fractions, schemas) catch the failure modes that actually happen in CDC pipelines, run in seconds on billion-row tables, and keep the adapter surface small enough to maintain. It monitors continuously โ€” this is a canary you run from cron, not a one-off migration validator.

The known blind spot: rows whose values are wrong while counts and null rates still match (e.g. lost UPDATE events). A sampled-checksum check is on the roadmap to cover that probabilistically without paying full-diff costs.

Checks

Check Signal Catches
row_delta source vs target row count (windowed) dropped rows, stalled backfill
freshness source vs target max(updated_at) lag replication lag, dead connector
null_rate source vs target null fraction per column schema-drift NULL corruption
schema_drift column set + coarse types added/missing columns, type changes
sampled_checksum row contents on a sampled key set (newest half + a rotating spread across the whole range) value-level corruption โ€” lossy casts, tz shifts, lost updates

Aggregate checks are symmetric queries โ€” any supported connector can be a source or a target. 3 connectors = 9 directions, including reverse ETL (BigQuery โ†’ MySQL) and same-engine replicas (PostgreSQL โ†’ PostgreSQL).

Performance

Measured on a PostgreSQL 16 โ†” PostgreSQL 16 pair in local Docker (Apple Silicon), single orders-shaped table, default options:

check 10M rows (0.9 GiB) 100M rows (8.6 GiB)
row_delta 1.6s 22s
freshness 0.7s 15s
null_rate (6 columns) 3.2s 101s
schema_drift <0.1s 0.1s
sampled_checksum (strategy: recent) <0.1s <0.1s
sampled_checksum (strategy: mixed, n=100) 1.4s 38s

Client peak memory was 57 MB at both scales โ€” every check either aggregates inside the database or fetches a fixed-size sample, so table growth costs the client nothing.

Two honest observations from the 100M run:

  • Full-scan checks (null_rate, the spread half of sampled_checksum) go I/O-bound once the table outgrows the database's cache โ€” the jump from 3s to 101s is disk, not the tool. On tables that size, scope them down via overrides (null_rate: false, strategy: recent, or a where window) if the full default set is slower than your check interval.
  • Index-backed checks (row_delta, freshness, recent sampling) stay cheap enough to run every few minutes at any of these scales.

Connectors (v0.1)

MySQL ยท PostgreSQL ยท BigQuery

Quickstart

Zero config โ€” point it at both ends and see what disagrees:

pip install cdcanary[mysql,bigquery]

cdcanary scan \
  --source mysql://readonly:$PW@prod-db/shop \
  --target bigquery://my-project/raw_shop

# โœ… ok    shop.orders    row_delta        rows match: 1,204,331 vs 1,204,318 (ฮ”0.00%)
# โœ… ok    shop.orders    freshness        replication lag 4m (within limit)
# ๐Ÿ”ด FAIL  shop.products  null_rate        null-rate drift: sale_status (src 0.0% vs tgt 5.9%)
# ๐Ÿ”ด FAIL  shop.products  sampled_checksum row content drift: 6/90 sampled rows differ (cols: price; e.g. id=75)
# ๐Ÿ”ด FAIL  shop.coupons   table_presence   table exists in source but not in target

Then make it permanent โ€” generate a config and put check on a schedule:

cdcanary init --discover --source mysql://... --target bigquery://...
cdcanary check -c cdcanary.yml        # cron / GitHub Actions / Airflow

Try the demo locally

The GIF above is a real run. examples/demo spins up a MySQL "source" and a PostgreSQL "replica" with five kinds of drift deliberately baked in โ€” a missing table, stalled replication, schema drift, a lossy price cast only row-content sampling can see, and the NULL-corruption case this tool exists for:

cd examples/demo && docker compose up -d --wait
cdcanary scan \
  --source mysql://root:demo@127.0.0.1:13306/shop \
  --target postgres://demo:demo@127.0.0.1:15432/shop/public
docker compose down -v   # cleanup

The same seeds double as the integration suite โ€” pytest -m integration boots them in throwaway testcontainers and asserts every seeded drift is caught (runs in CI on each push).

Run it on a schedule (GitHub Actions)

cdcanary check is designed to be a cron job, and GitHub Actions is the cheapest cron you already have. examples/github-actions has a copy-paste workflow: scheduled runs, secrets-based credentials, warn/fail mapped to run status, results kept as artifacts.

Configuration

Config describes rules, not snapshots. A schema pair re-discovers tables on every run, so tables added or dropped at the source are picked up without config edits โ€” and a new source table that hasn't reached the target yet is itself a finding (table_presence).

connections:
  mysql_prod:
    type: mysql
    host: prod-db.internal
    database: shop
    user: readonly
    password: env:MYSQL_PASSWORD      # secrets always come from the environment

  bq_raw:
    type: bigquery
    project: my-project
    credentials: env:GOOGLE_APPLICATION_CREDENTIALS

pairs:
  - name: shop
    source: { connection: mysql_prod, schema: shop }
    target: { connection: bq_raw, schema: raw_shop }
    tables: ["*", "!tmp_*"]
    # target_table: "shop_{table}"   # uncomment if the CDC tool renames tables           # globs; "!" excludes
    defaults:                         # applied to every discovered table
      row_delta:    { tolerance_pct: 0.5 }
      freshness:    { column: auto, max_lag_minutes: 60 }
      null_rate:    { columns: auto, max_diff_pp: 1.0 }
      schema_drift: { ignore_columns: [datastream_metadata] }
    overrides:                        # applied top-to-bottom; later rules win
      - match: "log_*"                # glob, exact name, or a list of either
        checks:
          null_rate: false            # too big โ€” skip the expensive check
      - match: orders
        checks:
          freshness: { column: paid_at, max_lag_minutes: 30 }
      - match: users
        target_table: customers       # irregular rename, right next to its exceptions

  # Single-table pair โ€” for tables that deserve hand-tuned checks.
  # Can point across schemas/names and coexist with schema pairs above.
  - name: payments
    source: { connection: mysql_prod, table: shop.payments }
    target: { connection: bq_raw, table: raw_shop.payments_v2 }
    checks:
      row_delta: { tolerance_pct: 0.1, where: "created_at < CURRENT_DATE" }
      freshness: { column: paid_at, max_lag_minutes: 15 }
      null_rate: { columns: [amount, status], max_diff_pp: 0.5 }

alerts:
  slack_webhook: env:CDCANARY_SLACK_WEBHOOK

column: auto picks the first timestamp column by convention (updated_at, modified_at, created_at, ...); columns: auto compares every column present on both sides. Schema pairs and table pairs can be mixed freely โ€” broad coverage from discovery, precise thresholds where it counts.

Table names that differ between source and target are handled in two layers: a pair-level target_table: "shop_{table}" template for systematic renames (CDC tools usually rename uniformly), and per-table target_table in overrides for the irregular few. If the mapping has no pattern at all, single-table pairs are the honest answer.

Exit codes are cron-friendly: 0 all green ยท 1 warnings ยท 2 failures.

Non-goals

  • Full row-level diffing โ€” comparing every row fits one-off migration validation better than scheduled monitoring. CDCanary keeps checks fast and cheap enough to run every hour: aggregates for the broad signals, plus sampled_checksum on a small deterministic sample for value-level verification โ€” never a full-table diff.
  • Auto-remediation โ€” CDCanary detects and alerts; deciding how to fix a pipeline is left to a human.
  • Web dashboard โ€” results are available as terminal output, --json, and Slack alerts, which integrate with whatever dashboarding you already run.

Roadmap

  • v0.1 โ€” 4 checks, 3 connectors, Slack alerts, CLI
  • v0.2 โ€” sampled_checksum check: compare row contents on a sampled key set (newest half for fresh breakage + a rotating spread that walks the whole table across runs, so lost UPDATEs on old rows surface too) โ€” value-level corruption that aggregate signals can't see, without the cost of a full row diff
  • v0.2 โ€” baseline state (trend-based anomaly instead of fixed thresholds)
  • v0.3 โ€” Snowflake connector, Prometheus exporter

Releasing

See RELEASING.md โ€” tags publish to PyPI via trusted publishing.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cdcanary-0.2.0.tar.gz (254.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cdcanary-0.2.0-py3-none-any.whl (33.1 kB view details)

Uploaded Python 3

File details

Details for the file cdcanary-0.2.0.tar.gz.

File metadata

  • Download URL: cdcanary-0.2.0.tar.gz
  • Upload date:
  • Size: 254.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cdcanary-0.2.0.tar.gz
Algorithm Hash digest
SHA256 e782df8f9ebd3c929d680cf996e93fa67e3e1079260975eaa5f47e29f3ec86fb
MD5 18adc66d3b65e904bc42cf2810983427
BLAKE2b-256 3d76a848360d4890142349aa9b4f5d3a4c3523cdf1b217df549a7e975b7a7298

See more details on using hashes here.

Provenance

The following attestation bundles were made for cdcanary-0.2.0.tar.gz:

Publisher: release.yml on thomas783/cdcanary

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cdcanary-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: cdcanary-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 33.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cdcanary-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 eea633bff08bfd3778fc668c696672efb2b9540340956d19fa42ad60af518e57
MD5 fa4bc3037eca775bb1925eb51ab83c18
BLAKE2b-256 d151e7e3fb4fa3c3b8b511d0c4e6a973b526b2ef89151b93a2c0a78fb7e398e4

See more details on using hashes here.

Provenance

The following attestation bundles were made for cdcanary-0.2.0-py3-none-any.whl:

Publisher: release.yml on thomas783/cdcanary

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page