From 1084f62911524a0f77f6970877821e62108a0009 Mon Sep 17 00:00:00 2001 From: kyscott18 <43524469+kyscott18@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:36:00 +0000 Subject: [PATCH] fix(logger): configure log level and format with env vars Add TYPEWRITER_LOG_LEVEL ("trace" | "debug" | "info" | "warn" | "error", default "info") and TYPEWRITER_LOG_FORMAT ("pretty" | "json", default "json" when NODE_ENV is "production", else "pretty"). An invalid value throws when the module loads. The harness benchmark now runs at "info" and does not mute the console. --- README.md | 7 +++ packages/typewriter/README.md | 7 +++ packages/typewriter/src/logger.ts | 46 +++++++++++++++++-- packages/typewriter/test/harness.benchmark.ts | 39 ++++++---------- 4 files changed, 72 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 4d2a8094..9c386e58 100644 --- a/README.md +++ b/README.md @@ -508,6 +508,13 @@ Optional runtime controls: `blockPollingIntervalMs` (default `200`), `confirmati The Solidity entrypoint must be importable by Bun. At startup, typewriter runs `forge build`, reads the compiled ABI/storage layout/AST, and derives app mutation IDs, params, and typed storage from the contract. +**Logging.** Two environment variables control runtime logs: + +- `TYPEWRITER_LOG_LEVEL` — `"trace" | "debug" | "info" | "warn" | "error"`. Default `"info"`. +- `TYPEWRITER_LOG_FORMAT` — `"pretty" | "json"`. Default `"json"` when `NODE_ENV` is `"production"`, else `"pretty"`. + +An invalid value throws when the server module loads. + #### `typewriter.manifest` ```ts diff --git a/packages/typewriter/README.md b/packages/typewriter/README.md index 4d2a8094..9c386e58 100644 --- a/packages/typewriter/README.md +++ b/packages/typewriter/README.md @@ -508,6 +508,13 @@ Optional runtime controls: `blockPollingIntervalMs` (default `200`), `confirmati The Solidity entrypoint must be importable by Bun. At startup, typewriter runs `forge build`, reads the compiled ABI/storage layout/AST, and derives app mutation IDs, params, and typed storage from the contract. +**Logging.** Two environment variables control runtime logs: + +- `TYPEWRITER_LOG_LEVEL` — `"trace" | "debug" | "info" | "warn" | "error"`. Default `"info"`. +- `TYPEWRITER_LOG_FORMAT` — `"pretty" | "json"`. Default `"json"` when `NODE_ENV` is `"production"`, else `"pretty"`. + +An invalid value throws when the server module loads. + #### `typewriter.manifest` ```ts diff --git a/packages/typewriter/src/logger.ts b/packages/typewriter/src/logger.ts index c7f1f2d9..3136b522 100644 --- a/packages/typewriter/src/logger.ts +++ b/packages/typewriter/src/logger.ts @@ -1,4 +1,4 @@ -import { Layer, Logger, References } from "effect"; +import { Layer, Logger, type LogLevel, References } from "effect"; const stringify: typeof JSON.stringify = (value, replacer, space) => JSON.stringify( @@ -14,12 +14,52 @@ const consoleJson = Logger.withConsoleLog( Logger.map(Logger.formatStructured, stringify), ); +const LOG_LEVELS = { + trace: "Trace", + debug: "Debug", + info: "Info", + warn: "Warn", + error: "Error", +} as const satisfies Record; + +const LOG_FORMATS = ["pretty", "json"] as const; + +function readEnv( + name: string, + values: readonly T[], + fallback: T, +): T { + const value = process.env[name]; + if (value === undefined || value === "") return fallback; + if (!(values as readonly string[]).includes(value)) { + throw new Error( + `${name} must be one of ${values.map((v) => `"${v}"`).join(", ")}, got "${value}"`, + ); + } + return value as T; +} + +const logLevel = + LOG_LEVELS[ + readEnv( + "TYPEWRITER_LOG_LEVEL", + Object.keys(LOG_LEVELS) as (keyof typeof LOG_LEVELS)[], + "info", + ) + ]; + +const logFormat = readEnv( + "TYPEWRITER_LOG_FORMAT", + LOG_FORMATS, + process.env.NODE_ENV === "production" ? "json" : "pretty", +); + export const logger = - process.env.NODE_ENV === "production" ? consoleJson : Logger.consolePretty(); + logFormat === "json" ? consoleJson : Logger.consolePretty(); export const loggerLayer = Layer.merge( Logger.layer([logger]), - Layer.succeed(References.MinimumLogLevel, "Debug"), + Layer.succeed(References.MinimumLogLevel, logLevel), ); export function startTimer(): number { diff --git a/packages/typewriter/test/harness.benchmark.ts b/packages/typewriter/test/harness.benchmark.ts index d5204caa..f607f7c6 100644 --- a/packages/typewriter/test/harness.benchmark.ts +++ b/packages/typewriter/test/harness.benchmark.ts @@ -17,7 +17,10 @@ import { secp256k1PublicKey, } from "./utils"; -process.env.NODE_ENV = "production"; +Object.assign(process.env, { + NODE_ENV: "production", + TYPEWRITER_LOG_LEVEL: "info", +}); const LATENCY_MUTATION_COUNT = 1_000; const THROUGHPUT_MUTATION_COUNT = 10_000; @@ -61,34 +64,22 @@ test("harness benchmark", async () => { const latencyMutations = mutations.slice(0, LATENCY_MUTATION_COUNT); const throughputMutations = mutations.slice(LATENCY_MUTATION_COUNT); - // Keep the cost of Debug log formatting, but do not print the lines. - const consoleLog = console.log; - const consoleDebug = console.debug; - console.log = () => {}; - console.debug = () => {}; + const typewriter = await createTypewriter(Harness, config); + await typewriter.execute(createAccount); const latenciesMs: number[] = []; - let throughputDurationMs: number; - try { - const typewriter = await createTypewriter(Harness, config); - await typewriter.execute(createAccount); - - for (const mutation of latencyMutations) { - const start = performance.now(); - await typewriter.execute(mutation); - latenciesMs.push(performance.now() - start); - } - + for (const mutation of latencyMutations) { const start = performance.now(); - await Promise.all( - throughputMutations.map((mutation) => typewriter.execute(mutation)), - ); - throughputDurationMs = performance.now() - start; - } finally { - console.log = consoleLog; - console.debug = consoleDebug; + await typewriter.execute(mutation); + latenciesMs.push(performance.now() - start); } + const start = performance.now(); + await Promise.all( + throughputMutations.map((mutation) => typewriter.execute(mutation)), + ); + const throughputDurationMs = performance.now() - start; + latenciesMs.sort((a, b) => a - b); const averageMs = latenciesMs.reduce((total, latency) => total + latency, 0) /