071faa482c
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口 完成项目 TypeScript/Prettier 与 Rust 全量格式化 修复 Pingora expected executable 门禁的空白敏感误报 同步开发运维文档与 AGC skill pack 格式化忽略规则
572 lines
15 KiB
JavaScript
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}`);
|
|
}
|
|
}
|