Health
At-a-glance cluster health dashboard with automated headless health-sweep alerts via webhook and Cloudflare Cron.
Get a single status grid of cluster health across replication, merges, errors, disks, load, and parts — plus a headless sweep endpoint that fires webhook alerts on a schedule.
Prop
Type
What it does
The Health page aggregates checks across multiple system tables into a single status grid. Each check has a severity level (ok / warning / critical) and a human-readable summary. Operators use this page as a starting point for incident triage.
Checks cover:
- Replication lag — replicas falling behind, readonly tables
- Merge backlog — active merge count and slow merges
- Error rate — recent errors from
system.errors - Disk usage — free space across all disks
- Query load — running query count and memory pressure
- Part health — excessive part counts
- TTL & Partition Health — MergeTree tables with partition bloat, a time-based
PARTITION BYand no table TTL, or a merge backlog (recommend-only; full inventory on/ttl-partition-health)
In addition to the UI, chmonitor exposes a headless health-sweep endpoint (GET /api/cron/health-sweep) that runs the same checks and dispatches webhook alerts. This is designed to be called on a schedule (e.g., Cloudflare Cron every 5 minutes) without a browser.
Pages
| Page | Route | What it shows | System tables |
|---|---|---|---|
| Health | /health | Status grid with per-check severity and details | system.metrics, system.asynchronous_metrics, system.replicas, system.merges, system.errors, system.disks, system.replication_queue, system.processes, system.query_log, system.parts, system.tables |
Using it
Open /health for the status grid — each check reports ok / warning / critical with a summary. Start incident triage here, then drill into the linked feature sections for detail.
To run the same checks without a browser, call the health-sweep endpoint (see below) on a schedule.
Alert Settings layout
/alert-settings has four tabs: Alerts, Thresholds, Activity, and Advanced. The Alerts tab answers "what alerts do I have, and where do they go?" on one screen:
- Built-in alerts — one row per health check, with its name and stable id.
- More ways to alert — Suggested alerts (thresholds derived from how this cluster behaved) and Custom rules (your own rule on any metric).
- Channels — browser, server delivery channels, and custom webhook targets.
- Delivery — Routing rules (which alerts go to which channel), Webhook subscriptions (POST alert events to your own endpoints), and Digest (batch non-critical alerts).
- Minimum severity — the default floor every channel inherits.
The Advanced tab now holds only Silencing: Quiet hours and Maintenance windows. Each item still opens in its own dialog. Old links keep working: ?tab=routing, ?tab=webhooks, ?tab=digest, ?tab=suggested, and ?tab=custom-rules open that dialog on the Alerts tab; ?tab=maintenance and ?tab=quiet-hours open it on Advanced. ?tab=define, ?tab=delivery, and ?tab=silencing open the tab that holds that group.
Naming built-in alerts
Each built-in alert can be renamed from the list (1–80 characters). The name is display-only in the dashboard: notifications still use the built-in check title, and the alert keeps its check id, so a rename never resets alert state or acknowledgements. Names are stored in the metadata database; with no database the list still renders, but rename is read-only.
Permissions & access
CHM_FEATURE_HEALTH_ACCESS=authenticatedCHM_FEATURE_HEALTH_ENABLED=false[features.health]
enabled = true
access = "authenticated"Configuration
Run alerts without a metadata database
Alerting works on Docker and Kubernetes with no D1 and no Postgres, but there is one boundary to learn up front: declarative config can fire alerts. It cannot acknowledge them.
| Data | Declarative? | Why |
|---|---|---|
| Alert definitions: rules, thresholds, routing, channels, webhook targets, quiet hours, maintenance windows, digest settings | Yes | Desired state. An operator declares intent. |
| Alert state: firing or cleared, hysteresis streak, incident start, last-notified | No | Observation, not intent. It cannot be authored. |
| ACKs, suggestion dismissals, digest buffer | No | Per-user runtime state. |
With no database, alert state lives in memory in each worker instance. So
hysteresis streaks and incident timers reset on every restart or deploy, and
Acknowledge, Clear and dismiss are disabled in the UI. POST and DELETE on
/api/v1/health/ack return 501. The ClickHouse state backend
(CHM_STATE_CLICKHOUSE_*) does not back the alert stores; the alert surface
runs on D1 or Postgres (DATABASE_URL / POSTGRES_URL). Attach one of them if
you need ACKs. See GET /api/v1/config capabilities.health
for what a deployment resolved.
In-memory alert state is not migrated when you attach a database later. On the first sweep that reaches the new database, the in-memory state is discarded and alerting starts fresh from the database: a pending hysteresis streak restarts, and a condition that is still firing opens a new incident and can notify again.
Where definitions come from
Definitions merge from four sources, highest first:
- D1 or Postgres rows, written from the UI.
- Config files in
CHM_HEALTH_CONFIG_DIRECTORY(default/etc/chmonitor/health.d). - Environment variables (
HEALTH_ALERT_*,HEALTH_THRESHOLD_*). - Built-in defaults.
Every source is always read; precedence only decides who wins a key. Per key:
- Most definitions (routes, custom rules, webhook targets, quiet hours, channels) merge field by field. A higher source overrides a field only when it sets a non-empty value, so a file can set
enabledwhile a D1 row owns a token. A higher source cannot clear a lower source's value back to empty. - Maintenance windows are replaced whole per
id, and windows from different sources are all active. - Digest settings are a single value: the highest source wins outright.
- A key that only a lower source defines disappears when that source stops defining it. There is no tombstone.
Declared entries are shared by every user (deployment-wide, like the env
fallbacks). In the UI they are read-only and show a Config file or Env
badge instead of Delete or Reset. Custom rules declared with id: foo appear as
custom:foo. The directory is read once per process, so a changed file needs
a pod restart. Secrets are read from the environment on each read.
The files
One YAML file per concern, named exactly <concern>.yaml (or .yml). Files with
other names are skipped with a warning.
| File | Top-level keys | Entry key |
|---|---|---|
alerts.yaml | rules, thresholds | id (rules), rule name (thresholds) |
routing.yaml | routes | id |
channels.yaml | channels, webhookTargets | channel, id |
quiet-hours.yaml | windows | id |
maintenance.yaml | windows | id |
digest.yaml | enabled, windowMinutes | none (single value) |
Every keyed entry carries its key explicitly. The schema is strict: unknown keys are rejected, so a typo is reported instead of silently ignored.
Secrets never go in these files. A ConfigMap is not a Secret. Secret fields
take the name of an environment variable (secretEnv, urlEnv,
headersEnv), which you back with a Kubernetes Secret. An unset variable skips
that entry with a warning that names the variable, never its value. Declared
outbound URLs must be HTTPS and pass the same SSRF check as the API. A URL that
is itself a credential (Slack or Discord webhook) belongs in a routes entry
secretEnv or a webhookTargets urlEnv, not in a channel target.url.
alerts.yaml declares custom rules. metric is a key from the rule
builder catalog (for example failed-mutations, readonly-replicas,
replication-max-lag, disk-usage-percent). op is one of >, >=, <,
<=, and warning and critical must be ordered for that operator.
rules:
- id: failed-mutations-any
name: Failed mutations
metric: failed-mutations
op: ">="
warning: 1
critical: 5alerts.yaml also accepts a thresholds: map (rule id to warning /
critical). The sweep and the current-findings snapshot apply it over the
built-in default. It is merged with the HEALTH_THRESHOLD_<RULE>_WARNING and
HEALTH_THRESHOLD_<RULE>_CRITICAL environment variables field by field, and
the file wins a collision: warning: 70 in the file with
HEALTH_THRESHOLD_DISK_USAGE_CRITICAL=95 in env gives 70 / 95. A rule id
that is not registered is skipped with a warning in the log. The file is read
once per process, so restart after editing it; env changes apply on the next
sweep. Only built-in rule ids are honoured by the current-findings snapshot.
thresholds:
disk-usage:
warning: 70routing.yaml declares routes. provider is webhook, pagerduty,
telegram, ntfy or pushover; matchRule and matchHost default to * and
accept globs. secretEnv names the variable holding the webhook URL, PagerDuty
routing key, Telegram bot token, ntfy token (optional) or Pushover app token.
target carries the non-secret fields: serviceName (pagerduty), chatId
(telegram), url (ntfy) and user (pushover).
routes:
- id: oncall-pagerduty
provider: pagerduty
matchRule: "*"
matchHost: "*"
minSeverity: critical
secretEnv: CHM_ROUTE_PAGERDUTY_KEY
target:
serviceName: chmonitor-prod
- id: team-telegram
provider: telegram
matchRule: "custom:*"
secretEnv: CHM_ROUTE_TELEGRAM_TOKEN
target:
chatId: "-1001234567890"channels.yaml declares the server delivery channels (webhook,
healthchecks, email, opsgenie, telegram, ntfy, pushover, twilio) and
custom webhook targets. A target's urlEnv is required; headersEnv names a
variable holding a JSON object of secret headers. Precedence against env is per
channel, not per field.
channels:
- channel: telegram
minSeverity: warning
secretEnv: HEALTH_CHANNEL_TELEGRAM_TOKEN
target:
chatId: "-1001234567890"
- channel: opsgenie
secretEnv: HEALTH_CHANNEL_OPSGENIE_KEY
target:
region: eu
webhookTargets:
- id: ops-chat
name: Ops chat
format: slack
minSeverity: warning
urlEnv: CHM_WEBHOOK_OPS_CHAT_URL
headersEnv: CHM_WEBHOOK_OPS_CHAT_HEADERSquiet-hours.yaml takes days (0 to 6), start and end as HH:mm, an
IANA timezone (default UTC) and an optional severityCap: critical.
windows:
- id: nightly
days: [1, 2, 3, 4, 5]
start: "22:00"
end: "07:00"
timezone: Asia/Ho_Chi_Minh
severityCap: criticalmaintenance.yaml takes an optional hostId (omit for all hosts), a
reason, and ISO-8601 startsAt / endsAt with endsAt later.
windows:
- id: upgrade-2026-10-05
hostId: 0
reason: ClickHouse 25.8 upgrade
startsAt: "2026-10-05T01:00:00Z"
endsAt: "2026-10-05T03:00:00Z"digest.yaml is a single object. windowMinutes is 0 to 1440.
enabled: true
windowMinutes: 15Fail-closed behaviour
- A missing directory, or a runtime with no filesystem (Cloudflare Workers), is a silent no-op.
- Bad YAML, unknown keys, schema violations, entries without a key and duplicate keys are skipped with a warning. The rest of the file and the other files still load, and the pod never fails to start.
- Warnings name the file and entry, never a field value, so a secret pasted into a ConfigMap by mistake does not reach the logs.
- An entry whose
*Envvariable is unset, or whose URL is not an allowed HTTPS endpoint, is skipped. - With no database and no declared custom rule, the rule builder still reports "not available" (
501) on write.
For the Kubernetes mount, see Alert definitions from a ConfigMap. Variable reference: CHM_HEALTH_CONFIG_DIRECTORY.
Health-sweep cron alerting
The health-sweep endpoint runs checks over all configured hosts and sends a webhook notification when a check meets or exceeds the minimum severity.
| Variable | Default | Description |
|---|---|---|
CRON_SECRET | (unset = endpoint disabled) | Required. Guards GET /api/cron/health-sweep and GET /api/cron/retention-prune. Pass as Authorization: Bearer <secret> only — query-string ?secret= is rejected because it lands in access logs. When it is unset/empty the endpoints fail closed and return HTTP 503 — they do not run. Note that retention-prune is destructive (it deletes conversation rows past each plan's retention window), which is why these routes refuse to run unauthenticated. |
CHM_HEALTH_SWEEP_ENABLED | (unset = enabled iff CRON_SECRET is set) | Gates the scheduled (headless) sweep fired by the Cloudflare Cron trigger. Set true to force-enable, false to pause scheduled sweeps without removing the cron. When unset it fails closed to the CRON_SECRET posture. Does not affect manual curl hits (those only need a valid CRON_SECRET). Truthy values: 1/true/yes/on. |
HEALTH_ALERT_ENABLED | false | Set to true to enable webhook dispatch. |
HEALTH_ALERT_WEBHOOK_URL | (required if enabled) | Incoming webhook URL. The channel is auto-detected from the URL: Slack (hooks.slack.com) and Discord (discord.com/api/webhooks) get channel-native rich bodies; Microsoft Teams (*.webhook.office.com, or a Workflows *.logic.azure.com URL) gets an Adaptive Card; Google Chat (chat.googleapis.com) gets a cardsV2 card; any other URL receives the plain {"text": "...", "content": "..."} wrapper. See Alerting to Slack and Discord for setup examples. |
HEALTH_ALERT_HEALTHCHECKS_URL | (unset = healthchecks disabled) | healthchecks.io ping URL. The sweep GETs it on each alert (bare URL) and appends /fail on recovery. Self-hosted or cloud healthchecks.io both work. Overridable from the Server delivery channels UI (see callout below). |
HEALTH_ALERT_MIN_SEVERITY | warning | Global default minimum severity that triggers a notification. Values: warning or critical. Per-channel and per-route overrides below take precedence. |
HEALTH_ALERT_<CHANNEL>_MIN_SEVERITY | (inherit global) | Per-channel severity floor that beats the global default for one channel only (<CHANNEL> ∈ WEBHOOK, EMAIL, OPSGENIE, PAGERDUTY, TELEGRAM, NTFY, PUSHOVER). Example: HEALTH_ALERT_WEBHOOK_MIN_SEVERITY=warning with a critical global sends warnings to the webhook only. Values: warning or critical. Twilio keeps its own HEALTH_ALERT_TWILIO_MIN_SEVERITY. |
HEALTH_ALERT_<CHANNEL>_ENABLED | (inherit) | Set false to silence one channel entirely without unsetting its config (<CHANNEL> as above). HEALTH_ALERT_WEBHOOK_ENABLED=false disables the webhook channel — routes + the legacy global URL — distinct from the master HEALTH_ALERT_ENABLED. |
HEALTH_ALERT_TELEGRAM_BOT_TOKEN | (unset = Telegram disabled) | Telegram Bot API token. Server-only secret — never exposed to the browser. Set together with HEALTH_ALERT_TELEGRAM_CHAT_ID to enable the global Telegram channel; each finding is sent to the chat via the Bot API sendMessage endpoint. Per-rule/per-host Telegram routes (Routing tab) override this fallback. |
HEALTH_ALERT_TELEGRAM_CHAT_ID | (unset = Telegram disabled) | Target Telegram chat id (e.g. -1001234567890, or @channelname). Both this and HEALTH_ALERT_TELEGRAM_BOT_TOKEN must be set for the global Telegram channel to fire. |
HEALTH_ALERT_NTFY_URL | (unset = ntfy disabled) | Full ntfy topic URL (e.g. https://ntfy.sh/my-topic, or a self-hosted server). Each finding is published with Title/Priority/Tags headers + a plain-text body (severity → priority: critical 5/urgent, warning 4/high, recovery 3/default). Per-rule/per-host ntfy routes (Routing tab) override this fallback. |
HEALTH_ALERT_NTFY_TOKEN | (unset = no auth) | Optional ntfy access token for a protected topic, sent as Authorization: Bearer <token>. Server-only secret — never exposed to the browser. |
HEALTH_ALERT_TWILIO_ACCOUNT_SID | (unset = Twilio disabled) | Twilio Account SID. Set together with HEALTH_ALERT_TWILIO_AUTH_TOKEN, HEALTH_ALERT_TWILIO_FROM, and HEALTH_ALERT_TWILIO_TO to enable the global Twilio SMS channel. There are no per-rule/per-host SMS routes, but the credentials are editable from the Server delivery channels UI (see callout below). |
HEALTH_ALERT_TWILIO_AUTH_TOKEN | (unset = Twilio disabled) | Twilio Auth Token. Server-only secret — never exposed to the browser. Authenticates as HTTP Basic (AccountSid:AuthToken) against the Twilio Messages API. |
HEALTH_ALERT_TWILIO_FROM | (unset = Twilio disabled) | The Twilio phone number (E.164, e.g. +15557654321) messages are sent from. |
HEALTH_ALERT_TWILIO_TO | (unset = Twilio disabled) | Comma-separated list of SMS recipients (E.164, e.g. +15551234567,+15559876543). One SMS is sent per recipient per finding. |
HEALTH_ALERT_TWILIO_MIN_SEVERITY | critical | Minimum severity that triggers an SMS specifically (independent of HEALTH_ALERT_MIN_SEVERITY above). Values: warning or critical. Defaults to critical because SMS is a last-resort paging channel that costs real money per message — set to warning to also page on warnings. |
HEALTH_ALERT_PUSHOVER_TOKEN | (unset = Pushover disabled) | Pushover application API token. Server-only secret — never exposed to the browser. Set together with HEALTH_ALERT_PUSHOVER_USER to enable the global Pushover channel; each finding is POSTed to the Messages API (severity → priority: critical 2/emergency with retry+expire, warning 0/normal, recovery -1/low-quiet). Per-rule/per-host Pushover routes (Routing tab) override this fallback. |
HEALTH_ALERT_PUSHOVER_USER | (unset = Pushover disabled) | Target Pushover user or group key. Both this and HEALTH_ALERT_PUSHOVER_TOKEN must be set for the global Pushover channel to fire. |
HEALTH_ALERT_DIGEST_MINUTES | 0 (off) | Optional time-window digest for alert batching. 0/unset dispatches every finding on the sweep tick (in-pass grouping still applies — see the callout below). A positive value buffers non-critical findings and flushes them together once the window elapses, collapsing a burst into one message per target; critical findings always bypass the buffer and page immediately. Requires a D1 binding for the buffer; with none it degrades to 0 (off). Overridable from Health → Settings → Alerts → Digest batching. |
Alert grouping & digest batching
When one sweep finds several alerts bound for the same delivery target (e.g. disk filling on 8 hosts, all routed to one Slack channel), they are combined into one message — a summary line (3 critical, 2 warning on 4 hosts) followed by a line per finding — instead of N separate pings. This in-pass grouping is always on and needs no configuration; it applies to the generic webhook, Slack, and Telegram channels (every other channel keeps per-finding sends). To also batch across time, enable the optional time-window digest (HEALTH_ALERT_DIGEST_MINUTES or the Alerts tab): non-critical findings wait up to the window and flush together, while criticals still page immediately.
Edit channels from the UI (no redeploy)
Every channel above is also editable from Health → Settings → Alerts → Server delivery channels. A channel saved there is persisted per-owner (in D1, when configured) and takes precedence over its HEALTH_ALERT_* environment variable on the next sweep — so you can add or rotate a destination without a redeploy. Leaving a secret field blank on save keeps the stored secret. A channel with no saved config falls back to its env variable (shown as "Configured via server env"), and a deployment with no D1 binding simply keeps using the env variables. Browser notifications stay per-browser (local to your session).
SMS costs real money
Every Twilio dispatch is a billed SMS — one message per configured recipient, per finding. HEALTH_ALERT_TWILIO_MIN_SEVERITY defaults to critical so routine warnings never page a phone; only raise it to warning if you understand the cost. Message bodies are also truncated to Twilio's 1600-character SMS limit.
CRON_SECRET is required
CRON_SECRET is required for the cron endpoints. When it is unset, /api/cron/health-sweep and /api/cron/retention-prune fail closed and return HTTP 503 (they do not run) — retention-prune is destructive, so it must never be reachable unauthenticated. Set CRON_SECRET and pass it as Authorization: Bearer <secret> from your cron caller.
Set secrets
Example for Cloudflare Workers (using wrangler secret put):
wrangler secret put CRON_SECRET
wrangler secret put HEALTH_ALERT_WEBHOOK_URL
# Optional: Telegram channel (bot token is a secret; chat id is a plain var)
wrangler secret put HEALTH_ALERT_TELEGRAM_BOT_TOKEN
# Optional: ntfy channel (topic URL is a plain var; access token is a secret)
wrangler secret put HEALTH_ALERT_NTFY_TOKEN
# Optional: Twilio SMS channel (auth token is a secret; SID/from/to are plain vars)
wrangler secret put HEALTH_ALERT_TWILIO_AUTH_TOKEN
# Optional: Pushover channel (app token is a secret; user key is a plain var)
wrangler secret put HEALTH_ALERT_PUSHOVER_TOKENEnable alerting
Example environment block:
HEALTH_ALERT_ENABLED=true
HEALTH_ALERT_MIN_SEVERITY=warning
HEALTH_ALERT_WEBHOOK_URL=https://hooks.slack.com/services/...
CRON_SECRET=<random-secret>Call it manually
curl -H "Authorization: Bearer $CRON_SECRET" \
https://your-chmonitor.example.com/api/cron/health-sweepThe endpoint returns a JSON array of check results. It always returns HTTP 200; alert dispatch happens server-side.
Scheduling (Cloudflare Cron)
The hosted deploy schedules the sweep every 10 minutes. In wrangler.toml:
[triggers]
crons = ["0 3 * * *", "0 8 * * 1", "*/10 * * * *"] # retention, weekly-report, health-sweepThe Cloudflare scheduled trigger is routed to the route's GET handler. Two gates apply, in order:
- Auth — the handler requires
CRON_SECRET(returns HTTP 503 when unset). Configure the secret and invoke withAuthorization: Bearer <secret>only — e.g. from an external scheduler, or a Worker Cron that forwards the secret. Query-string?secret=is rejected. - Enablement — after auth, the handler consults
CHM_HEALTH_SWEEP_ENABLED. When it is falsy the route returns200 {"skipped": true}without running the sweep, so you can pause scheduled alerting without touching the cron or the secret. When unset it defaults to enabled (sinceCRON_SECRETis already present at that point).
The */10 cadence (rather than */5) leaves CPU-time headroom: the sweep also generates AI insights per host, so its cost scales with the number of configured CLICKHOUSE_HOST entries. For very large host fleets, either lengthen the cadence or pause the scheduled sweep (CHM_HEALTH_SWEEP_ENABLED=false) and drive it from an external scheduler.
Weekly health report
Beyond the health-sweep, a second cron job builds a proactive weekly HTML report per opted-in host: a 7-day narrative combining the AI insights engine's top findings, the number of adaptive statistical baselines fitted to that cluster, and a disk-capacity outlook from the capacity forecaster.
Each report also includes real cluster data over the window, when the host's system tables allow it (each section is fail-open and simply omitted otherwise):
- Query activity — total and failed query counts, p50/p95 duration, and a per-day sparkline (from
system.query_log). - Ingestion — rows and bytes written by INSERTs per day, with sparklines (from
system.query_log; uncompressed measurement, same model as the Traffic page). - Storage — total on-disk size plus the top 5 tables by size, each annotated with the bytes written during the window (from active
system.parts).
Reports are opt-in, never opt-out — set CHM_WEEKLY_REPORT_HOSTS to a comma-separated list of host indices (e.g. 0,2) to enable it for those hosts. Leave it unset and no reports are generated, so a self-hosted deployment stays quiet by default.
| Variable | Default | Description |
|---|---|---|
CHM_WEEKLY_REPORT_HOSTS | unset (no reports) | Comma-separated host indices to opt in, e.g. 0,2. Garbage entries are dropped; an opted-in index with no matching configured host is skipped with a warning. |
The report is generated by GET /api/cron/weekly-report, guarded by the same CRON_SECRET as the health-sweep endpoint above, and scheduled weekly on Mondays at 08:00 UTC:
[triggers]
crons = ["0 8 * * 1"]The report is always persisted, even when no delivery channel is configured — so a deployment with no webhook still accrues viewable reports. When HEALTH_ALERT_WEBHOOK_URL is set, the report is also best-effort posted to that same webhook.
View or download the most recent persisted report:
# Self-contained HTML document
curl "https://your-chmonitor.example.com/api/v1/insights/weekly-report?host=0"
# Parsed JSON summary instead
curl "https://your-chmonitor.example.com/api/v1/insights/weekly-report?host=0&format=json"
# A specific past week (YYYY-MM-DD week-start)
curl "https://your-chmonitor.example.com/api/v1/insights/weekly-report?host=0&week=2026-07-06"
# PDF (Cloudflare Browser Rendering; falls back to HTML when unavailable)
curl -L "https://your-chmonitor.example.com/api/v1/insights/weekly-report?host=0&format=pdf" -o report.pdfScheduled reports (per-user)
Beyond the env-var opt-in above, Scheduled Reports (/report-settings in the dashboard) lets each user subscribe to a weekly or monthly cluster health report delivered through their already-configured alert channels — email (full HTML), webhook, Telegram, ntfy, and Pushover. Paging channels (Opsgenie, PagerDuty, Twilio) are deliberately excluded: a digest must never page anyone.
- Cadence — monthly (1st of the month, 30-day window,
GET /api/cron/monthly-report, cron0 8 1 * *) or weekly (Mondays, 7-day window, shares the weekly cron above). On chmonitor Cloud, weekly cadence requires a paid plan; monthly is included on Free. - Generate now — build a fresh report on demand and open/download the self-contained HTML. Works on any deployment with zero delivery configuration (
POST /api/v1/reports/generate). - Download PDF — render the same report to PDF via Cloudflare Browser Rendering (
POST /api/v1/reports/generatewith{ "format": "pdf" }, or?format=pdfon the read API). PDF export is a Pro+ (data_export) capability on chmonitor Cloud. It is optional and fail-closed: when noBROWSERbinding is configured (self-hosted Docker / K8s, or any Cloudflare account without Browser Rendering) or a render fails, the request degrades gracefully to HTML (signalled by anX-Report-PDF: unavailableheader). On Cloud, a Pro+ subscriber's scheduled email delivery also gets the PDF attached automatically. - Send test report — deliver one immediately through your configured channels and see the per-channel outcome (
POST /api/v1/reports/test-send). - Audit — the subscription stores the last delivery time and a compact per-channel status.
- Fleet report — a subscription covering more than one host is delivered as ONE combined fleet report instead of N separate ones: a comparison table (findings, critical, queries, ingested bytes, disk %) followed by each host's full sections. Per-host reports are still persisted individually.
Subscriptions live in the report_subscriptions D1 table (owner-scoped, like alert channels); deployments without D1 keep the env-var webhook flow and the on-demand Generate button. The AI agent can also produce the same report conversationally via its generate_cluster_report tool.
Notes & limitations
system.error_logis checked separately by the Errors page (under Operations). The Health page usessystem.errors(in-memory error counts), which resets on server restart.- If a system table is missing (e.g., no
system.replicason a standalone node), that check is skipped with an "unavailable" state rather than a false positive. - The webhook payload is a plain Slack/Discord-compatible JSON object (
{"text": "...", "content": "..."}). Custom payload shapes are not supported in v1. - The health-sweep endpoint queries all configured
CLICKHOUSE_HOSTentries. High host counts increase sweep latency.
Related
Metrics
Live counters, async metrics, and CPU profiling.
Tables
TTL & Partitions inventory and storage pages.
DBA workflows
TTL inventory, schema compare, and recommend-only advisor.
Cluster
Topology and Keeper/ZooKeeper coordination.
Feature permissions
Gate or disable features per section.
Authentication
Configure sign-in and access control.