Builder Logs — Redaction, Rotation & Admin API

The iec-builder runs under PM2 on the builder host and writes structured pino logs to ~/.pm2/logs/iec-builder-out.log and -error.log. Because pino-http logs request metadata, log lines historically contained plaintext Authorization: Bearer <JWT> tokens. Three layers now keep those logs safe and bounded.

1. Write-time redaction (pino redact)

The pino logger (src/utils/logger.ts) redacts secret-bearing paths from every record before it is written, censoring with [REDACTED]:

  • Request headers: req.headers.authorization, req.headers.cookie, req.headers["x-internal-key"], req.headers["x-api-key"], req.headers["set-cookie"]
  • Response headers: res.headers["set-cookie"]
  • Wildcard fields: *.authorization, *.password, *.secret, *.token
  • Named secrets: BIO_CLIENT_SECRET, CONFIG_ENCRYPTION_KEY

This prevents new tokens from ever being written.

2. Server-side rotation (pm2-logrotate)

The builder host runs pm2-logrotate so logs never grow unbounded:

SettingValue
max_size50M
retain14
compresstrue (gzip)
rotateInterval0 0 * * * (daily at midnight)
workerInterval30 (seconds)

Rotated archives are named iec-builder-{out,error}__<timestamp>.log.gz and live alongside the live logs in ~/.pm2/logs. The pre-existing 762 MB log was scrubbed of Bearer tokens before the first compression so no archive leaks secrets.

3. Read-time redaction + platform-admin Logs API

Historical lines (written before layer 1 existed) still contain tokens, so every line surfaced through the API is redacted again at read time via regex (Bearer <token>, "authorization":"…", "x-internal-key":"…", "cookie":"…").

Both endpoints live in src/routes/platform.ts and require super_admin or service role (tawa-web uses X-Internal-Key). They read from the same host the builder runs on.

GET /platform/logs

Returns the live log inventory, rotated archives, and rotation status.

{
  "success": true,
  "data": {
    "files": [
      { "stream": "out", "path": "...", "sizeBytes": 1234, "sizeHuman": "1.2 KB", "modifiedAt": "2026-06-24T…Z" }
    ],
    "rotated": [
      { "name": "iec-builder-out__2026-06-24_00-00-00.log.gz", "sizeBytes": 1234, "sizeHuman": "1.2 KB", "modifiedAt": "…Z" }
    ],
    "totalBytes": 2468,
    "totalHuman": "2.4 KB",
    "rotation": { "enabled": true, "maxSize": "50M", "retain": 14, "compress": true }
  }
}
  • files — the two live streams (out, error).
  • rotated — compressed archives in ~/.pm2/logs matching iec-builder-*.
  • totalBytes / totalHuman — sum of live + rotated.
  • rotation — best-effort from pm2 conf pm2-logrotate; fields are null when undeterminable.

GET /platform/logs/tail?stream=out|error&lines=N&level=all|error|warn

Tails a stream by reading only the end of the file (~2 KB budget per line — never the whole 762 MB file).

{
  "success": true,
  "data": {
    "stream": "out",
    "lines": [
      { "ts": "2026-06-24T…Z", "level": "error", "msg": "request errored", "raw": "{…redacted…}" }
    ],
    "returned": 1,
    "truncated": false
  }
}
Query paramDefaultNotes
streamoutout or error
lines200clamped to a max of 1000
levelallall, warn (warn+error+fatal), or error (error+fatal)

Each line is parsed as pino JSON for ts / level / msg; if parsing fails those fields are null but raw is still redacted. truncated is true when older lines were dropped to fit the requested lines.

Key facts

  • Redaction is two independent layers (write-time pino + read-time regex) — neither alone is sufficient because of historical lines.
  • The logs API never returns secrets even when reading old, unredacted log content.
  • PM2 log paths are resolved from pm2 jlist (pm_out_log_path / pm_err_log_path) with a $HOME/.pm2/logs/iec-builder-{out,error}.log fallback.

Last updated: July 15, 2026