#!/usr/bin/env node import { spawnSync } from 'node:child_process'; import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; const PARITY_SCRIPT = 'scripts/check-pingora-canary-access-log-parity.mjs'; const failures = []; const tmpRoot = mkdtempSync( path.join(tmpdir(), 'genarrative-pingora-canary-log-parity-'), ); try { main(); } finally { rmSync(tmpRoot, { recursive: true, force: true }); } if (failures.length > 0) { console.error('[check:pingora-canary-access-log-parity] FAILED'); for (const failure of failures) { console.error(`- ${failure}`); } process.exit(1); } console.log('[check:pingora-canary-access-log-parity] OK'); function main() { assertScriptShape(); assertParitySucceeds(); assertRealpathParitySucceeds(); assertMissingPingoraRecordFails(); assertStatusMismatchFails(); assertRequiredPathFails(); assertRejectsRelativeLogPaths(); assertRejectsFilesystemRootLogPaths(); assertRejectsLogPathControlCharacters(); assertRejectsPrefixAndPathControlCharacters(); assertRejectsParsedLogPathControlCharacters(); assertRejectsInvalidSinceLines(); } function assertScriptShape() { const content = readFileSync(PARITY_SCRIPT, 'utf8'); assertIncludes( content, '该脚本只读比较 Nginx canary handoff access log 和 Pingora access log', 'canary access log parity 脚本 usage 必须说明只读边界。', ); assertIncludes( content, 'request_id', 'canary access log parity 必须按 request_id 对照。', ); assertIncludes( content, 'realpath', 'canary access log parity 必须支持真实路径 canary 对账模式。', ); if ( content.includes('writeFile') || content.includes('rmSync(') || content.includes('nginx -s reload') ) { failures.push('canary access log parity 脚本不应写文件、删除文件或 reload Nginx。'); } } function assertParitySucceeds() { const fixture = prepareFixture('ok'); writeLogs(fixture, [ nginxLine('rid-health', 'GET', '/__genarrative_pingora_canary/healthz', 200), nginxLine( 'rid-api', 'GET', '/__genarrative_pingora_canary/api/assets/history', 200, ), ], [ pingoraLine('rid-health', 'GET', '/__genarrative_pingora/healthz', 200, { route: 'shadow_probe', }), pingoraLine('rid-api', 'GET', '/api/assets/history', 200, { route: 'api_proxy', proxyTarget: 'api-server', }), ]); const result = runParity(fixture, [ '--path', '/__genarrative_pingora_canary/healthz', '--path', '/__genarrative_pingora_canary/api/assets/history', '--json', ]); assertStatus(result, 0, '日志对照完整时必须通过。'); if (result.status !== 0) { return; } const payload = parseJson(result.stdout, '日志对照 JSON 输出'); assertEqual(payload.summary.matchedCount, 2, '应匹配两条 canary 请求。'); assertEqual(payload.summary.missingCount, 0, '不应缺少 Pingora 对应日志。'); } function assertRealpathParitySucceeds() { const fixture = prepareFixture('realpath-ok'); writeLogs(fixture, [ nginxLine( 'rid-real-health', 'GET', '/__genarrative_pingora_realpath_canary/healthz', 200, ), nginxLine('rid-real-api', 'GET', '/api/assets/history', 200), nginxLine('rid-real-asset', 'GET', '/assets/app.js', 200), ], [ pingoraLine('rid-real-health', 'GET', '/__genarrative_pingora/healthz', 200, { route: 'shadow_probe', }), pingoraLine('rid-real-api', 'GET', '/api/assets/history', 200, { route: 'api_proxy', proxyTarget: 'api-server', }), pingoraLine('rid-real-asset', 'GET', '/assets/app.js', 200, { route: 'static', }), ]); const result = runParity(fixture, [ '--realpath', '--path', '/__genarrative_pingora_realpath_canary/healthz', '--path', '/api/assets/history', '--path', '/assets/app.js', '--json', ]); assertStatus(result, 0, '真实路径日志对照完整时必须通过。'); if (result.status !== 0) { return; } const payload = parseJson(result.stdout, '真实路径日志对照 JSON 输出'); assertEqual(payload.mode, 'realpath', '真实路径对账 JSON 必须标记 realpath 模式。'); assertEqual(payload.summary.matchedCount, 3, '应匹配三条真实路径 canary 请求。'); assertEqual(payload.summary.missingCount, 0, '真实路径对账不应缺少 Pingora 对应日志。'); } function assertMissingPingoraRecordFails() { const fixture = prepareFixture('missing-pingora'); writeLogs(fixture, [ nginxLine('rid-missing', 'GET', '/__genarrative_pingora_canary/v1/identity', 200), ], []); const result = runParity(fixture); assertStatus(result, 1, '缺少同 request_id Pingora 日志时必须失败。'); assertIncludes( `${result.stdout}\n${result.stderr}`, '缺少对应 Pingora access log', '缺少 Pingora 日志时必须给出明确错误。', ); } function assertStatusMismatchFails() { const fixture = prepareFixture('status-mismatch'); writeLogs(fixture, [ nginxLine('rid-status', 'GET', '/__genarrative_pingora_canary/api/test', 200), ], [ pingoraLine('rid-status', 'GET', '/api/test', 503), ]); const result = runParity(fixture); assertStatus(result, 1, 'Nginx/Pingora 状态码不一致时必须失败。'); assertIncludes( `${result.stdout}\n${result.stderr}`, 'status 200 != 503', '状态码不一致必须给出明确错误。', ); } function assertRequiredPathFails() { const fixture = prepareFixture('missing-required-path'); writeLogs(fixture, [ nginxLine('rid-health', 'GET', '/__genarrative_pingora_canary/healthz', 200), ], [ pingoraLine('rid-health', 'GET', '/__genarrative_pingora/healthz', 200), ]); const result = runParity(fixture, [ '--path', '/__genarrative_pingora_canary/api/assets/history', ]); assertStatus(result, 1, '必需路径未出现在 Nginx canary 日志时必须失败。'); assertIncludes( `${result.stdout}\n${result.stderr}`, 'Nginx canary 日志缺少必需路径', '必需路径缺失必须给出明确错误。', ); } function assertRejectsRelativeLogPaths() { const fixture = prepareFixture('relative-path'); const result = spawnSync( 'node', [ PARITY_SCRIPT, '--nginx-log-file', 'nginx.log', '--pingora-log-file', fixture.pingoraLogFile, ], { cwd: process.cwd(), encoding: 'utf8', }, ); if ((result.status ?? 0) === 0) { failures.push('日志路径为相对路径时必须失败。'); } assertIncludes( `${result.stdout}\n${result.stderr}`, '--nginx-log-file 必须是绝对路径', '相对 Nginx 日志路径必须给出明确错误。', ); } function assertRejectsFilesystemRootLogPaths() { const fixture = prepareFixture('filesystem-root-path'); const result = spawnSync( 'node', [ PARITY_SCRIPT, '--nginx-log-file', '/', '--pingora-log-file', fixture.pingoraLogFile, ], { cwd: process.cwd(), encoding: 'utf8', }, ); if ((result.status ?? 0) === 0) { failures.push('日志路径指向文件系统根目录时必须失败。'); } assertIncludes( `${result.stdout}\n${result.stderr}`, '--nginx-log-file 不能是文件系统根目录', '文件系统根目录 Nginx 日志路径必须给出明确错误。', ); } function assertRejectsLogPathControlCharacters() { const fixture = prepareFixture('log-path-control-character'); const result = spawnSync( 'node', [ PARITY_SCRIPT, '--nginx-log-file', `${fixture.nginxLogFile}\nspoofed`, '--pingora-log-file', fixture.pingoraLogFile, ], { cwd: process.cwd(), encoding: 'utf8', }, ); if ((result.status ?? 0) === 0) { failures.push('日志路径包含换行时必须失败。'); } assertIncludes( `${result.stdout}\n${result.stderr}`, '--nginx-log-file 不能包含换行或 NUL 字符', '带控制字符的 Nginx 日志路径必须给出明确错误。', ); } function assertRejectsPrefixAndPathControlCharacters() { const fixture = prepareFixture('prefix-path-control-character'); writeLogs(fixture, [ nginxLine('rid-health', 'GET', '/__genarrative_pingora_canary/healthz', 200), ], [ pingoraLine('rid-health', 'GET', '/__genarrative_pingora/healthz', 200), ]); const prefixResult = runParity(fixture, [ '--prefix', '/__genarrative_pingora_canary\nspoofed', ]); if ((prefixResult.status ?? 0) === 0) { failures.push('canary prefix 包含换行时必须失败。'); } assertIncludes( `${prefixResult.stdout}\n${prefixResult.stderr}`, '--prefix 不能包含换行或 NUL 字符', '带控制字符的 canary prefix 必须给出明确错误。', ); const pathResult = runParity(fixture, [ '--path', '/__genarrative_pingora_canary/healthz\nspoofed', ]); if ((pathResult.status ?? 0) === 0) { failures.push('必需 canary path 包含换行时必须失败。'); } assertIncludes( `${pathResult.stdout}\n${pathResult.stderr}`, '--path 不能包含换行或 NUL 字符', '带控制字符的必需路径必须给出明确错误。', ); } function assertRejectsParsedLogPathControlCharacters() { const fixture = prepareFixture('parsed-log-path-control-character'); writeLogs(fixture, [ nginxLine('rid-health', 'GET', '/__genarrative_pingora_canary/healthz', 200), ], [ pingoraLine('rid-health', 'GET', '/__genarrative_pingora/healthz%0Aspoofed', 200), ]); const result = runParity(fixture); if ((result.status ?? 0) === 0) { failures.push('Pingora 日志 path 解析后包含换行时必须失败。'); } assertIncludes( `${result.stdout}\n${result.stderr}`, 'Pingora access log 第 1 行 path 不能包含换行或 NUL 字符', '带控制字符的 Pingora 日志 path 必须给出明确错误。', ); } function assertRejectsInvalidSinceLines() { const fixture = prepareFixture('invalid-since-lines'); writeLogs(fixture, [ nginxLine('rid-health', 'GET', '/__genarrative_pingora_canary/healthz', 200), ], [ pingoraLine('rid-health', 'GET', '/__genarrative_pingora/healthz', 200), ]); const cliResult = runParity(fixture, ['--since-lines', '0']); if ((cliResult.status ?? 0) === 0) { failures.push('非正数 --since-lines 必须失败。'); } assertIncludes( `${cliResult.stdout}\n${cliResult.stderr}`, '--since-lines 必须是正整数', '非法 --since-lines 必须给出明确错误。', ); const controlResult = runParity(fixture, ['--since-lines', '10\nspoofed']); if ((controlResult.status ?? 0) === 0) { failures.push('带控制字符的 --since-lines 必须失败。'); } assertIncludes( `${controlResult.stdout}\n${controlResult.stderr}`, '--since-lines 不能包含换行或 NUL 字符', '带控制字符的 --since-lines 必须给出明确错误。', ); const envResult = spawnSync( 'node', [ PARITY_SCRIPT, '--nginx-log-file', fixture.nginxLogFile, '--pingora-log-file', fixture.pingoraLogFile, ], { cwd: process.cwd(), encoding: 'utf8', env: { ...process.env, GENARRATIVE_PINGORA_CANARY_ACCESS_LOG_SINCE_LINES: 'abc', }, }, ); if ((envResult.status ?? 0) === 0) { failures.push('非法 env since-lines 必须失败。'); } assertIncludes( `${envResult.stdout}\n${envResult.stderr}`, 'GENARRATIVE_PINGORA_CANARY_ACCESS_LOG_SINCE_LINES 必须是正整数', '非法 env since-lines 必须给出明确错误。', ); } function prepareFixture(name) { const root = path.join(tmpRoot, name); mkdirSync(root, { recursive: true }); return { root, nginxLogFile: path.join(root, 'nginx.access.log'), pingoraLogFile: path.join(root, 'pingora.access.log'), }; } function writeLogs(fixture, nginxLines, pingoraLines) { writeFileSync(fixture.nginxLogFile, `${nginxLines.join('\n')}\n`, 'utf8'); writeFileSync(fixture.pingoraLogFile, `${pingoraLines.join('\n')}\n`, 'utf8'); } function nginxLine(requestId, method, uri, status) { return [ '127.0.0.1 - - [16/Jun/2026:02:00:00 +0800]', `"${method} ${uri} HTTP/1.1"`, `${status} 12 "-" "agent"`, 'request_time=0.001 upstream_connect_time=0.000', 'upstream_header_time=0.001 upstream_response_time=0.001', `upstream_status=${status} request_id=${requestId}`, ].join(' '); } function pingoraLine( requestId, method, requestPath, status, options = {}, ) { return [ `request_id=${requestId}`, `method=${method}`, `path=${requestPath}`, `uri=${requestPath}`, 'host=example.com', 'client_ip=127.0.0.1', `status=${status}`, `route=${options.route || 'api_proxy'}`, `proxy_target=${options.proxyTarget || '-'}`, 'upstream=127.0.0.1:8082', 'content_length=-', 'body_bytes_seen=0', 'protection_class=api', 'protection_client=127.0.0.1', 'elapsed_ms=2', 'error=-', ].join('\t'); } function runParity(fixture, args = []) { return spawnSync( 'node', [ PARITY_SCRIPT, '--nginx-log-file', fixture.nginxLogFile, '--pingora-log-file', fixture.pingoraLogFile, ...args, ], { cwd: process.cwd(), encoding: 'utf8', }, ); } function parseJson(text, label) { try { return JSON.parse(text); } catch (error) { failures.push(`${label} 不是合法 JSON: ${error.message}`); return {}; } } function assertStatus(result, expected, reason) { if ((result.status ?? 0) !== expected) { failures.push( `${reason} 实际退出码 ${result.status}。\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`, ); } } function assertEqual(actual, expected, reason) { if (actual !== expected) { failures.push(`${reason} 实际 ${actual},预期 ${expected}。`); } } function assertIncludes(value, expected, reason) { const haystack = Array.isArray(value) ? value.join('\n') : String(value); if (!haystack.includes(expected)) { failures.push(`${reason} 缺少: ${expected}`); } }