• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / logger / #127

pending completion
#127

push

TwitchBronBron
Fix crash when encountering bigint

126 of 128 branches covered (98.44%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

124 of 126 relevant lines covered (98.41%)

12.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.16
/src/Logger.ts
1
import * as safeJsonStringify from 'safe-json-stringify';
1✔
2
import { serializeError } from 'serialize-error';
1✔
3
import type { ChalkFunction } from 'chalk';
4
// eslint-disable-next-line @typescript-eslint/no-require-imports
5
import Chalk = require('chalk');
1✔
6
//export our instance of chalk for use in unit tests
7
export const chalk = new Chalk.Instance({ level: 3 });
1✔
8

9
export class Logger {
1✔
10

11
    constructor(prefix?: string);
12
    constructor(options?: Partial<LoggerOptions>);
13
    constructor(options?: Partial<LoggerOptions> | string) {
14
        this.options = this.sanitizeOptions(options);
66✔
15
    }
16

17
    /**
18
     * The options used to drive the functionality of this logger
19
     */
20
    private options: LoggerOptions;
21

22
    public get logLevel(): LogLevel {
23
        return this.options.logLevel ?? this.options.parent?.logLevel ?? 'log';
32✔
24
    }
25
    public set logLevel(value) {
26
        this.options.logLevel = value;
14✔
27
    }
28

29
    /**
30
     * Get the prefix of this logger and all its parents
31
     */
32
    private getPrefixes(): string[] {
33
        const prefixes = this.options.parent?.getPrefixes() ?? [];
40✔
34
        if (this.options.prefix) {
40✔
35
            prefixes.push(this.options.prefix);
8✔
36
        }
37
        return prefixes;
40✔
38
    }
39

40
    /**
41
     * The prefix for the current logger only. This excludes prefixes inherited from parent loggers.
42
     */
43
    public get prefix() {
44
        return this.options.prefix;
3✔
45
    }
46
    public set prefix(value: string | undefined) {
47
        this.options.prefix = value;
2✔
48
    }
49

50
    public get parent(): Logger | undefined {
51
        return this.options.parent;
2✔
52
    }
53
    public set parent(value: Logger | undefined) {
54
        this.options.parent = value;
1✔
55
    }
56

57
    public get transports() {
58
        return this.options.transports;
4✔
59
    }
60
    public set transports(value: Transport[]) {
61
        this.options.transports = value;
6✔
62
    }
63

64
    /**
65
     * If true, colors will be used in transports that support it.
66
     */
67
    public get enableColor(): boolean {
68
        return this.options.enableColor ?? this.options.parent?.enableColor ?? true;
12✔
69
    }
70
    public set enableColor(value: boolean) {
71
        this.options.enableColor = value;
3✔
72
    }
73

74
    /**
75
     * Should the log level be padded with trailing spaces when printed
76
     */
77
    public get consistentLogLevelWidth(): boolean {
78
        return this.options.consistentLogLevelWidth ?? this.options.parent?.consistentLogLevelWidth ?? false;
17!
79
    }
80
    public set consistentLogLevelWidth(value: boolean) {
81
        this.options.consistentLogLevelWidth = value;
×
82
    }
83

84
    /**
85
    * Get notified about every log message
86
    * @param subscriber a function that is called with the given log message
87
    * @returns an unsubscribe function
88
    */
89
    public subscribe(subscriber: MessageHandler) {
90
        return this.addTransport({
6✔
91
            pipe: subscriber
92
        });
93
    }
94

95
    /**
96
     * Register a transport handler to be notified of all log events
97
     */
98
    public addTransport(transport: Transport) {
99
        this.options.transports.push(transport);
11✔
100
        return () => {
11✔
101
            this.removeTransport(transport);
2✔
102
        };
103
    }
104

105
    /**
106
     * Remove a transport from this logger instance (but not parents
107
     */
108
    public removeTransport(transport: Transport) {
109
        const index = this.options.transports.indexOf(transport);
2✔
110
        if (index > -1) {
2✔
111
            this.options.transports.splice(index, 1);
1✔
112
        }
113
    }
114

115
    private emit(message: LogMessage) {
116
        for (const transport of this.options.transports ?? []) {
21✔
117
            transport.pipe(message);
13✔
118
        }
119
        //emit to parent as well
120
        this.options.parent?.emit(message);
21✔
121
    }
122

123
    public formatTimestamp(date: Date) {
124
        return date.getHours().toString().padStart(2, '0') +
31✔
125
            ':' +
126
            date.getMinutes().toString().padStart(2, '0') +
127
            ':' +
128
            date.getSeconds().toString().padStart(2, '0') +
129
            '.' + date.getMilliseconds().toString().padEnd(3, '0').substring(0, 3);
130
    }
131

132
    /**
133
     * Get the current date. Mostly here to allow mocking for unit tests
134
     */
135
    private getCurrentDate() {
136
        return new Date();
1✔
137
    }
138

139
    /**
140
     * Given an array of args, stringify them
141
     */
142
    public stringifyArgs(args: unknown[]) {
143
        let argsText = '';
43✔
144
        for (let i = 0; i < args.length; i++) {
43✔
145
            let arg = args[i];
46✔
146
            //separate args with a space
147
            if (i > 0) {
46✔
148
                argsText += ' ';
3✔
149
            }
150
            const argType = typeof arg;
46✔
151
            switch (argType) {
46✔
152
                case 'string':
153
                    argsText += arg;
28✔
154
                    break;
28✔
155
                case 'undefined':
156
                    argsText += 'undefined';
1✔
157
                    break;
1✔
158
                case 'object':
159
                    if (toString.call(arg) === '[object RegExp]') {
6✔
160
                        argsText += (arg as RegExp).toString();
1✔
161
                    } else {
162
                        argsText += safeJsonStringify(
5✔
163
                            serializeError(arg),
164
                            (_, value) => {
165
                                return typeof value === 'bigint' ? value.toString() : value;
10✔
166
                            }
167
                        );
168
                    }
169
                    break;
6✔
170
                default:
171
                    argsText += (arg as any).toString();
11✔
172
                    break;
11✔
173
            }
174
        }
175
        return argsText;
43✔
176
    }
177

178
    /**
179
     * Get all the leading parts of the message. This includes timestamp, log level, any message prefixes.
180
     * This excludes actual body of the messages.
181
     */
182
    public formatLeadingMessageParts(message: LogMessage, enableColor = this.enableColor) {
5✔
183
        let timestampText = '[' + message.timestamp + ']';
12✔
184
        let logLevelText = message.logLevel.toUpperCase();
12✔
185
        if (this.consistentLogLevelWidth) {
12!
186
            logLevelText = logLevelText.padEnd(5, ' ');
×
187
        }
188
        if (enableColor) {
12✔
189
            timestampText = chalk.grey(timestampText);
5✔
190
            const logColorFn = LogLevelColor[message.logLevel] ?? LogLevelColor.log;
5✔
191
            logLevelText = logColorFn(logLevelText);
5✔
192
        }
193

194
        let result = timestampText + '[' + logLevelText + ']';
12✔
195

196
        const prefix = message.prefixes.join('');
12✔
197
        if (prefix.length > 0) {
12✔
198
            result += ' ' + prefix;
2✔
199
        }
200

201
        return result;
12✔
202
    }
203

204
    /**
205
     * Build a single string from the LogMessage in the Logger-standard format
206
     */
207
    public formatMessage(message: LogMessage, enableColor = false) {
4✔
208
        return this.formatLeadingMessageParts(message, enableColor) + ' ' + message.argsText;
7✔
209
    }
210

211
    /**
212
     * The base logging function. Provide a level
213
     */
214
    public buildLogMessage(logLevel: LogLevel, ...args: unknown[]) {
215
        const date = this.getCurrentDate();
30✔
216
        const timestamp = this.formatTimestamp(date);
30✔
217

218
        return {
30✔
219
            date: date,
220
            timestamp: timestamp,
221
            prefixes: this.getPrefixes(),
222
            logLevel: logLevel,
223
            args: args,
224
            argsText: this.stringifyArgs(args),
225
            logger: this
226
        } as LogMessage;
227
    }
228

229
    /**
230
     * Determine if the specified logLevel is currently active.
231
     */
232
    public isLogLevelEnabled(logLevel: LogLevel) {
233
        const lowerLogLevel = logLevel.toLowerCase();
20✔
234
        const incomingPriority = LogLevelPriority[lowerLogLevel] ?? LogLevelPriority.log;
20✔
235
        return LogLevelPriority[this.logLevel] >= incomingPriority;
20✔
236
    }
237

238
    /**
239
     * Write a log entry IF the specified logLevel is enabled
240
     */
241
    public write(logLevel: LogLevel, ...args: unknown[]) {
242
        if (this.isLogLevelEnabled(logLevel)) {
20✔
243
            const message = this.buildLogMessage(logLevel, ...args);
19✔
244
            this.emit(message);
19✔
245
        }
246
    }
247

248
    public trace(...messages: unknown[]) {
249
        this.write('trace', ...messages);
1✔
250
    }
251

252
    public debug(...messages: unknown[]) {
253
        this.write('debug', ...messages);
1✔
254
    }
255

256
    public info(...messages: unknown[]) {
257
        this.write('info', ...messages);
1✔
258
    }
259

260
    public log(...messages: unknown[]) {
261
        this.write('log', ...messages);
12✔
262
    }
263

264
    public warn(...messages: unknown[]) {
265
        this.write('warn', ...messages);
3✔
266
    }
267

268
    public error(...messages: unknown[]) {
269
        this.write('error', ...messages);
1✔
270
    }
271

272
    /**
273
     * Create a new logger that inherits all the properties of this current logger.
274
     * This is a one-time copy of the parent's properties to the child, so future changes to the parent logger will not
275
     * be reflected on the child logger.
276
     */
277
    public createLogger(): Logger;
278
    public createLogger(prefix: string): Logger;
279
    public createLogger(options: Partial<LoggerOptions>): Logger;
280
    public createLogger(param?: string | Partial<LoggerOptions>): Logger {
281
        const options = typeof param === 'string' ? { prefix: param } : param;
15✔
282
        return new Logger({
15✔
283
            ...options ?? {},
45✔
284
            parent: this
285
        });
286
    }
287

288
    /**
289
     * Create a new logger and pass it in as the first parameter of a callback.
290
     * This allows to created nested namespaced loggers without explicitly creating the
291
     * intermediary logger variable.
292
     * @returns any return value that the callback produces.
293
     */
294
    public useLogger<T>(prefix: string, callback: (logger: Logger) => T): T;
295
    public useLogger<T>(options: Partial<LoggerOptions>, callback: (logger: Logger) => T): T;
296
    public useLogger<T>(param: Partial<LoggerOptions> | string, callback: (logger: Logger) => T): T {
297
        const logger = this.createLogger(param as string); //typecast as string (to tell typescript to chill out)
2✔
298
        return callback(logger);
2✔
299
    }
300

301
    /**
302
     * Ensure we have a stable set of options.
303
     * @param options
304
     * @returns
305
     */
306
    private sanitizeOptions(param?: Partial<LoggerOptions> | string) {
307
        const options = typeof param === 'string' ? { prefix: param } : param;
68✔
308
        const result = {
68✔
309
            transports: [],
310
            prefix: undefined,
311
            ...options ?? {}
204✔
312
        } as LoggerOptions;
313
        result.logLevel = result.logLevel?.toLowerCase() as LogLevel;
68✔
314
        return result;
68✔
315
    }
316

317
    public destroy() {
318
        for (const transport of this.options?.transports ?? []) {
5✔
319
            transport?.destroy?.();
3✔
320
        }
321
        if (this.options) {
5✔
322
            this.options.transports = [];
4✔
323
            this.options.parent = undefined;
4✔
324
        }
325
    }
326
}
327

328
export const LogLevelPriority = {
1✔
329
    off: 0,
330
    error: 1,
331
    warn: 2,
332
    log: 3,
333
    info: 4,
334
    debug: 5,
335
    trace: 6
336
} as Record<string, number>;
337

338
export type LogLevel = 'off' | 'error' | 'warn' | 'log' | 'info' | 'debug' | 'trace';
339

340
export interface LoggerOptions {
341
    /**
342
     * A prefix applied to every log entry. Appears directly after the logLevel
343
     */
344
    prefix: string | undefined;
345
    /**
346
     * The level of logging that should be emitted.
347
     */
348
    logLevel: LogLevel;
349
    /**
350
     * A list of functions that will be called whenever a log message is received
351
     */
352
    transports: Transport[];
353
    /**
354
     * A parent logger. Any unspecified options in the current logger will be loaded from the parent.
355
     */
356
    parent?: Logger;
357
    /**
358
     * If true, colors will be used in transports that support it.
359
     */
360
    enableColor?: boolean;
361
    /**
362
     * Should the log level be padded with trailing spaces when printed
363
     */
364
    consistentLogLevelWidth?: boolean;
365
}
366

367
export interface LogMessage {
368
    /**
369
     * A js Date instance when the log message was created
370
     */
371
    date: Date;
372
    /**
373
     * A formatted timestamp string
374
     */
375
    timestamp: string;
376
    /**
377
     * The LogLevel this LogMessage was emitted with.
378
     */
379
    logLevel: LogLevel;
380
    /**
381
     * The list of prefixes at the time of this LogMessage. Empty prefixes are omitted.
382
     */
383
    prefixes: string[];
384
    /**
385
     * The arguments passed to the log function
386
     */
387
    args: unknown[];
388
    /**
389
     * The stringified version of the arguments
390
     */
391
    argsText: string;
392
    /**
393
     * The instance of the logger this message was created with
394
     */
395
    logger: Logger;
396
}
397

398
export type MessageHandler = (message: LogMessage) => void;
399

400
export interface Transport {
401
    /**
402
     * Receives the incoming message
403
     */
404
    pipe(message: LogMessage): void;
405
    /**
406
     * Called whenever the logger is destroyed, allows the transport to clean itself up
407
     */
408
    destroy?(): void;
409
}
410

411
export const LogLevelColor = {
1✔
412
    off: x => x,
1✔
413
    error: chalk.red,
414
    warn: chalk.yellow,
415
    log: x => x,
2✔
416
    info: chalk.green,
417
    debug: chalk.blue,
418
    trace: chalk.magenta
419
} as Record<string, ChalkFunction>;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc