@oh-my-pi/pi-utils 12.17.2 → 12.18.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/package.json +1 -1
- package/src/logger.ts +108 -7
- package/src/ring.ts +169 -0
package/package.json
CHANGED
package/src/logger.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Each log entry includes process.pid for traceability.
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
|
+
import { RingBuffer } from "@oh-my-pi/pi-utils/ring";
|
|
8
9
|
import winston from "winston";
|
|
9
10
|
import DailyRotateFile from "winston-daily-rotate-file";
|
|
10
11
|
import { getLogsDir } from "./dirs";
|
|
@@ -57,13 +58,6 @@ const winstonLogger = winston.createLogger({
|
|
|
57
58
|
exitOnError: false,
|
|
58
59
|
});
|
|
59
60
|
|
|
60
|
-
/** Logger type exposed to plugins and internal code */
|
|
61
|
-
export interface Logger {
|
|
62
|
-
error(message: string, context?: Record<string, unknown>): void;
|
|
63
|
-
warn(message: string, context?: Record<string, unknown>): void;
|
|
64
|
-
debug(message: string, context?: Record<string, unknown>): void;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
61
|
/**
|
|
68
62
|
* Centralized logger for omp.
|
|
69
63
|
*
|
|
@@ -79,6 +73,19 @@ export interface Logger {
|
|
|
79
73
|
* logger.debug("LSP fallback triggered", { reason });
|
|
80
74
|
* ```
|
|
81
75
|
*/
|
|
76
|
+
export interface Logger {
|
|
77
|
+
error(message: string, context?: Record<string, unknown>): void;
|
|
78
|
+
warn(message: string, context?: Record<string, unknown>): void;
|
|
79
|
+
debug(message: string, context?: Record<string, unknown>): void;
|
|
80
|
+
time<T>(op: string, fn: () => T): T;
|
|
81
|
+
timeAsync<T>(op: string, fn: () => PromiseLike<T>): Promise<T>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Log an error message.
|
|
86
|
+
* @param message - The message to log.
|
|
87
|
+
* @param context - The context to log.
|
|
88
|
+
*/
|
|
82
89
|
export function error(message: string, context?: Record<string, unknown>): void {
|
|
83
90
|
try {
|
|
84
91
|
winstonLogger.error(message, context);
|
|
@@ -87,6 +94,11 @@ export function error(message: string, context?: Record<string, unknown>): void
|
|
|
87
94
|
}
|
|
88
95
|
}
|
|
89
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Log a warning message.
|
|
99
|
+
* @param message - The message to log.
|
|
100
|
+
* @param context - The context to log.
|
|
101
|
+
*/
|
|
90
102
|
export function warn(message: string, context?: Record<string, unknown>): void {
|
|
91
103
|
try {
|
|
92
104
|
winstonLogger.warn(message, context);
|
|
@@ -95,6 +107,11 @@ export function warn(message: string, context?: Record<string, unknown>): void {
|
|
|
95
107
|
}
|
|
96
108
|
}
|
|
97
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Log a debug message.
|
|
112
|
+
* @param message - The message to log.
|
|
113
|
+
* @param context - The context to log.
|
|
114
|
+
*/
|
|
98
115
|
export function debug(message: string, context?: Record<string, unknown>): void {
|
|
99
116
|
try {
|
|
100
117
|
winstonLogger.debug(message, context);
|
|
@@ -102,3 +119,87 @@ export function debug(message: string, context?: Record<string, unknown>): void
|
|
|
102
119
|
// Silently ignore logging failures
|
|
103
120
|
}
|
|
104
121
|
}
|
|
122
|
+
|
|
123
|
+
const LOGGED_TIMING_THRESHOLD_MS = 5;
|
|
124
|
+
|
|
125
|
+
const longOpBuffer = new RingBuffer<[op: string, duration: number]>(1000);
|
|
126
|
+
let longOpRecord = false;
|
|
127
|
+
|
|
128
|
+
function logTiming(op: string, duration: number): void {
|
|
129
|
+
if (duration > LOGGED_TIMING_THRESHOLD_MS) {
|
|
130
|
+
warn(`${op} took ${duration}ms`, { duration, op });
|
|
131
|
+
if (longOpRecord) {
|
|
132
|
+
longOpBuffer.push([op, duration]);
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
debug(`${op} took ${duration}ms`, { duration, op });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Print all collected long operation timings to stderr.
|
|
141
|
+
* To be called at the end of a startup or timing window.
|
|
142
|
+
*/
|
|
143
|
+
export function printTimings(): void {
|
|
144
|
+
// Use stderr for timings output, do not use logger (see AGENTS.md).
|
|
145
|
+
console.error("\n--- Startup Timings ---");
|
|
146
|
+
let totalDuration = 0;
|
|
147
|
+
for (const [op, duration] of longOpBuffer) {
|
|
148
|
+
console.error(` ${op}: ${duration}ms`);
|
|
149
|
+
totalDuration += duration;
|
|
150
|
+
}
|
|
151
|
+
console.error(` TOTAL: ${totalDuration}ms`);
|
|
152
|
+
console.error("------------------------\n");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Begin recording long operation timings.
|
|
157
|
+
* Typically called at the beginning of startup.
|
|
158
|
+
*/
|
|
159
|
+
export function startTiming(): void {
|
|
160
|
+
longOpBuffer.clear();
|
|
161
|
+
longOpRecord = true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* End timing window and print all timings.
|
|
166
|
+
* Disables further buffering until next startTiming().
|
|
167
|
+
*/
|
|
168
|
+
export function endTiming(): void {
|
|
169
|
+
longOpBuffer.clear();
|
|
170
|
+
longOpRecord = false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Time a synchronous operation and log the duration.
|
|
175
|
+
* @param op - The operation name.
|
|
176
|
+
* @param fn - The function to time.
|
|
177
|
+
* @returns The result of the function.
|
|
178
|
+
*/
|
|
179
|
+
export function time<T, A extends unknown[]>(op: string, fn: (...args: A) => T, ...args: A): T {
|
|
180
|
+
const start = performance.now();
|
|
181
|
+
try {
|
|
182
|
+
return fn(...args);
|
|
183
|
+
} finally {
|
|
184
|
+
logTiming(op, performance.now() - start);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Time an asynchronous operation and log the duration.
|
|
190
|
+
* @param op - The operation name.
|
|
191
|
+
* @param fn - The function to time.
|
|
192
|
+
* @returns The result of the function.
|
|
193
|
+
*/
|
|
194
|
+
export async function timeAsync<R, A extends unknown[]>(
|
|
195
|
+
op: string,
|
|
196
|
+
fn: (...args: A) => R,
|
|
197
|
+
...args: A
|
|
198
|
+
): Promise<Awaited<R>> {
|
|
199
|
+
const start = performance.now();
|
|
200
|
+
try {
|
|
201
|
+
return await fn(...args);
|
|
202
|
+
} finally {
|
|
203
|
+
logTiming(op, performance.now() - start);
|
|
204
|
+
}
|
|
205
|
+
}
|
package/src/ring.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fixed-capacity circular buffer that supports efficient push/pop/shift/unshift operations.
|
|
3
|
+
* When the buffer is full, adding new items overwrites the oldest items (FIFO behavior).
|
|
4
|
+
*
|
|
5
|
+
* @template T The type of elements stored in the buffer.
|
|
6
|
+
*/
|
|
7
|
+
export class RingBuffer<T> {
|
|
8
|
+
#buf: (T | undefined)[];
|
|
9
|
+
#head = 0;
|
|
10
|
+
#size = 0;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates a new ring buffer with the specified capacity.
|
|
14
|
+
*
|
|
15
|
+
* @param capacity - The maximum number of elements the buffer can hold. Must be positive.
|
|
16
|
+
*/
|
|
17
|
+
constructor(public readonly capacity: number) {
|
|
18
|
+
this.#buf = new Array(capacity);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The number of elements currently in the buffer.
|
|
23
|
+
*/
|
|
24
|
+
get length(): number {
|
|
25
|
+
return this.#size;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether the buffer is at full capacity.
|
|
30
|
+
*/
|
|
31
|
+
get isFull(): boolean {
|
|
32
|
+
return this.#size === this.capacity;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether the buffer is empty (contains no elements).
|
|
37
|
+
*/
|
|
38
|
+
get isEmpty(): boolean {
|
|
39
|
+
return this.#size === 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Adds an item to the end of the buffer.
|
|
44
|
+
* If the buffer is full, the oldest item is overwritten and returned.
|
|
45
|
+
*
|
|
46
|
+
* @param item - The item to add.
|
|
47
|
+
* @returns The overwritten item if the buffer was full, otherwise `undefined`.
|
|
48
|
+
*/
|
|
49
|
+
push(item: T): T | undefined {
|
|
50
|
+
const idx = (this.#head + this.#size) % this.capacity;
|
|
51
|
+
const overwritten = this.#size === this.capacity ? this.#buf[idx] : undefined;
|
|
52
|
+
this.#buf[idx] = item;
|
|
53
|
+
if (this.#size === this.capacity) {
|
|
54
|
+
this.#head = (this.#head + 1) % this.capacity;
|
|
55
|
+
} else {
|
|
56
|
+
this.#size++;
|
|
57
|
+
}
|
|
58
|
+
return overwritten;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Removes and returns the first (oldest) item from the buffer.
|
|
63
|
+
*
|
|
64
|
+
* @returns The removed item, or `undefined` if the buffer is empty.
|
|
65
|
+
*/
|
|
66
|
+
shift(): T | undefined {
|
|
67
|
+
if (this.#size === 0) return undefined;
|
|
68
|
+
const item = this.#buf[this.#head];
|
|
69
|
+
this.#buf[this.#head] = undefined;
|
|
70
|
+
this.#head = (this.#head + 1) % this.capacity;
|
|
71
|
+
this.#size--;
|
|
72
|
+
return item;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Removes and returns the last (newest) item from the buffer.
|
|
77
|
+
*
|
|
78
|
+
* @returns The removed item, or `undefined` if the buffer is empty.
|
|
79
|
+
*/
|
|
80
|
+
pop(): T | undefined {
|
|
81
|
+
if (this.#size === 0) return undefined;
|
|
82
|
+
const idx = (this.#head + this.#size - 1) % this.capacity;
|
|
83
|
+
const item = this.#buf[idx];
|
|
84
|
+
this.#buf[idx] = undefined;
|
|
85
|
+
this.#size--;
|
|
86
|
+
return item;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Adds an item to the beginning of the buffer.
|
|
91
|
+
* If the buffer is full, the newest item is overwritten and returned.
|
|
92
|
+
*
|
|
93
|
+
* @param item - The item to add.
|
|
94
|
+
* @returns The overwritten item if the buffer was full, otherwise `undefined`.
|
|
95
|
+
*/
|
|
96
|
+
unshift(item: T): T | undefined {
|
|
97
|
+
this.#head = (this.#head - 1 + this.capacity) % this.capacity;
|
|
98
|
+
const overwritten = this.#size === this.capacity ? this.#buf[this.#head] : undefined;
|
|
99
|
+
this.#buf[this.#head] = item;
|
|
100
|
+
if (this.#size < this.capacity) this.#size++;
|
|
101
|
+
return overwritten;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Returns the element at the specified index without removing it.
|
|
106
|
+
* Supports negative indices (e.g., `-1` for the last element).
|
|
107
|
+
*
|
|
108
|
+
* @param index - The zero-based index, or negative index from the end.
|
|
109
|
+
* @returns The element at the index, or `undefined` if the index is out of bounds.
|
|
110
|
+
*/
|
|
111
|
+
at(index: number): T | undefined {
|
|
112
|
+
if (index < 0) index += this.#size;
|
|
113
|
+
if (index < 0 || index >= this.#size) return undefined;
|
|
114
|
+
return this.#buf[(this.#head + index) % this.capacity];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Returns the first (oldest) element without removing it.
|
|
119
|
+
*
|
|
120
|
+
* @returns The first element, or `undefined` if the buffer is empty.
|
|
121
|
+
*/
|
|
122
|
+
peek(): T | undefined {
|
|
123
|
+
return this.at(0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Returns the last (newest) element without removing it.
|
|
128
|
+
*
|
|
129
|
+
* @returns The last element, or `undefined` if the buffer is empty.
|
|
130
|
+
*/
|
|
131
|
+
peekBack(): T | undefined {
|
|
132
|
+
return this.at(this.#size - 1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Removes all elements from the buffer, resetting it to an empty state.
|
|
137
|
+
*/
|
|
138
|
+
clear(): void {
|
|
139
|
+
this.#buf.fill(undefined, 0, this.capacity);
|
|
140
|
+
this.#head = 0;
|
|
141
|
+
this.#size = 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Returns an iterator that yields elements in logical order (oldest to newest).
|
|
146
|
+
* Allows the buffer to be used with `for...of` loops and spread syntax.
|
|
147
|
+
*
|
|
148
|
+
* @yields Elements in FIFO order.
|
|
149
|
+
*/
|
|
150
|
+
*[Symbol.iterator](): Iterator<T> {
|
|
151
|
+
for (let i = 0; i < this.#size; i++) {
|
|
152
|
+
yield this.#buf[(this.#head + i) % this.capacity] as T;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Creates a new array containing all elements in logical order (oldest to newest).
|
|
158
|
+
*
|
|
159
|
+
* @returns A new array with all buffer elements.
|
|
160
|
+
*/
|
|
161
|
+
toArray(): T[] {
|
|
162
|
+
if (this.#head + this.#size <= this.capacity) {
|
|
163
|
+
return this.#buf.slice(this.#head, this.#head + this.#size) as T[];
|
|
164
|
+
}
|
|
165
|
+
const tail = this.#buf.slice(this.#head, this.capacity);
|
|
166
|
+
const head = this.#buf.slice(0, (this.#head + this.#size) % this.capacity);
|
|
167
|
+
return tail.concat(head) as T[];
|
|
168
|
+
}
|
|
169
|
+
}
|