@sylphx/lens-server 2.3.2 → 2.4.1
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/index.d.ts +241 -23
- package/dist/index.js +353 -19
- package/package.json +1 -1
- package/src/handlers/http.test.ts +227 -2
- package/src/handlers/http.ts +223 -22
- package/src/handlers/index.ts +2 -0
- package/src/handlers/ws-types.ts +39 -0
- package/src/handlers/ws.test.ts +559 -0
- package/src/handlers/ws.ts +99 -0
- package/src/index.ts +21 -0
- package/src/logging/index.ts +20 -0
- package/src/logging/structured-logger.test.ts +367 -0
- package/src/logging/structured-logger.ts +335 -0
- package/src/server/create.test.ts +198 -0
- package/src/server/create.ts +78 -9
- package/src/server/types.ts +1 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sylphx/lens-server - Logging Module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
createStructuredLogger,
|
|
7
|
+
type ErrorContext,
|
|
8
|
+
jsonOutput,
|
|
9
|
+
type LogContext,
|
|
10
|
+
type LogEntry,
|
|
11
|
+
type LogLevel,
|
|
12
|
+
type LogOutput,
|
|
13
|
+
type PerformanceContext,
|
|
14
|
+
prettyOutput,
|
|
15
|
+
type RequestContext,
|
|
16
|
+
type StructuredLogger,
|
|
17
|
+
type StructuredLoggerOptions,
|
|
18
|
+
toBasicLogger,
|
|
19
|
+
type WebSocketContext,
|
|
20
|
+
} from "./structured-logger.js";
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sylphx/lens-server - Structured Logger Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it, spyOn } from "bun:test";
|
|
6
|
+
import {
|
|
7
|
+
createStructuredLogger,
|
|
8
|
+
jsonOutput,
|
|
9
|
+
type LogEntry,
|
|
10
|
+
type LogOutput,
|
|
11
|
+
prettyOutput,
|
|
12
|
+
toBasicLogger,
|
|
13
|
+
} from "./structured-logger.js";
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// Tests
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
describe("createStructuredLogger", () => {
|
|
20
|
+
describe("basic logging", () => {
|
|
21
|
+
it("creates a logger with all log methods", () => {
|
|
22
|
+
const logger = createStructuredLogger();
|
|
23
|
+
|
|
24
|
+
expect(typeof logger.debug).toBe("function");
|
|
25
|
+
expect(typeof logger.info).toBe("function");
|
|
26
|
+
expect(typeof logger.warn).toBe("function");
|
|
27
|
+
expect(typeof logger.error).toBe("function");
|
|
28
|
+
expect(typeof logger.fatal).toBe("function");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("logs messages with correct structure", () => {
|
|
32
|
+
const entries: LogEntry[] = [];
|
|
33
|
+
const testOutput: LogOutput = {
|
|
34
|
+
write(entry) {
|
|
35
|
+
entries.push(entry);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const logger = createStructuredLogger({
|
|
40
|
+
output: testOutput,
|
|
41
|
+
service: "test-service",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
logger.info("Test message");
|
|
45
|
+
|
|
46
|
+
expect(entries.length).toBe(1);
|
|
47
|
+
expect(entries[0].level).toBe("info");
|
|
48
|
+
expect(entries[0].message).toBe("Test message");
|
|
49
|
+
expect(entries[0].service).toBe("test-service");
|
|
50
|
+
expect(entries[0].timestamp).toBeDefined();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("includes context in log entries", () => {
|
|
54
|
+
const entries: LogEntry[] = [];
|
|
55
|
+
const testOutput: LogOutput = {
|
|
56
|
+
write(entry) {
|
|
57
|
+
entries.push(entry);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const logger = createStructuredLogger({ output: testOutput });
|
|
62
|
+
|
|
63
|
+
logger.info("User action", { userId: "123", action: "login" });
|
|
64
|
+
|
|
65
|
+
expect(entries[0].userId).toBe("123");
|
|
66
|
+
expect(entries[0].action).toBe("login");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("log levels", () => {
|
|
71
|
+
it("respects minimum log level", () => {
|
|
72
|
+
const entries: LogEntry[] = [];
|
|
73
|
+
const testOutput: LogOutput = {
|
|
74
|
+
write(entry) {
|
|
75
|
+
entries.push(entry);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const logger = createStructuredLogger({
|
|
80
|
+
output: testOutput,
|
|
81
|
+
level: "warn",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
logger.debug("Debug message");
|
|
85
|
+
logger.info("Info message");
|
|
86
|
+
logger.warn("Warn message");
|
|
87
|
+
logger.error("Error message");
|
|
88
|
+
|
|
89
|
+
expect(entries.length).toBe(2);
|
|
90
|
+
expect(entries[0].level).toBe("warn");
|
|
91
|
+
expect(entries[1].level).toBe("error");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("logs all levels when set to debug", () => {
|
|
95
|
+
const entries: LogEntry[] = [];
|
|
96
|
+
const testOutput: LogOutput = {
|
|
97
|
+
write(entry) {
|
|
98
|
+
entries.push(entry);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const logger = createStructuredLogger({
|
|
103
|
+
output: testOutput,
|
|
104
|
+
level: "debug",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
logger.debug("Debug");
|
|
108
|
+
logger.info("Info");
|
|
109
|
+
logger.warn("Warn");
|
|
110
|
+
logger.error("Error");
|
|
111
|
+
logger.fatal("Fatal");
|
|
112
|
+
|
|
113
|
+
expect(entries.length).toBe(5);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("error handling", () => {
|
|
118
|
+
it("extracts error properties from Error objects", () => {
|
|
119
|
+
const entries: LogEntry[] = [];
|
|
120
|
+
const testOutput: LogOutput = {
|
|
121
|
+
write(entry) {
|
|
122
|
+
entries.push(entry);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const logger = createStructuredLogger({ output: testOutput });
|
|
127
|
+
const error = new Error("Something went wrong");
|
|
128
|
+
error.name = "ValidationError";
|
|
129
|
+
|
|
130
|
+
logger.error("Operation failed", { error });
|
|
131
|
+
|
|
132
|
+
expect(entries[0].errorType).toBe("ValidationError");
|
|
133
|
+
expect(entries[0].errorMessage).toBe("Something went wrong");
|
|
134
|
+
expect(entries[0].error).toBeUndefined(); // Original error object removed
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("includes stack trace when configured", () => {
|
|
138
|
+
const entries: LogEntry[] = [];
|
|
139
|
+
const testOutput: LogOutput = {
|
|
140
|
+
write(entry) {
|
|
141
|
+
entries.push(entry);
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const logger = createStructuredLogger({
|
|
146
|
+
output: testOutput,
|
|
147
|
+
includeStackTrace: true,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const error = new Error("With stack");
|
|
151
|
+
logger.error("Error with stack", { error });
|
|
152
|
+
|
|
153
|
+
expect(entries[0].stack).toBeDefined();
|
|
154
|
+
expect(entries[0].stack).toContain("Error: With stack");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("excludes stack trace by default", () => {
|
|
158
|
+
const entries: LogEntry[] = [];
|
|
159
|
+
const testOutput: LogOutput = {
|
|
160
|
+
write(entry) {
|
|
161
|
+
entries.push(entry);
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const logger = createStructuredLogger({ output: testOutput });
|
|
166
|
+
|
|
167
|
+
const error = new Error("Without stack");
|
|
168
|
+
logger.error("Error without stack", { error });
|
|
169
|
+
|
|
170
|
+
expect(entries[0].stack).toBeUndefined();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("child logger", () => {
|
|
175
|
+
it("creates child logger with inherited context", () => {
|
|
176
|
+
const entries: LogEntry[] = [];
|
|
177
|
+
const testOutput: LogOutput = {
|
|
178
|
+
write(entry) {
|
|
179
|
+
entries.push(entry);
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const parent = createStructuredLogger({
|
|
184
|
+
output: testOutput,
|
|
185
|
+
defaultContext: { app: "test-app" },
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const child = parent.child({ requestId: "req-123" });
|
|
189
|
+
|
|
190
|
+
child.info("Child message");
|
|
191
|
+
|
|
192
|
+
expect(entries[0].app).toBe("test-app");
|
|
193
|
+
expect(entries[0].requestId).toBe("req-123");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("child context overrides parent context", () => {
|
|
197
|
+
const entries: LogEntry[] = [];
|
|
198
|
+
const testOutput: LogOutput = {
|
|
199
|
+
write(entry) {
|
|
200
|
+
entries.push(entry);
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const parent = createStructuredLogger({
|
|
205
|
+
output: testOutput,
|
|
206
|
+
defaultContext: { version: "1.0" },
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const child = parent.child({ version: "2.0" });
|
|
210
|
+
|
|
211
|
+
child.info("Override test");
|
|
212
|
+
|
|
213
|
+
expect(entries[0].version).toBe("2.0");
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("request logging", () => {
|
|
218
|
+
it("logs with request ID", () => {
|
|
219
|
+
const entries: LogEntry[] = [];
|
|
220
|
+
const testOutput: LogOutput = {
|
|
221
|
+
write(entry) {
|
|
222
|
+
entries.push(entry);
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const logger = createStructuredLogger({ output: testOutput });
|
|
227
|
+
|
|
228
|
+
logger.request("req-456", "Request started", { path: "/api/users" });
|
|
229
|
+
|
|
230
|
+
expect(entries[0].requestId).toBe("req-456");
|
|
231
|
+
expect(entries[0].path).toBe("/api/users");
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("operation tracking", () => {
|
|
236
|
+
it("tracks operation duration on success", async () => {
|
|
237
|
+
const entries: LogEntry[] = [];
|
|
238
|
+
const testOutput: LogOutput = {
|
|
239
|
+
write(entry) {
|
|
240
|
+
entries.push(entry);
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const logger = createStructuredLogger({
|
|
245
|
+
output: testOutput,
|
|
246
|
+
level: "debug",
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const done = logger.startOperation("getUser", { requestId: "req-789" });
|
|
250
|
+
|
|
251
|
+
// Simulate some work
|
|
252
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
253
|
+
|
|
254
|
+
done();
|
|
255
|
+
|
|
256
|
+
// Should have start and completion logs
|
|
257
|
+
expect(entries.length).toBe(2);
|
|
258
|
+
expect(entries[0].message).toContain("started");
|
|
259
|
+
expect(entries[1].message).toContain("completed");
|
|
260
|
+
// Check duration is tracked (avoid exact timing assertions for CI stability)
|
|
261
|
+
expect(typeof entries[1].durationMs).toBe("number");
|
|
262
|
+
expect(entries[1].durationMs).toBeGreaterThanOrEqual(0);
|
|
263
|
+
expect(entries[1].operation).toBe("getUser");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("tracks operation duration on error", async () => {
|
|
267
|
+
const entries: LogEntry[] = [];
|
|
268
|
+
const testOutput: LogOutput = {
|
|
269
|
+
write(entry) {
|
|
270
|
+
entries.push(entry);
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const logger = createStructuredLogger({
|
|
275
|
+
output: testOutput,
|
|
276
|
+
level: "debug",
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const done = logger.startOperation("createUser");
|
|
280
|
+
|
|
281
|
+
done({ error: new Error("Validation failed") });
|
|
282
|
+
|
|
283
|
+
expect(entries.length).toBe(2);
|
|
284
|
+
expect(entries[1].message).toContain("failed");
|
|
285
|
+
expect(entries[1].errorMessage).toBe("Validation failed");
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe("outputs", () => {
|
|
291
|
+
describe("jsonOutput", () => {
|
|
292
|
+
it("outputs JSON to console", () => {
|
|
293
|
+
const consoleSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
294
|
+
|
|
295
|
+
jsonOutput.write({
|
|
296
|
+
timestamp: "2024-01-01T00:00:00.000Z",
|
|
297
|
+
level: "info",
|
|
298
|
+
message: "Test",
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
302
|
+
'{"timestamp":"2024-01-01T00:00:00.000Z","level":"info","message":"Test"}',
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
consoleSpy.mockRestore();
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe("prettyOutput", () => {
|
|
310
|
+
it("outputs formatted message to console", () => {
|
|
311
|
+
const consoleSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
312
|
+
|
|
313
|
+
prettyOutput.write({
|
|
314
|
+
timestamp: "2024-01-01T00:00:00.000Z",
|
|
315
|
+
level: "info",
|
|
316
|
+
message: "Test message",
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
320
|
+
const output = consoleSpy.mock.calls[0][0] as string;
|
|
321
|
+
expect(output).toContain("INFO");
|
|
322
|
+
expect(output).toContain("Test message");
|
|
323
|
+
|
|
324
|
+
consoleSpy.mockRestore();
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe("toBasicLogger", () => {
|
|
330
|
+
it("adapts structured logger to basic interface", () => {
|
|
331
|
+
const entries: LogEntry[] = [];
|
|
332
|
+
const testOutput: LogOutput = {
|
|
333
|
+
write(entry) {
|
|
334
|
+
entries.push(entry);
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const structuredLogger = createStructuredLogger({ output: testOutput });
|
|
339
|
+
const basicLogger = toBasicLogger(structuredLogger);
|
|
340
|
+
|
|
341
|
+
basicLogger.info("Info message");
|
|
342
|
+
basicLogger.warn("Warn message");
|
|
343
|
+
basicLogger.error("Error message");
|
|
344
|
+
|
|
345
|
+
expect(entries.length).toBe(3);
|
|
346
|
+
expect(entries[0].level).toBe("info");
|
|
347
|
+
expect(entries[1].level).toBe("warn");
|
|
348
|
+
expect(entries[2].level).toBe("error");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("handles error arguments in basic logger", () => {
|
|
352
|
+
const entries: LogEntry[] = [];
|
|
353
|
+
const testOutput: LogOutput = {
|
|
354
|
+
write(entry) {
|
|
355
|
+
entries.push(entry);
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const structuredLogger = createStructuredLogger({ output: testOutput });
|
|
360
|
+
const basicLogger = toBasicLogger(structuredLogger);
|
|
361
|
+
|
|
362
|
+
const error = new Error("Test error");
|
|
363
|
+
basicLogger.error("Failed:", error);
|
|
364
|
+
|
|
365
|
+
expect(entries[0].errorMessage).toBe("Test error");
|
|
366
|
+
});
|
|
367
|
+
});
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sylphx/lens-server - Structured Logging
|
|
3
|
+
*
|
|
4
|
+
* Production-ready structured logging with JSON output.
|
|
5
|
+
* Compatible with log aggregators (DataDog, Splunk, ELK, etc.)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// Types
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Log levels (RFC 5424 severity)
|
|
14
|
+
*/
|
|
15
|
+
export type LogLevel = "debug" | "info" | "warn" | "error" | "fatal";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Base log context - always included
|
|
19
|
+
*/
|
|
20
|
+
export interface LogContext {
|
|
21
|
+
/** Timestamp in ISO format */
|
|
22
|
+
timestamp: string;
|
|
23
|
+
/** Log level */
|
|
24
|
+
level: LogLevel;
|
|
25
|
+
/** Log message */
|
|
26
|
+
message: string;
|
|
27
|
+
/** Service/component name */
|
|
28
|
+
service?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Request context for API operations
|
|
33
|
+
*/
|
|
34
|
+
export interface RequestContext {
|
|
35
|
+
/** Unique request/correlation ID */
|
|
36
|
+
requestId?: string;
|
|
37
|
+
/** Operation being executed */
|
|
38
|
+
operation?: string;
|
|
39
|
+
/** Client identifier */
|
|
40
|
+
clientId?: string;
|
|
41
|
+
/** Request duration in milliseconds */
|
|
42
|
+
durationMs?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Error context for error logs
|
|
47
|
+
*/
|
|
48
|
+
export interface ErrorContext {
|
|
49
|
+
/** Error name/type */
|
|
50
|
+
errorType?: string;
|
|
51
|
+
/** Error message */
|
|
52
|
+
errorMessage?: string;
|
|
53
|
+
/** Stack trace (only in development) */
|
|
54
|
+
stack?: string;
|
|
55
|
+
/** Error code */
|
|
56
|
+
errorCode?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* WebSocket context
|
|
61
|
+
*/
|
|
62
|
+
export interface WebSocketContext {
|
|
63
|
+
/** Message type */
|
|
64
|
+
messageType?: string;
|
|
65
|
+
/** Subscription ID */
|
|
66
|
+
subscriptionId?: string;
|
|
67
|
+
/** Entity type */
|
|
68
|
+
entity?: string;
|
|
69
|
+
/** Entity ID */
|
|
70
|
+
entityId?: string;
|
|
71
|
+
/** Connection count */
|
|
72
|
+
connectionCount?: number;
|
|
73
|
+
/** Subscription count */
|
|
74
|
+
subscriptionCount?: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Performance context
|
|
79
|
+
*/
|
|
80
|
+
export interface PerformanceContext {
|
|
81
|
+
/** Memory usage in bytes */
|
|
82
|
+
memoryUsed?: number;
|
|
83
|
+
/** CPU usage percentage */
|
|
84
|
+
cpuPercent?: number;
|
|
85
|
+
/** Active connections */
|
|
86
|
+
activeConnections?: number;
|
|
87
|
+
/** Active subscriptions */
|
|
88
|
+
activeSubscriptions?: number;
|
|
89
|
+
/** Messages per second */
|
|
90
|
+
messagesPerSecond?: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Full log entry type
|
|
95
|
+
*/
|
|
96
|
+
export type LogEntry = LogContext &
|
|
97
|
+
Partial<RequestContext> &
|
|
98
|
+
Partial<ErrorContext> &
|
|
99
|
+
Partial<WebSocketContext> &
|
|
100
|
+
Partial<PerformanceContext> & {
|
|
101
|
+
[key: string]: unknown;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Logger output destination
|
|
106
|
+
*/
|
|
107
|
+
export interface LogOutput {
|
|
108
|
+
write(entry: LogEntry): void;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Structured logger configuration
|
|
113
|
+
*/
|
|
114
|
+
export interface StructuredLoggerOptions {
|
|
115
|
+
/** Service name for log entries */
|
|
116
|
+
service?: string;
|
|
117
|
+
/** Minimum log level to output */
|
|
118
|
+
level?: LogLevel;
|
|
119
|
+
/** Include stack traces in error logs */
|
|
120
|
+
includeStackTrace?: boolean;
|
|
121
|
+
/** Custom output destination */
|
|
122
|
+
output?: LogOutput;
|
|
123
|
+
/** Additional context to include in all logs */
|
|
124
|
+
defaultContext?: Record<string, unknown>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// =============================================================================
|
|
128
|
+
// Log Level Priority
|
|
129
|
+
// =============================================================================
|
|
130
|
+
|
|
131
|
+
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
|
|
132
|
+
debug: 0,
|
|
133
|
+
info: 1,
|
|
134
|
+
warn: 2,
|
|
135
|
+
error: 3,
|
|
136
|
+
fatal: 4,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// =============================================================================
|
|
140
|
+
// Default Outputs
|
|
141
|
+
// =============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* JSON console output (production)
|
|
145
|
+
*/
|
|
146
|
+
export const jsonOutput: LogOutput = {
|
|
147
|
+
write(entry: LogEntry): void {
|
|
148
|
+
console.log(JSON.stringify(entry));
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Pretty console output (development)
|
|
154
|
+
*/
|
|
155
|
+
export const prettyOutput: LogOutput = {
|
|
156
|
+
write(entry: LogEntry): void {
|
|
157
|
+
const { timestamp, level, message, ...rest } = entry;
|
|
158
|
+
const color = {
|
|
159
|
+
debug: "\x1b[36m", // cyan
|
|
160
|
+
info: "\x1b[32m", // green
|
|
161
|
+
warn: "\x1b[33m", // yellow
|
|
162
|
+
error: "\x1b[31m", // red
|
|
163
|
+
fatal: "\x1b[35m", // magenta
|
|
164
|
+
}[level];
|
|
165
|
+
const reset = "\x1b[0m";
|
|
166
|
+
|
|
167
|
+
const contextStr = Object.keys(rest).length > 0 ? ` ${JSON.stringify(rest)}` : "";
|
|
168
|
+
|
|
169
|
+
console.log(`${timestamp} ${color}[${level.toUpperCase()}]${reset} ${message}${contextStr}`);
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// =============================================================================
|
|
174
|
+
// Structured Logger
|
|
175
|
+
// =============================================================================
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Structured logger interface
|
|
179
|
+
*/
|
|
180
|
+
export interface StructuredLogger {
|
|
181
|
+
debug: (message: string, context?: Record<string, unknown>) => void;
|
|
182
|
+
info: (message: string, context?: Record<string, unknown>) => void;
|
|
183
|
+
warn: (message: string, context?: Record<string, unknown>) => void;
|
|
184
|
+
error: (message: string, context?: Record<string, unknown>) => void;
|
|
185
|
+
fatal: (message: string, context?: Record<string, unknown>) => void;
|
|
186
|
+
child: (childContext: Record<string, unknown>) => StructuredLogger;
|
|
187
|
+
request: (requestId: string, message: string, context?: Record<string, unknown>) => void;
|
|
188
|
+
startOperation: (
|
|
189
|
+
operation: string,
|
|
190
|
+
context?: Record<string, unknown>,
|
|
191
|
+
) => (result?: { error?: Error; data?: unknown }) => void;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Create a structured logger instance.
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```typescript
|
|
199
|
+
* const logger = createStructuredLogger({
|
|
200
|
+
* service: 'lens-server',
|
|
201
|
+
* level: 'info',
|
|
202
|
+
* });
|
|
203
|
+
*
|
|
204
|
+
* logger.info('Request started', { requestId: 'abc', operation: 'getUser' });
|
|
205
|
+
* logger.error('Request failed', { requestId: 'abc', error: err });
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
export function createStructuredLogger(options: StructuredLoggerOptions = {}): StructuredLogger {
|
|
209
|
+
const {
|
|
210
|
+
service = "lens",
|
|
211
|
+
level: minLevel = "info",
|
|
212
|
+
includeStackTrace = false,
|
|
213
|
+
output = jsonOutput,
|
|
214
|
+
defaultContext = {},
|
|
215
|
+
} = options;
|
|
216
|
+
|
|
217
|
+
const minPriority = LOG_LEVEL_PRIORITY[minLevel];
|
|
218
|
+
|
|
219
|
+
function shouldLog(level: LogLevel): boolean {
|
|
220
|
+
return LOG_LEVEL_PRIORITY[level] >= minPriority;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function log(level: LogLevel, message: string, context: Record<string, unknown> = {}): void {
|
|
224
|
+
if (!shouldLog(level)) return;
|
|
225
|
+
|
|
226
|
+
// Extract error if present
|
|
227
|
+
const error = context.error instanceof Error ? context.error : undefined;
|
|
228
|
+
const errorContext: Partial<ErrorContext> = {};
|
|
229
|
+
|
|
230
|
+
if (error) {
|
|
231
|
+
errorContext.errorType = error.name;
|
|
232
|
+
errorContext.errorMessage = error.message;
|
|
233
|
+
if (includeStackTrace && error.stack) {
|
|
234
|
+
errorContext.stack = error.stack;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Build log entry
|
|
239
|
+
const entry: LogEntry = {
|
|
240
|
+
timestamp: new Date().toISOString(),
|
|
241
|
+
level,
|
|
242
|
+
message,
|
|
243
|
+
service,
|
|
244
|
+
...defaultContext,
|
|
245
|
+
...context,
|
|
246
|
+
...errorContext,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Remove the original error object from context
|
|
250
|
+
if ("error" in entry && entry.error instanceof Error) {
|
|
251
|
+
delete entry.error;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
output.write(entry);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
debug: (message: string, context?: Record<string, unknown>) => log("debug", message, context),
|
|
259
|
+
info: (message: string, context?: Record<string, unknown>) => log("info", message, context),
|
|
260
|
+
warn: (message: string, context?: Record<string, unknown>) => log("warn", message, context),
|
|
261
|
+
error: (message: string, context?: Record<string, unknown>) => log("error", message, context),
|
|
262
|
+
fatal: (message: string, context?: Record<string, unknown>) => log("fatal", message, context),
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Create a child logger with additional default context.
|
|
266
|
+
*/
|
|
267
|
+
child: (childContext: Record<string, unknown>) => {
|
|
268
|
+
return createStructuredLogger({
|
|
269
|
+
...options,
|
|
270
|
+
defaultContext: { ...defaultContext, ...childContext },
|
|
271
|
+
});
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Log with request context (for tracking request lifecycle).
|
|
276
|
+
*/
|
|
277
|
+
request: (requestId: string, message: string, context?: Record<string, unknown>) => {
|
|
278
|
+
log("info", message, { requestId, ...context });
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Log operation start (returns a function to log completion).
|
|
283
|
+
*/
|
|
284
|
+
startOperation: (
|
|
285
|
+
operation: string,
|
|
286
|
+
context?: Record<string, unknown>,
|
|
287
|
+
): ((result?: { error?: Error; data?: unknown }) => void) => {
|
|
288
|
+
const startTime = Date.now();
|
|
289
|
+
const requestId = context?.requestId as string | undefined;
|
|
290
|
+
|
|
291
|
+
log("debug", `Operation started: ${operation}`, { operation, ...context });
|
|
292
|
+
|
|
293
|
+
return (result?: { error?: Error; data?: unknown }) => {
|
|
294
|
+
const durationMs = Date.now() - startTime;
|
|
295
|
+
|
|
296
|
+
if (result?.error) {
|
|
297
|
+
log("error", `Operation failed: ${operation}`, {
|
|
298
|
+
operation,
|
|
299
|
+
requestId,
|
|
300
|
+
durationMs,
|
|
301
|
+
error: result.error,
|
|
302
|
+
});
|
|
303
|
+
} else {
|
|
304
|
+
log("info", `Operation completed: ${operation}`, {
|
|
305
|
+
operation,
|
|
306
|
+
requestId,
|
|
307
|
+
durationMs,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Adapter to make structured logger compatible with basic logger interface.
|
|
317
|
+
*/
|
|
318
|
+
export function toBasicLogger(structuredLogger: StructuredLogger): {
|
|
319
|
+
info: (message: string, ...args: unknown[]) => void;
|
|
320
|
+
warn: (message: string, ...args: unknown[]) => void;
|
|
321
|
+
error: (message: string, ...args: unknown[]) => void;
|
|
322
|
+
} {
|
|
323
|
+
return {
|
|
324
|
+
info: (message: string, ...args: unknown[]) => {
|
|
325
|
+
structuredLogger.info(message, args.length > 0 ? { args } : undefined);
|
|
326
|
+
},
|
|
327
|
+
warn: (message: string, ...args: unknown[]) => {
|
|
328
|
+
structuredLogger.warn(message, args.length > 0 ? { args } : undefined);
|
|
329
|
+
},
|
|
330
|
+
error: (message: string, ...args: unknown[]) => {
|
|
331
|
+
const error = args.find((arg) => arg instanceof Error) as Error | undefined;
|
|
332
|
+
structuredLogger.error(message, error ? { error } : args.length > 0 ? { args } : undefined);
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
}
|