From 68d67ac26700c7a28ecfd2c10c62613f2d7edf0b Mon Sep 17 00:00:00 2001 From: Avocado Date: Thu, 27 Aug 2026 15:50:47 +0900 Subject: [PATCH 1/2] fix(core): respect log level in worker threads The log level set by `--log-level` was only applied on the main thread. Worker threads load their own module graph, so the default logger export creates a separate instance there that keeps the default `info` level. Warnings emitted from inside workers were therefore printed regardless of the requested level. Pass the level through Piscina `workerData` and apply it at the worker entry point. This adds a `getLogLevel()` accessor, as there was no way to read the configured level back. Fixes: https://github.com/nodejs/doc-kit/issues/1032 Signed-off-by: Avocado --- .changeset/worker-log-level.md | 5 +++++ packages/core/src/logger/logger.mjs | 8 ++++++++ packages/core/src/threading/chunk-worker.mjs | 7 +++++++ packages/core/src/threading/index.mjs | 1 + 4 files changed, 21 insertions(+) create mode 100644 .changeset/worker-log-level.md diff --git a/.changeset/worker-log-level.md b/.changeset/worker-log-level.md new file mode 100644 index 000000000..2c2b995eb --- /dev/null +++ b/.changeset/worker-log-level.md @@ -0,0 +1,5 @@ +--- +'@doc-kit/core': patch +--- + +fix: respect `--log-level` in worker threads diff --git a/packages/core/src/logger/logger.mjs b/packages/core/src/logger/logger.mjs index 2a0a9a470..24cbabce0 100644 --- a/packages/core/src/logger/logger.mjs +++ b/packages/core/src/logger/logger.mjs @@ -165,6 +165,13 @@ export const createLogger = ( } }; + /** + * Gets the current log level for this logger instance. + * + * @returns {number} The current numeric log level + */ + const getLogLevel = () => currentLevel; + return { info, warn, @@ -173,5 +180,6 @@ export const createLogger = ( debug, child, setLogLevel, + getLogLevel, }; }; diff --git a/packages/core/src/threading/chunk-worker.mjs b/packages/core/src/threading/chunk-worker.mjs index cf324fb6d..7bfbae112 100644 --- a/packages/core/src/threading/chunk-worker.mjs +++ b/packages/core/src/threading/chunk-worker.mjs @@ -1,6 +1,13 @@ +import { workerData } from 'node:worker_threads'; + import { loadGenerator } from '#generators/loader.mjs'; +import logger from '#logger/index.mjs'; import { setConfig } from '#utils/configuration/index.mjs'; +if (workerData?.logLevel !== undefined) { + logger.setLogLevel(workerData.logLevel); +} + /** * Processes a chunk of items using the specified generator's processChunk method. * This is the worker entry point for Piscina. diff --git a/packages/core/src/threading/index.mjs b/packages/core/src/threading/index.mjs index a158f89bd..2cd4c9a19 100644 --- a/packages/core/src/threading/index.mjs +++ b/packages/core/src/threading/index.mjs @@ -23,5 +23,6 @@ export default function createWorkerPool(threads) { minThreads: 0, maxThreads: threads, idleTimeout: 1_000, + workerData: { logLevel: logger.getLogLevel() }, }); } From 59cba196fc7ce9e108f86ade7c959c2449304a73 Mon Sep 17 00:00:00 2001 From: Avocado Date: Thu, 27 Aug 2026 16:47:35 +0900 Subject: [PATCH 2/2] test(core): cover log level propagation to workers Assert that the pool forwards the current level through `workerData` and that a worker actually applies it, using a fixture generator that reports the level it sees. Removing the fix makes both fail, while the default level case keeps passing. Also cover the new `getLogLevel()` accessor. Signed-off-by: Avocado --- .../core/src/logger/__tests__/logger.test.mjs | 31 +++++++ .../__tests__/fixtures/log-level-reporter.mjs | 19 +++++ .../src/threading/__tests__/index.test.mjs | 83 +++++++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 packages/core/src/threading/__tests__/fixtures/log-level-reporter.mjs create mode 100644 packages/core/src/threading/__tests__/index.test.mjs diff --git a/packages/core/src/logger/__tests__/logger.test.mjs b/packages/core/src/logger/__tests__/logger.test.mjs index 1673b453e..76e0d02fd 100644 --- a/packages/core/src/logger/__tests__/logger.test.mjs +++ b/packages/core/src/logger/__tests__/logger.test.mjs @@ -380,4 +380,35 @@ describe('createLogger', () => { strictEqual(transport.mock.callCount(), 1); // Debug should be filtered }); }); + + describe('getLogLevel', () => { + it('should return the level the logger was created with', t => { + const logger = createLogger(t.mock.fn(), LogLevel.warn); + + strictEqual(logger.getLogLevel(), LogLevel.warn); + }); + + it('should default to info when no level is given', t => { + const logger = createLogger(t.mock.fn()); + + strictEqual(logger.getLogLevel(), LogLevel.info); + }); + + it('should reflect a level set afterwards', t => { + const logger = createLogger(t.mock.fn(), LogLevel.info); + + logger.setLogLevel('fatal'); + + strictEqual(logger.getLogLevel(), LogLevel.fatal); + }); + + it('should reflect the propagated level on children', t => { + const logger = createLogger(t.mock.fn(), LogLevel.info); + const child = logger.child('module'); + + logger.setLogLevel(LogLevel.error); + + strictEqual(child.getLogLevel(), LogLevel.error); + }); + }); }); diff --git a/packages/core/src/threading/__tests__/fixtures/log-level-reporter.mjs b/packages/core/src/threading/__tests__/fixtures/log-level-reporter.mjs new file mode 100644 index 000000000..efe535ded --- /dev/null +++ b/packages/core/src/threading/__tests__/fixtures/log-level-reporter.mjs @@ -0,0 +1,19 @@ +import logger from '#logger/index.mjs'; + +/** + * Test generator that reports the log level seen inside the worker, so the + * propagation of the level across the thread boundary can be asserted. + * + * @type {GeneratorMetadata} + */ +export default { + name: 'log-level-reporter', + version: '1.0.0', + description: 'Reports the log level active inside the worker', + dependsOn: 'ast', + processChunk: async (_input, itemIndices) => + itemIndices.map(() => logger.getLogLevel()), + async generate() { + return [logger.getLogLevel()]; + }, +}; diff --git a/packages/core/src/threading/__tests__/index.test.mjs b/packages/core/src/threading/__tests__/index.test.mjs new file mode 100644 index 000000000..b5f6b63fa --- /dev/null +++ b/packages/core/src/threading/__tests__/index.test.mjs @@ -0,0 +1,83 @@ +import { strictEqual } from 'node:assert'; +import { describe, it } from 'node:test'; +import { fileURLToPath } from 'node:url'; + +import { LogLevel } from '../../logger/constants.mjs'; +import logger from '../../logger/index.mjs'; +import createWorkerPool from '../index.mjs'; + +const reporterSpecifier = fileURLToPath( + import.meta.resolve('./fixtures/log-level-reporter.mjs') +); + +/** + * Runs a function with the logger temporarily set to the given level. + * + * @template T + * @param {number} level - Log level to apply for the duration of the callback + * @param {() => Promise} fn - Callback to run + * @returns {Promise} + */ +const withLogLevel = async (level, fn) => { + const original = logger.getLogLevel(); + + logger.setLogLevel(level); + + try { + return await fn(); + } finally { + logger.setLogLevel(original); + } +}; + +describe('createWorkerPool', () => { + it('should forward the current log level to workers', async () => { + await withLogLevel(LogLevel.fatal, async () => { + const pool = createWorkerPool(1); + + try { + strictEqual(pool.options.workerData.logLevel, LogLevel.fatal); + } finally { + await pool.destroy(); + } + }); + }); + + it('should apply the forwarded log level inside the worker', async () => { + await withLogLevel(LogLevel.fatal, async () => { + const pool = createWorkerPool(1); + + try { + const [levelInWorker] = await pool.run({ + generatorSpecifier: reporterSpecifier, + input: [null], + itemIndices: [0], + extra: {}, + configuration: {}, + }); + + strictEqual(levelInWorker, LogLevel.fatal); + } finally { + await pool.destroy(); + } + }); + }); + + it('should leave workers at the default level when it is not changed', async () => { + const pool = createWorkerPool(1); + + try { + const [levelInWorker] = await pool.run({ + generatorSpecifier: reporterSpecifier, + input: [null], + itemIndices: [0], + extra: {}, + configuration: {}, + }); + + strictEqual(levelInWorker, LogLevel.info); + } finally { + await pool.destroy(); + } + }); +});