Zaroz
Workload guides

Read a Workload's logs

Continues from Deploy a Workload from a public image. It uses the api helper from that guide, plus PROJECT_ID and SERVICE_ID. What is stored and for how long is in Logs and metrics.

TerminalCode
export LOGS="$ZAROZ_API/projects/$PROJECT_ID/service-instances/$SERVICE_ID/units/logs"

API status

The units routes are used by the console and are tagged console-only in the API reference. They accept your session token or a personal access token.

The newest lines

With no parameters you get the 500 newest lines of all units merged. Lines come back oldest first within the page.

TerminalCode
curl -s "$LOGS?limit=100" -H "Authorization: Bearer $ZAROZ_TOKEN" | jq -r '.lines[] | "\(.at) u\(.unit_ordinal) \(.text)"'
TypeScriptCode
const base = `/projects/${projectId}/service-instances/${serviceId}/units/logs`; const page = await api("GET", `${base}?limit=100`); for (const l of page.lines) console.log(l.at, `u${l.unit_ordinal}`, l.text); console.log(page.coverage);
Code
base = f"/projects/{project_id}/service-instances/{service_id}/units/logs" page = api("GET", base, limit=100) for l in page["lines"]: print(l["at"], f"u{l['unit_ordinal']}", l["text"]) print(page["coverage"])

Scroll back

next_cursor is where to continue in the direction you read. Hand it back exactly as given. It is null once the 30 days of retention are exhausted.

TypeScriptCode
let cursor: string | null = null; for (let i = 0; i < 5; i++) { const q = new URLSearchParams({ limit: "500", ...(cursor ? { cursor } : {}) }); const p = await api("GET", `${base}?${q}`); for (const l of p.lines) console.log(l.at, l.text); cursor = p.next_cursor; if (!cursor) break; }
Code
cursor = None for _ in range(5): p = api("GET", base, limit=500, **({"cursor": cursor} if cursor else {})) for l in p["lines"]: print(l["at"], l["text"]) cursor = p["next_cursor"] if not cursor: break
TerminalCode
curl -s "$LOGS?limit=500&cursor=$NEXT_CURSOR" -H "Authorization: Bearer $ZAROZ_TOKEN"

Filter

  • search=error: case-insensitive substring, at most 200 characters, not a regular expression.
  • unit_id=<uuid>: one unit, live or already scaled away. The ids are in GET .../units.
  • container=<name>: one container name.
  • from and to: RFC 3339 instants, clamped to the last 30 days.
TerminalCode
curl -s "$LOGS?search=timeout&from=2026-10-13T00:00:00Z" -H "Authorization: Bearer $ZAROZ_TOKEN"

Follow live

Read forward from the follow_cursor of your last page, and wait poll_after_ms between polls instead of hard-coding an interval.

TerminalCode
curl -s "$LOGS?direction=forward&cursor=$FOLLOW_CURSOR" -H "Authorization: Bearer $ZAROZ_TOKEN"
TypeScriptCode
let follow: string | null = (await api("GET", `${base}?limit=20`)).follow_cursor; for (;;) { const q = new URLSearchParams({ direction: "forward", ...(follow ? { cursor: follow } : {}) }); const p = await api("GET", `${base}?${q}`); for (const l of p.lines) console.log(l.at, `u${l.unit_ordinal}`, l.text); follow = p.follow_cursor ?? follow; await new Promise((r) => setTimeout(r, p.poll_after_ms)); }
Code
import time follow = api("GET", base, limit=20)["follow_cursor"] while True: p = api("GET", base, direction="forward", **({"cursor": follow} if follow else {})) for l in p["lines"]: print(l["at"], f"u{l['unit_ordinal']}", l["text"]) follow = p["follow_cursor"] or follow time.sleep(p["poll_after_ms"] / 1000)

A line has the same id wherever it is read, so if you restart a follower you can drop lines you already printed.

Markers

markers holds lifecycle facts between the lines: a pod started, replaced or lost, and container_restarted with a reason such as OOMKilled or Error. When a container writes more than 500 lines per second, the rest go to an archive and an overflow marker says how many and when; nothing is lost, and the download route puts them back in order.

Download

GET .../units/logs/download returns a file with the archived overflow merged back in, for when you need every line.

Last modified on