> ## Documentation Index
> Fetch the complete documentation index at: https://docs.levelrail.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Log archive

> Ship container logs to storage as compressed NDJSON on a schedule.

Container logs are stored node-local (see [observability](/observability)) with a bounded retention window. Log archive ships them to a [storage destination](/object-storage) as gzip-compressed NDJSON, on a schedule you set per app or once for every app, so logs outlive the node-local store and you need no third-party log shipper.

A storage destination must exist first; see [object storage](/object-storage) for connecting one.

## What gets archived

Every line a container writes to stdout or stderr, exactly as the node-local log store received it. Objects are written to:

```
log-archive/<kind>/<name>/<yyyy>/<mm>/<dd>/<hh>/<start-ns>.ndjson.gz
```

for example `log-archive/service/web/2026/09/24/13/1790255400000000000.ndjson.gz`. The prefix can be changed with `APP_LOG_ARCHIVE_PREFIX`. Each line is a JSON object:

```json theme={"dark"}
{"resource":"service:web","stream":"stdout","ts":"2026-09-24T13:10:00.123Z","message":"{\"msg\":\"request completed\",\"status\":200}","structured":true,"fields":{"msg":"request completed","status":200}}
```

`fields` is only present when `message` parsed as a JSON object (`structured: true`); plain-text lines carry `message` only.

## How it behaves

* The archiver pulls, it does not push. A scheduler wakes once a minute (`APP_LOG_ARCHIVE_TICK`), checks which policies are due, and reads new lines from the node-local store.
* Each policy keeps a watermark. Objects are named by their chunk start, so a retry after a failure overwrites the same key instead of duplicating lines. A failed window keeps the watermark in place and is retried on the next run.
* Runs are bounded: one at a time, at most 24 hour-windows per scheduled run, and at most 200,000 lines per object (`APP_LOG_ARCHIVE_MAX_LINES_PER_OBJECT`). Lines are spooled to a temp file first so the log database is never held open during an upload. A new policy archives forward from the moment it is created; use a dump for history.
* Each scheduled tick or manual dump is recorded as a run with a status (`running`, `succeeded`, `failed`), the time range it covered, and object/line/byte counts, visible via `levelrail-cli logs archive status` or `GET /api/v1/log-archive/runs`.

## Retention

Set "Keep for (days)" on a policy (0 means forever) to have the archiver delete objects older than that after each successful scheduled run, up to 500 deletions per run. The global policy (no app set) leaves apps that have their own policy alone, so a per-app retention setting always wins over the global one. If you would rather let the provider manage deletion, add a bucket lifecycle rule on the `log-archive/` prefix and leave retention at 0.

## Manage policies

<Tabs>
  <Tab title="Dashboard">
    Use an app's **Logs, Archive** tab for a per-app policy, or **Settings, Storage destinations** for the global policy that covers every app.
  </Tab>

  <Tab title="CLI">
    ```bash theme={"dark"}
    levelrail-cli logs archive set --target <id> --app web --interval 1h --retention-days 30
    levelrail-cli logs archive set --target <id> --interval 6h        # every app
    levelrail-cli logs archive status
    levelrail-cli logs archive remove --app web
    ```

    `set --disable` keeps a policy but pauses it.
  </Tab>
</Tabs>

Intervals run from 5 minutes to 24 hours and default to 1 hour (`PUT /api/v1/log-archive/policy`).

## Dump now

Archive a past range immediately, without waiting for the schedule:

```bash theme={"dark"}
levelrail-cli logs dump --target <id> --app web --from 24h --wait
levelrail-cli logs dump --target <id> --from 2026-09-24T00:00:00Z --to 2026-09-24T06:00:00Z
```

`--from` and `--to` take an RFC3339 timestamp or a duration back from now. The range is capped at 31 days. The dashboard has the same control on the Archive tab. Under the hood this is `POST /api/v1/log-archive/dump`, which answers `202 Accepted` with a run to poll (`GET /api/v1/log-archive/runs`) rather than blocking until the dump finishes.

## Retrieving archived logs

```bash theme={"dark"}
levelrail-cli logs ls --target <id> --app web
levelrail-cli logs fetch --target <id> --key log-archive/service/web/2026/09/24/13/1790255400000000000.ndjson.gz
```

Equivalently, `GET /api/v1/log-archive/objects?target_id=<id>&app=web` lists keys page by page (100 per page, follow `next`), and `GET /api/v1/log-archive/objects/download?target_id=<id>&key=<key>` streams the object back with `Content-Type: application/gzip`. Download is limited to keys under the archive prefix; a key outside it is rejected.

Read an object with `zcat file.ndjson.gz | jq .`. Archived objects have no search index; use your bucket provider's tooling (Athena, DuckDB, `zgrep`) to search them.

## Alerting

Create a `log_archive_stale` alert rule (dashboard: Alerts, or `levelrail-cli apps alerts create <app> --kind log_archive_stale`) to be notified when a policy's last run failed, or when it has gone longer than the rule's `for_duration` without a success (default: three intervals, at least two hours).

## API and MCP

REST routes live under `/api/v1/log-archive` (see the [API reference](/api-reference)). Reads need the `read` ability, policy changes and dumps need `write`. The MCP server exposes `list_log_archive_policies`, `set_log_archive_policy`, `start_log_archive_dump`, `list_log_archive_runs`, and `list_archived_logs`.

## Next steps

<CardGroup cols={2}>
  <Card title="Object storage" href="/object-storage">
    Connect the bucket that archived logs are written to.
  </Card>

  <Card title="Observability" href="/observability">
    Live logs, metrics, and alert rules.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.