Observability and audit
Every signal the platform records, what each is good for, and exactly how long it is kept — request logs, sampled metrics, agent traces, deployment records, and your app's own business event feed.
Five independent signals, with different fidelity and different retention. Choosing the right one for a question is most of the skill.
What to use for what
| Question | Signal | Why |
|---|---|---|
| "What did this specific request return?" | Request logs | Exact, per request |
| "Is error rate rising?" | Analytics | Aggregate trend, cheap to query |
| "What happened to this customer's order?" | Your event feed | Business-level, in your own database |
| "Did the deploy apply everything?" | Deployment records | Per-resource outcomes |
| "Did the agent really check inventory?" | Agent traces | Tool-call edges don't lie |
| "Who changed this code?" | Commits | Attributed, including AI turns |
Retention, precisely
| Signal | Retention | Fidelity |
|---|---|---|
| Request logs (metadata) | Platform-managed rolling window | Exact per request |
| Request/response bodies | 14 days, opt-in per app | JSON and text only, secrets redacted |
| Analytics metrics | 3 months, not configurable | Sampled — weight by the sampling interval |
| Agent traces and turn costs | Platform-managed | Exact per turn |
| Deployment records | Full history | Exact |
| Commits and file versions | Full history | Exact |
| Your own event feed | As long as you keep it | Exactly what you declared |
Two consequences to design around. Analytics is sampled and capped at three months, so it cannot be your audit trail or your billing source — Cloudflare does not guarantee retrieval of any individual record. And the only signal whose retention you control is your own event feed, which is why anything with a regulatory retention period belongs there.
Request logs
sf logs --since 6h --errors # filter to failures
sf logs "status>=500 path:/api" --expand
sf logs --env staging --since 1h
Bodies are off by default. To capture them for a specific investigation:
sf firewall set --body-capture on
sf logs body <rayId>
Then turn it back off. Two caveats worth internalizing: a cache hit produces no log (it never ran your code), and log delivery lags live traffic by up to about five minutes because of batching — this is a debugging tool, not a real-time monitor.
Your own event feed — the one that matters for audit
Platform signals tell you what the infrastructure did. Business facts — an order accepted, a refund issued, a guardrail refusal — are yours to record, in your own database, with retention you control.
// app/events.ts — declare the vocabulary; emitting an undeclared type throws
export default defineEvents({
"order.accepted": {
audience: ["business"], severity: "info",
title: "Order {orderId} accepted for {customer}",
fields: z.object({ orderId: z.string(), customer: z.string() }),
},
"guardrail.refused": {
audience: ["business"], severity: "warn",
title: "Refused: {rule}",
fields: z.object({ rule: z.string() }),
},
});
// Emit from DOMAIN code, so the same event fires whichever caller acted
await emitAppEvent(env, "order.accepted", { orderId, customer }, { actor });
Events land in _spike_events in your app's database, indexed on time and type:
SELECT ts, type, severity, title, actor, fields
FROM _spike_events
WHERE ts > unixepoch('now', '-1 day') * 1000
ORDER BY ts DESC;
The doctrine that makes this valuable: every write-path domain function emits its outcome, every scheduled job emits completion, every guardrail refusal emits. Then "what happened yesterday, and who did it?" is a query instead of an investigation — and because it's a table in your database, it inherits the 30-day point-in-time recovery and can be exported anywhere.
Two conventions: titles name domain objects rather than people (a person goes in fields, so the feed doesn't read like surveillance), and event types should be things a colleague would say out loud.
Agent activity
Where AI acts on your data, the audit question is sharper: did it actually do what it says?
sf api get "/apps/<appId>/agent-turns?limit=100" # calls, tokens, cost per turn
sf api get "/apps/<appId>/agent-trace?agent=<route>&session=<sid>&traceId=<tid>&format=mermaid&detail=1"
sf api get "/apps/<appId>/agent-describe" # what each deployed agent may do
sf api get "/apps/<appId>/agent-messages?agent=<route>&session=<sid>"
Every tool call, delegation, and model step is an edge with duration and token counts, recorded without instrumentation. The habit worth institutionalizing:
Verify claims against trace edges, not against the agent's summary. If an agent says it checked inventory and there is no corresponding tool edge, it produced a plausible sentence.
agent-describe answers "what is this agent permitted to do?" from the deployed code rather than from a registry that can drift — because the deployed code is the registry.
Metrics
Opt in, then write from your Worker and query with SQL:
sf analytics enable && sf deploy # the binding attaches on the NEXT deploy
sf analytics query "SELECT index1 AS route, sum(_sample_interval) AS hits
FROM ANALYTICS
WHERE timestamp > now() - INTERVAL '1' DAY
GROUP BY route"
Always weight aggregates by _sample_interval — a bare count() silently undercounts once sampling begins. Keep queries time-bounded and index-filtered.
Gaps, stated plainly
- No built-in SIEM or log streaming. Export from your own event table on a schedule if you need it.
- No synthetic monitoring or alerting built in. Point your existing monitoring at your app's URL and a health route.
- Analytics cannot be an audit trail — sampled, three months, no per-record guarantee.
- Logs lag by minutes. For real-time alerting, monitor externally.
If any of these blocks a control you must satisfy, raise it early.
Next
- Isolation and tenancy — the rest of the security-review answers.
- SLAs, limits and quotas — retention figures alongside every other limit.
- Change management — the deployment audit trail.
Frequently asked questions
Are request and response bodies logged?
Not by default — that's a deliberate privacy default. You can opt in per app, after which bodies are retained for 14 days, limited to JSON and text, with secrets redacted.
Can we rely on the metrics for billing or audit?
No. Analytics data is sampled and Cloudflare does not guarantee retrieval of any individual record. Use it for trends and rates; use request logs for exact events and your app's own event table for business facts.
Why are there no logs for a request we know happened?
Almost certainly a cache hit. A response served from the edge cache never runs your code, so there is nothing to log. Check the x-sf-cache header on the same route.
How do we prove what an AI agent did?
Every turn is traced automatically — each tool call, delegation, and model step is an edge with duration and token counts — and every code change it made is an attributed commit. Verify claims against trace edges, not against the agent's summary.
Can we export logs to our own SIEM?
Not as a built-in integration today. What you can do is record business-level events into your app's own database and export from there on a schedule. Tell us if SIEM streaming is a requirement.