obs(selfhost): add Cloudflare D1 size/row-count probe, metrics, alerts, panel - #4153
Conversation
…s, panel Advances #3810 -- the dedup/retention fix (dedupeSignalSnapshots) already shipped in #3857; this covers the remaining, deliberately-deferred half: observability for the ~10GB D1 storage cap that incident hit. Adds an opt-in Cloudflare Management API probe (src/selfhost/d1-size-probe.ts, gated on CLOUDFLARE_D1_MONITOR_ACCOUNT_ID/DATABASE_ID/API_TOKEN -- absent on almost every self-host install, which runs its own SQLite/Postgres backend) that polls D1's file size and RETENTION_POLICY's monitored tables' row counts every 15 minutes from the self-host process, publishing gittensory_d1_database_size_bytes, gittensory_d1_table_row_count{table=...}, and gittensory_signal_snapshots_rows_per_key (scoped to the latest-only-dedup signal types dedupeSignalSnapshots converges to ~1 row per key, so a climbing ratio means that job has stopped running or its allowlist regressed). Size and each table's row count are fetched independently and a failure in one never blanks the other or the previous good reading, recorded via gittensory_d1_probe_errors_total. Adds four Prometheus alerts (D1 size warn ~70% / critical ~90% of the ~10GB cap, the signal_snapshots dedup-regression ratio, and probe-failure) and a "Cloudflare D1 (Central Cloud)" row on the self-host Grafana dashboard.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
gittensory-ui | c5e062c | Commit Preview URL Branch Preview URL |
Jul 08 2026, 08:03 AM |
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4153 +/- ##
==========================================
+ Coverage 93.72% 93.73% +0.01%
==========================================
Files 385 386 +1
Lines 36243 36303 +60
Branches 13281 13296 +15
==========================================
+ Hits 33968 34028 +60
Misses 1618 1618
Partials 657 657
🚀 New features to boost your workflow:
|
|
Warning 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨 ⏸️ Gittensory review result - manual review recommendedReview updated: 2026-07-08 08:16:14 UTC
⏸️ Suggested Action - Manual Review
Review summary Nits — 7 non-blocking
Linked issue satisfactionPartially addressed Review context
Contributor next steps
Signal definitions
🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.
|
|
Re: the "Linked issue satisfaction: Partially addressed" note above (advisory, not a gate blocker) — the dedup/retention job it says is "not present in this diff" is correct about this diff, but that half of #3810 already shipped and is live on
So #3857 + #4153 together fully resolve #3810's requirement list; this PR was never meant to reintroduce the dedup job itself. Leaving this open for the separate verification pass rather than merging. |
Summary
dedupeSignalSnapshots) already shipped and merged in fix(db): dedupe signal_snapshots to bounded latest-per-key retention #3857. This PR covers the remaining, deliberately-deferred half the issue and fix(db): dedupe signal_snapshots to bounded latest-per-key retention #3857 both called out: observability for the Cloudflare D1 storage cap that the 2026-07-06 incident hit (~10GB, 342,243signal_snapshotsrows for 2,183 keys).src/selfhost/d1-size-probe.ts), gated on three newCLOUDFLARE_D1_MONITOR_{ACCOUNT_ID,DATABASE_ID,API_TOKEN}env vars (presence-gated, same convention asisOrbBrokerMode'sORB_ENROLLMENT_SECRETcheck) — absent on almost every self-host install, which runs its own SQLite/Postgres backend and has no real Cloudflare D1 to watch. Wired into the self-host process's own boot-time timer (mirrors the existing Orb relay registration retry timer inserver.ts), polling every 15 minutes.src/selfhost/metrics.tsregistry:gittensory_d1_database_size_bytes— the monitored database's file size.gittensory_d1_table_row_count{table=...}— row count perRETENTION_POLICYtable (single source of truth with the age-based retention policy,src/db/retention.ts).gittensory_signal_snapshots_rows_per_key— rows-per-distinct-(signal_type, target_key), scoped to exactly the four latest-only-dedup signal typesdedupeSignalSnapshotsconverges to ~1 row per key (not the whole table, which intentionally keeps bounded multi-row history for other signal types like queue-health). A climbing ratio means the daily dedup job stopped running or its allowlist regressed.gittensory_d1_probe_errors_total{part=...}— the probe's own failure counter, so an operator can tell "the probe broke" apart from "the database stopped growing".prometheus/rules/alerts.yml, newgittensory-d1-storagegroup): D1 size warn (~70% of the ~10GB cap) / critical (~90%), the signal_snapshots dedup-regression ratio (warn >10, vs. the incident's actual ~157 ratio), and probe-failure.grafana/dashboards/gittensory.json— the general self-host observability dashboard the issue itself points at; deliberately notmaintainer-reviews.json, which sibling PRs in this batch are touching).Scope
type(scope): short summaryConventional Commit format, for examplefix(api): restore profile access checks.CONTRIBUTING.mdand does not reintroduce GitHub Pages, VitePress,site/, orCNAME.Closes #123) — a linked open issue is required for every contributor PR.Validation
git diff --checknpm run actionlintnpm run typechecknpm run test:coveragelocally;codecov/patchrequires ≥99% coverage of the lines AND branches you changed (aim for 100% on your diff so CI variance does not fail near the threshold). Global coverage is a non-blocking trend with a loose 90% backstop, not the gate.npm run test:workersnpm run build:mcpnpm run test:mcp-packnpm run ui:openapi:checknpm run ui:lintnpm run ui:typechecknpm run ui:buildnpm audit --audit-level=moderatesrc/selfhost/d1-size-probe.tsis 100% covered (statements/branches/functions/lines) both in isolation and inside the fullnpm run test:coveragerun (26 dedicated tests intest/unit/selfhost-d1-size-probe.test.ts, covering: config resolution with each of the 3 env vars individually missing; the Cloudflare API client's success/HTTP-error/success:false/malformed-JSON paths; the unsafe-table-identifier guard; signal_snapshots' numbered-placeholder dedup query and its distinct-key ratio, including the division-by-zero guard; andrunD1SizeProbe's independent size-vs-table-count failure isolation, including "keeps the previous sample on a later failure" and "one bad table doesn't block the others"). Also ran the full local gate:npm run selfhost:validate-observability,npm run selfhost:env-reference:check,npm run cf-typegen:check,npm run db:migrations:check,npm run db:schema-drift:check, and the existingtest/unit/selfhost-grafana-dashboard.test.ts/test/unit/worker-entry-boundary.test.ts/test/unit/observability-ci.test.ts/test/unit/docs-selfhost-troubleshooting-metric-names.test.tssuites — all green, none needed changes since this PR is purely additive (new panels/alerts/metric names, no existing ones touched).If any required check was skipped, explain why:
Safety
ui:openapi:checkconfirms no drift).apps/gittensory-ui/src/lib/selfhost-env-reference.tsis mechanically regenerated (npm run selfhost:env-reference) to add 3 rows in the same format as the other 96, not hand-authored UI.UI Evidencesection below with JPG/JPEG or PNG screenshots. — N/A, no visible UI change (see above).docs.self-hosting-troubleshooting.tsxin this PR (keeping scope to the code + observability-config surface the issue asks for);test/unit/docs-selfhost-troubleshooting-metric-names.test.tsonly checks that names mentioned in that doc are real, so it doesn't require adding a new section, and CHANGELOG.md is never edited in a normal PR per house rules.UI Evidence
N/A — no visible UI surface (backend/observability-only change; see Safety above).
Notes
dedupeSignalSnapshots(the actual root-cause fix, fix(db): dedupe signal_snapshots to bounded latest-per-key retention #3857) is unaffected by this PR — this is purely additive observability layered on top of it.server.tsrather than the Cloudflare Worker'sscheduled()cron:server.tsis Codecov-exempt (real reason documented incodecov.yml— "exercised by the Docker build+boot smoke test, not unit-coverable without booting a server/subprocess") and is the one place that already runs a persistent process capable of carrying an in-memory sample from a periodic probe through to a later/metricsscrape; the ephemeral, multi-isolate Cloudflare Worker request lifecycle can't reliably do that, and (today) has no/metricsroute at all..gittensory.yml), matching the existing convention that raw secrets/credentials (GitHub App keys,ORB_ENROLLMENT_SECRET, etc.) stay env-only while policy/tunables live in config-as-code.Closes #3810