-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlogging.ts
More file actions
113 lines (102 loc) · 2.95 KB
/
logging.ts
File metadata and controls
113 lines (102 loc) · 2.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import { AsyncLocalStorage } from "node:async_hooks";
import { serializeConsoleArgumentsToString } from "./logging/serialization.ts";
export interface Log {
timestamp: number;
type: "stdout" | "stderr";
text: string;
}
interface LogContext {
logs: Log[] | undefined;
}
const originalConsoleMethods = { ...console };
const asyncLocalStorage = new AsyncLocalStorage<LogContext>();
export function patchConsoleGlobal() {
const regularConsoleMethods: Array<keyof typeof console> = [
"log",
"error",
"warn",
"info",
"debug",
"table",
];
for (const methodName of regularConsoleMethods) {
const originalMethod = console[methodName];
console[methodName] = (...args) => {
const logs = asyncLocalStorage.getStore()?.logs;
if (logs) {
const timestamp = Date.now();
const text = serializeConsoleArgumentsToString(args) + "\n";
logs.push({
timestamp,
type: methodName === "error" ? "stderr" : "stdout",
text,
});
}
return originalMethod.apply(console, args);
};
}
const timingStarts = new Map<string, number>();
console.time = (label = "default") => {
timingStarts.set(label, Date.now());
};
console.timeEnd = (label = "default") => {
const start = timingStarts.get(label);
if (start === undefined) {
console.warn(`Timer '${label}' does not exist`);
return;
}
const duration = Date.now() - start;
timingStarts.delete(label);
console.log(`${label}: ${duration}ms`);
};
}
/** Used by tests so tests don't have to patch the console global */
export function manualLog(log: Log) {
const logs = asyncLocalStorage.getStore()?.logs;
if (logs) {
logs.push(log);
} else {
throw new Error("manualLog called outside of logging context");
}
}
export interface Logger {
log(...args: unknown[]): void;
error(...args: unknown[]): void;
}
export async function runInLoggingContext<T>(
fn: (logger: Logger) => Awaitable<T>,
): Promise<{ logs: Log[]; error: string | undefined }> {
const logs: Log[] = [];
const logContext: LogContext = { logs };
const logger: Logger = {
log: (...args) => {
const timestamp = Date.now();
originalConsoleMethods.log.apply(console, args);
logContext.logs?.push({
timestamp,
type: "stdout",
text: serializeConsoleArgumentsToString(args) + "\n",
});
},
error: (...args) => {
const timestamp = Date.now();
originalConsoleMethods.error.apply(console, args);
logContext.logs?.push({
timestamp,
type: "stderr",
text: serializeConsoleArgumentsToString(args) + "\n",
});
},
};
let error: string | undefined;
try {
await asyncLocalStorage.run(logContext, () => fn(logger));
} catch (e) {
logger.error(e);
error = serializeConsoleArgumentsToString([e]);
} finally {
logContext.logs = undefined;
}
return { logs, error };
}
type Awaitable<T> = PromiseLike<T> | T;