feat(tasks): OpenWorkflow-backed TaskEngine, task registry, outbox relay and op-ledger fencing (W1-4)
- 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]>
This commit is contained in:
1 parent
ec023a8405
commit
1262340856
22 files changed
+1818
-8
No files matched your search
@@ -0,0 +1,64 @@
|
||||
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 }),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user