Reading logs
Debugging a broken app? Reach for
debug_appfirst.debug_app({ applicationId })is a read-only bundle that returns, in one call: the full health snapshot with pod diagnostics (CrashLoopBackOff / OOMKilled / ImagePullBackOff reasons, storage warnings, same asget_app'shealthblock withdiagnostics:true), filtered build and runtime logs (defaultwarn+), env key names + their sources (likelist_env_vars), the health-gate verdicts of recent deploys, and the recent per-app audit entries (who deployed / rolled back / changed env). It saves 5+ sequential round-trips. Tune it withlogTail,buildLogLevel/runtimeLogLevel,since(an ISO timestamp to window everything to an incident), andincludeRuntimePrevious:true(the last-terminated container's crash output, useful for CrashLoopBackOff). Every signal is best-effort: one unavailable signal comes back null/empty rather than failing the call. Use the targetedget_logsbelow when you already know you only need one log stream.
get_logs returns an app's logs in two flavors, selected with source: build/deploy logs (source: "build", the default) and runtime container logs (source: "runtime"). Both come back as raw logs text plus parsed, filterable entries.
source: "build"is the first thing to reach for when adeploycomes back withstatus: "error": that bare status tells you the build failed, and the build log tells you why.source: "runtime"is what you want when the deploy succeeded but the app misbehaves once it's live (500s, crashes on a request). It's the running container's own stdout/stderr.
What source: "build" covers
- Build logs are
docker build/ buildpack output: dependency installs, compile errors, missing files, failedRUNsteps. - Deploy logs cover the platform pushing the image and starting the service: the steps the platform runs around your container.
These are stored as files on the host; the build source tails the file and returns the text. Under the hood it resolves a deployment, then reads its log content via the platform's log API.
What source: "runtime" covers
- Runtime / container logs are your app's own stdout/stderr once it's running (request logs,
console.log, stack traces from a live request), aggregated across all replicas of the service and timestamped.
DeployMill reads these straight from the running app and returns parsed runtime entries just like the build source. No extra infrastructure to configure.
- If the app isn't running (stopped / never deployed), you get a
service_not_found-stylenoteand empty entries. Checkget_app(itshealthblock), then deploy or start it. - If the backend is momentarily unreachable, you get an
unreachablenoterather than an error. Build logs (source: "build") still work, and you can fall back to the edge-probe signal fromdeploy/rollback(theedges/edgeNotefields) plus a readiness route (probePath).
Usage
get_logs({ applicationId }) → latest deployment, last 200 lines, parsed
get_logs({ applicationId, tail: 1000 }) → last 1000 lines of the latest deployment
get_logs({ applicationId, deploymentId, tail: 500 }) → a specific historical deployment
get_logs({ applicationId, level: "error+" }) → only error/fatal entries
get_logs({ applicationId, grep: "npm ERR|ENOENT" }) → lines matching a regex (case-insensitive)
get_logs({ applicationId, grep: "out of memory", grepRegex: false }) → literal substring
get_logs({ applicationId, since: "2026-05-30T23:00:00Z" }) → entries at/after an instant
get_logs({ applicationId, source: "runtime" }) → running container stdout/stderr, last 200 lines
get_logs({ applicationId, source: "runtime", level: "error+", tail: 1000 }) → runtime errors only
get_logs({ applicationId, source: "runtime", previous: true }) → the LAST-TERMINATED container's crash output (CrashLoopBackOff)
- Omit
deploymentIdto read the most recent deployment (the usual case right after a failed deploy). (deploymentIdis ignored forsource: "runtime", which reads the live service.) - All the filters (
grep/level/since) andtailwork identically across both sources, using the same parser. tailbounds how many trailing lines are fetched (default200, max10000) before filtering. Build logs can be long; start small and raisetailif the error is scrolled off the top.- Get a specific
deploymentIdfromlist_deploymentswhen you want an older build (e.g. comparing the last good deploy against the broken one). previous(source: "runtime"only, defaultfalse) reads the last-terminated container's logs, the crash output from before the most recent restart. Reach for it when an app is stuck in a CrashLoopBackOff and the live logs only show the newest (re)start. If nothing prior exists, or the previous container's logs already rotated away (common in a fast crash loop), it falls back to the current container and sets aprevious_logs_unavailable_fell_back_to_currentnote. Ignored forsource: "build".
Filtering
All three filters are best-effort and applied server-side over the fetched window:
grepis matched against the raw line, case-insensitive. A regex by default. An invalid pattern silently falls back to a substring match. SetgrepRegex: falseto force a literal substring.levelkeeps only entries whose level was parsed off the line. Accepts a single level (trace/debug/info/warn/error/fatal) orwarn+/error+for "that level or worse". Lines with no detectable level are dropped when this is set.sincekeeps entries timestamped at/after an ISO instant. Lines with no parseable timestamp are kept (we can't prove they're older).
Return shape
{
"deploymentId": "…",
"status": "error",
"title": "…",
"tail": 200,
"logs": "…raw build output…",
"entries": [
{ "line": "2026-05-30T23:12:18Z ERROR npm ERR! missing script: build",
"ts": "2026-05-30T23:12:18.000Z", "level": "error",
"message": "ERROR npm ERR! missing script: build" }
],
"total": 200,
"matched": 1,
"truncated": true,
"redacted": false
}
logsis the log text, after secret redaction (see below). An empty string means the log file is missing or not yet written (the deploy may still be starting, or the log rotated). The response adds anoteflagging that.entriesis the parsed, filtered view: one object per non-blank line, with a best-efforttsandlevelwhen they can be detected, andmessage(the line minus any leading timestamp). Branch on these instead of regexinglogs.totalis how many entries were parsed from the fetched window;matchedis how many passed the filters (and equalsentries.length).truncatedistruewhen the fetched window was completely full (total >= tail), i.e. older lines almost certainly exist, so raisetailto see them. (The REST surface gives no exact line total, so this is a heuristic.)redactedistruewhen the redaction pass replaced at least one secret-shaped span in this response (see Secret redaction below);falsewhen nothing matched. Whentrue, thenotealso says so.- If the app has no deployments yet,
deploymentIdisnull,entriesis[], and anotesays to deploy first.
For source: "runtime" the shape is the same logs/entries/total/matched/truncated/redacted, but instead of deploymentId/status/title you get source: "runtime" and serviceName (the running service read). A note explains the empty case when the app isn't running or the backend is momentarily unreachable.
Secret redaction
Both sources scrub secret-shaped material out of logs and entries before returning. Log output routinely contains the very credentials DeployMill injected (DATABASE_URL, the S3 access keys, bound secrets) and the app's own end-user PII, and get_logs hands its result straight into an agent transcript. This keeps the same names-only posture as list_env_vars: managed secret values aren't supposed to cross back to the client.
Two passes run before the response is built:
- Known injected values. DeployMill resolves the secret-shaped env values it set for this app (keys matching
*SECRET*,*TOKEN*,*PASSWORD*,*_KEY*,DATABASE_URL, DSNs, …) and replaces any literal occurrence with***. - Structural shapes. A best-effort scrub of common patterns regardless of the env: connection-string passwords (
postgres://user:***@host), AWS access-key ids (AKIA…/ASIA…),Bearer …tokens, andpassword=/token=/api_key=-style pairs.
When anything was replaced, redacted: true and the note flags it. Limits (best-effort, not a guarantee): it won't catch novel/encoded secrets, a custom credential format DeployMill didn't inject, or arbitrary PII (names, emails, IP addresses) in free-form log text. Treat get_logs output as lower-risk, not secret-free. Don't paste it somewhere it could be indexed without a glance. Short secret values (under 6 characters) are intentionally not literal-scrubbed to avoid shredding unrelated log text. The *** you see in logs is DeployMill's redaction, not your app's output.
The failed-deploy loop
1. deploy({ applicationId }) → status: "error" (+ failNote pointing here)
2. get_logs({ applicationId }) → read the build output, find the failing step
3. fix the cause (Dockerfile, deps, source) and push
4. deploy({ applicationId }) again → repeat until status: "done"
start_project and deploy both surface this pointer on failure, so an agent that hits an error status knows to call get_logs next instead of guessing.
Troubleshooting
logsis empty right after triggering a deploy → the build hasn't written to the file yet. Wait fordeployto reach a terminal status (done/error), then read.- Error is cut off at the top → raise
tail. - Deploy succeeded but the app 502s / throws on requests → that's a runtime problem, not a build one. Read
get_logs({ applicationId, source: "runtime" })for the container's own stdout/stderr. If it comes back empty with anunreachablenote, the backend is momentarily unavailable, so fall back to the edge probe and your app's readiness route.