Skip to content

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 ​

  1. In daemonitor, add a system for the Cloudflare account and copy its system key. That key is the reporting key.
  2. Create a Cloudflare API token with the permissions below.
  3. Deploy cloudflare-worker.js and suspect-networks.js from the template as one Worker, with a cron trigger.
  4. Set the secrets and variables below on that Worker.
  5. Wait for the first cron run, or call the Worker's /test endpoint 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.

PermissionUsed forNamed in
Account > Pages > ReadPages project list and deploymentsINTEGRATION.md, README.md
Account > Account Settings > ReadAccount lookup in /diagnostics and /test-authINTEGRATION.md
Account > Account Analytics > ReadZone traffic, cache and security-event analytics. Also required for RUMINTEGRATION.md, worker comments
Zone > Zone > ReadZone list and zone detailsREADME.md
Zone > Analytics > ReadZone analyticsREADME.md
Account > Turnstile Sites > ReadTurnstile widgets, only with INCLUDE_TURNSTILE=trueworker 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 this

If 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 1

The 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.

VariableDefaultWhat it does
REPORTER_CLOUDFLARE_API_TOKENrequiredCloudflare API token, or a Global API Key.
REPORTER_CLOUDFLARE_ACCOUNT_IDrequiredThe account to watch.
DAEMONITOR_API_URLrequiredFull ingest URL ending in /api/clientstate/cloudflare. A bare /api path is corrected to it.
DAEMONITOR_REPORTING_KEYrequiredThe daemonitor system key. Must be at least 10 characters.
REPORTER_CLOUDFLARE_EMAILdummy@example.comEmail sent with the X-Auth-Key fallback. Only matters for a Global API Key.
REPORTER_CLOUDFLARE_ZONE_IDSall zonesComma-separated zone ids to watch. Empty means every zone in the account.
REPORTER_CLOUDFLARE_PAGES_PROJECTSall projectsComma-separated Pages project names to watch.
INCLUDE_ANALYTICSonAnything but "false" turns it on. Pages, Workers and zone analytics, including security, cache, traffic quality, bot and RUM.
INCLUDE_WORKERSon"false" skips Workers entirely.
INCLUDE_TURNSTILEoff"true" adds Turnstile widgets and solve rates per zone. Needs Turnstile Sites Read.
INCLUDE_RUMoff"true" adds Web Analytics pageviews and visits per zone. Needs Account Analytics Read.
DEPLOYMENT_ALIAScloudflareNames this reporter. Part of the account record's id, and scopes inventory cleanup.
MODULO_DIVISOR3Splits projects, Workers and zones into this many shards. Each run collects detail for one shard.
MODULO_REMAINDER0Offset into the shards. Use it to split one account across several Workers.
CRON_MINUTES1Minutes between cron runs. Must match the real schedule, or some shards are never collected.
HOME_COUNTRYUSThe 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.htmlPath prefixes that return HTML but are not pages a reader opens.
IGNORE_ASNSnoneComma-separated ASNs never to flag, for example your office line.

Worker endpoints ​

The cron trigger collects and pushes. The Worker also answers HTTP.

PathAuthWhat it does
/test-authnoneChecks whether the API token can read the account. Never pushes.
/testreporting keyCollects and returns the payload without pushing.
/diagnosticsreporting keyRuns token checks and writes the results to the Worker logs.
any other pathreporting keyCollects 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/test

What it reports ​

Each run sends one JSON payload to /api/clientstate/cloudflare. The server fans it out into five kinds of record.

Record typeUnique idHolds
cloudflare-accountcf-<account>-<alias>Counts of projects, zones and Workers, Workers totals, per-script requests and errors.
cloudflare-projectcf-project-<project id>One Pages project, its domains, latest deployment and request analytics.
cloudflare-workerscf-workers-<account>The Workers block as sent, for the aggregate card.
cloudflare-worker-scriptcf-worker-script-<name>One Worker's 24h requests and errors, plus the last 60 minutes.
cloudflare-domaincf-domain-<zone id>One zone. Status, plan, analytics, security, cache, traffic quality, bot scores, RUM, Turnstile.

Main payload fields:

  • timestamp, account, alias and modulo_info.
  • inventory. The full, unsharded list of Pages project ids, Worker script names and zone ids, each with a complete flag. complete is false when the list call failed.
  • pages.projects[]. Name, id, domains, latest_deployment with status, environment, commit hash and message, and analytics with requests, errors and CPU time percentiles.
  • workers.scripts[], workers.analytics.summary and workers.byScript[]. byScript rows carry script, requests, errors over 24h and recentRequests, recentErrors over the last 60 minutes.
  • zones[<zone id>].info with the zone's status.
  • domains[]. Per zone, detailedAnalytics.summary covers 30 days of requests, cache hit ratio, bandwidth saved, HTTPS share, status classes, error rates and top countries. detailedAnalytics.timeseries has one row per day. security covers 24h of firewall events by action, source and rule, with a mitigated count. cache is the 24h cache-status breakdown. trafficQuality holds 24h country and ASN shares, suspect networks and the Worker's own alert. bot, rum and turnstile appear 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.

ConditionSeverityReason text
A zone's info.status is not activeerrorZones not active: <name> (<status>)
A pushed Pages project's latest deployment is failure, error, canceled or cancelledwarningFailed deployments: <names>
workers.error is setwarningWorkers error: <message>
Any Worker has errors in the last 60 minuteswarningWorker 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:

RecordConditionSeverity
Pages projectLatest deployment failed or canceledwarning, Deployment <status>
Pages projectBuild in progress, such as active, building or queuedstays online, reason Deployment in progress
Worker scriptrecentErrors above 0 in the last 60 minuteswarning
Worker scriptNo recent window in the payload, and 24h errors above 0warning
ZoneStatus not activeerror
ZoneWAF pressure, see belowwarning
ZoneTraffic quality alert from the Workerwarning 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_TOKEN and CF_ZONE_IDS. The Worker reads only the REPORTER_CLOUDFLARE_* names in the table above.
  • The README and INTEGRATION.md say INCLUDE_ANALYTICS defaults to false. The code turns it on unless it is exactly "false".
  • Keep CRON_MINUTES equal to the real cron period. The shard advances once per run. A five-minute schedule with the default of 1 skips shards.
  • /test-auth needs 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. Without recentErrors, Worker health falls back to 24h totals, so a fixed Worker stays amber for a day. Without cacheHitRatio, the card computes it from cached and total requests.
  • Panels hide when their field is missing. Security shows only with totalEvents, an error or unavailable. Cache needs byStatus. Bot needs total. 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 noArticleSignal and flags nothing. Set ARTICLE_PATH_LIKE if 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.