Skip to content

feat(logger): persistent file logging and structured API errors - #1082

Open
pdesoyres-cc wants to merge 12 commits into
masterfrom
enhance-logger
Open

feat(logger): persistent file logging and structured API errors#1082
pdesoyres-cc wants to merge 12 commits into
masterfrom
enhance-logger

Conversation

@pdesoyres-cc

@pdesoyres-cc pdesoyres-cc commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Context

The previous Logger only wrote to stdout, so output vanished as soon as the process exited — bug
reports were impossible to triage after the fact. API errors went through an ad-hoc processError
that rendered a fields map, hid the raw response body, and made support tickets painful. A long
tail of direct console.* calls in commands meant CLEVER_QUIET / CLEVER_VERBOSE applied
unevenly across the CLI.

Changes

  • Adopt @clevercloud/scribe and persist every log level to a rotating file at OS-conventional
    paths: $XDG_STATE_HOME/clever-cloud/ (Linux), ~/Library/Logs/clever-cloud/ (macOS),
    %LOCALAPPDATA%\clever-cloud\Logs\ (Windows). Rotation kicks in around 250k, capped at 50 files.
  • Introduce ApiError extends Error exposing code, status, raw body, url, headers. The
    legacy processError fields rendering is gone — ApiError.body carries the raw response.
  • Unify the pretty-print path: a single prettyLog handles every severity, including stack traces
    under CLEVER_VERBOSE=1 and styled [ERROR] / [WARN] / [INFO] / [DEBUG] prefixes.
  • Migrate the remaining direct console.log / console.table / console.error / stderr writes
    through Logger.println / printTable / printErrorLine / warn, and split user-facing
    warnings from the internal warn channel.
  • Route every exit through a new src/lib/exit.js helper that awaits scribe's shutdown() before
    calling process.exit, so the file transport's async buffer is flushed on success, failure,
    EPIPE, SIGINT/SIGTERM, prompt cancellation, and ssh/curl/config error paths.
  • Add ESLint rules scoped to bin/*.js and src/**/*.js: no-console to lock in the Logger
    migration, and a no-restricted-properties rule forbidding process.exit outside the new
    exit helper.

Implementation notes

The pretty-print path was rewritten rather than patched: the old code had divergent branches for
error (red prefix, stack trace, stderr) and the other severities (no styling, stdout, gated on
IS_VERBOSE). Routing every severity through prettyLog makes the IS_QUIET / IS_VERBOSE and
stderr-vs-stdout decisions explicit instead of scattered.

Cliparse's own parse-time process.exit(1) calls are deliberately left untouched — they fire
before any meaningful logging happens, so the lost file lines aren't worth a global process.exit
shim. The ESLint rule's ignores entry exempts src/lib/exit.js itself, which is the one
sanctioned caller of process.exit.

How to review

  1. Start with src/logger.jsprettyLog and getLogFilePath are the load-bearing changes.
  2. Read src/lib/api-error.js plus toApiError / parseError* in src/models/send-to-api.js
    to see how API failures now flow to the user.
  3. Read src/lib/exit.js and the call sites in bin/clever.js, src/lib/cliparse-patched.js,
    src/lib/prompts.js, and the ssh/curl/config commands to confirm every exit path flushes.
  4. Run any clever command and confirm a log file appears under the OS-specific path above; force
    a few rotations to check the 250k / 50-files cap.
  5. CLEVER_QUIET=1 suppresses stdout but the file is still written; CLEVER_VERBOSE=1 surfaces
    [DEBUG] / [INFO] entries on stdout and prints full stack traces for thrown Errors.
  6. Trigger a 4xx/5xx API call and confirm the user sees <message> [<code>] and that
    ApiError.body carries the raw response payload.
  7. Send SIGINT mid-command and verify the last log lines actually land in the file (i.e. the
    buffer was flushed before exit).

@pdesoyres-cc
pdesoyres-cc requested a review from a team as a code owner April 21, 2026 08:26
@github-actions

github-actions Bot commented Apr 21, 2026

Copy link
Copy Markdown

🔎 A preview has been automatically published!

If you created the alias to the preview script, you can run this command to download and install this preview:

clever-preview update enhance-logger

You can also run it from your local repository:

