- TaskEngine port + OpenWorkflowEngine ([email protected].1, schema openworkflow): enqueue/signal/cancel/get/list/startWorker/stop - defineTask registry with zod input schema; 9 stub task definitions (§3.1-8) - TaskContext: step.run/sleep/waitForFact, redacting logger, traceId, attemptToken, abortSignal driven by PG cancel flag - signal persists fact to tasks_fact first; waitForFact checks DB before waiting - OutboxRelay: FOR UPDATE SKIP LOCKED, idempotencyKey outbox:<id> - withAttemptToken fencing on tasks_op_ledger (own migration, merged in W2) - tests: kill -9 crash recovery (child worker), cancel, idempotency, concurrent relays, early fact, lease-loss fencing Co-Authored-By: Claude Fable 5.1 <[email protected]>
65 lines
2.3 KiB
TypeScript
65 lines
2.3 KiB
TypeScript
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
|
|
export interface LogRecord {
|
|
level: LogLevel;
|
|
msg: string;
|
|
at: string;
|
|
fields: Record<string, unknown>;
|
|
}
|
|
|
|
export type LogSink = (record: LogRecord) => void;
|
|
|
|
export interface TaskLogger {
|
|
debug(msg: string, fields?: Record<string, unknown>): void;
|
|
info(msg: string, fields?: Record<string, unknown>): void;
|
|
warn(msg: string, fields?: Record<string, unknown>): void;
|
|
error(msg: string, fields?: Record<string, unknown>): void;
|
|
child(fields: Record<string, unknown>): TaskLogger;
|
|
}
|
|
|
|
const SENSITIVE_KEY = /pass(word|key)?|secret|token|cookie|authorization|api[-_]?key|credential|session/i;
|
|
// URL 查询参数里的凭证(passkey、token、apikey、sign 等)
|
|
const SENSITIVE_QUERY = /([?&](?:passkey|token|apikey|api_key|key|sign|secret|auth)=)[^&#\s]+/gi;
|
|
const REDACTED = "***";
|
|
|
|
/** 深拷贝并脱敏:敏感键名整体替换,字符串里的凭证型 URL 参数替换 */
|
|
export function redact(value: unknown, depth = 0): unknown {
|
|
if (depth > 8) return "[depth]";
|
|
if (typeof value === "string") return value.replace(SENSITIVE_QUERY, `$1${REDACTED}`);
|
|
if (Array.isArray(value)) return value.map((v) => redact(v, depth + 1));
|
|
if (value instanceof Error) return { name: value.name, message: redact(value.message, depth + 1) };
|
|
if (value && typeof value === "object") {
|
|
const out: Record<string, unknown> = {};
|
|
for (const [k, v] of Object.entries(value)) out[k] = SENSITIVE_KEY.test(k) ? REDACTED : redact(v, depth + 1);
|
|
return out;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export const consoleSink: LogSink = (r) => {
|
|
const line = JSON.stringify(r);
|
|
if (r.level === "error" || r.level === "warn") console.error(line);
|
|
else console.log(line);
|
|
};
|
|
|
|
export const silentSink: LogSink = () => {};
|
|
|
|
export function createLogger(sink: LogSink = consoleSink, base: Record<string, unknown> = {}): TaskLogger {
|
|
const emit =
|
|
(level: LogLevel) =>
|
|
(msg: string, fields: Record<string, unknown> = {}) =>
|
|
sink({
|
|
level,
|
|
msg: redact(msg) as string,
|
|
at: new Date().toISOString(),
|
|
fields: redact({ ...base, ...fields }) as Record<string, unknown>,
|
|
});
|
|
return {
|
|
debug: emit("debug"),
|
|
info: emit("info"),
|
|
warn: emit("warn"),
|
|
error: emit("error"),
|
|
child: (fields) => createLogger(sink, { ...base, ...fields }),
|
|
};
|
|
}
|