mirror of
https://github.com/misenhower/splatoon2.ink.git
synced 2026-09-28 04:06:52 -05:00
Make run log capture an explicit class
This commit is contained in:
@@ -1,24 +1,59 @@
|
||||
import { AsyncLocalStorage } from 'node:async_hooks';
|
||||
|
||||
// Capture application messages for one run without replacing the global console.
|
||||
// Associate messages with their run across awaits, without replacing the global console.
|
||||
const currentRun = new AsyncLocalStorage();
|
||||
const MAX_LINES = 200;
|
||||
const MAX_LINE_LENGTH = 500;
|
||||
const MAX_BYTES = 32_000;
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
export function createRunLog(secrets = []) {
|
||||
let snapshot = { lines: [], omitted: 0 };
|
||||
let context = {
|
||||
snapshot,
|
||||
bytes: 0,
|
||||
secrets: secrets.filter(value => typeof value === 'string' && value.length >= 6),
|
||||
};
|
||||
// The admin panel reads snapshot during the run and saves it with the final result.
|
||||
export class RunLog {
|
||||
constructor(secrets = []) {
|
||||
this.snapshot = { lines: [], omitted: 0 };
|
||||
this.bytes = 0;
|
||||
this.secrets = secrets.filter(value => typeof value === 'string' && value.length >= 6);
|
||||
}
|
||||
|
||||
return {
|
||||
snapshot,
|
||||
run: callback => currentRun.run(context, callback),
|
||||
};
|
||||
run(callback) {
|
||||
return currentRun.run(this, callback);
|
||||
}
|
||||
|
||||
append(level, message, fields) {
|
||||
let line = { at: Date.now(), level, text: this.formatMessage(message, fields) };
|
||||
this.snapshot.lines.push(line);
|
||||
this.bytes += this.lineSize(line);
|
||||
|
||||
while (this.snapshot.lines.length > MAX_LINES || this.bytes > MAX_BYTES) {
|
||||
let removed = this.snapshot.lines.shift();
|
||||
this.bytes -= this.lineSize(removed);
|
||||
this.snapshot.omitted++;
|
||||
}
|
||||
}
|
||||
|
||||
formatMessage(message, fields) {
|
||||
let text = message instanceof Error ? message.message : String(message);
|
||||
|
||||
// Keep readable progress messages; the full structured result has its own JSON view.
|
||||
if (fields?.updater)
|
||||
text = `[${fields.updater}] ${text}`;
|
||||
if (fields?.error)
|
||||
text += `: ${fields.error}`;
|
||||
if (fields?.attempt)
|
||||
text += ` (retry ${fields.attempt})`;
|
||||
|
||||
for (let secret of this.secrets)
|
||||
text = text.replaceAll(secret, '[redacted]');
|
||||
text = text.replace(/Bearer\s+[^\s,;]+/gi, 'Bearer [redacted]');
|
||||
|
||||
if (text.length > MAX_LINE_LENGTH)
|
||||
text = text.slice(0, MAX_LINE_LENGTH) + '…';
|
||||
return text;
|
||||
}
|
||||
|
||||
lineSize(line) {
|
||||
return encoder.encode(JSON.stringify(line)).length;
|
||||
}
|
||||
}
|
||||
|
||||
export function logMessage(level, message, fields) {
|
||||
@@ -30,30 +65,7 @@ export function logMessage(level, message, fields) {
|
||||
console[level](message, fields);
|
||||
}
|
||||
|
||||
let context = currentRun.getStore();
|
||||
if (!context)
|
||||
return;
|
||||
|
||||
let text = message instanceof Error ? message.message : String(message);
|
||||
// Keep readable progress messages; the full structured result has its own JSON view.
|
||||
if (fields?.updater)
|
||||
text = `[${fields.updater}] ${text}`;
|
||||
if (fields?.error)
|
||||
text += `: ${fields.error}`;
|
||||
if (fields?.attempt)
|
||||
text += ` (retry ${fields.attempt})`;
|
||||
for (let secret of context.secrets)
|
||||
text = text.replaceAll(secret, '[redacted]');
|
||||
text = text.replace(/Bearer\s+[^\s,;]+/gi, 'Bearer [redacted]');
|
||||
const { snapshot } = context;
|
||||
snapshot.lines.push({
|
||||
at: Date.now(),
|
||||
level,
|
||||
text: text.length > MAX_LINE_LENGTH ? text.slice(0, MAX_LINE_LENGTH) + '…' : text,
|
||||
});
|
||||
context.bytes += encoder.encode(JSON.stringify(snapshot.lines.at(-1))).length;
|
||||
while (snapshot.lines.length > MAX_LINES || context.bytes > MAX_BYTES) {
|
||||
context.bytes -= encoder.encode(JSON.stringify(snapshot.lines.shift())).length;
|
||||
snapshot.omitted++;
|
||||
}
|
||||
let run = currentRun.getStore();
|
||||
if (run)
|
||||
run.append(level, message, fields);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { test, mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createRunLog, logMessage } from '../../src/app/log.js';
|
||||
import { RunLog, logMessage } from '../../src/app/log.js';
|
||||
|
||||
test('bounds retained logs, redacts secrets, and preserves normal console output', async () => {
|
||||
const consoleLog = mock.method(console, 'info', () => {});
|
||||
try {
|
||||
const capture = createRunLog(['secret-value']);
|
||||
const capture = new RunLog(['secret-value']);
|
||||
await capture.run(async () => {
|
||||
for (let i = 0; i < 205; i++) logMessage('info', `line ${i}`);
|
||||
logMessage('info', 'secret-value Bearer sensitive-token');
|
||||
@@ -21,8 +21,8 @@ test('bounds retained logs, redacts secrets, and preserves normal console output
|
||||
test('concurrent run contexts do not capture each other or unrelated messages', async () => {
|
||||
mock.method(console, 'info', () => {});
|
||||
try {
|
||||
const a = createRunLog(),
|
||||
b = createRunLog();
|
||||
const a = new RunLog(),
|
||||
b = new RunLog();
|
||||
await Promise.all([
|
||||
a.run(async () => {
|
||||
await Promise.resolve();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createRunLog, logMessage } from '../../../src/app/log.js';
|
||||
import { RunLog, logMessage } from '../../../src/app/log.js';
|
||||
// One owner for the hourly update → social pipeline and authenticated manual runs.
|
||||
// The alarm targets :00:10; the cron watchdog repairs a missing alarm. Alarms can be late.
|
||||
import { DurableObject } from 'cloudflare:workers';
|
||||
@@ -127,7 +127,7 @@ export class Scheduler extends DurableObject {
|
||||
const secrets = Object.entries({ ...process.env, ...this.env })
|
||||
.filter(([key]) => /TOKEN|PASSWORD|SESSION|ACCOUNT_ID|SECRET/.test(key))
|
||||
.map(([, value]) => value);
|
||||
const capture = createRunLog(secrets);
|
||||
const capture = new RunLog(secrets);
|
||||
this.#activeRun = { mode, startedAt: Date.now(), logs: capture.snapshot };
|
||||
try {
|
||||
return await capture.run(async () => {
|
||||
|
||||
Reference in New Issue
Block a user