From 2fdaf8a0471242ac41850f21baf11e8c5ba490d9 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 21 May 2026 10:29:31 -0700 Subject: [PATCH] Limit server console logs to errors --- server/logger.ts | 33 +++++--- .../server/logger.separation.test.ts | 84 +++++++++++++++++-- 2 files changed, 96 insertions(+), 21 deletions(-) diff --git a/server/logger.ts b/server/logger.ts index 7896f7434..5f233f0a0 100644 --- a/server/logger.ts +++ b/server/logger.ts @@ -18,6 +18,7 @@ const DEFAULT_SESSION_LIFECYCLE_LOG_SUFFIX = '.jsonl' const DEFAULT_SESSION_LIFECYCLE_LOG_SIZE: SizeString = '10M' const DEFAULT_SESSION_LIFECYCLE_LOG_MAX_FILES = 10 export const DEFAULT_NON_DEBUG_LOG_LEVEL: LevelWithSilent = 'warn' +const DEFAULT_CONSOLE_LOG_LEVEL: LevelWithSilent = 'error' const SOURCE_ENTRY_MATCHERS = [/(^|\/)server\/index\.ts$/i, /(^|\/)server\/index\.js$/i] const DIST_ENTRY_MATCHERS = [/(^|\/)dist\/server\/index\.js$/i] type LogMode = 'development' | 'production' @@ -270,10 +271,17 @@ export function createLogger(destination?: DestinationStream) { const shouldPrettyPrint = env !== 'production' && env !== 'test' const consoleStream = createConsoleStream(shouldPrettyPrint) - const consoleLogger = pino(createPinoOptions(), consoleStream) + const consoleLogger = pino(createPinoOptions({ level: DEFAULT_CONSOLE_LOG_LEVEL }), consoleStream) const streams: Array<{ stream: DestinationStream; level: LevelWithSilent }> = [ - { stream: consoleStream, level: DEFAULT_NON_DEBUG_LOG_LEVEL }, + { stream: consoleStream, level: DEFAULT_CONSOLE_LOG_LEVEL }, ] + let resolvedDebugLog: + | { + filePath: string + debugMode: LogMode + debugInstance: string + } + | undefined const debugLogPath = resolveDebugLogPath() if (debugLogPath) { @@ -283,20 +291,21 @@ export function createLogger(destination?: DestinationStream) { const debugStream = createDebugFileStream(debugLogPath) streams.push({ stream: debugStream, level: 'debug' }) attachDebugStreamWarnings(debugStream, consoleLogger, debugLogPath) - consoleLogger.info( - { - filePath: debugLogPath, - debugMode, - debugInstance, - }, - 'Resolved debug log path', - ) + resolvedDebugLog = { + filePath: debugLogPath, + debugMode, + debugInstance, + } } catch (err) { - consoleLogger.warn({ err, filePath: debugLogPath }, 'Debug log file disabled') + consoleLogger.error({ err, filePath: debugLogPath }, 'Debug log file disabled') } } - return pino(createPinoOptions(), pino.multistream(streams)) + const nextLogger = pino(createPinoOptions(), pino.multistream(streams)) + if (resolvedDebugLog) { + nextLogger.info(resolvedDebugLog, 'Resolved debug log path') + } + return nextLogger } export const logger = createLogger() diff --git a/test/integration/server/logger.separation.test.ts b/test/integration/server/logger.separation.test.ts index 6dc8c0ea9..c786f870c 100644 --- a/test/integration/server/logger.separation.test.ts +++ b/test/integration/server/logger.separation.test.ts @@ -9,7 +9,6 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' import { startServerProcess, stopProcess, - waitForResolvedPath, type LoggerServerProcess, } from './logger.separation.harness.js' @@ -34,6 +33,16 @@ const DIST_LOGGER_PROBE = [ ' setTimeout(() => process.exit(0), 25)', '})()', ].join('\n') +const LOG_LEVEL_PROBE = [ + '(async () => {', + " const { logger } = await import('./server/logger.ts')", + " logger.debug('debug-level file only')", + " logger.info('info-level file only')", + " logger.warn('warn-level file only')", + " logger.error('error-level console and file')", + ' setTimeout(() => process.exit(0), 50)', + '})()', +].join('\n') const activeProcesses: LoggerServerProcess[] = [] const activeLogDirs: string[] = [] @@ -120,7 +129,57 @@ async function startDistLoggerProcess(env: NodeJS.ProcessEnv) { ) } +async function waitForFileContent(filePath: string, pattern: RegExp, timeoutMs = 5000): Promise { + const deadline = Date.now() + timeoutMs + let lastContent = '' + + while (Date.now() < deadline) { + const content = await fsp.readFile(filePath, 'utf8').catch(() => '') + if (content) { + lastContent = content + if (pattern.test(content)) return content + } + + await new Promise((resolve) => setTimeout(resolve, 120)) + } + + throw new Error(`Timed out waiting for ${pattern} in ${filePath}. Log: ${lastContent}`) +} + describe('debug log separation', () => { + it( + 'keeps stdout and stderr error-only while preserving debug file verbosity', + { timeout: DEFAULT_TEST_TIMEOUT_MS }, + async () => { + await withLogDir(async (logDir) => { + const debugLogPath = path.join(logDir, 'server-debug.jsonl') + const proc = await startServerProcess( + [process.execPath, getTSXCLI(), '-e', LOG_LEVEL_PROBE], + { + LOG_DEBUG_PATH: debugLogPath, + NODE_ENV: 'production', + }, + REPO_ROOT, + ) + activeProcesses.push(proc) + + const fileContent = await waitForFileContent(debugLogPath, /error-level console and file/) + const processOutput = readFileSync(proc.stderrLogPath, 'utf8') + + expect(processOutput).toContain('error-level console and file') + expect(processOutput).not.toContain('Resolved debug log path') + expect(processOutput).not.toContain('debug-level file only') + expect(processOutput).not.toContain('info-level file only') + expect(processOutput).not.toContain('warn-level file only') + + expect(fileContent).toContain('debug-level file only') + expect(fileContent).toContain('info-level file only') + expect(fileContent).toContain('warn-level file only') + expect(fileContent).toContain('error-level console and file') + }) + }, + ) + it( 'dist and source launches choose different mode-specific filenames', { timeout: DEFAULT_TEST_TIMEOUT_MS }, @@ -142,8 +201,10 @@ describe('debug log separation', () => { ) activeProcesses.push(devProc, distProc) - const devPath = await waitForResolvedPath(devProc) - const distPath = await waitForResolvedPath(distProc) + const devPath = path.join(logDir, 'server-debug.development.source-mode.jsonl') + const distPath = path.join(logDir, 'server-debug.production.dist-mode.jsonl') + await waitForFileContent(devPath, /Resolved debug log path/) + await waitForFileContent(distPath, /Resolved debug log path/) expect(devPath).toContain('server-debug.development.source-mode.jsonl') expect(distPath).toContain('server-debug.production.dist-mode.jsonl') @@ -173,8 +234,10 @@ describe('debug log separation', () => { ) activeProcesses.push(processA, processB) - const pathA = await waitForResolvedPath(processA) - const pathB = await waitForResolvedPath(processB) + const pathA = path.join(logDir, 'server-debug.development.concurrent-a.jsonl') + const pathB = path.join(logDir, 'server-debug.development.concurrent-b.jsonl') + await waitForFileContent(pathA, /Resolved debug log path/) + await waitForFileContent(pathB, /Resolved debug log path/) expect(pathA).toContain('server-debug.development.concurrent-a.jsonl') expect(pathB).toContain('server-debug.development.concurrent-b.jsonl') @@ -204,8 +267,10 @@ describe('debug log separation', () => { ) activeProcesses.push(procA, procB) - const pathA = await waitForResolvedPath(procA) - const pathB = await waitForResolvedPath(procB) + const pathA = path.join(logDir, 'server-debug.development.alpha.jsonl') + const pathB = path.join(logDir, 'server-debug.production.ci-run-beta.jsonl') + await waitForFileContent(pathA, /Resolved debug log path/) + await waitForFileContent(pathB, /Resolved debug log path/) expect(pathA).toContain('server-debug.development.alpha.jsonl') expect(pathB).toContain('server-debug.production.ci-run-beta.jsonl') }) @@ -227,10 +292,11 @@ describe('debug log separation', () => { ) activeProcesses.push(proc) - const resolvedPath = await waitForResolvedPath(proc) + const resolvedPath = path.join(logDir, 'server-debug.production.ci-run-1.jsonl') + await waitForFileContent(resolvedPath, /Resolved debug log path/) expect(resolvedPath).toContain('server-debug.production.ci-run-1.jsonl') - const startupLog = readFileSync(proc.stderrLogPath, 'utf8') + const startupLog = readFileSync(resolvedPath, 'utf8') const startupPayload = parseStartupLogPayload(startupLog) expect(startupPayload).not.toBeNull() expect(startupPayload).toMatchObject({