./scripts/preview.js update enhance-logger
OS SHA256 checksum
🐧 linux 9e2c595f6675c6cd8047e6bad45b397b2ea2d59d4a3133363620a8382ce8bd4d
🍏 macos 17814b0b6d47aded84f561107f7b168773d4badda894dff8a4925a178887b5a0

This preview will be deleted once this PR is closed.

Pierre DE SOYRES added 10 commits May 6, 2026 10:48
Move API error construction out of the logger and into the send-to-api
layer so failures propagate as real Error subclasses. The logger no
longer needs to know the shape of API response bodies, and the full
response body is preserved on the error for callers that need it.
Mirror the writeStderr helper so stdout and stderr go through the
same path, and make Logger output easier to stub in tests.
Adopting @clevercloud/scribe pulled in pino → thread-stream, whose
CJS shape makes @rollup/plugin-commonjs emit virtual
`package.json?commonjs-proxy` modules. The preview-version transform
matched those proxies and tried to JSON.parse JavaScript, failing
`scripts/bundle-cjs.js` and the downstream binary compilation.
Add `Logger.printWarning` for terminal-facing messages (yellow ⚠ prefix on stdout) and migrate
the four call sites that were using `Logger.warn` for that purpose. The scribe migration in
progress on this branch repurposes `warn` for structured logging, so user-facing CLI warnings
need their own channel.
The two call sites converted here aren't logging exceptions — they print a usage check
(curl) and a post-failure hint (k8s) to stderr before returning. Routing them through
`Logger.error` added an `[ERROR]` prefix and, in verbose mode, stacktrace output that
wasn't relevant. Mirrors the warn / printWarning split from 6040158: the scribe migration
in progress on this branch repurposes `error` for structured logging, so user-facing CLI
errors need their own channel.
…intln

Finishes the console.log migration started in 0b6f00e so every stdout write goes through the
Logger facade — a prerequisite for swapping the backend to scribe without leaving stray
unstructured prints behind.

Also drops a leftover debug log of the SSH key API responseBody id that was only useful while
narrowing down the 505 error case.
Continues the migration to centralize console output behind the Logger so the underlying
backend can be swapped without touching command code. Adds a thin printTable helper and
updates every direct console.table call site to use it.
…ation

The recent refactor series routed every console call in bin/ and src/ through the Logger
abstraction. Add no-console to prevent regressions and keep all CLI output going through a
single sink. Scripts stay exempt since release tooling has no Logger and prints directly.
The Logger's own console.table sink keeps an inline disable as the sanctioned escape hatch.
Adds @clevercloud/scribe so every Logger call is written to a per-OS log file
(Windows LOCALAPPDATA, macOS ~/Library/Logs, Linux XDG_STATE_HOME) with size-based
rotation. Gives users and support a durable trace even when the CLI is run
non-verbose, where stderr is intentionally kept minimal.

The console renderer (renamed prettyLog) now gates on IS_VERBOSE for every severity,
warn included — the file already captures it, so there is no reason to leak it to
stderr by default. Also guards the stacktrace branch on `error instanceof Error` so
string-only errors no longer try to dump a non-existent stack.
The error branch duplicated styling, stacktrace handling, and stderr routing that the other
severities did not have. Folding error into prettyLog with a SEVERITY_STYLES map collapses both
paths into one, so styled prefixes, API-error processing, and stream selection are decided in a
single place — and warn/info/debug pick up the same styled prefix treatment along the way.
Pierre DE SOYRES added 2 commits May 6, 2026 11:01
Scribe's file transport is backed by a Pino worker thread, so anything still in its async
buffer is lost when the CLI exits via a synchronous `process.exit()`. We now route every
exit through a small helper that awaits `shutdown()` first, covering command success and
failure (via the cliparse wrapper), EPIPE on stdout, SIGINT/SIGTERM, prompt cancellation,
ssh subprocess teardown, and curl/config validation errors.

Cliparse's own parse-time `process.exit(1)` calls are untouched — they fire before any
meaningful logging, so the lost file lines aren't worth a global `process.exit` shim.
The previous commit routed every exit through `src/lib/exit.js` so log buffers get flushed
before the process dies. Without a lint guard, future code will quietly reintroduce direct
`process.exit()` calls and silently drop log lines again. The rule keeps the helper as the
single sanctioned exit point.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant