관리자 서버 진단에 lease 상태와 영속 장애 이력 추가
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
/** 관리자 장애 기록에서도 연결 URL과 인증값을 보존하지 않는다. */
|
||||
export const sanitizeRuntimeErrorText = (text: string): string =>
|
||||
text
|
||||
.replace(/\b(?:https?|postgres(?:ql)?|rediss?):\/\/[^\s"'<>]+/gi, '[REDACTED_URL]')
|
||||
.replace(/\bBearer\s+[^\s"',;]+/gi, 'Bearer [REDACTED]')
|
||||
.replace(
|
||||
/((?:password|passwd|token|secret|authorization|cookie|api[_-]?key)["']?\s*[:=]\s*)(?:"[^"\n]*"|'[^'\n]*'|[^\s,;]+)/gi,
|
||||
'$1[REDACTED]'
|
||||
)
|
||||
.slice(0, 2000);
|
||||
|
||||
export const describeRuntimeError = (error: unknown): { code: string; message: string; frames: string[] } => ({
|
||||
code: error instanceof Error ? error.name.slice(0, 100) : 'RuntimeError',
|
||||
message: sanitizeRuntimeErrorText(error instanceof Error ? error.message : String(error)),
|
||||
frames:
|
||||
error instanceof Error
|
||||
? (error.stack ?? '')
|
||||
.split('\n')
|
||||
.filter((line) => /^\s*at\s/.test(line))
|
||||
.slice(0, 8)
|
||||
.map(sanitizeRuntimeErrorText)
|
||||
: [],
|
||||
});
|
||||
|
||||
export interface ProfileRuntimeDiagnostics {
|
||||
profileName: string;
|
||||
checkedAt: string;
|
||||
database: 'AVAILABLE' | 'UNAVAILABLE' | 'UNINITIALIZED';
|
||||
processObservation: 'AVAILABLE' | 'UNAVAILABLE';
|
||||
processes: Array<{ name: string; status: string; restartCount: number; exitCode: number | null }>;
|
||||
lease: {
|
||||
ownerId: string;
|
||||
fencingEpoch: string;
|
||||
heartbeatAt: string;
|
||||
leaseUntil: string;
|
||||
heartbeatAgeMs: number;
|
||||
valid: boolean;
|
||||
clockReady: boolean;
|
||||
} | null;
|
||||
clock: {
|
||||
phase: string;
|
||||
revision: string;
|
||||
tick: string | null;
|
||||
lastTurnTick: string | null;
|
||||
year: number;
|
||||
month: number;
|
||||
wallAnchor: string | null;
|
||||
recoveryStartWallAt: string | null;
|
||||
recoveryEndTick: string | null;
|
||||
} | null;
|
||||
}
|
||||
@@ -28,6 +28,7 @@ export * from './auth/accountIconProjection.js';
|
||||
export * from './logging/formatLegacyLogHtml.js';
|
||||
export * from './legacyArchive/ArchivedGeneralSnapshot.js';
|
||||
export * from './gateway/profileStatus.js';
|
||||
export * from './gateway/runtimeDiagnostics.js';
|
||||
export * from './game/accessPenalty.js';
|
||||
export * from './http/trpcTransport.js';
|
||||
export * from './webPush/types.js';
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describeRuntimeError } from '../src/gateway/runtimeDiagnostics.js';
|
||||
|
||||
describe('runtime failure records', () => {
|
||||
it('keeps the cause and frames while removing connection and authentication values', () => {
|
||||
const error = new Error(
|
||||
'database failed postgresql://admin:private@host/db password="hidden value" token=abc Bearer xyz'
|
||||
);
|
||||
error.stack = `${error.message}\n at flush (/srv/app/flush.ts:42:7)`;
|
||||
const record = describeRuntimeError(error);
|
||||
expect(record.code).toBe('Error');
|
||||
expect(record.message).toContain('database failed');
|
||||
for (const secret of ['private', 'hidden value', 'abc', 'xyz'])
|
||||
expect(JSON.stringify(record)).not.toContain(secret);
|
||||
expect(record.frames).toEqual([' at flush (/srv/app/flush.ts:42:7)']);
|
||||
});
|
||||
it('bounds untrusted messages and stack depth', () => {
|
||||
const error = new Error('x'.repeat(4000));
|
||||
error.stack = Array.from({ length: 30 }, () => ' at run (/app/run.ts:1:1)').join('\n');
|
||||
expect(describeRuntimeError(error).message).toHaveLength(2000);
|
||||
expect(describeRuntimeError(error).frames).toHaveLength(8);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user