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 7f23153633bc225a7d06e07d05ed901d830c837dca0c869993a33f7f16dd2477
🍏 macos 8807834dfb6f04b8cc8c62df66e2856fd9059b5f8fcd213a5809d13713e56dea

This preview will be deleted once this PR is closed.

@pdesoyres-cc pdesoyres-cc self-assigned this Apr 21, 2026
@pdesoyres-cc
pdesoyres-cc force-pushed the enhance-logger branch 2 times, most recently from 3b6b530 to 79e36b8 Compare May 6, 2026 09:01
@hsablonniere hsablonniere added this to the 5.0.0 milestone Sep 3, 2026
Pierre DE SOYRES added 12 commits September 9, 2026 15:09
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.
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.
@hsablonniere

Copy link
Copy Markdown
Member

@pdesoyres-cc I rebased to ease the review and tests

@davlgd

davlgd commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Tested dbb42344 against master (0ed8f5cf) on macOS, fresh npm ci on both sides. Source and CJS tests ran on Node 24.20.0; the generated macOS ARM64 binary embeds Node 24.18.1. Each point says whether it used the live API or a replaced response. File logging does work: running --version with a fresh home directory creates Library/Logs/clever-cloud/clever-tools.2026-09-10.1.log, and normal runs write JSON lines into it.

1. The generated CJS bundle and macOS ARM64 binary fail to start.
node scripts/bundle-cjs.js 4.11.0 false then node build/4.11.0/clever.cjs profileError: Cannot find module '.../build/4.11.0/lib/worker.js'. The same bundle on master prints the profile.
node scripts/build-binary.js 4.11.0 builds fine (exit 0), but running the binary it produces — ./build/4.11.0/macos/clever-tools-4.11.0_macos/clever --version — fails before printing anything to stdout: Error: unable to determine transport target for "pino-roll" at fixTarget (/snapshot/branch/build/4.11.0/clever.cjs)pino.transportsetup, exit 1. The same binary built from master prints 4.11.0 and exits 0.

2. A handled error can exit 0.
node bin/clever.js env -a doesnotexist123; echo $? → master 1, branch 0, both printing [ERROR] There is no linked or targeted application....
In that run process.exit was not reached: instrumenting it shows only beforeExit(0) / exit(0) — node exits before the awaited shutdown() completes. The outcome depends on whether another handle keeps the event loop alive: with a preloaded setInterval, the same command exits 1.
CLEVER_QUIET=1 node bin/clever.js env -a doesnotexist → no stdout, no stderr, exit 0.

3. Crash when the log directory isn't writable.
HOME pointed at a chmod 555 directory, clever --version: the branch prints 4.11.0, then RangeError: The value of "targetStart" is out of range. It must be >= 0. Received -2 at node_modules/thread-stream/index.js:510:11. master prints 4.11.0 and exits clean. Same branch with a writable directory is fine.

4. npm run validate fails (exit 2). lint and format:check pass, typecheck doesn't:
node_modules/thread-stream/index.d.ts(96,73): error TS2694: Namespace '"worker_threads"' has no exported member 'TransferListItem'. On master, npm run typecheck passes.

5. Logger.println() with no argument prints undefined. Live:

$ node bin/clever.js applications list
undefined
• Organization 'David Legrand' (user_…) with 265 applications:
undefined
APPLICATION ID  NAME …

master prints blank lines there. grep -rn "Logger.println()" src bin returns 24 matches.

6. The deprecation warning is missing.
node bin/clever.js env --add-export -a doesnotexist → master prints Warning: --add-export is deprecated, use `--format shell` instead.; the branch omits it (the app-not-found error still prints). With CLEVER_VERBOSE=1 the branch does print [WARN] --add-export is deprecated….
With open stubbed to reject and simulated API responses, master prints the browser-opening warning at login.command.js:60 and the branch omits it; both still print the manual fallback instruction.

7. Errors that used to be matched on responseBody / response.

  • emails add <the account's own primary address>, live: master [ERROR] This address already belongs to your account, branch [ERROR] This email address already belongs to you [101]. The specific message for 101 is lost.
  • profile with GET /v2/self and GET /v2/self/tokens/current replaced by HTTP 401: master [ERROR] Your token is invalid or has expired, use clever login command, branch [ERROR] You're not logged in, use clever login command….
  • domain overview -F json, live except one /vhosts response replaced by HTTP 403 (one app out of 265): master warns on stderr and still prints the full inventory (159 KB of JSON, exit 0); the branch writes [ERROR] Forbidden domains [4003] to stderr and produces no JSON on stdout. Without the injected 403 both outputs are identical.

8. activity warns on known states.

$ node bin/clever.js activity --app app_98e2ee70-…
⚠ Unknown deployment state: FAIL
2026-09-10T17:41:47+02:00  deployment_e3b58a1f-…  FAIL  DEPLOY  84d81eb4  Git
⚠ Unknown deployment state: OK
…

Every row of the output I got carried one, including OK and FAIL. Looks like the Logger.printWarning at activity.command.js:31, first statement of getColoredState.

The eight issues were also reproduced from separate source snapshots with fresh npm ci installs; the API-error and browser-failure checks used stubs.

@hsablonniere hsablonniere removed this from the 5.0.0 milestone Sep 11, 2026
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.

3 participants