ntlogger 2.7.0 → 2.8.2

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/README.md CHANGED
@@ -11,6 +11,13 @@ NightTimeLogger is a custom logging wrapper built on top of the Winston logging
11
11
  - Custom session ID generation for tracking log sessions.
12
12
  - Support for both file and console log formatters.
13
13
  - Ability to configure log levels and formats to suit specific requirements.
14
+ - Native call site path reporting - automatically capture file path, line number, and call chain where each log statement is executed.
15
+ - **Child loggers** - Create contextual loggers with persistent metadata (perfect for multi-threaded applications).
16
+ - **Log sampling and rate limiting** - Reduce console spam with configurable sampling rates and rate limits per log level.
17
+ - **Log deduplication** - Automatically group and squish duplicate log messages (e.g., "Ticket already exists (x11)").
18
+ - **Performance metrics** - Development-only performance tracking with `time()` and `timeEnd()` methods.
19
+ - **Non-blocking operations** - All logging operations are asynchronous and won't block your application.
20
+ - **TypeScript support** - Full TypeScript definitions included for excellent IDE integration (IntelliJ, VS Code, etc.).
14
21
 
15
22
  ## Installation
16
23
 
@@ -59,6 +66,143 @@ Check out [Full Configuration](https://github.com/NightSquawk/NightTimeLogger/bl
59
66
  - `maxFiles`: The maximum number of log files to retain (rotating file strategy).
60
67
  - `timestamp`: Whether to include timestamps in log messages. Defaults to `true`.
61
68
  - `debug`: Whether to enable debug mode, which logs internal messages. Defaults to `false`.
69
+ - `reportPath`: Whether to enable call site path reporting. When enabled, automatically captures the file path, line number, column number, and call chain where each log statement is executed. The path is added as metadata (JSON field `filePath`), not in the formatted message string. Defaults to `false`.
70
+ - `sampling`: Object with level-based sampling rates (e.g., `{ debug: 0.01, trace: 0.001 }`). Values between 0.0 and 1.0, where 1.0 = log all, 0.1 = log 10%. Defaults to `{}` (no sampling).
71
+ - `rateLimit`: Object with level-based rate limits (e.g., `{ error: { max: 10, window: 60000 } }`). Prevents console spam by limiting logs per level within a time window. Defaults to `{}` (no rate limiting).
72
+ - `deduplication`: Object with `{ enabled: boolean, threshold: number, window: number }`. Groups similar log messages and squishes duplicates (e.g., "Ticket already exists (x11)"). Defaults to `{ enabled: false, threshold: 3, window: 60000 }`.
73
+ - `performanceMetrics`: Enable performance tracking (default: `NODE_ENV === 'development'`). Adds `time()` and `timeEnd()` methods.
74
+ - `statsInterval`: Interval in milliseconds to automatically log statistics (default: `0` = disabled). Useful for tuning sampling/rate limiting parameters.
75
+
76
+ ### Call Site Path Reporting
77
+
78
+ When `reportPath` is enabled, each log entry includes a `filePath` field in its metadata showing where the log was called:
79
+
80
+ ```javascript
81
+ const logger = require('ntlogger');
82
+
83
+ const log = logger('MyApp', {
84
+ reportPath: true
85
+ });
86
+
87
+ log.info('User logged in'); // filePath will show: "./src/routes/auth.js:45:12 [handleLogin ← router.post]"
88
+ ```
89
+
90
+ The `filePath` field appears in:
91
+ - JSON metadata (for plugins like OpenObserve)
92
+ - Console output (appended to location)
93
+ - File logs (appended to location)
94
+
95
+ **Smart Internal Function Filtering**: The logger automatically filters out internal Node.js functions (like `_onTimeout`, `listOnTimeout`, `setImmediate`, etc.) from the call chain, ensuring you only see your actual application code. This works even when logs are called from within `setTimeout`, `setImmediate`, or Promise callbacks.
96
+
97
+ Note: The `location` field represents the logger instance name, while `filePath` shows the actual call site where the log statement was written.
98
+
99
+ ### Child Loggers
100
+
101
+ Create child loggers with persistent context that's automatically merged into all log entries. Perfect for request-scoped logging or multi-threaded applications:
102
+
103
+ ```javascript
104
+ const logger = require('ntlogger');
105
+
106
+ const log = logger('MyApp');
107
+
108
+ // Create a child logger with context
109
+ const requestLogger = log.child({
110
+ requestId: 'abc123',
111
+ userId: 456,
112
+ endpoint: '/api/users'
113
+ });
114
+
115
+ // All logs from requestLogger automatically include the context
116
+ requestLogger.info('Processing request');
117
+ // Logs: { requestId: 'abc123', userId: 456, endpoint: '/api/users', message: 'Processing request' }
118
+
119
+ // Support nested children
120
+ const operationLogger = requestLogger.child({ operation: 'validate' });
121
+ operationLogger.debug('Validating input'); // Includes all parent context
122
+ ```
123
+
124
+ ### Log Sampling and Rate Limiting
125
+
126
+ Reduce console spam with configurable sampling and rate limiting:
127
+
128
+ ```javascript
129
+ const log = logger('MyApp', {
130
+ // Sample 1% of debug logs, 0.1% of trace logs
131
+ sampling: {
132
+ debug: 0.01,
133
+ trace: 0.001
134
+ },
135
+ // Limit errors to 10 per minute
136
+ rateLimit: {
137
+ error: { max: 10, window: 60000 }
138
+ },
139
+ // Log statistics every 5 minutes to help tune parameters
140
+ statsInterval: 300000
141
+ });
142
+
143
+ // Get statistics
144
+ const stats = log.getStats();
145
+ console.log(stats);
146
+ // {
147
+ // sampling: { total: { error: 150 }, sampled: { debug: 99 }, rateLimited: { error: 5 } },
148
+ // deduplication: { totalDeduplicated: 50, uniqueMessages: 20 },
149
+ // performance: { enabled: true, avgLogProcessingTime: 0.5 }
150
+ // }
151
+ ```
152
+
153
+ ### Log Deduplication
154
+
155
+ Automatically group and squish duplicate log messages:
156
+
157
+ ```javascript
158
+ const log = logger('MyApp', {
159
+ deduplication: {
160
+ enabled: true,
161
+ threshold: 3, // After 3 duplicates, start squishing
162
+ window: 60000 // 60 second window
163
+ }
164
+ });
165
+
166
+ // If this message appears 11 times:
167
+ log.debug('Ticket already exists for threshold a481199f-ed1c-47c6-834d-9cf54cdc394e and device 2642');
168
+
169
+ // It will be logged once as:
170
+ // "Ticket already exists for threshold * and device * (x11)"
171
+ ```
172
+
173
+ ### Performance Metrics (Development Only)
174
+
175
+ Track performance in development mode:
176
+
177
+ ```javascript
178
+ const log = logger('MyApp', {
179
+ performanceMetrics: true // Auto-enabled in development
180
+ });
181
+
182
+ log.time('database-query');
183
+ // ... do work ...
184
+ const duration = log.timeEnd('database-query');
185
+ // Logs: "Timer 'database-query' completed in 45.23ms"
186
+ ```
187
+
188
+ ### Non-Blocking Operations
189
+
190
+ All logging operations are non-blocking and return immediately:
191
+
192
+ ```javascript
193
+ const log = logger('MyApp');
194
+
195
+ // All these return immediately, processing happens asynchronously
196
+ log.info('Message 1');
197
+ log.info('Message 2');
198
+ log.info('Message 3');
199
+
200
+ // Flush all pending logs (returns Promise)
201
+ await log.flush();
202
+
203
+ // Close logger gracefully (returns Promise)
204
+ await log.close();
205
+ ```
62
206
 
63
207
  ## Custom Levels and Colors
64
208
  NightTimeLogger provides custom log levels and colors for enhanced logging experience:
package/index.d.ts ADDED
@@ -0,0 +1,225 @@
1
+ /**
2
+ * TypeScript definitions for NightTimeLogger
3
+ * @file index.d.ts
4
+ */
5
+
6
+ /**
7
+ * Log levels supported by NightTimeLogger
8
+ */
9
+ export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'internal';
10
+
11
+ /**
12
+ * Sampling configuration for log levels
13
+ */
14
+ export interface SamplingConfig {
15
+ [level: string]: number; // 0.0 to 1.0, where 1.0 = log all, 0.1 = log 10%
16
+ }
17
+
18
+ /**
19
+ * Rate limit configuration for a log level
20
+ */
21
+ export interface RateLimitConfig {
22
+ max: number; // Maximum number of logs
23
+ window: number; // Time window in milliseconds
24
+ }
25
+
26
+ /**
27
+ * Rate limiting configuration for log levels
28
+ */
29
+ export interface RateLimitConfigMap {
30
+ [level: string]: RateLimitConfig;
31
+ }
32
+
33
+ /**
34
+ * Deduplication configuration
35
+ */
36
+ export interface DeduplicationConfig {
37
+ enabled: boolean;
38
+ threshold: number; // Number of duplicates before squishing (default: 3)
39
+ window: number; // Time window in milliseconds (default: 60000)
40
+ }
41
+
42
+ /**
43
+ * Plugin configuration
44
+ */
45
+ export interface PluginConfig {
46
+ name: string;
47
+ enabled?: boolean;
48
+ config?: Record<string, any>;
49
+ }
50
+
51
+ /**
52
+ * Logger configuration options
53
+ */
54
+ export interface LoggerConfig {
55
+ /** Minimum log level to be logged */
56
+ level?: LogLevel;
57
+ /** Enable console logging (default: true) */
58
+ console?: boolean;
59
+ /** Enable file logging (default: true) */
60
+ file?: boolean;
61
+ /** Filename for log file (default: `${location}.log`) */
62
+ filename?: string;
63
+ /** Directory path where log files will be saved (default: './logs') */
64
+ path?: string;
65
+ /** Maximum size of log file in bytes (default: 1048576 = 1MB) */
66
+ maxSize?: number;
67
+ /** Maximum number of log files to retain (default: 5) */
68
+ maxFiles?: number;
69
+ /** Include timestamp in log messages (default: true) */
70
+ timestamp?: boolean;
71
+ /** Skip cache and create new logger instance (default: false) */
72
+ skipCache?: boolean;
73
+ /** Enable call site path reporting (default: false) */
74
+ reportPath?: boolean;
75
+ /** Enable debug mode for logger itself (default: false) */
76
+ debug?: boolean;
77
+ /** Log sampling rates per level */
78
+ sampling?: SamplingConfig;
79
+ /** Rate limiting configuration per level */
80
+ rateLimit?: RateLimitConfigMap;
81
+ /** Log deduplication configuration */
82
+ deduplication?: DeduplicationConfig;
83
+ /** Enable performance metrics (default: NODE_ENV === 'development') */
84
+ performanceMetrics?: boolean;
85
+ /** Interval in milliseconds to log statistics (default: 0 = disabled) */
86
+ statsInterval?: number;
87
+ /** Plugin configurations */
88
+ plugins?: PluginConfig[];
89
+ }
90
+
91
+ /**
92
+ * Statistics object returned by getStats()
93
+ */
94
+ export interface LoggerStats {
95
+ sampling: {
96
+ total: Record<string, number>;
97
+ sampled: Record<string, number>;
98
+ rateLimited: Record<string, number>;
99
+ } | null;
100
+ deduplication: {
101
+ totalDeduplicated: number;
102
+ uniqueMessages: number;
103
+ activeEntries: number;
104
+ } | null;
105
+ performance: {
106
+ enabled: boolean;
107
+ avgLogProcessingTime?: number;
108
+ avgTransportTime?: number;
109
+ logProcessingSamples?: number;
110
+ transportSamples?: number;
111
+ } | null;
112
+ }
113
+
114
+ /**
115
+ * Logger instance interface
116
+ */
117
+ export interface Logger {
118
+ /**
119
+ * Log an informational message
120
+ * @param message - The message to log
121
+ * @param meta - Optional metadata object
122
+ */
123
+ info(message: string, meta?: Record<string, any>): void;
124
+ info(message: Record<string, any>): void;
125
+
126
+ /**
127
+ * Log a warning message
128
+ * @param message - The message to log
129
+ * @param meta - Optional metadata object
130
+ */
131
+ warn(message: string, meta?: Record<string, any>): void;
132
+ warn(message: Record<string, any>): void;
133
+
134
+ /**
135
+ * Log an error message
136
+ * @param message - The message to log
137
+ * @param meta - Optional metadata object
138
+ */
139
+ error(message: string, meta?: Record<string, any>): void;
140
+ error(message: Record<string, any>): void;
141
+
142
+ /**
143
+ * Log a debug message
144
+ * @param message - The message to log
145
+ * @param meta - Optional metadata object
146
+ */
147
+ debug(message: string, meta?: Record<string, any>): void;
148
+ debug(message: Record<string, any>): void;
149
+
150
+ /**
151
+ * Log a trace message
152
+ * @param message - The message to log
153
+ * @param meta - Optional metadata object
154
+ */
155
+ trace(message: string, meta?: Record<string, any>): void;
156
+ trace(message: Record<string, any>): void;
157
+
158
+ /**
159
+ * Log a fatal message
160
+ * @param message - The message to log
161
+ * @param meta - Optional metadata object
162
+ */
163
+ fatal(message: string, meta?: Record<string, any>): void;
164
+ fatal(message: Record<string, any>): void;
165
+
166
+ /**
167
+ * Log an internal message (logger system messages)
168
+ * @param message - The message to log
169
+ * @param meta - Optional metadata object
170
+ */
171
+ internal(message: string, meta?: Record<string, any>): void;
172
+ internal(message: Record<string, any>): void;
173
+
174
+ /**
175
+ * Create a child logger with persistent context
176
+ * @param context - Context object to merge into all logs
177
+ * @returns Child logger instance
178
+ */
179
+ child(context: Record<string, any>): Logger;
180
+
181
+ /**
182
+ * Start a performance timer (development mode only)
183
+ * @param label - Timer label
184
+ */
185
+ time?(label: string): void;
186
+
187
+ /**
188
+ * End a performance timer and log duration (development mode only)
189
+ * @param label - Timer label
190
+ * @returns Duration in milliseconds or null if timer not found
191
+ */
192
+ timeEnd?(label: string): number | null;
193
+
194
+ /**
195
+ * Get logging statistics
196
+ * @returns Statistics object
197
+ */
198
+ getStats(): LoggerStats;
199
+
200
+ /**
201
+ * Flush all pending logs (returns Promise)
202
+ * @returns Promise that resolves when all logs are flushed
203
+ */
204
+ flush(): Promise<void>;
205
+
206
+ /**
207
+ * Close the logger and flush all pending logs (returns Promise)
208
+ * @returns Promise that resolves when logger is closed
209
+ */
210
+ close(): Promise<void>;
211
+
212
+ /** Current log level */
213
+ level: string;
214
+ }
215
+
216
+ /**
217
+ * Main logger function
218
+ * @param location - The location/name of the logger instance
219
+ * @param config - Configuration options for the logger
220
+ * @returns Logger instance
221
+ */
222
+ declare function logger(location?: string, config?: LoggerConfig): Logger;
223
+
224
+ export = logger;
225
+
@@ -0,0 +1,146 @@
1
+ /**
2
+ * @file /lib/logDeduplicator.js
3
+ * @description Handles log deduplication/squishing by grouping similar messages.
4
+ */
5
+
6
+ const crypto = require('crypto');
7
+ const { normalizeMessage } = require('./messageNormalizer');
8
+
9
+ /**
10
+ * Creates a fingerprint for a log message
11
+ * @param {string} level - Log level
12
+ * @param {string} message - Log message
13
+ * @param {string} location - Logger location
14
+ * @param {string} filePath - File path (if available)
15
+ * @returns {string} - Message fingerprint
16
+ */
17
+ function createFingerprint(level, message, location, filePath) {
18
+ const normalized = normalizeMessage(message);
19
+ const key = `${level}:${normalized}:${location || ''}:${filePath || ''}`;
20
+ return crypto.createHash('md5').update(key).digest('hex');
21
+ }
22
+
23
+ /**
24
+ * Log deduplicator class
25
+ */
26
+ class LogDeduplicator {
27
+ constructor(config = {}) {
28
+ this.enabled = config.enabled || false;
29
+ this.threshold = config.threshold || 3;
30
+ this.window = config.window || 60000; // 60 seconds default
31
+ this.entries = new Map(); // fingerprint -> { count, firstSeen, lastSeen, sample }
32
+ this.stats = {
33
+ totalDeduplicated: 0,
34
+ uniqueMessages: 0,
35
+ };
36
+
37
+ // Cleanup old entries periodically
38
+ if (this.enabled) {
39
+ this.cleanupInterval = setInterval(() => this.cleanup(), this.window);
40
+ // Use unref() to allow process to exit if this is the only timer
41
+ if (this.cleanupInterval.unref) {
42
+ this.cleanupInterval.unref();
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Check if a log should be deduplicated
49
+ * @param {string} level - Log level
50
+ * @param {string} message - Log message
51
+ * @param {object} meta - Log metadata
52
+ * @returns {object|null} - Returns { shouldLog: boolean, count: number, message: string } or null if not deduplicated
53
+ */
54
+ check(level, message, meta = {}) {
55
+ if (!this.enabled) {
56
+ return null;
57
+ }
58
+
59
+ const fingerprint = createFingerprint(level, message, meta.location, meta.filePath);
60
+ const now = Date.now();
61
+ const entry = this.entries.get(fingerprint);
62
+
63
+ if (!entry) {
64
+ // First occurrence
65
+ this.entries.set(fingerprint, {
66
+ count: 1,
67
+ firstSeen: now,
68
+ lastSeen: now,
69
+ sample: { level, message, meta },
70
+ });
71
+ this.stats.uniqueMessages++;
72
+ return { shouldLog: true, count: 1, message };
73
+ }
74
+
75
+ // Update entry
76
+ entry.count++;
77
+ entry.lastSeen = now;
78
+
79
+ if (entry.count < this.threshold) {
80
+ // Still below threshold, log normally
81
+ return { shouldLog: true, count: entry.count, message };
82
+ } else if (entry.count === this.threshold) {
83
+ // Just reached threshold, log with count
84
+ this.stats.totalDeduplicated += entry.count - 1; // Count suppressed logs
85
+ return {
86
+ shouldLog: true,
87
+ count: entry.count,
88
+ message: `${message} (x${entry.count})`,
89
+ };
90
+ } else {
91
+ // Above threshold, suppress but update count
92
+ this.stats.totalDeduplicated++;
93
+ return { shouldLog: false, count: entry.count, message: `${message} (x${entry.count})` };
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Clean up old entries
99
+ */
100
+ cleanup() {
101
+ const now = Date.now();
102
+ for (const [fingerprint, entry] of this.entries.entries()) {
103
+ if (now - entry.lastSeen > this.window) {
104
+ this.entries.delete(fingerprint);
105
+ }
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Get statistics
111
+ * @returns {object} - Statistics object
112
+ */
113
+ getStats() {
114
+ return {
115
+ ...this.stats,
116
+ activeEntries: this.entries.size,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Reset statistics
122
+ */
123
+ resetStats() {
124
+ this.stats = {
125
+ totalDeduplicated: 0,
126
+ uniqueMessages: 0,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Destroy the deduplicator
132
+ */
133
+ destroy() {
134
+ if (this.cleanupInterval) {
135
+ clearInterval(this.cleanupInterval);
136
+ this.cleanupInterval = null;
137
+ }
138
+ this.entries.clear();
139
+ }
140
+ }
141
+
142
+ module.exports = {
143
+ LogDeduplicator,
144
+ createFingerprint,
145
+ };
146
+
@@ -0,0 +1,139 @@
1
+ /**
2
+ * @file /lib/logSampler.js
3
+ * @description Handles log sampling and rate limiting with statistics.
4
+ */
5
+
6
+ /**
7
+ * Log sampler and rate limiter class
8
+ */
9
+ class LogSampler {
10
+ constructor(config = {}) {
11
+ this.sampling = config.sampling || {};
12
+ this.rateLimit = config.rateLimit || {};
13
+ this.stats = {
14
+ total: {},
15
+ sampled: {},
16
+ rateLimited: {},
17
+ };
18
+
19
+ // Rate limit tracking: level -> { count: number, windowStart: number }
20
+ this.rateLimitCounters = new Map();
21
+
22
+ // Cleanup old rate limit entries periodically
23
+ if (Object.keys(this.rateLimit).length > 0) {
24
+ this.cleanupInterval = setInterval(() => this.cleanupRateLimits(), 60000);
25
+ // Use unref() to allow process to exit if this is the only timer
26
+ if (this.cleanupInterval.unref) {
27
+ this.cleanupInterval.unref();
28
+ }
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Check if a log should be processed based on sampling and rate limiting
34
+ * @param {string} level - Log level
35
+ * @returns {boolean} - True if log should be processed, false if skipped
36
+ */
37
+ shouldProcess(level) {
38
+ // Initialize stats for level if needed
39
+ if (!this.stats.total[level]) {
40
+ this.stats.total[level] = 0;
41
+ this.stats.sampled[level] = 0;
42
+ this.stats.rateLimited[level] = 0;
43
+ }
44
+
45
+ this.stats.total[level]++;
46
+
47
+ // Check rate limiting first (more restrictive)
48
+ if (this.rateLimit[level]) {
49
+ const limit = this.rateLimit[level];
50
+ const now = Date.now();
51
+ const key = level;
52
+ const counter = this.rateLimitCounters.get(key);
53
+
54
+ if (counter) {
55
+ // Check if we're still in the same window
56
+ if (now - counter.windowStart < limit.window) {
57
+ if (counter.count >= limit.max) {
58
+ // Rate limit exceeded
59
+ this.stats.rateLimited[level]++;
60
+ return false;
61
+ }
62
+ counter.count++;
63
+ } else {
64
+ // New window
65
+ this.rateLimitCounters.set(key, { count: 1, windowStart: now });
66
+ }
67
+ } else {
68
+ // First log in this level
69
+ this.rateLimitCounters.set(key, { count: 1, windowStart: now });
70
+ }
71
+ }
72
+
73
+ // Check sampling
74
+ if (this.sampling[level] !== undefined) {
75
+ const rate = this.sampling[level];
76
+ if (rate < 1.0 && Math.random() > rate) {
77
+ // Sample rejected
78
+ this.stats.sampled[level]++;
79
+ return false;
80
+ }
81
+ }
82
+
83
+ return true;
84
+ }
85
+
86
+ /**
87
+ * Clean up old rate limit entries
88
+ */
89
+ cleanupRateLimits() {
90
+ const now = Date.now();
91
+ for (const [level, limit] of Object.entries(this.rateLimit)) {
92
+ const key = level;
93
+ const counter = this.rateLimitCounters.get(key);
94
+ if (counter && now - counter.windowStart >= limit.window) {
95
+ // Entry expired, will be reset on next use
96
+ this.rateLimitCounters.delete(key);
97
+ }
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Get statistics
103
+ * @returns {object} - Statistics object
104
+ */
105
+ getStats() {
106
+ return {
107
+ total: { ...this.stats.total },
108
+ sampled: { ...this.stats.sampled },
109
+ rateLimited: { ...this.stats.rateLimited },
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Reset statistics
115
+ */
116
+ resetStats() {
117
+ this.stats = {
118
+ total: {},
119
+ sampled: {},
120
+ rateLimited: {},
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Destroy the sampler
126
+ */
127
+ destroy() {
128
+ if (this.cleanupInterval) {
129
+ clearInterval(this.cleanupInterval);
130
+ this.cleanupInterval = null;
131
+ }
132
+ this.rateLimitCounters.clear();
133
+ }
134
+ }
135
+
136
+ module.exports = {
137
+ LogSampler,
138
+ };
139
+