-
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathlog.js
More file actions
385 lines (346 loc) · 12.1 KB
/
log.js
File metadata and controls
385 lines (346 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
import ora from 'ora'
import winston from 'winston'
/**
* Log class for logging messages with different severity levels.
* Supports enabling/disabling debug mode with token masking for security.
* Uses singleton pattern to ensure only one instance is created throughout the application.
* Handles both console output and file logging depending on debug mode.
*/
export class Log {
#entity
#token
#isDebug
#spinner
#logger
/**
* Creates a new Log instance with debug mode configuration.
* @param {string} entity - The entity name used for log file naming
* @param {string} token - The authentication token to mask in logs
* @param {boolean} [isDebug=false] - Enable debug mode
*/
constructor(entity, token, isDebug = false) {
this.#entity = entity
this.#token = token
this.#isDebug = Boolean(isDebug) || process.env.DEBUG === 'true'
this.#spinner = this.#isDebug ? null : ora()
if (this.#isDebug) {
this.#logger = this.createWinstonLogger()
}
}
/**
* Creates Winston logger configuration for debug mode.
* @returns {winston.Logger} Configured Winston logger instance
* @private
*/
createWinstonLogger() {
// Common format for timestamp and message formatting
const commonFormat = winston.format.printf(({timestamp, level, message, ...meta}) => {
const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta, null, 2)}` : ''
return `${timestamp} [${level}]: ${message}${metaStr}`
})
// Console transport with colors
const consoleFormat = winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss',
}),
winston.format.colorize(),
commonFormat,
)
// File transport without colors or TTY formatting
const fileFormat = winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss',
}),
winston.format.uncolorize(),
commonFormat,
)
return winston.createLogger({
level: 'debug',
transports: [
new winston.transports.Console({
format: consoleFormat,
}),
new winston.transports.File({
level: 'debug',
filename: `logs/debug.${this.#entity.replace('/', '_')}.${new Date().toISOString().slice(0, 10)}.log`,
format: fileFormat,
}),
],
})
}
/**
* Formats message for logging, converting objects to JSON strings.
* @param {any} msg - The message to format
* @returns {string} Formatted message string
* @private
*/
formatMessage(msg) {
return typeof msg === 'object' && msg !== null ? JSON.stringify(msg) : msg
}
/**
* Logs a message in debug mode using Winston logger or falls back to console.
* Provides a consistent logging interface regardless of logger availability.
* Note: Messages should already be masked before calling this method.
* @param {string} message - The message to log (should be pre-masked)
* @param {string} [level='info'] - The log level (info, warn, error, debug)
* @private
*/
logInDebugMode(message, level = 'info') {
if (this.#logger) {
this.#logger.log(level, message)
} else {
console.log(message)
}
}
get entity() {
return this.#entity
}
/**
* Gets the debug mode status.
* @returns {boolean} True if debug mode is enabled
*/
get isDebug() {
return this.#isDebug
}
/**
* Masks sensitive tokens in objects or strings.
* Recursively looks for and replaces any occurrences of the authentication token
* to prevent accidental exposure in logs.
* @param {any} value - The value to mask (object or string)
* @returns {any} The masked value with sensitive information replaced by '***'
*/
maskSensitive(value) {
// Early return if no token to mask or value is null/undefined
if (!this.#token || value == null) {
return value
}
// Handle string values directly
if (typeof value === 'string') {
// Using a safe string replacement with global flag to replace all occurrences
return this.#token ? value.replace(new RegExp(this.escapeRegExp(this.#token), 'g'), '***') : value
}
// Handle objects (including arrays)
if (typeof value === 'object') {
const clone = Array.isArray(value) ? [...value] : {...value}
// Use Object.keys() to iterate only own enumerable properties
const keys = Array.isArray(clone) ? clone.keys() : Object.keys(clone)
for (const key of keys) {
// Special case for property named 'token'
if (key === 'token' && typeof clone[key] === 'string') {
clone[key] = '***'
continue
}
// Handle string values that contain the token
if (typeof clone[key] === 'string' && this.#token && clone[key].includes(this.#token)) {
clone[key] = clone[key].replace(new RegExp(this.escapeRegExp(this.#token), 'g'), '***')
}
// Recursively process nested objects, but skip prototype-pollution
// vectors to avoid writing into __proto__/constructor/prototype sinks
else if (typeof clone[key] === 'object' && clone[key] !== null) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue
}
clone[key] = this.maskSensitive(clone[key])
}
}
return clone
}
return value
}
/**
* Escapes special characters in a string for use in a RegExp.
* @param {string} string - The string to escape
* @returns {string} The escaped string
* @private
*/
escapeRegExp(string) {
// Escape special RegExp characters to avoid regex syntax errors
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
/**
* Helper method to handle logging with optional prefix and appropriate console method.
* @param {Function} consoleMethod - The console method to use (log, warn, error, debug)
* @param {string|object} msg - The message to log
* @param {...any} args - Additional arguments to log
* @private
*/
logWithPrefix(consoleMethod, msg, ...args) {
// Mask sensitive data once for both message and arguments
const maskedMsg = this.maskSensitive(msg)
const maskedArgs = args.map(arg => this.maskSensitive(arg))
if (this.#isDebug && this.#logger) {
// Use Winston for debug mode logging
const level = this.getWinstonLevel(consoleMethod)
const message = this.formatMessage(maskedMsg)
if (args.length > 0) {
this.#logger.log(level, message, maskedArgs)
} else {
this.#logger.log(level, message)
}
} else {
// Fallback to regular console logging for non-debug mode
if (typeof msg === 'object' && msg !== null) {
consoleMethod(maskedMsg)
} else {
consoleMethod(maskedMsg, maskedArgs)
}
}
}
/**
* Maps console methods to Winston log levels.
* @param {Function} consoleMethod - The console method
* @returns {string} The corresponding Winston log level
* @private
*/
getWinstonLevel(consoleMethod) {
// Map console methods to Winston log levels for proper log categorization
switch (consoleMethod) {
case console.error:
return 'error'
case console.warn:
return 'warn'
case console.debug:
return 'debug'
case console.log:
default:
return 'info'
}
}
/**
* Logs a message without any prefix.
* @param {string|object} msg - The message to log
* @param {...any} args - Additional arguments to log
*/
log(msg, ...args) {
this.logWithPrefix(console.log, msg, ...args)
}
/**
* Logs a message with the '[INFO]' prefix.
* @param {string|object} msg - The message to log
* @param {...any} args - Additional arguments to log
*/
info(msg, ...args) {
this.logWithPrefix(console.log, msg, ...args)
}
/**
* Logs a warning message with the '[WARN]' prefix.
* @param {string|object} msg - The message to log
* @param {...any} args - Additional arguments to log
*/
warn(msg, ...args) {
// Skip warning logging when not in debug mode for better performance
if (!this.#isDebug) return
this.logWithPrefix(console.warn, msg, ...args)
}
/**
* Logs an error message with the '[ERROR]' prefix.
* @param {string|object} msg - The message to log
* @param {...any} args - Additional arguments to log
*/
error(msg, ...args) {
this.logWithPrefix(console.error, msg, ...args)
}
/**
* Logs a debug message with the '[DEBUG]' prefix.
* This method is only active if debug mode is enabled.
* @param {string|object} msg - The message to log
* @param {...any} args - Additional arguments to log
*/
debug(msg, ...args) {
// Skip debug logging when not in debug mode for better performance
if (!this.#isDebug) return
this.logWithPrefix(console.debug, msg, ...args)
}
/**
* Starts a spinner with the given text or logs the text in debug mode.
* Any sensitive tokens in the text are automatically masked.
* @param {string} text - The text to display
*/
start(text) {
const maskedText = this.maskSensitive(text)
if (this.#spinner) {
this.#spinner.start(maskedText)
} else if (this.#logger) {
this.logInDebugMode(maskedText)
}
}
/**
* Stops the spinner and persists the message or logs directly in debug mode.
* All text components are automatically checked for sensitive tokens and masked.
* @param {object} options - Options object
* @param {string} options.symbol - The symbol to display
* @param {string} options.text - The text to display
* @param {string} [options.prefixText=''] - Optional prefix text to prepend
* @param {string} [options.suffixText=''] - Optional suffix text to append
*/
async stopAndPersist({symbol, text, prefixText = '', suffixText = ''}) {
// Mask all text components
const maskedText = this.maskSensitive(text)
const maskedPrefixText = this.maskSensitive(prefixText)
const maskedSuffixText = this.maskSensitive(suffixText)
if (this.#spinner) {
this.#spinner.stopAndPersist({
symbol,
text: maskedText,
suffixText: maskedSuffixText,
prefixText: maskedPrefixText,
})
} else if (this.#logger) {
const message = [maskedPrefixText, symbol, maskedText, maskedSuffixText].join(' ')
this.logInDebugMode(message)
}
}
/**
* Shows a failure message with spinner or logs directly in debug mode.
* Uses error level for debug mode logging to indicate a failure condition.
* Automatically masks any sensitive tokens in the text.
* @param {string} text - The failure message
*/
fail(text) {
const maskedText = this.maskSensitive(text)
if (this.#spinner) {
this.#spinner.fail(maskedText)
} else if (this.#logger) {
this.logInDebugMode(maskedText, 'error')
}
}
/**
* Updates the spinner text or logs the update in debug mode.
* Automatically masks any sensitive tokens in the new text.
* @param {string} newText - The new text to display
*/
set text(newText) {
const maskedText = this.maskSensitive(newText)
if (this.#spinner) {
this.#spinner.text = maskedText
} else if (this.#logger) {
this.logInDebugMode(maskedText)
}
}
/**
* Gets the current spinner text.
* @returns {string} The current spinner text or empty string in debug mode
*/
get text() {
return this.#isDebug ? '' : this.#spinner?.text || ''
}
}
// Singleton pattern to ensure only one instance of Log class is created
let instance = null
/**
* Returns a singleton instance of the Log class.
* @param {string} entity - The entity name used for log file naming
* @param {string} token - The authentication token to mask in logs
* @param {boolean} [isDebug=false] - Enable debug mode
* @param {boolean} [createNewInstance=false] - Create a new instance if true, otherwise return existing instance
* @returns {Log} Instance of Log class
*/
export default function log(entity, token, isDebug = false, createNewInstance = false) {
if (createNewInstance === true) {
return new Log(entity, token, isDebug)
}
if (!instance) {
instance = new Log(entity, token, isDebug)
}
return instance
}