@oh-my-pi/pi-utils 17.1.8 → 17.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/types/postmortem.d.ts +7 -2
- package/package.json +3 -3
- package/src/logger.ts +104 -65
- package/src/postmortem.ts +11 -5
- package/src/winston-daily-rotate-file.d.ts +6 -0
|
@@ -64,10 +64,15 @@ export declare function register(id: string, callback: (reason: Reason) => void
|
|
|
64
64
|
* Use this in workers or when you need to clean up but continue execution.
|
|
65
65
|
*/
|
|
66
66
|
export declare function cleanup(): Promise<void>;
|
|
67
|
+
/** Controls how manual process shutdown handles terminal output. */
|
|
68
|
+
export interface QuitOptions {
|
|
69
|
+
/** Wait for buffered stdout before exiting; disable after the terminal has disconnected. */
|
|
70
|
+
drainStdout?: boolean;
|
|
71
|
+
}
|
|
67
72
|
/**
|
|
68
73
|
* Runs all cleanup callbacks and exits through the current `process.exit`.
|
|
69
74
|
*
|
|
70
|
-
* In main thread: waits for stdout drain, then calls `process.exit()`.
|
|
75
|
+
* In main thread: waits for stdout drain unless disabled, then calls `process.exit()`.
|
|
71
76
|
* In workers: runs cleanup only (process.exit would kill entire process).
|
|
72
77
|
*/
|
|
73
|
-
export declare function quit(code?: number): Promise<void>;
|
|
78
|
+
export declare function quit(code?: number, options?: QuitOptions): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-utils",
|
|
4
|
-
"version": "17.
|
|
4
|
+
"version": "17.2.0",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@oh-my-pi/pi-natives": "17.
|
|
34
|
+
"@oh-my-pi/pi-natives": "17.2.0",
|
|
35
35
|
"handlebars": "^4.7.9",
|
|
36
36
|
"winston": "^3.19.0",
|
|
37
|
-
"winston-daily-rotate-file": "
|
|
37
|
+
"winston-daily-rotate-file": "5.0.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/bun": "^1.3.14"
|
package/src/logger.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/// <reference path="./winston-daily-rotate-file.d.ts" />
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Centralized logger for omp.
|
|
3
5
|
*
|
|
@@ -11,10 +13,13 @@
|
|
|
11
13
|
*/
|
|
12
14
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
15
|
import * as fs from "node:fs";
|
|
16
|
+
import * as os from "node:os";
|
|
14
17
|
import * as path from "node:path";
|
|
15
18
|
import { isPromise } from "node:util/types";
|
|
16
|
-
import
|
|
17
|
-
|
|
19
|
+
import type DailyRotateFile from "winston-daily-rotate-file";
|
|
20
|
+
// Import the implementation directly because the package index imports and mutates
|
|
21
|
+
// Winston. The exact workspace catalog pin protects this internal entrypoint.
|
|
22
|
+
import DailyRotateFileImplementation from "winston-daily-rotate-file/daily-rotate-file.js";
|
|
18
23
|
import { getLogsDir } from "./dirs";
|
|
19
24
|
import { drainModuleLoadEvents } from "./timing-buffer";
|
|
20
25
|
/** Severity names accepted by the centralized logger. */
|
|
@@ -143,36 +148,68 @@ function jsonReplacer(_key: string, value: unknown): unknown {
|
|
|
143
148
|
return value;
|
|
144
149
|
}
|
|
145
150
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
return JSON.stringify(entry, jsonReplacer);
|
|
166
|
-
}),
|
|
151
|
+
interface NormalizedLogInfo extends Record<string, unknown> {
|
|
152
|
+
level: LogLevel;
|
|
153
|
+
message: unknown;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function padTimestampPart(value: number, width = 2): string {
|
|
157
|
+
return String(value).padStart(width, "0");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function formatLocalTimestamp(date: Date): string {
|
|
161
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
162
|
+
const absoluteOffset = Math.abs(offsetMinutes);
|
|
163
|
+
const offsetSign = offsetMinutes >= 0 ? "+" : "-";
|
|
164
|
+
return (
|
|
165
|
+
`${padTimestampPart(date.getFullYear(), 4)}-${padTimestampPart(date.getMonth() + 1)}-${padTimestampPart(date.getDate())}` +
|
|
166
|
+
`T${padTimestampPart(date.getHours())}:${padTimestampPart(date.getMinutes())}:${padTimestampPart(date.getSeconds())}` +
|
|
167
|
+
`.${padTimestampPart(date.getMilliseconds(), 3)}${offsetSign}${padTimestampPart(Math.floor(absoluteOffset / 60))}` +
|
|
168
|
+
`:${padTimestampPart(absoluteOffset % 60)}`
|
|
167
169
|
);
|
|
168
|
-
return logFormat;
|
|
169
170
|
}
|
|
170
171
|
|
|
172
|
+
const FORMAT_TOKEN_PATTERN = /%[scdjifoO%]/;
|
|
173
|
+
|
|
174
|
+
function normalizeLogInfo(
|
|
175
|
+
level: LogLevel,
|
|
176
|
+
message: string,
|
|
177
|
+
context: Record<string, unknown> | undefined,
|
|
178
|
+
): NormalizedLogInfo {
|
|
179
|
+
const metadata =
|
|
180
|
+
!FORMAT_TOKEN_PATTERN.test(message) && context !== null && typeof context === "object" ? context : undefined;
|
|
181
|
+
const info = Object.assign({}, metadata, { level, message }) as NormalizedLogInfo;
|
|
182
|
+
if (metadata?.message) info.message = `${message} ${metadata.message}`;
|
|
183
|
+
if (metadata?.stack) info.stack = metadata.stack;
|
|
184
|
+
if (metadata?.cause) info.cause = metadata.cause;
|
|
185
|
+
return info;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function formatLogInfo(info: NormalizedLogInfo): string {
|
|
189
|
+
const timestamp = formatLocalTimestamp(new Date());
|
|
190
|
+
info.timestamp = timestamp;
|
|
191
|
+
const entry: Record<string, unknown> = {
|
|
192
|
+
timestamp,
|
|
193
|
+
level: info.level,
|
|
194
|
+
pid: process.pid,
|
|
195
|
+
message: info.message,
|
|
196
|
+
};
|
|
197
|
+
for (const [key, value] of Object.entries(info)) {
|
|
198
|
+
if (key !== "level" && key !== "timestamp" && key !== "message") entry[key] = value;
|
|
199
|
+
}
|
|
200
|
+
return JSON.stringify(entry, jsonReplacer) as string;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
type FileTransport = DailyRotateFile & {
|
|
204
|
+
log(info: Record<symbol, string>, callback: () => void): void;
|
|
205
|
+
close(): void;
|
|
206
|
+
};
|
|
207
|
+
|
|
171
208
|
/** Build a rotating file transport with process-local rotation and shared retention. */
|
|
172
|
-
function makeFileTransport(dir?: string):
|
|
209
|
+
function makeFileTransport(dir?: string): FileTransport {
|
|
173
210
|
const logsDir = ensureDir(dir ?? getLogsDir());
|
|
174
211
|
pruneStaleProcessLogs(logsDir);
|
|
175
|
-
return new
|
|
212
|
+
return new DailyRotateFileImplementation({
|
|
176
213
|
dirname: logsDir,
|
|
177
214
|
filename: `omp.%DATE%.${process.pid}.log`,
|
|
178
215
|
datePattern: "YYYY-MM-DD",
|
|
@@ -180,45 +217,48 @@ function makeFileTransport(dir?: string): winston.transport {
|
|
|
180
217
|
maxFiles: 5,
|
|
181
218
|
zippedArchive: false,
|
|
182
219
|
auditFile: path.join(logsDir, `.omp.${process.pid}-audit.json`),
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function makeConsoleTransport(): winston.transport {
|
|
187
|
-
return new winston.transports.Console({ format: getLogFormat() });
|
|
220
|
+
}) as FileTransport;
|
|
188
221
|
}
|
|
189
222
|
|
|
190
223
|
/**
|
|
191
|
-
* Desired transport configuration, applied when
|
|
224
|
+
* Desired transport configuration, applied when local logging is initialized.
|
|
192
225
|
* Default: file ON (TUI-safe), console OFF.
|
|
193
226
|
*/
|
|
194
227
|
let transportOpts: { console?: boolean; file?: boolean | string } = { file: true };
|
|
195
228
|
|
|
196
|
-
|
|
197
|
-
|
|
229
|
+
interface LocalTransports {
|
|
230
|
+
readonly file: FileTransport | undefined;
|
|
231
|
+
readonly console: boolean;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Local transports, constructed lazily on first log emission. */
|
|
235
|
+
let activeTransports: LocalTransports | undefined;
|
|
236
|
+
|
|
237
|
+
const TRANSPORT_MESSAGE = Symbol.for("message");
|
|
238
|
+
const onTransportLogged = (): void => {};
|
|
198
239
|
|
|
199
|
-
function buildTransports(opts: { console?: boolean; file?: boolean | string }):
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
240
|
+
function buildTransports(opts: { console?: boolean; file?: boolean | string }): LocalTransports {
|
|
241
|
+
return {
|
|
242
|
+
file: opts.file ? makeFileTransport(typeof opts.file === "string" ? opts.file : undefined) : undefined,
|
|
243
|
+
console: opts.console === true,
|
|
244
|
+
};
|
|
204
245
|
}
|
|
205
246
|
|
|
206
|
-
function
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
return winstonLogger;
|
|
247
|
+
function getLocalTransports(): LocalTransports {
|
|
248
|
+
activeTransports ??= buildTransports(transportOpts);
|
|
249
|
+
return activeTransports;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function emitLocally(level: LogLevel, message: string, context: Record<string, unknown> | undefined): void {
|
|
253
|
+
const transports = getLocalTransports();
|
|
254
|
+
const info = normalizeLogInfo(level, message, context);
|
|
255
|
+
if (!transports.file && !transports.console) return;
|
|
256
|
+
|
|
257
|
+
// Winston applied the shared format before dispatch, then the same format a
|
|
258
|
+
// second time inside Console. Keep those evaluation and timestamp semantics.
|
|
259
|
+
const line = formatLogInfo(info);
|
|
260
|
+
if (transports.file) transports.file.log({ [TRANSPORT_MESSAGE]: line }, onTransportLogged);
|
|
261
|
+
if (transports.console) process.stdout.write(`${formatLogInfo(info)}${os.EOL}`);
|
|
222
262
|
}
|
|
223
263
|
|
|
224
264
|
/**
|
|
@@ -228,12 +268,11 @@ function getWinstonLogger(): winston.Logger {
|
|
|
228
268
|
*/
|
|
229
269
|
export function setTransports(opts: { console?: boolean; file?: boolean | string }): void {
|
|
230
270
|
transportOpts = opts;
|
|
231
|
-
if (!
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
winstonLogger.silent = transports.length === 0;
|
|
271
|
+
if (!activeTransports) return; // applied lazily when local logging is first initialized
|
|
272
|
+
const previousTransports = activeTransports;
|
|
273
|
+
activeTransports = { file: undefined, console: false };
|
|
274
|
+
previousTransports.file?.close();
|
|
275
|
+
activeTransports = buildTransports(opts);
|
|
237
276
|
}
|
|
238
277
|
|
|
239
278
|
/**
|
|
@@ -243,7 +282,7 @@ export function setTransports(opts: { console?: boolean; file?: boolean | string
|
|
|
243
282
|
*/
|
|
244
283
|
export function error(message: string, context?: Record<string, unknown>): void {
|
|
245
284
|
try {
|
|
246
|
-
|
|
285
|
+
emitLocally("error", message, context);
|
|
247
286
|
} catch {
|
|
248
287
|
// Silently ignore logging failures
|
|
249
288
|
}
|
|
@@ -257,7 +296,7 @@ export function error(message: string, context?: Record<string, unknown>): void
|
|
|
257
296
|
*/
|
|
258
297
|
export function warn(message: string, context?: Record<string, unknown>): void {
|
|
259
298
|
try {
|
|
260
|
-
|
|
299
|
+
emitLocally("warn", message, context);
|
|
261
300
|
} catch {
|
|
262
301
|
// Silently ignore logging failures
|
|
263
302
|
}
|
|
@@ -271,7 +310,7 @@ export function warn(message: string, context?: Record<string, unknown>): void {
|
|
|
271
310
|
*/
|
|
272
311
|
export function info(message: string, context?: Record<string, unknown>): void {
|
|
273
312
|
try {
|
|
274
|
-
|
|
313
|
+
emitLocally("info", message, context);
|
|
275
314
|
} catch {
|
|
276
315
|
// Silently ignore logging failures
|
|
277
316
|
}
|
|
@@ -285,7 +324,7 @@ export function info(message: string, context?: Record<string, unknown>): void {
|
|
|
285
324
|
*/
|
|
286
325
|
export function debug(message: string, context?: Record<string, unknown>): void {
|
|
287
326
|
try {
|
|
288
|
-
|
|
327
|
+
emitLocally("debug", message, context);
|
|
289
328
|
} catch {
|
|
290
329
|
// Silently ignore logging failures
|
|
291
330
|
}
|
package/src/postmortem.ts
CHANGED
|
@@ -319,14 +319,20 @@ export function cleanup(): Promise<void> {
|
|
|
319
319
|
return runCleanup(Reason.MANUAL);
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
-
|
|
322
|
+
/** Controls how manual process shutdown handles terminal output. */
|
|
323
|
+
export interface QuitOptions {
|
|
324
|
+
/** Wait for buffered stdout before exiting; disable after the terminal has disconnected. */
|
|
325
|
+
drainStdout?: boolean;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function runQuit(code: number, exitMode: "guarded" | "native", options: QuitOptions = {}): Promise<void> {
|
|
323
329
|
await runCleanup(Reason.MANUAL);
|
|
324
330
|
|
|
325
331
|
if (!isMainThread) {
|
|
326
332
|
return; // Workers: cleanup done, let worker exit naturally
|
|
327
333
|
}
|
|
328
334
|
|
|
329
|
-
if (process.stdout.writableLength > 0) {
|
|
335
|
+
if (options.drainStdout !== false && process.stdout.writableLength > 0) {
|
|
330
336
|
const { promise, resolve } = Promise.withResolvers<void>();
|
|
331
337
|
process.stdout.once("drain", resolve);
|
|
332
338
|
await Promise.race([promise, Bun.sleep(5000)]);
|
|
@@ -343,9 +349,9 @@ async function runQuit(code: number, exitMode: "guarded" | "native"): Promise<vo
|
|
|
343
349
|
/**
|
|
344
350
|
* Runs all cleanup callbacks and exits through the current `process.exit`.
|
|
345
351
|
*
|
|
346
|
-
* In main thread: waits for stdout drain, then calls `process.exit()`.
|
|
352
|
+
* In main thread: waits for stdout drain unless disabled, then calls `process.exit()`.
|
|
347
353
|
* In workers: runs cleanup only (process.exit would kill entire process).
|
|
348
354
|
*/
|
|
349
|
-
export function quit(code: number = 0): Promise<void> {
|
|
350
|
-
return runQuit(code, "guarded");
|
|
355
|
+
export function quit(code: number = 0, options: QuitOptions = {}): Promise<void> {
|
|
356
|
+
return runQuit(code, "guarded", options);
|
|
351
357
|
}
|