Skip to content

Commit 2ac0456

Browse files
committed
wip(rest): declared OS_REST_LOG level seam on logError
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
1 parent 7f62536 commit 2ac0456

2 files changed

Lines changed: 93 additions & 5 deletions

File tree

‎packages/rest/src/log.ts‎

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,96 @@
99
* is the "two spellings of one thing" shape this repo pays for repeatedly. It
1010
* is deliberately NOT re-exported from the package index: an internal shim, not
1111
* a logging API.
12+
*
13+
* ── The declared level seam (#15484) ──────────────────────────────────────
14+
*
15+
* Every fault this package reports goes through `logError`, and `logError`
16+
* hands its varargs — an `Error` object among them — straight to
17+
* `console.error`, which Node formats with the error's full stack and its
18+
* `[cause]` chain. Measured on one green `packages/rest` run: 2,095 indented
19+
* `at ` frame lines, 36.7% of the captured output, and 100% of them arrive
20+
* through this file (1,197 from `error-response.ts`, 841 from
21+
* `rest-server.ts`, 57 from `cause` chains).
22+
*
23+
* ⛔ That volume is NOT a defect and the frames are NOT dead weight. They are
24+
* read, and the repo pins that they are read: at `logWithheldServerFault` the
25+
* client is told nothing and the log is the operator's only copy of the driver
26+
* text — which lives on `error.cause` and is printed only because a whole
27+
* `Error` is passed; at `logUnexpectedRouteError` the frames are the only
28+
* location diagnostic a bare `TypeError` has. Four assertions across
29+
* `rest-5xx-message-sanitization.test.ts` and `rest-expected-error-logging.test.ts`
30+
* hold that, by asserting the IDENTITY of the `Error` reaching `console.error`
31+
* (#5437 / #4886 / #5489). ⛔ Do not "quieten" this shim by formatting the
32+
* `Error` down to a string or a summary — that is the repair those pins exist
33+
* to stop, and it deletes from the LOG what was deliberately withheld from the
34+
* CLIENT.
35+
*
36+
* So the seam is a DECLARATION, not a quieter product: `OS_REST_LOG` names a
37+
* level, exactly as `OS_REGISTRY_LOG` does for `@objectstack/objectql`'s
38+
* `SchemaRegistry`, with the same five-level vocabulary and the same shipped
39+
* `'info'` default. ⛔ The shipped default is unchanged and must stay
40+
* unchanged: a reported fault keeps printing the full `Error` — message,
41+
* `cause` chain and frames — for every real caller. Suppression is only ever
42+
* something a HARNESS opts into, declared where a gate can read it
43+
* (`scripts/check-rest-log-declared.mjs`), never the product's default.
44+
*
45+
* An operator learns the variable from `packages/rest/README.md`, from this
46+
* block, and from `docs/audits/2026-09-test-log-volume-census.md`; an
47+
* unrecognised value falls back to the default rather than silently silencing
48+
* anything, which is the same failure direction `OS_REGISTRY_LOG` chose.
49+
*/
50+
51+
/**
52+
* The levels `OS_REST_LOG` accepts — deliberately the same vocabulary as
53+
* `@objectstack/objectql`'s `REGISTRY_LOG_LEVELS`, so the two declarations are
54+
* one contract with two populations rather than two ad-hoc environment
55+
* variables. Ordered loudest-first; `scripts/check-rest-log-declared.mjs`
56+
* READS this array rather than copying it.
57+
*/
58+
export const REST_LOG_LEVELS = ['debug', 'info', 'warn', 'error', 'silent'] as const;
59+
60+
/** One of {@link REST_LOG_LEVELS}. */
61+
export type RestLogLevel = (typeof REST_LOG_LEVELS)[number];
62+
63+
/**
64+
* The SHIPPED default — what every real caller gets when `OS_REST_LOG` is
65+
* unset or unrecognised. ⛔ Never lower this to quieten a log: at any level
66+
* below `'warn'` this package stops reporting faults it is the only reporter
67+
* of. `rest-log-declared-level-seam.test.ts` pins it.
68+
*/
69+
export const REST_LOG_DEFAULT_LEVEL: RestLogLevel = 'info';
70+
71+
/** Emission threshold per level: a site emits when its own rank <= the level's. */
72+
const LEVEL_RANK: Readonly<Record<RestLogLevel, number>> = {
73+
debug: 4, info: 3, warn: 2, error: 1, silent: 0,
74+
};
75+
76+
/** `logError`'s rank, and `logWarn`'s — a site speaks while the level reaches it. */
77+
const ERROR_RANK = LEVEL_RANK.error;
78+
const WARN_RANK = LEVEL_RANK.warn;
79+
80+
/**
81+
* The level in force right now.
82+
*
83+
* Read from the environment on every call, not memoised at module load: a
84+
* harness that declares the level through vitest's `env` block, and a test that
85+
* restores the shipped default around one assertion, both have to be observed
86+
* by a module that may already be imported. This is a fault path — it runs once
87+
* per reported fault, never per request.
1288
*/
89+
export function restLogLevel(): RestLogLevel {
90+
const raw = String((globalThis as any).process?.env?.OS_REST_LOG ?? '').toLowerCase();
91+
return (REST_LOG_LEVELS as readonly string[]).includes(raw)
92+
? (raw as RestLogLevel)
93+
: REST_LOG_DEFAULT_LEVEL;
94+
}
1395

1496
// Node-safe logger — avoids importing 'console' which is absent from ES2020 lib typings.
15-
export const logError = (...args: unknown[]) => (globalThis as any).console?.error(...args);
16-
export const logWarn = (...args: unknown[]) => ((globalThis as any).console?.warn ?? (globalThis as any).console?.error)?.(...args);
97+
export const logError = (...args: unknown[]) => {
98+
if (LEVEL_RANK[restLogLevel()] < ERROR_RANK) return;
99+
(globalThis as any).console?.error(...args);
100+
};
101+
export const logWarn = (...args: unknown[]) => {
102+
if (LEVEL_RANK[restLogLevel()] < WARN_RANK) return;
103+
((globalThis as any).console?.warn ?? (globalThis as any).console?.error)?.(...args);
104+
};

‎packages/rest/vitest.config.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export default defineConfig({
2828
// #13517: quiet the registry's per-item registration chatter — the
2929
// engine's own `OS_REGISTRY_LOG` seam, not a change to its shipped
3030
// default. Enforced by scripts/check-registry-log-declared.mjs.
31-
env: { OS_REGISTRY_LOG: 'warn' },
31+
env: { OS_REGISTRY_LOG: 'warn', OS_REST_LOG: 'silent' },
3232
// A late console.* must not redden a green suite (#10374); see the root
3333
// block. A ROOT-level value is inert for a project run, so it is
3434
// declared here as well (scripts/check-console-intercept-disarm.mjs).
@@ -44,7 +44,7 @@ export default defineConfig({
4444
// #13517: quiet the registry's per-item registration chatter — the
4545
// engine's own `OS_REGISTRY_LOG` seam, not a change to its shipped
4646
// default. Enforced by scripts/check-registry-log-declared.mjs.
47-
env: { OS_REGISTRY_LOG: 'warn' },
47+
env: { OS_REGISTRY_LOG: 'warn', OS_REST_LOG: 'silent' },
4848
// A late console.* must not redden a green suite (#10374); see the root
4949
// block. A ROOT-level value is inert for a project run, so it is
5050
// declared here as well (scripts/check-console-intercept-disarm.mjs).
@@ -71,7 +71,7 @@ export default defineConfig({
7171
// The ADR-0005 `[Registry] Collision` diagnostics go through a bare
7272
// `console.warn` the level never gates, so a real shadowing still speaks.
7373
// Enforced by scripts/check-registry-log-declared.mjs.
74-
env: { OS_REGISTRY_LOG: 'warn' },
74+
env: { OS_REGISTRY_LOG: 'warn', OS_REST_LOG: 'silent' },
7575
globals: true,
7676
environment: 'node',
7777
},

0 commit comments

Comments
 (0)