Files
Genarrative/scripts/check-pingora-canary-access-log-parity-check.mjs
kdletters 071faa482c 统一 Rust 与 TypeScript 格式化门禁
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口

完成项目 TypeScript/Prettier 与 Rust 全量格式化

修复 Pingora expected executable 门禁的空白敏感误报

同步开发运维文档与 AGC skill pack 格式化忽略规则
2026-09-01 16:28:34 +08:00

572 lines
15 KiB
JavaScript

#!/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}`);
}
}