Appearance
cloudflare
The Cloudflare integration watches one Cloudflare account. It covers Pages deployments, Worker errors, zone status, WAF bypass and traffic quality. A Worker that you deploy in your own Cloudflare account collects the data and pushes it to daemonitor. Your Cloudflare API token stays in that account. daemonitor never holds it.
daemonitor does not run this Worker for you. Each tenant deploys its own copy from the template in plugins/examples/cloudflare-worker/.
Set it up
- In daemonitor, add a system for the Cloudflare account and copy its system key. That key is the reporting key.
- Create a Cloudflare API token with the permissions below.
- Deploy
cloudflare-worker.jsandsuspect-networks.jsfrom the template as one Worker, with a cron trigger. - Set the secrets and variables below on that Worker.
- Wait for the first cron run, or call the Worker's
/testendpoint to see what it collects.
You do not need to add anything to an agent's client.config.json. The Worker posts straight to the server. The agent-side cloudflare plugin in the example config is a leftover. Nothing calls its receiveUpdate(), so it never sees the Worker's data.
API token permissions
The template's docs and code name these. They do not agree on one list, so this table says where each comes from.
| Permission | Used for | Named in |
|---|---|---|
| Account > Pages > Read | Pages project list and deployments | INTEGRATION.md, README.md |
| Account > Account Settings > Read | Account lookup in /diagnostics and /test-auth | INTEGRATION.md |
| Account > Account Analytics > Read | Zone traffic, cache and security-event analytics. Also required for RUM | INTEGRATION.md, worker comments |
| Zone > Zone > Read | Zone list and zone details | README.md |
| Zone > Analytics > Read | Zone analytics | README.md |
| Account > Turnstile Sites > Read | Turnstile widgets, only with INCLUDE_TURNSTILE=true | worker comments |
The security, cache and status breakdowns are best effort. If the token lacks Analytics read, those sections carry an error and the rest of the push still reports.
Some data depends on the zone's plan, not the token. Firewall events need Pro or higher. Bot Management scores need Enterprise. On a lower plan the Worker skips the query and records { unavailable: "plan", requires: "Pro" } or requires: "Enterprise".
The Worker also accepts a Global API Key in place of a token. Every call tries Authorization: Bearer first and falls back to X-Auth-Email plus X-Auth-Key. For the fallback, set REPORTER_CLOUDFLARE_EMAIL to the account's email.
wrangler.toml
The template ships this file. It holds no account id and no secrets.
toml
name = "daemonitor-cloudflare-monitor"
main = "cloudflare-worker.js"
compatibility_date = "2023-10-16"
keep_vars = true # config lives in secrets and dashboard vars; never wipe them on deploy
[triggers]
crons = ["*/1 * * * *"] # Every minute; keep CRON_MINUTES in sync if you change thisIf your Wrangler setup needs an account id in the file, use a placeholder such as account_id = "<ACCOUNT_ID>" and never commit the real value.
Secrets and variables
Set the required values as secrets.
bash
wrangler secret put REPORTER_CLOUDFLARE_API_TOKEN
wrangler secret put REPORTER_CLOUDFLARE_ACCOUNT_ID
wrangler secret put DAEMONITOR_API_URL # https://<your-daemonitor-host>/api/clientstate/cloudflare
wrangler secret put DAEMONITOR_REPORTING_KEY # the system key from step 1The template's package.json also has pnpm deploy:secrets, which runs wrangler secret bulk .env. You can set the same values in the Cloudflare dashboard under the Worker's settings instead.
| Variable | Default | What it does |
|---|---|---|
REPORTER_CLOUDFLARE_API_TOKEN | required | Cloudflare API token, or a Global API Key. |
REPORTER_CLOUDFLARE_ACCOUNT_ID | required | The account to watch. |
DAEMONITOR_API_URL | required | Full ingest URL ending in /api/clientstate/cloudflare. A bare /api path is corrected to it. |
DAEMONITOR_REPORTING_KEY | required | The daemonitor system key. Must be at least 10 characters. |
REPORTER_CLOUDFLARE_EMAIL | dummy@example.com | Email sent with the X-Auth-Key fallback. Only matters for a Global API Key. |
REPORTER_CLOUDFLARE_ZONE_IDS | all zones | Comma-separated zone ids to watch. Empty means every zone in the account. |
REPORTER_CLOUDFLARE_PAGES_PROJECTS | all projects | Comma-separated Pages project names to watch. |
INCLUDE_ANALYTICS | on | Anything but "false" turns it on. Pages, Workers and zone analytics, including security, cache, traffic quality, bot and RUM. |
INCLUDE_WORKERS | on | "false" skips Workers entirely. |
INCLUDE_TURNSTILE | off | "true" adds Turnstile widgets and solve rates per zone. Needs Turnstile Sites Read. |
INCLUDE_RUM | off | "true" adds Web Analytics pageviews and visits per zone. Needs Account Analytics Read. |
DEPLOYMENT_ALIAS | cloudflare | Names this reporter. Part of the account record's id, and scopes inventory cleanup. |
MODULO_DIVISOR | 3 | Splits projects, Workers and zones into this many shards. Each run collects detail for one shard. |
MODULO_REMAINDER | 0 | Offset into the shards. Use it to split one account across several Workers. |
CRON_MINUTES | 1 | Minutes between cron runs. Must match the real schedule, or some shards are never collected. |
HOME_COUNTRY | US | The audience's country, as an ISO code. Also accepts a JSON map, {"<ZONE_ID>": "GB", "default": "US"}. |
ARTICLE_PATH_LIKE | /20% | Cloudflare like pattern for an article URL. The default fits WordPress /YYYY/MM/slug/. String or JSON map per zone. |
PAGE_EXCLUDE_PREFIXES | /cdn-cgi/,/wp-,/t/,/not-found/,/challenge.html | Path prefixes that return HTML but are not pages a reader opens. |
IGNORE_ASNS | none | Comma-separated ASNs never to flag, for example your office line. |
Worker endpoints
The cron trigger collects and pushes. The Worker also answers HTTP.
| Path | Auth | What it does |
|---|---|---|
/test-auth | none | Checks whether the API token can read the account. Never pushes. |
/test | reporting key | Collects and returns the payload without pushing. |
/diagnostics | reporting key | Runs token checks and writes the results to the Worker logs. |
| any other path | reporting key | Collects and pushes, like a cron run. ?dry-run=true returns the payload instead. |
Send the reporting key as Authorization: Bearer <REPORTING_KEY> or X-System-Key: <REPORTING_KEY>.
bash
curl -H "Authorization: Bearer <REPORTING_KEY>" https://<worker-name>.<subdomain>.workers.dev/testWhat it reports
Each run sends one JSON payload to /api/clientstate/cloudflare. The server fans it out into five kinds of record.
| Record type | Unique id | Holds |
|---|---|---|
cloudflare-account | cf-<account>-<alias> | Counts of projects, zones and Workers, Workers totals, per-script requests and errors. |
cloudflare-project | cf-project-<project id> | One Pages project, its domains, latest deployment and request analytics. |
cloudflare-workers | cf-workers-<account> | The Workers block as sent, for the aggregate card. |
cloudflare-worker-script | cf-worker-script-<name> | One Worker's 24h requests and errors, plus the last 60 minutes. |
cloudflare-domain | cf-domain-<zone id> | One zone. Status, plan, analytics, security, cache, traffic quality, bot scores, RUM, Turnstile. |
Main payload fields:
timestamp,account,aliasandmodulo_info.inventory. The full, unsharded list of Pages project ids, Worker script names and zone ids, each with acompleteflag.completeis false when the list call failed.pages.projects[]. Name, id, domains,latest_deploymentwithstatus,environment, commit hash and message, andanalyticswith requests, errors and CPU time percentiles.workers.scripts[],workers.analytics.summaryandworkers.byScript[].byScriptrows carryscript,requests,errorsover 24h andrecentRequests,recentErrorsover the last 60 minutes.zones[<zone id>].infowith the zone's status.domains[]. Per zone,detailedAnalytics.summarycovers 30 days of requests, cache hit ratio, bandwidth saved, HTTPS share, status classes, error rates and top countries.detailedAnalytics.timeserieshas one row per day.securitycovers 24h of firewall events by action, source and rule, with amitigatedcount.cacheis the 24h cache-status breakdown.trafficQualityholds 24h country and ASN shares, suspect networks and the Worker's ownalert.bot,rumandturnstileappear when enabled and available.
The server also upserts each zone's daily rows into cf_zone_daily. Cloudflare only serves 30 days of daily data, so this table is how the dashboard shows longer ranges.
Detail arrives one shard at a time. With the default MODULO_DIVISOR of 3 and a one-minute cron, each project and zone updates every three minutes. byScript covers every Worker on every run, so per-script records do not rotate.
The server uses inventory to retire records. A project, Worker or zone missing from a complete inventory is archived with status retired. If it comes back at Cloudflare, the server un-archives it. Records you archived by hand stay archived.
When it alerts
The account record and the system get one status per push.
| Condition | Severity | Reason text |
|---|---|---|
A zone's info.status is not active | error | Zones not active: <name> (<status>) |
A pushed Pages project's latest deployment is failure, error, canceled or cancelled | warning | Failed deployments: <names> |
workers.error is set | warning | Workers error: <message> |
| Any Worker has errors in the last 60 minutes | warning | Worker errors: <script> (<errors>/<requests>) |
The zone rule outranks the others. The Workers rule only applies when the status is not already error.
When the account status is not online, the server sends one notification with source cloudflare. It dedupes on type, so there is one open alert per severity for the account even as the reason text changes. A push that comes back online resolves it. There is no hold for a second report. The first bad push alerts.
Per-record rules:
| Record | Condition | Severity |
|---|---|---|
| Pages project | Latest deployment failed or canceled | warning, Deployment <status> |
| Pages project | Build in progress, such as active, building or queued | stays online, reason Deployment in progress |
| Worker script | recentErrors above 0 in the last 60 minutes | warning |
| Worker script | No recent window in the payload, and 24h errors above 0 | warning |
| Zone | Status not active | error |
| Zone | WAF pressure, see below | warning |
| Zone | Traffic quality alert from the Worker | warning or error |
WAF pressure counts firewall events that were not mitigated, totalEvents - mitigated. It fires when that number is at least 500 and at least half of all events. It sends its own warning notification per zone, with title WAF bypass · <zone>. It needs firewall events, so it only works on Pro and higher zones.
The Worker computes the traffic quality verdict and the server trusts it. It warns when:
- one foreign country is 25% or more of requests, or
- one ASN is 20% or more of requests, or
- networks that load pages without opening articles add up to 5% or more of pages.
It is an error when the foreign country reaches 50%, the ASN reaches 40%, or a new suspect network passes 1% of pages. A network is new when its share is four times its share over the prior week.
Threat counts do not affect status. A high threats number means Cloudflare blocked or challenged traffic.
On the dashboard
Cloudflare records go OFFLINE after 900 seconds without an update. Pages projects and Worker scripts are exempt, because they are inventory and a gap only means they were in another shard or had no traffic.
- The account card shows PAGES, ZONES and WORKERS counts, plus WORKER REQS and WORKER ERRORS when present. It lists up to 12 Workers by 24h errors, each linked to the Cloudflare dashboard. Clicking the card opens the whole stack.
- A Pages card shows DEPLOY as LIVE, BUILDING, FAILED or the raw status, plus REQUESTS and ERRORS.
- A Worker card shows REQUESTS, ERRORS and ERROR RATE. ERRORS counts 24h but takes its color from the last 60 minutes. A Worker with no requests, or whose record is more than 26 hours old, reads
IDLE · no traffic 24h. - The Workers aggregate card shows REQUESTS, ERRORS and CPU P95.
- A zone card shows REQUESTS, CACHE hit percentage, THREATS, BLOCKED, an HTTP status bar with the 5xx rate, and BANDWIDTH. The sparkline is daily threats. The detail view adds a traffic chart over 30 days, 90 days, a year or all stored days, plus cache, security, top countries, bot, Turnstile and suspect network panels.
Links go to the zone, its security events and traffic analytics, the Pages project, and each Worker's page and logs in the Cloudflare dashboard.
Gotchas
- The template README still uses old variable names such as
CF_API_TOKENandCF_ZONE_IDS. The Worker reads only theREPORTER_CLOUDFLARE_*names in the table above. - The README and INTEGRATION.md say
INCLUDE_ANALYTICSdefaults to false. The code turns it on unless it is exactly"false". - Keep
CRON_MINUTESequal to the real cron period. The shard advances once per run. A five-minute schedule with the default of 1 skips shards. /test-authneeds no key. It only returns success or failure and Cloudflare's error body, but anyone with the Worker URL can call it.- Older tenant Workers send less. Without
inventory, nothing is retired. WithoutrecentErrors, Worker health falls back to 24h totals, so a fixed Worker stays amber for a day. WithoutcacheHitRatio, the card computes it from cached and total requests. - Panels hide when their field is missing. Security shows only with
totalEvents, anerrororunavailable. Cache needsbyStatus. Bot needstotal. Turnstile appears only when the zone has widgets. Pageviews and visits appear only when the zone summary has a value above 0, which in practice needs RUM. - On a plan below Pro, the security panel names the plan it needs rather than showing an empty window.
- A suspect network check needs article URLs. If articles are under 10% of pages, the Worker reports
noArticleSignaland flags nothing. SetARTICLE_PATH_LIKEif your URLs are not dated. - Traffic quality findings change the zone's status but send no notification. Only WAF pressure and the account rollup notify.
- WAF bypass alerts are not resolved by a later clean push. The cleanup in this endpoint only resolves the account alert.