Skip to content

Commit ac173c3

Browse files
committed
feat(node-console-log-transport): expose NodeConsoleLogTransport
BREAKING CHANGE: - NodeConsoleLogTransport no longer stringifies but uses node:utils#inspect
1 parent c575382 commit ac173c3

4 files changed

Lines changed: 233 additions & 0 deletions

File tree

packages/logger/src/public-api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export * from './model/logger.js'
44
export * from './services/base-logger.service.js'
55
export * from './transports/console-json-log-transport/console-json-log.transport.js'
66
export * from './transports/console-json-log-transport/console-json-log-transport-config.js'
7+
export * from './transports/node-console-log-transport/node-console-log.transport.js'
8+
export * from './transports/node-console-log-transport/node-console-log-transport-config.js'
79
export * from './utils/create-json-log-object-data.function.js'
810
export * from './utils/format-error.function.js'
911
export * from './utils/format-error.function.js'
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { InspectOptions } from 'node:util'
2+
3+
import { LogLevel } from '../../model/log-level.enum.js'
4+
5+
export interface NodeConsoleLogTransportConfig {
6+
logLevel: LogLevel
7+
nodeInspectOptions?: InspectOptions
8+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { afterEach, beforeEach, describe, expect, test } from '@jest/globals'
2+
3+
import { ConsoleMock, mockConsole, restoreConsole } from '../../../test/console-mock.function.js'
4+
import { LogLevel } from '../../model/log-level.enum.js'
5+
import { stringToColor } from '../../utils/logger-helper.js'
6+
import { NodeConsoleLogTransport } from './node-console-log.transport.js'
7+
8+
/**
9+
* Formats a Date object to HH:mm:ss.SSS format
10+
* @param {Date} date - The date to format
11+
* @returns {string} Formatted time string in HH:mm:ss.SSS format (e.g., "14:23:45.123")
12+
*/
13+
function formatTime(date: Date): string {
14+
const hours = String(date.getHours()).padStart(2, '0')
15+
const minutes = String(date.getMinutes()).padStart(2, '0')
16+
const seconds = String(date.getSeconds()).padStart(2, '0')
17+
const milliseconds = String(date.getMilliseconds()).padStart(3, '0')
18+
19+
return `${hours}:${minutes}:${seconds}.${milliseconds}`
20+
}
21+
22+
describe('uses console statement according to levels', () => {
23+
let logger: NodeConsoleLogTransport
24+
let logDate: Date
25+
let logArgs: any[]
26+
let formattedDate: string
27+
let consoleMock: ConsoleMock
28+
const className = 'MyClass'
29+
const color = stringToColor(className)
30+
31+
beforeEach(() => {
32+
consoleMock = mockConsole()
33+
logger = new NodeConsoleLogTransport({ logLevel: LogLevel.DEBUG })
34+
logDate = new Date()
35+
logArgs = ['foo bar']
36+
formattedDate = formatTime(logDate)
37+
})
38+
afterEach(restoreConsole)
39+
40+
test('calls correct console level for DEBUG', () => {
41+
logger.log(LogLevel.DEBUG, className, color, logDate, logArgs)
42+
expect(consoleMock.debug).toHaveBeenCalled()
43+
const loggedStr = <string>consoleMock.debug.mock.calls[0][0]
44+
expect(loggedStr.includes(className)).toBeTruthy()
45+
expect(loggedStr.includes(logArgs[0])).toBeTruthy()
46+
expect(loggedStr.includes(formattedDate)).toBeTruthy()
47+
})
48+
test('calls correct console level for INFO', () => {
49+
logger.log(LogLevel.INFO, className, color, logDate, logArgs)
50+
expect(consoleMock.info).toHaveBeenCalled()
51+
const loggedStr = <string>consoleMock.info.mock.calls[0][0]
52+
expect(loggedStr.includes(className)).toBeTruthy()
53+
expect(loggedStr.includes(logArgs[0])).toBeTruthy()
54+
expect(loggedStr.includes(formattedDate)).toBeTruthy()
55+
})
56+
test('calls correct console level for WARN', () => {
57+
logger.log(LogLevel.WARN, className, color, logDate, logArgs)
58+
expect(consoleMock.warn).toHaveBeenCalled()
59+
const loggedStr = <string>consoleMock.warn.mock.calls[0][0]
60+
expect(loggedStr.includes(className)).toBeTruthy()
61+
expect(loggedStr.includes(logArgs[0])).toBeTruthy()
62+
expect(loggedStr.includes(formattedDate)).toBeTruthy()
63+
})
64+
test('calls correct console level for ERROR', () => {
65+
logger.log(LogLevel.ERROR, className, color, logDate, logArgs)
66+
expect(consoleMock.error).toHaveBeenCalled()
67+
const loggedStr = <string>consoleMock.error.mock.calls[0][0]
68+
expect(loggedStr.includes(className)).toBeTruthy()
69+
expect(loggedStr.includes(logArgs[0])).toBeTruthy()
70+
expect(loggedStr.includes(formattedDate)).toBeTruthy()
71+
})
72+
})
73+
74+
describe('respects the configured level', () => {
75+
let consoleMock: ConsoleMock
76+
77+
beforeEach(() => {
78+
consoleMock = mockConsole()
79+
})
80+
afterEach(restoreConsole)
81+
82+
test('respects level DEBUG', () => {
83+
const logger = new NodeConsoleLogTransport({ logLevel: LogLevel.DEBUG })
84+
logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar debug'])
85+
logger.log(LogLevel.INFO, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar info'])
86+
logger.log(LogLevel.WARN, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar warn'])
87+
logger.log(LogLevel.ERROR, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar error'])
88+
expect(consoleMock.debug).toHaveBeenCalledTimes(1)
89+
expect(consoleMock.info).toHaveBeenCalledTimes(1)
90+
expect(consoleMock.warn).toHaveBeenCalledTimes(1)
91+
expect(consoleMock.error).toHaveBeenCalledTimes(1)
92+
})
93+
test('respects level INFO', () => {
94+
const logger = new NodeConsoleLogTransport({ logLevel: LogLevel.INFO })
95+
logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar debug'])
96+
logger.log(LogLevel.INFO, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar info'])
97+
logger.log(LogLevel.WARN, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar warn'])
98+
logger.log(LogLevel.ERROR, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar error'])
99+
expect(consoleMock.debug).toHaveBeenCalledTimes(0)
100+
expect(consoleMock.info).toHaveBeenCalledTimes(1)
101+
expect(consoleMock.warn).toHaveBeenCalledTimes(1)
102+
expect(consoleMock.error).toHaveBeenCalledTimes(1)
103+
})
104+
test('respects level WARN', () => {
105+
const logger = new NodeConsoleLogTransport({ logLevel: LogLevel.WARN })
106+
logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar debug'])
107+
logger.log(LogLevel.INFO, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar info'])
108+
logger.log(LogLevel.WARN, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar warn'])
109+
logger.log(LogLevel.ERROR, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar error'])
110+
expect(consoleMock.debug).toHaveBeenCalledTimes(0)
111+
expect(consoleMock.info).toHaveBeenCalledTimes(0)
112+
expect(consoleMock.warn).toHaveBeenCalledTimes(1)
113+
expect(consoleMock.error).toHaveBeenCalledTimes(1)
114+
})
115+
test('respects level ERROR', () => {
116+
const logger = new NodeConsoleLogTransport({ logLevel: LogLevel.ERROR })
117+
logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar debug'])
118+
logger.log(LogLevel.INFO, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar info'])
119+
logger.log(LogLevel.WARN, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar warn'])
120+
logger.log(LogLevel.ERROR, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar error'])
121+
expect(consoleMock.debug).toHaveBeenCalledTimes(0)
122+
expect(consoleMock.info).toHaveBeenCalledTimes(0)
123+
expect(consoleMock.warn).toHaveBeenCalledTimes(0)
124+
expect(consoleMock.error).toHaveBeenCalledTimes(1)
125+
})
126+
})
127+
128+
describe('prints all the given arguments', () => {
129+
const className = 'MyClass'
130+
const logDate = new Date()
131+
const color = stringToColor(className)
132+
let logger: NodeConsoleLogTransport
133+
let consoleMock: ConsoleMock
134+
135+
beforeEach(() => {
136+
consoleMock = mockConsole()
137+
logger = new NodeConsoleLogTransport({ logLevel: LogLevel.DEBUG })
138+
})
139+
afterEach(restoreConsole)
140+
141+
test('logs Error as Error', () => {
142+
const error = new Error('fail')
143+
logger.log(LogLevel.ERROR, className, color, logDate, [error])
144+
const usedArgs = consoleMock.error.mock.calls[0]
145+
expect(typeof usedArgs[0]).toBe('string')
146+
expect(usedArgs[1]).toMatch(/^Error: fail\n(\s+at.*)\n/) // stack trace follows
147+
})
148+
149+
test('stringifies objects', () => {
150+
const obj = { propA: true }
151+
logger.log(LogLevel.DEBUG, className, color, logDate, [obj])
152+
const usedArgs = consoleMock.debug.mock.calls[0]
153+
expect(typeof usedArgs[0]).toBe('string')
154+
expect(usedArgs[1]).toBe('{ propA: true }') // not json, but util.inspect style
155+
})
156+
157+
})
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import util from 'node:util'
2+
3+
import { colorizeForConsole } from '@shiftcode/utilities'
4+
5+
import { LogLevel } from '../../model/log-level.enum.js'
6+
import { LogTransport } from '../../model/log-transport.js'
7+
import { NodeConsoleLogTransportConfig } from './node-console-log-transport-config.js'
8+
9+
export const logLevelEmoji = ['🐞', '💬', '💣', '🔥']
10+
11+
const timeFormat = new Intl.DateTimeFormat('en-US', {
12+
hour: '2-digit',
13+
minute: '2-digit',
14+
second: '2-digit',
15+
fractionalSecondDigits: 3,
16+
hour12: false,
17+
})
18+
19+
export class NodeConsoleLogTransport extends LogTransport {
20+
private readonly inspectOpts: util.InspectOptions
21+
22+
constructor(config: NodeConsoleLogTransportConfig) {
23+
super(config.logLevel)
24+
this.inspectOpts = config.nodeInspectOptions ?? {}
25+
}
26+
27+
log(level: LogLevel, clazzName: string, hexColor: string, timestamp: Date, args: any[]) {
28+
if (this.isLevelEnabled(level)) {
29+
const now = timeFormat.format(timestamp)
30+
// make sure to not alter the input args array
31+
if (typeof args[0] === 'string') {
32+
// if first arg is string, also colorize it
33+
args = [
34+
`${logLevelEmoji[level]} ${colorizeForConsole(`${now} - ${clazzName} :: ${args[0]}`, hexColor)}`,
35+
...args.slice(1).map((a) => util.inspect(a, this.inspectOpts)),
36+
]
37+
} else {
38+
args = [
39+
`${logLevelEmoji[level]} ${colorizeForConsole(`${now} - ${clazzName} ::`, hexColor)}`,
40+
...args.map((a) => util.inspect(a, this.inspectOpts))
41+
]
42+
}
43+
44+
/* eslint-disable prefer-spread,no-console */
45+
switch (level) {
46+
case LogLevel.DEBUG:
47+
console.debug.apply<Console, any[], void>(console, args)
48+
break
49+
case LogLevel.ERROR:
50+
console.error.apply<Console, any[], void>(console, args)
51+
break
52+
case LogLevel.INFO:
53+
console.info.apply<Console, any[], void>(console, args)
54+
break
55+
case LogLevel.WARN:
56+
console.warn.apply<Console, any[], void>(console, args)
57+
break
58+
case LogLevel.OFF:
59+
break
60+
default:
61+
return level // exhaustive check
62+
}
63+
/* eslint-enable prefer-spread,no-console */
64+
}
65+
}
66+
}

0 commit comments

Comments
 (0)