Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions server/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve debug-stream failure warnings on console

Setting consoleLogger to error here suppresses all consoleLogger.warn(...) calls from attachDebugStreamWarnings, including runtime warning/error events emitted by rotating-file-stream. In that failure mode the debug log stream can degrade or stop, but operators get no console signal because those warnings are filtered out; this is a regression from the previous behavior where these diagnostics were visible.

Useful? React with 👍 / 👎.

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) {
Expand All @@ -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()
Expand Down
84 changes: 75 additions & 9 deletions test/integration/server/logger.separation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'
import {
startServerProcess,
stopProcess,
waitForResolvedPath,
type LoggerServerProcess,
} from './logger.separation.harness.js'

Expand All @@ -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[] = []
Expand Down Expand Up @@ -120,7 +129,57 @@ async function startDistLoggerProcess(env: NodeJS.ProcessEnv) {
)
}

async function waitForFileContent(filePath: string, pattern: RegExp, timeoutMs = 5000): Promise<string> {
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<void>((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 },
Expand All @@ -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')
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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')
})
Expand All @@ -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({
Expand Down
Loading