From e3ce4032c6b44f7425f80464314a5ecd0e53eb3c Mon Sep 17 00:00:00 2001 From: John Date: Thu, 2 Jul 2026 08:09:02 +0800 Subject: [PATCH] feat: write runtime slo daily reports --- .../portal/scripts/runtime-slo-report.mjs | 88 +++++++++++++++++++ scripts/runtime-slo-report.mjs | 88 +++++++++++++++++++ 2 files changed, 176 insertions(+) diff --git a/.runtime/portal/scripts/runtime-slo-report.mjs b/.runtime/portal/scripts/runtime-slo-report.mjs index 88ec96f..77309cf 100755 --- a/.runtime/portal/scripts/runtime-slo-report.mjs +++ b/.runtime/portal/scripts/runtime-slo-report.mjs @@ -87,6 +87,30 @@ function uniquePaths(paths) { return result; } +function parseArgs(argv) { + const args = { + writeReport: false, + reportDir: process.env.MEMIND_RUNTIME_REPORT_DIR || '', + }; + for (let i = 0; i < argv.length; i += 1) { + const item = argv[i]; + if (item === '--write-report') args.writeReport = true; + else if (item === '--report-dir') args.reportDir = String(argv[++i] ?? args.reportDir); + else if (item === '--help' || item === '-h') args.help = true; + } + return args; +} + +function printHelp() { + console.log([ + 'Usage:', + ' node scripts/runtime-slo-report.mjs [--write-report] [--report-dir ]', + '', + 'Default behavior is read-only and writes nothing.', + '--write-report writes JSON and Markdown snapshots to an operations report directory.', + ].join('\n')); +} + function tableCountQueries() { return [ ['h5_users', 'users'], @@ -178,6 +202,11 @@ function summarizeRuntime(runtimeJson) { const envFile = process.env.MEMIND_ENV_FILE || path.join(process.cwd(), '.env'); const appRoot = process.env.MEMIND_APP_ROOT || path.dirname(path.resolve(envFile)); loadEnvFile(envFile); +const args = parseArgs(process.argv.slice(2)); +if (args.help) { + printHelp(); + process.exit(0); +} const publicBase = (process.env.H5_PUBLIC_BASE_URL || 'https://mm.tkmind.cn').replace(/\/$/, ''); const redisUrl = process.env.MEMIND_RUNTIME_REDIS_URL || 'redis://127.0.0.1:6379/0'; @@ -244,8 +273,67 @@ const report = { database: false, mindSpace: false, redis: false, + report: Boolean(args.writeReport), }, }; +function markdownReport(payload) { + const workers = payload.runtime?.workers ?? []; + const lines = [ + `# Memind Runtime SLO Report`, + '', + `- checkedAt: ${payload.checkedAt}`, + `- ok: ${payload.ok}`, + `- failures: ${payload.failures.length ? payload.failures.join(', ') : 'none'}`, + `- publicBase: ${payload.publicBase}`, + `- appRoot: ${payload.appRoot}`, + '', + '## Workers', + '', + '| worker | healthy | active | errors | first-token 5m p50/p95 | first-token 1h p50/p95 | metricsFresh | score |', + '| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |', + ]; + for (const worker of workers) { + lines.push([ + worker.id, + worker.healthy, + worker.activeStreams, + worker.streamErrorCount, + `${worker.firstToken5m?.p50Ms ?? 0}/${worker.firstToken5m?.p95Ms ?? 0}`, + `${worker.firstToken1h?.p50Ms ?? 0}/${worker.firstToken1h?.p95Ms ?? 0}`, + worker.metricsFresh, + worker.score, + ].join(' | ').replace(/^/, '| ').replace(/$/, ' |')); + } + lines.push( + '', + '## Tool Queue', + '', + '```json', + JSON.stringify(payload.runtime?.toolQueue ?? null, null, 2), + '```', + '', + '## Writes', + '', + '```json', + JSON.stringify(payload.writes, null, 2), + '```', + '', + ); + return lines.join('\n'); +} + +if (args.writeReport) { + const reportDir = path.resolve(args.reportDir || path.join(appRoot, 'reports', 'runtime-slo')); + fs.mkdirSync(reportDir, { recursive: true }); + const stamp = new Date().toISOString().replace(/[:.]/g, '-'); + report.reportFiles = { + json: path.join(reportDir, `${stamp}.json`), + markdown: path.join(reportDir, `${stamp}.md`), + }; + fs.writeFileSync(report.reportFiles.json, `${JSON.stringify(report, null, 2)}\n`); + fs.writeFileSync(report.reportFiles.markdown, markdownReport(report)); +} + console.log(JSON.stringify(report, null, 2)); process.exit(report.ok ? 0 : 1); diff --git a/scripts/runtime-slo-report.mjs b/scripts/runtime-slo-report.mjs index 88ec96f..77309cf 100755 --- a/scripts/runtime-slo-report.mjs +++ b/scripts/runtime-slo-report.mjs @@ -87,6 +87,30 @@ function uniquePaths(paths) { return result; } +function parseArgs(argv) { + const args = { + writeReport: false, + reportDir: process.env.MEMIND_RUNTIME_REPORT_DIR || '', + }; + for (let i = 0; i < argv.length; i += 1) { + const item = argv[i]; + if (item === '--write-report') args.writeReport = true; + else if (item === '--report-dir') args.reportDir = String(argv[++i] ?? args.reportDir); + else if (item === '--help' || item === '-h') args.help = true; + } + return args; +} + +function printHelp() { + console.log([ + 'Usage:', + ' node scripts/runtime-slo-report.mjs [--write-report] [--report-dir ]', + '', + 'Default behavior is read-only and writes nothing.', + '--write-report writes JSON and Markdown snapshots to an operations report directory.', + ].join('\n')); +} + function tableCountQueries() { return [ ['h5_users', 'users'], @@ -178,6 +202,11 @@ function summarizeRuntime(runtimeJson) { const envFile = process.env.MEMIND_ENV_FILE || path.join(process.cwd(), '.env'); const appRoot = process.env.MEMIND_APP_ROOT || path.dirname(path.resolve(envFile)); loadEnvFile(envFile); +const args = parseArgs(process.argv.slice(2)); +if (args.help) { + printHelp(); + process.exit(0); +} const publicBase = (process.env.H5_PUBLIC_BASE_URL || 'https://mm.tkmind.cn').replace(/\/$/, ''); const redisUrl = process.env.MEMIND_RUNTIME_REDIS_URL || 'redis://127.0.0.1:6379/0'; @@ -244,8 +273,67 @@ const report = { database: false, mindSpace: false, redis: false, + report: Boolean(args.writeReport), }, }; +function markdownReport(payload) { + const workers = payload.runtime?.workers ?? []; + const lines = [ + `# Memind Runtime SLO Report`, + '', + `- checkedAt: ${payload.checkedAt}`, + `- ok: ${payload.ok}`, + `- failures: ${payload.failures.length ? payload.failures.join(', ') : 'none'}`, + `- publicBase: ${payload.publicBase}`, + `- appRoot: ${payload.appRoot}`, + '', + '## Workers', + '', + '| worker | healthy | active | errors | first-token 5m p50/p95 | first-token 1h p50/p95 | metricsFresh | score |', + '| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |', + ]; + for (const worker of workers) { + lines.push([ + worker.id, + worker.healthy, + worker.activeStreams, + worker.streamErrorCount, + `${worker.firstToken5m?.p50Ms ?? 0}/${worker.firstToken5m?.p95Ms ?? 0}`, + `${worker.firstToken1h?.p50Ms ?? 0}/${worker.firstToken1h?.p95Ms ?? 0}`, + worker.metricsFresh, + worker.score, + ].join(' | ').replace(/^/, '| ').replace(/$/, ' |')); + } + lines.push( + '', + '## Tool Queue', + '', + '```json', + JSON.stringify(payload.runtime?.toolQueue ?? null, null, 2), + '```', + '', + '## Writes', + '', + '```json', + JSON.stringify(payload.writes, null, 2), + '```', + '', + ); + return lines.join('\n'); +} + +if (args.writeReport) { + const reportDir = path.resolve(args.reportDir || path.join(appRoot, 'reports', 'runtime-slo')); + fs.mkdirSync(reportDir, { recursive: true }); + const stamp = new Date().toISOString().replace(/[:.]/g, '-'); + report.reportFiles = { + json: path.join(reportDir, `${stamp}.json`), + markdown: path.join(reportDir, `${stamp}.md`), + }; + fs.writeFileSync(report.reportFiles.json, `${JSON.stringify(report, null, 2)}\n`); + fs.writeFileSync(report.reportFiles.markdown, markdownReport(report)); +} + console.log(JSON.stringify(report, null, 2)); process.exit(report.ok ? 0 : 1);