Logs
Runtime lines, deploy output, cron runs and error groups in one stream — with a live tail, search and natural-language queries.
One stream, not four dashboards. Your app's request lines and stdout, platform and deploy events, cron runs and grouped errors all land on the same timeline. Open Logs in the console, or tail from the terminal:
camplax logs # recent entries
camplax logs --tail # live stream until Ctrl+C
camplax logs --filter errors # only error-level lines and 5xxs
camplax logs --search "timeout" # filter expression
Runtime logs — nothing to configure
Your app does not need a logging client. The Camplax runtime wrapper around your process captures two things on its own:
- Every HTTP request — method, path, status and duration, with a
requestIdper request. - Everything your code writes to stdout and stderr —
console.log, stack traces, framework output.
The platform injects CAMPLAX_LOG_INGEST (the ingest URL) and CAMPLAX_LOG_INGEST_TOKEN (the bearer) into your app's environment, and the runtime ships batched entries to POST /v1/logs/ingest. Production and every preview get their own token, so a preview's lines are always marked as preview. Both are stripped before your code starts — they belong to the platform, so a dump of your app's env is not a credential leak. Writing to standard out is the whole integration.
The ingest endpoint itself accepts a platform bearer token and a JSON body:
{
"projectSlug": "my-app",
"entries": [
{
"level": "info",
"kind": "order.created",
"message": "Order 1847 created",
"method": "POST",
"path": "/api/orders",
"status": 201,
"durationMs": 42,
"requestId": "req_9f2…"
}
]
}
- Up to 100 entries per call.
messageis capped at 16 KB,stackat 32 KB. - Fields an entry can carry:
id,atortimestamp,level(debug,info,warn,error),source,kind,environment(productionorpreview),method,path,status,durationMs,requestId,traceId,stack. - A
statusof 500 or higher is stored as error-level regardless of the statedlevel. - Error-level entries are fingerprinted into error issues on the way in, so a crashing route shows up in the inbox too.
Deploy logs
Build output lives with the deploy it came from — open a deploy in the console to read the full builder log. Every deploy log also has a shareable link:
GET /v1/deploy-logs/<slug>/<deployId>?token=<token>
The link returns plain text with no sign-in — it is the same URL that goes into deploy-failure emails, so you can paste it into a chat or an issue. Treat the link itself as a secret: anyone holding it can read that build's output.
When a deploy builds but its app crashes on start, Camplax puts the previous version back so your app stays up. Before it does, it saves the last lines the crashed app printed (up to 32 KB). Open the failed deploy and pick Runtime log to read them — they stay with the deploy for as long as your plan keeps deploy history (7, 14 or 30 days).
Reading the stream
| Surface | Endpoint or flag | What you get |
|---|---|---|
| Recent + history | GET /v1/projects/:slug/logs | Merged entries; ?filter=errors, ?search=<q>, ?mode=filter|regex, ?limit=≤500, ?cursor= paging |
| Structured search | POST /v1/projects/:slug/logs/search | { "query", "mode": "filter" or "regex", "limit" ≤200, "cursor" } |
| Natural language | POST /v1/projects/:slug/logs/nl | { "question": "show me 5xx on /api/orders yesterday" } → a filter expression you can run or edit. Uses the same included AI allowance as Lee and the SQL copilot. Once that is used up for the month, it still answers with a simpler rule-based filter and adds "ai": "allowance_used" until the 1st ("allowance_unavailable" if the allowance could not be checked) |
| Live tail | GET /v1/projects/:slug/logs/stream | Server-sent events; camplax logs --tail reads this. The console Live tab uses a WebSocket at /logs/socket |
| Error groups | GET /v1/projects/:slug/logs/errors | Grouped issues; detail at /logs/errors/:groupId |
Search has two modes: the default filter expression (field matches and free text) and regex mode for patterns. Regex runs in a bounded engine — it reports a regexLimitation note rather than backtracking forever.
Request ids
Every request line carries a requestId, and entries you emit can carry one too — put it in the entry and the same id correlates your own lines with the request that produced them. Searching a request id returns every line for that request.
Every response your app sends carries the id in its X-Camplax-Request-Id header. GET /v1/projects/:slug/logs/traces/:requestId returns that request's trace: the request line (method, path, status, duration) and every line that carries the same id, oldest first. It reads the same 7-day window as search, so an older or unknown id answers 404 trace_not_found.
Log drains
A drain sends every runtime line to a service you already use — Better Stack, Axiom, Datadog, or your own collector. Add one under Logs → Drains (project owners), or through the API:
| Method | Path | What it does |
|---|---|---|
GET | /v1/projects/:slug/logs/drains | List drains (builders and owners) |
POST | /v1/projects/:slug/logs/drains | Add a drain: { url, environments?, samplingPercent?, headers? } |
DELETE | /v1/projects/:slug/logs/drains/:id | Remove a drain |
Lines arrive as newline-delimited JSON (application/x-ndjson), batched as they are ingested, each stamped with a project block so one collector can take several apps. Pick production, preview or both, and a sampling percentage (sampled per line, the same way on a retry). If your service wants its own ingest token, put it in headers (for example Authorization) — it is stored with the project's secrets and only its name is ever shown back. Up to five drains per project.
The URL must be https on port 443 and resolve to a public address; redirects are never followed and each post times out after 10 seconds.
A failed batch is retried, not dropped. It goes back on a queue and is sent again after 30 seconds, 2 minutes, 8 minutes, 32 minutes and 2 hours — six attempts in all — with a fresh signature and the same Camplax-Delivery id each time, so a receiver that saw the first attempt can dedupe. Retries carry the batch itself (split into pieces under 100 KB; a single line bigger than that is not retried — it is still in the Logs page). After the last attempt the batch is given up and the drain says so. An answer that will not change — a 4xx other than 408/429, a redirect, or a host that is not allowed — is not retried at all. A drain that fails 50 batches in a row still switches itself off and says why; retries do not count toward those 50, and a removed or switched-off drain drops its pending retries.
Request health
Camplax counts your production app's requests from the same request lines it already logs — nothing to add or switch on.
The chart. Logs → Live opens with your app's last 24 hours: how many requests it answered, how many failed with a server error (5xx), and how long the slowest 1 in 20 took (p95), with one bar per hour. It appears once there is a request to count. The failed figure gets a red dot only while the last 10 minutes are over the alert line below. Only request lines count — not your app's own console.log output, platform events or Camplax's health checks on /health, /healthz and /readyz — and a preview's requests never count toward production. The same numbers, in 5-minute buckets, are at GET /v1/projects/:slug/logs/requests (?environment=preview for previews).
The alert. Every 5 minutes Camplax checks every live production app. When 1 in 10 requests failed over the last 10 minutes, with at least 20 requests, the project's alert recipients (the same list uptime uses) get an email, and channels subscribed to Requests failing get a post (new channels are; tick it on a channel made before this alert existed). It says which app, what share failed and which path most failures came from, and links to Logs filtered to the failed requests. When fewer than 1 in 20 fail — again over at least 20 requests — you hear that it recovered. You get one alert per incident, not one every 5 minutes. A quiet app with no traffic is never reported as recovered. Anyone who would rather not hear it switches Your app starts failing requests off under Account → Notifications.
A full outage is uptime's job. When your app is not running at all, the router answers 503 before any request reaches it, so nothing is logged and this alert stays quiet. The uptime monitor on every production app catches that one.
Log-pattern alerts
Tell Camplax what a bad day looks like in your logs — "payment failed 5 times in 10 minutes" — and it posts to your alert channels (Slack, Discord or a signed webhook, set up under Settings → Alerts with the Log pattern event) and emails the project's alert recipients — the same list uptime uses — when it happens. Add rules under Logs → Alerts, or through the API:
| Method | Path | What it does |
|---|---|---|
GET | /v1/projects/:slug/logs/patterns | List rules |
POST | /v1/projects/:slug/logs/patterns | { name, pattern, level?, environment?, threshold?, windowMinutes?, cooldownMinutes? } |
PATCH | /v1/projects/:slug/logs/patterns/:id | { enabled } |
DELETE | /v1/projects/:slug/logs/patterns/:id | Remove a rule |
Channels created before log-pattern alerts existed do not hear them until you tick Log pattern on the channel; the Alerts tab says so when no channel does.
A pattern is plain text matched case-insensitively against the message and path — not a regex, because it runs on every line you send. level is any, warn (warn and error) or error; threshold lines within windowMinutes (1–60) fire the alert, and then the rule is quiet for cooldownMinutes (5–1,440). Up to 10 rules per project. Checking costs nothing on a line that matches no rule.
Verifying drains and alerts
Log drain batches, alert webhooks and status-page webhooks are signed by Camplax with its own key — the same key as cron calls. There is no secret on your side: nothing to create, copy or store. Your receiver checks the signature against Camplax's public key, which anyone can fetch.
Each request carries:
| Header | Value |
|---|---|
Camplax-Timestamp | Unix seconds when it was signed |
Camplax-Key | Which public key signed it |
Camplax-Signature | v1= + base64url Ed25519 signature |
Camplax-Purpose | log-drain, alert or status (a status-page webhook) |
Camplax-Project | The project id it was signed for |
In a Plax app, check the raw body before you parse it:
import { readRawBody, sendFail, sendOk, verifyCamplaxWebhook } from "@camplax/plax";
"POST /hooks/camplax-alerts": async (req, res) => {
const raw = await readRawBody(req);
const verified = await verifyCamplaxWebhook(req, raw, { purpose: "alert" });
if (!verified.ok) return sendFail(res, verified); // 401
// skip it if you already handled this delivery id (it is in the body)
sendOk(res, { ok: true });
},
purpose is required: a drain batch never passes as an alert, and neither passes as a cron call. Say which project you expect. Anyone with a Camplax project can point a drain or an alert at any URL, so a valid signature only proves Camplax sent it for some project. verifyCamplaxWebhook checks against your app's own CAMPLAX_PROJECT_ID by default; a collector that takes several projects passes { projectId: ["prj_…", "prj_…"] }. Your project id is shown next to drains and alert channels in the console.
Outside Plax, fetch https://camplax.dev/v1/signing/keys (public, no credential; also served at /v1/cron/keys), take the key whose kid matches Camplax-Key, and verify the Ed25519 signature over ` ${purpose}.${timestamp}.${projectId}.${rawBody} — where purpose is the one you expect and projectId is one you allow. Reject a timestamp more than 5 minutes from your clock, and dedupe: alerts carry a delivery id in the body, and every drain line has its own id. Cache the keys, and refetch when an unknown Camplax-Key` shows up — that is a key rotation.
Verifying is optional. Better Stack, Axiom and Datadog ignore these headers and authenticate with their own token instead.
Retention and limits
- The live tail keeps the last 20,000 entries (up to 32 MB) or 7 days, whichever ends first. It is a rolling buffer for debugging, not an archive.
- Error groups, activity events and cron history are durable records — they outlive the tail.
- Export is possible at the platform level: the control plane can mirror entries to an OTLP/HTTP collector when one is configured for the deployment.
The same stream, three ways
- Console — the Logs page merges all sources with chips, search and the live tail.
- CLI —
camplax logs [--tail] [--filter errors] [--search <q>]; see the CLI reference. - API — the endpoints above accept a session, a
cx_cli_token, or a project key with thelogs:readscope — see API keys.