diff --git a/packages/kap-server/src/services/pinoLoggerService.ts b/packages/kap-server/src/services/pinoLoggerService.ts index 65aaa89cb07..39d9a28ae6d 100644 --- a/packages/kap-server/src/services/pinoLoggerService.ts +++ b/packages/kap-server/src/services/pinoLoggerService.ts @@ -1,4 +1,4 @@ -import { pino, type Logger, type LoggerOptions } from 'pino'; +import { pino, type DestinationStream, type Logger, type LoggerOptions } from 'pino'; export type ServerLogger = Logger; @@ -6,6 +6,7 @@ export type ServerLogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'tr export interface CreateLoggerOptions { level: ServerLogLevel; + stream?: DestinationStream; } export function createServerLogger(opts: CreateLoggerOptions): ServerLogger { @@ -14,5 +15,5 @@ export function createServerLogger(opts: CreateLoggerOptions): ServerLogger { base: { name: 'kimi-server-v2' }, timestamp: pino.stdTimeFunctions.isoTime, }; - return pino(base); + return opts.stream === undefined ? pino(base) : pino(base, opts.stream); } diff --git a/packages/kap-server/src/start.ts b/packages/kap-server/src/start.ts index a1c3cb0e334..e35bf625c76 100644 --- a/packages/kap-server/src/start.ts +++ b/packages/kap-server/src/start.ts @@ -171,11 +171,10 @@ export async function startServer(opts: ServerStartOptions): Promise { - logger.fatal( + logger.error( { err: err instanceof Error ? err : new Error(String(err)) }, 'uncaughtException', ); - process.exit(1); }; const authFailureLimiter = exposureClass === 'loopback' ? undefined : createAuthFailureLimiter({ logger }); diff --git a/packages/kap-server/test/boot.test.ts b/packages/kap-server/test/boot.test.ts index 4f66a7895cd..f75331ec5ef 100644 --- a/packages/kap-server/test/boot.test.ts +++ b/packages/kap-server/test/boot.test.ts @@ -2,6 +2,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { createServer, type Server } from 'node:net'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; +import { Writable } from 'node:stream'; import { pino } from 'pino'; import { afterEach, describe, expect, it, vi } from 'vitest'; @@ -233,26 +234,56 @@ describe('server-v2 boot', () => { expect(await listLiveServerInstances(home)).toEqual([]); }); - it('installs process-level rejection handlers while running and removes them on close', async () => { + it('logs process-level exceptions without exiting and removes the handlers on close', async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-')); - const rejectionBefore = process.listenerCount('unhandledRejection'); - const exceptionBefore = process.listenerCount('uncaughtException'); + const lines: string[] = []; + const stream = new Writable({ + write(chunk, _encoding, callback) { + lines.push(String(chunk)); + callback(); + }, + }); + const rejectionBefore = process.listeners('unhandledRejection'); + const exceptionBefore = process.listeners('uncaughtException'); server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, - logLevel: 'silent', + logger: pino({ level: 'error' }, stream), }); - expect(process.listenerCount('unhandledRejection')).toBe(rejectionBefore + 1); - expect(process.listenerCount('uncaughtException')).toBe(exceptionBefore + 1); + expect(process.listenerCount('unhandledRejection')).toBe(rejectionBefore.length + 1); + expect(process.listenerCount('uncaughtException')).toBe(exceptionBefore.length + 1); + + const onUncaughtException = process + .listeners('uncaughtException') + .find((listener) => !exceptionBefore.includes(listener)) as + | ((error: Error) => void) + | undefined; + const onUnhandledRejection = process + .listeners('unhandledRejection') + .find((listener) => !rejectionBefore.includes(listener)) as + | ((reason: unknown) => void) + | undefined; + expect(onUncaughtException).toBeDefined(); + expect(onUnhandledRejection).toBeDefined(); + + onUncaughtException?.(new Error('synthetic uncaught')); + onUnhandledRejection?.(new Error('synthetic rejection')); + + const output = lines.join(''); + expect(output).toContain('"msg":"uncaughtException"'); + expect(output).toContain('"msg":"unhandledRejection"'); + + const healthz = await fetch(`http://127.0.0.1:${server.port}/api/v1/healthz`); + expect(healthz.status).toBe(200); await server.close(); server = undefined; - expect(process.listenerCount('unhandledRejection')).toBe(rejectionBefore); - expect(process.listenerCount('uncaughtException')).toBe(exceptionBefore); + expect(process.listenerCount('unhandledRejection')).toBe(rejectionBefore.length); + expect(process.listenerCount('uncaughtException')).toBe(exceptionBefore.length); }); it('does not leave process handlers installed when startup fails', async () => {