node-logy 0.2.1 → 0.2.3

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.js CHANGED
@@ -1,717 +1,4 @@
1
- import fs from "node:fs";
2
- import path, { dirname } from "node:path";
3
- import { LOG_LEVEL, METHOD, } from "./protocol.js";
4
- import { Worker } from "node:worker_threads";
5
- import { fileURLToPath } from "node:url";
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = dirname(__filename);
8
- /**
9
- * Numeric priority for log levels (higher = more severe)
10
- */
11
- const LogLevelPriority = {
12
- [LOG_LEVEL.DEBUG]: 0,
13
- [LOG_LEVEL.INFO]: 1,
14
- [LOG_LEVEL.WARN]: 2,
15
- [LOG_LEVEL.ERROR]: 3,
16
- [LOG_LEVEL.FATAL]: 4,
17
- };
18
- // ANSI color codes
19
- const Colors = {
20
- reset: "\x1b[0m",
21
- bright: "\x1b[1m",
22
- dim: "\x1b[2m",
23
- red: "\x1b[31m",
24
- green: "\x1b[32m",
25
- yellow: "\x1b[33m",
26
- blue: "\x1b[34m",
27
- magenta: "\x1b[35m",
28
- cyan: "\x1b[36m",
29
- white: "\x1b[37m",
30
- gray: "\x1b[90m",
31
- };
32
- const defaultLoggerOptions = {
33
- basePath: "./logs",
34
- outputToConsole: true,
35
- saveToLogFiles: false,
36
- useColoredOutput: true,
37
- colorMap: {
38
- [LOG_LEVEL.INFO]: Colors.cyan,
39
- [LOG_LEVEL.WARN]: Colors.yellow,
40
- [LOG_LEVEL.ERROR]: Colors.red,
41
- [LOG_LEVEL.DEBUG]: Colors.gray,
42
- [LOG_LEVEL.FATAL]: Colors.magenta,
43
- },
44
- showTimestamps: true,
45
- timestampType: "iso",
46
- showLogLevel: true,
47
- logLevelMap: {
48
- [LOG_LEVEL.INFO]: "INFO",
49
- [LOG_LEVEL.WARN]: "WARN",
50
- [LOG_LEVEL.ERROR]: "ERROR",
51
- [LOG_LEVEL.DEBUG]: "DEBUG",
52
- [LOG_LEVEL.FATAL]: "FATAL",
53
- },
54
- };
55
- /**
56
- * Custom error for logger initialization failures
57
- */
58
- export class LoggerInitializationError extends Error {
59
- constructor(message) {
60
- super(message);
61
- this.name = "LoggerInitializationError";
62
- }
63
- }
64
- /**
65
- * Used to log to console and also the log files
66
- */
67
- export class Logger {
68
- /**
69
- * Local reference to options passed
70
- */
71
- _options;
72
- /**
73
- * Holds the worker thread
74
- */
75
- _worker = null;
76
- /**
77
- * A request's id
78
- */
79
- _id = 1;
80
- /**
81
- * Gets the next ID; if it exceeds 1 million then rolls back to zero
82
- */
83
- _getNextId = () => {
84
- if (this._id > 1000000) {
85
- this._id = 1;
86
- }
87
- return this._id++;
88
- };
89
- /**
90
- * Holds batch of LOG requests only (fire-and-forget)
91
- */
92
- _logBatch = [];
93
- /**
94
- * How long it will wait until it flushes / sends the logs to the worker
95
- */
96
- _logRequestFlushMs = 100;
97
- /**
98
- * Holds the timeout for log batch flushing
99
- */
100
- _logBatchTimeout = null;
101
- /**
102
- * How large we want the batch array to get before we send it
103
- */
104
- _logBatchMaxSize = 250;
105
- /**
106
- * Holds pending requests that expect a response (FLUSH, RELOAD, SHUTDOWN)
107
- */
108
- _pending = new Map();
109
- constructor(options = {}) {
110
- const mergedColorMap = {
111
- ...defaultLoggerOptions.colorMap,
112
- ...options.colorMap,
113
- };
114
- const mergedLogLevelMap = {
115
- ...defaultLoggerOptions.logLevelMap,
116
- ...options.logLevelMap,
117
- };
118
- this._options = {
119
- ...defaultLoggerOptions,
120
- ...options,
121
- colorMap: mergedColorMap,
122
- logLevelMap: mergedLogLevelMap,
123
- };
124
- this._validateBasePath();
125
- this._initializeDirectory();
126
- this._initWorker();
127
- }
128
- /**
129
- * Get the path to the worker
130
- * @returns Path to the worker
131
- */
132
- _getWorkerPath() {
133
- return path.join(__dirname, "worker.js");
134
- }
135
- /**
136
- * Inits the worker thread
137
- */
138
- _initWorker() {
139
- if (!this._options.saveToLogFiles)
140
- return;
141
- try {
142
- const workerPath = this._getWorkerPath();
143
- if (!fs.existsSync(workerPath)) {
144
- throw new Error("Worker file not found");
145
- }
146
- this._worker = new Worker(workerPath);
147
- this._worker.on("message", (response) => {
148
- this._handleResponse(response);
149
- });
150
- this._worker.stderr.on("data", (chunk) => {
151
- process.stderr.write(`Sidecar error: ${chunk.toString()}`);
152
- });
153
- this._worker.on("error", (err) => {
154
- this._clearPending();
155
- process.stderr.write(`Sidecar error: ${this._stringify(err)}`);
156
- });
157
- this._worker.on("exit", () => {
158
- this._clearPending();
159
- this._worker = null;
160
- });
161
- }
162
- catch (error) {
163
- process.stderr.write(`Failed to spawn sidecar: ${this._stringify(error)}`);
164
- }
165
- }
166
- /**
167
- * Flush the current log batch to worker immediately
168
- */
169
- _flushLogBatch() {
170
- if (!this._worker || this._logBatch.length === 0) {
171
- return;
172
- }
173
- this._worker.postMessage(this._logBatch);
174
- this._logBatch = [];
175
- this._stopLogBatchTimer();
176
- }
177
- /**
178
- * Starts the timer to flush log batch after delay
179
- */
180
- _startLogBatchTimer() {
181
- if (this._logBatchTimeout)
182
- return;
183
- this._logBatchTimeout = setTimeout(() => {
184
- this._flushLogBatch();
185
- }, this._logRequestFlushMs);
186
- }
187
- /**
188
- * Stops the log batch flush timer
189
- */
190
- _stopLogBatchTimer() {
191
- if (this._logBatchTimeout) {
192
- clearTimeout(this._logBatchTimeout);
193
- this._logBatchTimeout = null;
194
- }
195
- }
196
- /**
197
- * Adds a LOG request to the batch (fire-and-forget)
198
- */
199
- _addToLogBatch(request) {
200
- this._logBatch.push(request);
201
- if (this._logBatch.length >= this._logBatchMaxSize) {
202
- this._flushLogBatch();
203
- }
204
- else {
205
- this._startLogBatchTimer();
206
- }
207
- }
208
- /**
209
- * Clears pending requests on process exit/error
210
- */
211
- _clearPending() {
212
- const error = new Error("Sidecar process terminated");
213
- for (const [_, { reject }] of this._pending) {
214
- reject(error);
215
- }
216
- this._pending.clear();
217
- }
218
- /**
219
- * Handle a decoded response from worker
220
- */
221
- _handleResponse(response) {
222
- this._resolvePending(response);
223
- if (!response.success) {
224
- process.stderr.write(`Log operation failed: id=${response.id}, method=${response.method}, level=${response.level}\n`);
225
- }
226
- }
227
- /**
228
- * Resolve any pending requests that expected a response
229
- */
230
- _resolvePending(response) {
231
- const pending = this._pending.get(response.id);
232
- if (!pending)
233
- return;
234
- if (response.success) {
235
- pending.resolve();
236
- }
237
- else {
238
- pending.reject(new Error("Request failed"));
239
- }
240
- this._pending.delete(response.id);
241
- }
242
- /**
243
- * Send a request that expects a response (FLUSH, RELOAD, SHUTDOWN)
244
- * These are sent immediately, not batched
245
- */
246
- _sendControlRequest(request) {
247
- const id = request.id;
248
- if (!id)
249
- throw new Error("Request must contain and ID");
250
- return new Promise((resolve, reject) => {
251
- const timeout = setTimeout(() => {
252
- if (this._pending.has(id)) {
253
- this._pending
254
- .get(id)
255
- ?.reject(new Error(`Request timed out: ${this._stringify(request)}`));
256
- this._pending.delete(id);
257
- }
258
- }, 4000);
259
- this._pending.set(id, {
260
- reject: (reason) => {
261
- clearTimeout(timeout);
262
- reject(reason);
263
- },
264
- resolve: (value) => {
265
- clearTimeout(timeout);
266
- resolve(value);
267
- },
268
- });
269
- this._worker?.postMessage([request]);
270
- });
271
- }
272
- /**
273
- * Validates the basePath option
274
- */
275
- _validateBasePath() {
276
- const { basePath } = this._options;
277
- if (basePath === undefined || basePath === null) {
278
- throw new LoggerInitializationError("basePath is required and cannot be null or undefined");
279
- }
280
- if (typeof basePath !== "string") {
281
- throw new LoggerInitializationError(`basePath must be a string, received ${typeof basePath}`);
282
- }
283
- if (basePath.trim().length === 0) {
284
- throw new LoggerInitializationError("basePath cannot be an empty string");
285
- }
286
- this._options.basePath = path.resolve(basePath);
287
- }
288
- /**
289
- * Creates the log directory if file logging is enabled
290
- */
291
- _initializeDirectory() {
292
- if (!this._options.saveToLogFiles) {
293
- return;
294
- }
295
- try {
296
- fs.mkdirSync(this._options.basePath, { recursive: true });
297
- }
298
- catch (error) {
299
- const nodeError = error;
300
- throw new LoggerInitializationError(`Failed to create log directory at "${this._options.basePath}": ${nodeError.message}`);
301
- }
302
- this._verifyWritable();
303
- }
304
- /**
305
- * Extract call site information (file:line:column) from stack trace
306
- */
307
- _getCallSite() {
308
- const originalPrepareStackTrace = Error.prepareStackTrace;
309
- try {
310
- Error.prepareStackTrace = (_, stack) => stack;
311
- const err = new Error();
312
- const stack = err.stack;
313
- // Base index: 0 = _getCallSite, 1 = _formatMessage, 2 = log, 3 = actual caller
314
- let frameIndex = 3;
315
- // Skip internal logger methods to find actual caller
316
- // Use path.basename to compare just the filename, not full paths
317
- // This handles path format differences between compiled and source
318
- const thisFileName = path.basename(__filename);
319
- while (frameIndex < stack.length) {
320
- const frame = stack[frameIndex];
321
- const frameFileName = frame?.getFileName();
322
- // Stop if we hit a frame without a filename or outside this logger file
323
- if (!frameFileName)
324
- break;
325
- // Check if this frame is still in the logger file
326
- const isLoggerFile = frameFileName === __filename ||
327
- path.basename(frameFileName) === thisFileName ||
328
- (frameFileName.includes("node-logger") &&
329
- frameFileName.includes("index.js"));
330
- if (!isLoggerFile) {
331
- break;
332
- }
333
- frameIndex++;
334
- }
335
- const frame = stack[frameIndex];
336
- if (!frame) {
337
- return "unknown";
338
- }
339
- const fileName = frame.getFileName() || "unknown";
340
- const lineNumber = frame.getLineNumber() || 0;
341
- const columnNumber = frame.getColumnNumber() || 0;
342
- // Use full path or short name based on option
343
- const useFullPath = this._options.callSiteOptions?.fullFilePath ?? false;
344
- const displayFileName = useFullPath ? fileName : path.basename(fileName);
345
- return `${displayFileName}:${lineNumber}:${columnNumber}`;
346
- }
347
- catch {
348
- return "unknown";
349
- }
350
- finally {
351
- Error.prepareStackTrace = originalPrepareStackTrace;
352
- }
353
- }
354
- /**
355
- * Check if a log level should be processed based on filtering rules
356
- */
357
- _shouldLog(level) {
358
- const { minLevel, maxLevel, includeLevels, excludeLevels, filter } = this._options;
359
- // Check whitelist (if specified, only these levels pass)
360
- if (includeLevels && includeLevels.length > 0) {
361
- if (!includeLevels.includes(level)) {
362
- return false;
363
- }
364
- }
365
- // Check blacklist
366
- if (excludeLevels && excludeLevels.length > 0) {
367
- if (excludeLevels.includes(level)) {
368
- return false;
369
- }
370
- }
371
- // Check min level (filter out lower severity)
372
- if (minLevel !== undefined) {
373
- if (LogLevelPriority[level] < LogLevelPriority[minLevel]) {
374
- return false;
375
- }
376
- }
377
- // Check max level (filter out higher severity)
378
- if (maxLevel !== undefined) {
379
- if (LogLevelPriority[level] > LogLevelPriority[maxLevel]) {
380
- return false;
381
- }
382
- }
383
- // Check custom filter function
384
- if (filter) {
385
- // Note: filter function is checked in log() with message available
386
- return true; // Defer to log() method for filter check
387
- }
388
- return true;
389
- }
390
- /**
391
- * Verifies the log directory is writable
392
- */
393
- _verifyWritable() {
394
- try {
395
- const testFile = path.join(this._options.basePath, ".write-test");
396
- fs.writeFileSync(testFile, "", { flag: "wx" });
397
- fs.unlinkSync(testFile);
398
- }
399
- catch (error) {
400
- const nodeError = error;
401
- throw new LoggerInitializationError(`Log directory "${this._options.basePath}" is not writable: ${nodeError.message}`);
402
- }
403
- }
404
- /**
405
- * Get the string representation of a log level
406
- */
407
- _getLevelString(level) {
408
- return this._options.logLevelMap[level] ?? "UNKNOWN";
409
- }
410
- /**
411
- * Format timestamp based on the configured timestampType
412
- */
413
- _formatTimestamp(date) {
414
- const { timestampType, customTimestampFormat } = this._options;
415
- switch (timestampType) {
416
- case "iso":
417
- return date.toISOString();
418
- case "locale":
419
- return date.toLocaleString();
420
- case "utc":
421
- return date.toUTCString();
422
- case "unix":
423
- return Math.floor(date.getTime() / 1000).toString();
424
- case "unix_ms":
425
- return date.getTime().toString();
426
- case "date":
427
- return date.toISOString().split("T")[0];
428
- case "time":
429
- return date.toTimeString().split(" ")[0];
430
- case "datetime":
431
- return date.toISOString().replace("T", " ").replace("Z", "");
432
- case "short":
433
- const d = date;
434
- const day = d.getDate().toString().padStart(2, "0");
435
- const month = (d.getMonth() + 1).toString().padStart(2, "0");
436
- const year = d.getFullYear();
437
- const hours = d.getHours().toString().padStart(2, "0");
438
- const minutes = d.getMinutes().toString().padStart(2, "0");
439
- return `${day}/${month}/${year} ${hours}:${minutes}`;
440
- case "custom":
441
- if (!customTimestampFormat) {
442
- return date.toISOString();
443
- }
444
- return this._applyCustomFormat(date, customTimestampFormat);
445
- default:
446
- return date.toISOString();
447
- }
448
- }
449
- /**
450
- * Apply custom format string to date
451
- * Tokens: YYYY=year, MM=month, DD=day, HH=hour, mm=minute, ss=second, ms=millisecond
452
- */
453
- _applyCustomFormat(date, format) {
454
- const tokens = {
455
- YYYY: date.getFullYear().toString(),
456
- MM: (date.getMonth() + 1).toString().padStart(2, "0"),
457
- DD: date.getDate().toString().padStart(2, "0"),
458
- HH: date.getHours().toString().padStart(2, "0"),
459
- mm: date.getMinutes().toString().padStart(2, "0"),
460
- ss: date.getSeconds().toString().padStart(2, "0"),
461
- ms: date.getMilliseconds().toString().padStart(3, "0"),
462
- };
463
- return format.replace(/YYYY|MM|DD|HH|mm|ss|ms/g, (match) => tokens[match] || match);
464
- }
465
- /**
466
- * Convert any value to string representation
467
- */
468
- _stringify(value) {
469
- const valueType = typeof value;
470
- if (value === null)
471
- return "null";
472
- if (value === undefined)
473
- return "undefined";
474
- if (valueType === "string")
475
- return value;
476
- if (valueType === "number")
477
- return String(value);
478
- if (valueType === "boolean")
479
- return String(value);
480
- if (valueType === "bigint")
481
- return `${value}n`;
482
- if (valueType === "symbol")
483
- return value.toString();
484
- if (valueType === "function")
485
- return `[Function: ${value.name || "anonymous"}]`;
486
- // Check if value is an Error instance (must be object, so check after primitives)
487
- if (value instanceof Error) {
488
- const errorParts = [
489
- `name: ${value.name}`,
490
- `message: ${value.message}`,
491
- ];
492
- if (value.stack)
493
- errorParts.push(`stack: ${value.stack}`);
494
- if ("code" in value && value.code !== undefined)
495
- errorParts.push(`code: ${value.code}`);
496
- if ("errno" in value && value.errno !== undefined)
497
- errorParts.push(`errno: ${value.errno}`);
498
- if ("syscall" in value && value.syscall !== undefined)
499
- errorParts.push(`syscall: ${value.syscall}`);
500
- if ("path" in value && value.path !== undefined)
501
- errorParts.push(`path: ${value.path}`);
502
- // Capture custom properties
503
- const standardProps = new Set([
504
- "name",
505
- "message",
506
- "stack",
507
- "code",
508
- "errno",
509
- "syscall",
510
- "path",
511
- ]);
512
- for (const prop of Object.getOwnPropertyNames(value)) {
513
- if (standardProps.has(prop))
514
- continue;
515
- try {
516
- const propValue = value[prop];
517
- errorParts.push(`${prop}: ${typeof propValue === "object" && propValue !== null ? "[object]" : propValue}`);
518
- }
519
- catch {
520
- errorParts.push(`${prop}: [unreadable]`);
521
- }
522
- }
523
- return `Error { ${errorParts.join(", ")} }`;
524
- }
525
- if (valueType === "object") {
526
- const keys = Object.keys(value);
527
- if (keys.length === 0)
528
- return "{}";
529
- const entries = keys.map((key) => {
530
- try {
531
- const val = value[key];
532
- // Show type for nested objects/arrays, value for primitives
533
- const display = val === null
534
- ? "null"
535
- : Array.isArray(val)
536
- ? "[Array]"
537
- : typeof val === "object"
538
- ? "[Object]"
539
- : typeof val === "function"
540
- ? "[Function]"
541
- : typeof val === "symbol"
542
- ? "[Symbol]"
543
- : String(val);
544
- return `${key}: ${display}`;
545
- }
546
- catch {
547
- return `${key}: [unreadable]`;
548
- }
549
- });
550
- return `{ ${entries.join(", ")} }`;
551
- }
552
- // Fallback (shouldn't reach here with standard JS types)
553
- return String(value);
554
- }
555
- /**
556
- * Format a log message with optional fields
557
- */
558
- _formatMessage(level, message, additionalMessages) {
559
- const parts = [];
560
- // Add call site if enabled
561
- if (this._options.showCallSite) {
562
- const callSite = this._getCallSite();
563
- parts.push(`[${callSite}]`);
564
- }
565
- // Add timestamp if enabled
566
- if (this._options.showTimestamps) {
567
- const timestamp = this._formatTimestamp(new Date());
568
- parts.push(`[${timestamp}]`);
569
- }
570
- // Add log level if enabled
571
- if (this._options.showLogLevel) {
572
- const levelStr = this._getLevelString(level);
573
- parts.push(`[${levelStr}]`);
574
- }
575
- // Add user prefixes
576
- const addPrefixes = this._options.additionalPrefixes;
577
- if (addPrefixes) {
578
- for (let i = 0; i < addPrefixes.length; i++) {
579
- parts.push(addPrefixes[i]);
580
- }
581
- }
582
- // Build the message content
583
- const mainMessage = this._stringify(message);
584
- const additionalStr = additionalMessages
585
- .map((msg) => this._stringify(msg))
586
- .join(" ");
587
- const fullMessage = additionalStr
588
- ? `${mainMessage} ${additionalStr}`
589
- : mainMessage;
590
- // Combine parts with message
591
- if (parts.length > 0) {
592
- return `${parts.join(" ")}: ${fullMessage}`;
593
- }
594
- else {
595
- return fullMessage;
596
- }
597
- }
598
- /**
599
- * Apply color to the entire message if colored output is enabled
600
- */
601
- _colorize(level, message) {
602
- if (!this._options.useColoredOutput) {
603
- return message;
604
- }
605
- const color = this._options.colorMap[level] || Colors.reset;
606
- return `${color}${message}${Colors.reset}`;
607
- }
608
- /**
609
- * Log a specific level and content
610
- * @param level The specific level to log
611
- * @param message The content of the message
612
- * @param messages Any additional messages
613
- */
614
- log(level, message, ...messages) {
615
- if (!this._shouldLog(level)) {
616
- return;
617
- }
618
- if (this._options.filter) {
619
- const fullMessage = messages.length > 0
620
- ? [message, ...messages].map((m) => this._stringify(m)).join(" ")
621
- : this._stringify(message);
622
- if (!this._options.filter(level, fullMessage)) {
623
- return;
624
- }
625
- }
626
- const formattedMessage = this._formatMessage(level, message, messages);
627
- if (this._options.saveToLogFiles) {
628
- this._addToLogBatch({
629
- // we don't need ID and level
630
- method: METHOD.LOG,
631
- payload: formattedMessage,
632
- });
633
- }
634
- if (this._options.outputToConsole) {
635
- const coloredMessage = this._colorize(level, formattedMessage);
636
- if (level === LOG_LEVEL.ERROR || level === LOG_LEVEL.FATAL) {
637
- process.stderr.write(coloredMessage + "\n");
638
- }
639
- else {
640
- process.stdout.write(coloredMessage + "\n");
641
- }
642
- }
643
- }
644
- /**
645
- * Convenience method for INFO level
646
- */
647
- info(message, ...messages) {
648
- this.log(LOG_LEVEL.INFO, message, ...messages);
649
- }
650
- /**
651
- * Convenience method for WARN level
652
- */
653
- warn(message, ...messages) {
654
- this.log(LOG_LEVEL.WARN, message, ...messages);
655
- }
656
- /**
657
- * Convenience method for ERROR level
658
- */
659
- error(message, ...messages) {
660
- this.log(LOG_LEVEL.ERROR, message, ...messages);
661
- }
662
- /**
663
- * Convenience method for DEBUG level
664
- */
665
- debug(message, ...messages) {
666
- this.log(LOG_LEVEL.DEBUG, message, ...messages);
667
- }
668
- /**
669
- * Convenience method for FATAL level
670
- */
671
- fatal(message, ...messages) {
672
- this.log(LOG_LEVEL.FATAL, message, ...messages);
673
- }
674
- /**
675
- * Flush remaining buffer to log files
676
- */
677
- flush() {
678
- this._flushLogBatch();
679
- return this._sendControlRequest({
680
- id: this._getNextId(),
681
- level: LOG_LEVEL.INFO,
682
- method: METHOD.FLUSH,
683
- payload: "",
684
- });
685
- }
686
- /**
687
- * Used to reload / refresh the process
688
- */
689
- reload() {
690
- this._flushLogBatch();
691
- return this._sendControlRequest({
692
- id: this._getNextId(),
693
- level: LOG_LEVEL.INFO,
694
- method: METHOD.RELOAD,
695
- payload: "",
696
- });
697
- }
698
- /**
699
- * Used to shut down the child process and clean up
700
- */
701
- shutdown() {
702
- this._flushLogBatch();
703
- return this._sendControlRequest({
704
- id: this._getNextId(),
705
- level: LOG_LEVEL.INFO,
706
- method: METHOD.SHUTDOWN,
707
- payload: "",
708
- });
709
- }
710
- /**
711
- * Get current logger options (read-only copy)
712
- */
713
- get options() {
714
- return { ...this._options };
715
- }
716
- }
1
+ export * from "./logger.js";
2
+ export * from "./protocol.js";
3
+ export * from "./worker.js";
717
4
  //# sourceMappingURL=index.js.map