From 3107f322f86009d8e6f33419905c75973f68ebf8 Mon Sep 17 00:00:00 2001 From: Peter McIntyre Date: Fri, 19 May 2023 15:11:56 +1000 Subject: [PATCH 1/2] feat: console logger --- package.json | 1 + .../ConsoleSerializer/index.test.ts | 11 +++ src/serializers/ConsoleSerializer/index.ts | 73 +++++++++++++++++++ src/serializers/JSONSerializer/index.test.ts | 1 - src/serializers/JSONSerializer/index.ts | 4 +- 5 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 src/serializers/ConsoleSerializer/index.test.ts create mode 100644 src/serializers/ConsoleSerializer/index.ts diff --git a/package.json b/package.json index ce4da51..a217628 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "clean": "rm -rf dist coverage", "lint": "prettier --write .", "test": "node --require ts-node/register --test ./src/**/*.test.ts", + "test:watch": "node --require ts-node/register --test --watch ./src/**/*.test.ts", "prebuild": "npm run clean && npm run lint && npm run test", "build": "tsc", "benchmark": "ts-node ./benchmark.ts", diff --git a/src/serializers/ConsoleSerializer/index.test.ts b/src/serializers/ConsoleSerializer/index.test.ts new file mode 100644 index 0000000..6ac6309 --- /dev/null +++ b/src/serializers/ConsoleSerializer/index.test.ts @@ -0,0 +1,11 @@ +import assert from 'node:assert' +import { describe, test } from 'node:test' +import { ConsoleSerializer } from '.' +import { LogLevel } from '../../Logger' + +describe(`when serializing`, () => { + const got = ConsoleSerializer({ level: LogLevel[LogLevel.INFO], message: "thing happend", error: new Error('example') }) + test('should stringify the error', () => { + assert.strictEqual(got, '\x1B[32mINFO\x1B[0m | thing happend\n\t{"error":"Error: example"}') + }) +}) diff --git a/src/serializers/ConsoleSerializer/index.ts b/src/serializers/ConsoleSerializer/index.ts new file mode 100644 index 0000000..db5fbef --- /dev/null +++ b/src/serializers/ConsoleSerializer/index.ts @@ -0,0 +1,73 @@ +import { JSONSerializer, Serializer } from '../..'; + +// colours enum +export enum Colours { + Reset = '\x1b[0m', + Bright = '\x1b[1m', + Dim = '\x1b[2m', + Underscore = '\x1b[4m', + Blink = '\x1b[5m', + Reverse = '\x1b[7m', + Hidden = '\x1b[8m', + + FgBlack = '\x1b[30m', + FgRed = '\x1b[31m', + FgGreen = '\x1b[32m', + FgYellow = '\x1b[33m', + FgBlue = '\x1b[34m', + FgMagenta = '\x1b[35m', + FgCyan = '\x1b[36m', + FgWhite = '\x1b[37m', + + BgBlack = '\x1b[40m', + BgRed = '\x1b[41m', + BgGreen = '\x1b[42m', + BgYellow = '\x1b[43m', + BgBlue = '\x1b[44m', + BgMagenta = '\x1b[45m', + BgCyan = '\x1b[46m', + BgWhite = '\x1b[47m' +} + +// colour level map +export const LevelColours: Colours[] = [ + Colours.FgCyan, //debug + Colours.FgGreen, //info + Colours.FgYellow, //warn + Colours.FgRed, //error + Colours.FgRed, //fatal +] + +const colors = { + DEBUG: Colours.FgCyan, + INFO: Colours.FgGreen, + WARNING: Colours.FgYellow, + ERROR: Colours.FgRed, + FATAL: Colours.FgRed, +}; + +function getColor(level: string): Colours { + switch (level) { + case "DEBUG": + return Colours.FgCyan + case "INFO": + return Colours.FgGreen + case "WARNING": + return Colours.FgYellow + case "ERROR": + return Colours.FgRed + case "FATAL": + return Colours.FgRed + default: + return Colours.Reset; + } +} + +function wrap(colour: Colours, key: string): string { + return `${colour}${key}${Colours.Reset}` +} + +export const ConsoleSerializer: Serializer = (msg: any) => { + const { level, message, timestamp, ...rest } = msg + return `${wrap(getColor(level), level)} | ${message}\n\t${JSONSerializer(rest)}` +} diff --git a/src/serializers/JSONSerializer/index.test.ts b/src/serializers/JSONSerializer/index.test.ts index e6b384b..cd44813 100644 --- a/src/serializers/JSONSerializer/index.test.ts +++ b/src/serializers/JSONSerializer/index.test.ts @@ -6,7 +6,6 @@ describe(`when serializing errors`, () => { test('should stringify the error', () => { const got = JSONSerializer({ error: new Error('example') }) assert.strictEqual(got, `{"error":"Error: example"}`) - assert.strictEqual(1, 1) }) }) diff --git a/src/serializers/JSONSerializer/index.ts b/src/serializers/JSONSerializer/index.ts index 9d60ad3..d96a66f 100644 --- a/src/serializers/JSONSerializer/index.ts +++ b/src/serializers/JSONSerializer/index.ts @@ -4,7 +4,7 @@ export const JSONSerializer: Serializer = (msg: any) => JSON.stringify(msg, getC // getCircularReplacer is Mozilla's suggested approach to dealing with circular references // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value -function getCircularReplacer() { +export function getCircularReplacer() { const seen = new WeakSet() return (key: any, value: any) => { if (typeof value === 'object' && value !== null) { @@ -18,7 +18,7 @@ function getCircularReplacer() { } // toStringers strigifies some specific types -function toStringers(_: any, value: any) { +export function toStringers(_: any, value: any) { // exit early for null and undefined if (value === undefined || value === null) return value From 4a366c4a7f7e0790cbec23cb9eb75117e2d68cc1 Mon Sep 17 00:00:00 2001 From: Peter McIntyre Date: Fri, 26 May 2023 16:35:46 +1000 Subject: [PATCH 2/2] fixes --- benchmark.ts | 22 ++-- example.ts | 6 ++ src/StandardLogger/index.ts | 14 ++- .../ConsoleSerializer/index.test.ts | 6 +- src/serializers/ConsoleSerializer/index.ts | 101 +++++++----------- 5 files changed, 73 insertions(+), 76 deletions(-) create mode 100755 example.ts diff --git a/benchmark.ts b/benchmark.ts index a42d4e8..3de413d 100755 --- a/benchmark.ts +++ b/benchmark.ts @@ -1,6 +1,6 @@ #!/usr/bin/env -S npx ts-node import { performance } from 'perf_hooks' -import { Logger, LogLevel } from './src' +import { LogLevel, StandardLogger } from './src' // test runner function run(name: string, fn: () => any, count: number = 1000) { @@ -23,15 +23,19 @@ function run(name: string, fn: () => any, count: number = 1000) { return { name, sum, avg, count } } -// setup -const NullWriter = () => null -const logger = new Logger({ level: LogLevel.INFO, write: NullWriter }) +// return a random log level +const keys = Object.keys(LogLevel).filter((k) => typeof LogLevel[k as keyof typeof LogLevel] === 'number') +const randLevel = () => { + const index = Math.floor(Math.random() * keys.length) + return LogLevel[keys[index] as keyof typeof LogLevel] +} // run tests ;[ - run('simple', () => logger.info('hi')), - run('error', () => logger.info('hi', { error: new Error('fail') })), - run('extra', () => logger.info('hi', { extra: 'abcd' })), - run('with simple', () => logger.with({ with: 'abcd' }).info('hi')), - run('with extra', () => logger.with({ with: 'abcd' }).info('hi', { extra: 'abcd' })), + run('simple', () => StandardLogger.info( 'hi')), + run('error', () => StandardLogger.info( 'hi', { error: new Error('fail') })), + run('extra', () => StandardLogger.info( 'hi', { extra: 'abcd' })), + run('with simple', () => StandardLogger.with({ with: 'abcd' }).info( 'hi')), + run('with extra', () => StandardLogger.with({ with: 'abcd' }).info( 'hi', { extra: 'abcd' })), + run('rainbow level', () => StandardLogger.log( randLevel(), 'thing', { extra: 'abcd' })), ].forEach((a) => console.log(a)) diff --git a/example.ts b/example.ts new file mode 100755 index 0000000..94482e8 --- /dev/null +++ b/example.ts @@ -0,0 +1,6 @@ +#!/usr/bin/env -S npx ts-node +import { StandardLogger } from './src' +StandardLogger.debug('about to begin') +StandardLogger.warn('cache failure') +StandardLogger.info('user authenticated', { id: '000001' }) +StandardLogger.error('failed to complete', { error: new Error('fail') }) diff --git a/src/StandardLogger/index.ts b/src/StandardLogger/index.ts index afd8476..c73d8b9 100644 --- a/src/StandardLogger/index.ts +++ b/src/StandardLogger/index.ts @@ -1,11 +1,23 @@ -import { Logger, LogLevel } from '..' +import { JSONSerializer, Logger, LogLevel, Serializer } from '..' +import { ConsoleSerializer } from '../serializers/ConsoleSerializer' // parse level from env var and set default logger const level = (process.env.LOG_LEVEL || 'INFO') as keyof typeof LogLevel +// parse format from env var and set default serializer +const serialize = ((format: string): Serializer => { + switch (format.toUpperCase()) { + case 'TEXT': + return ConsoleSerializer() + default: + return JSONSerializer + } +})(process.env.LOG_FORMAT || '') + // StandardLogger is a globally available Logger with a JSON Serializer, timstamps with RFC3339 timestamps, and writer to std out export const StandardLogger = new Logger({ level: LogLevel[level], + serialize, }) export default Logger diff --git a/src/serializers/ConsoleSerializer/index.test.ts b/src/serializers/ConsoleSerializer/index.test.ts index 6ac6309..48c6496 100644 --- a/src/serializers/ConsoleSerializer/index.test.ts +++ b/src/serializers/ConsoleSerializer/index.test.ts @@ -4,7 +4,11 @@ import { ConsoleSerializer } from '.' import { LogLevel } from '../../Logger' describe(`when serializing`, () => { - const got = ConsoleSerializer({ level: LogLevel[LogLevel.INFO], message: "thing happend", error: new Error('example') }) + const got = ConsoleSerializer({ + level: LogLevel[LogLevel.INFO], + message: 'thing happend', + error: new Error('example'), + }) test('should stringify the error', () => { assert.strictEqual(got, '\x1B[32mINFO\x1B[0m | thing happend\n\t{"error":"Error: example"}') }) diff --git a/src/serializers/ConsoleSerializer/index.ts b/src/serializers/ConsoleSerializer/index.ts index db5fbef..42725bd 100644 --- a/src/serializers/ConsoleSerializer/index.ts +++ b/src/serializers/ConsoleSerializer/index.ts @@ -1,73 +1,44 @@ -import { JSONSerializer, Serializer } from '../..'; +import { JSONSerializer, LogLevel, Serializer } from '../..' -// colours enum export enum Colours { - Reset = '\x1b[0m', - Bright = '\x1b[1m', - Dim = '\x1b[2m', - Underscore = '\x1b[4m', - Blink = '\x1b[5m', - Reverse = '\x1b[7m', - Hidden = '\x1b[8m', - - FgBlack = '\x1b[30m', - FgRed = '\x1b[31m', - FgGreen = '\x1b[32m', - FgYellow = '\x1b[33m', - FgBlue = '\x1b[34m', - FgMagenta = '\x1b[35m', - FgCyan = '\x1b[36m', - FgWhite = '\x1b[37m', - - BgBlack = '\x1b[40m', - BgRed = '\x1b[41m', - BgGreen = '\x1b[42m', - BgYellow = '\x1b[43m', - BgBlue = '\x1b[44m', - BgMagenta = '\x1b[45m', - BgCyan = '\x1b[46m', - BgWhite = '\x1b[47m' + reset = '\u001b[0m', + hicolor = '\u001b[1m', + underline = '\u001b[4m', + inverse = '\u001b[7m', + black = '\u001b[30m', + red = '\u001b[31m', + green = '\u001b[32m', + yellow = '\u001b[33m', + blue = '\u001b[34m', + magenta = '\u001b[35m', + cyan = '\u001b[36m', + white = '\u001b[37m', + bg_black = '\u001b[40m', + bg_red = '\u001b[41m', + bg_green = '\u001b[42m', + bg_yellow = '\u001b[43m', + bg_blue = '\u001b[44m', + bg_magenta = '\u001b[45m', + bg_cyan = '\u001b[46m', + bg_white = '\u001b[47m', } -// colour level map -export const LevelColours: Colours[] = [ - Colours.FgCyan, //debug - Colours.FgGreen, //info - Colours.FgYellow, //warn - Colours.FgRed, //error - Colours.FgRed, //fatal -] +export const DefaultColourMap: Map = new Map([ + [LogLevel.DEBUG, Colours.bg_cyan + Colours.black], + [LogLevel.INFO, Colours.bg_green + Colours.black], + [LogLevel.WARN, Colours.bg_yellow + Colours.black], + [LogLevel.ERROR, Colours.bg_red + Colours.black], +]) -const colors = { - DEBUG: Colours.FgCyan, - INFO: Colours.FgGreen, - WARNING: Colours.FgYellow, - ERROR: Colours.FgRed, - FATAL: Colours.FgRed, -}; - -function getColor(level: string): Colours { - switch (level) { - case "DEBUG": - return Colours.FgCyan - case "INFO": - return Colours.FgGreen - case "WARNING": - return Colours.FgYellow - case "ERROR": - return Colours.FgRed - case "FATAL": - return Colours.FgRed - default: - return Colours.Reset; - } +export function wrap(colour: string, key: string): string { + return `${colour}${key}${Colours.reset}` } -function wrap(colour: Colours, key: string): string { - return `${colour}${key}${Colours.Reset}` -} - -export const ConsoleSerializer: Serializer = (msg: any) => { - const { level, message, timestamp, ...rest } = msg - return `${wrap(getColor(level), level)} | ${message}\n\t${JSONSerializer(rest)}` +export function ConsoleSerializer(mapping: Map = DefaultColourMap): Serializer { + const colours: Map = new Map(Array.from(mapping.entries()).map(([k, v]) => [LogLevel[k], v])) + const mapper = (level: string): string => colours.get(level) || Colours.reset + return (msg: any) => { + const { level, message, ...rest } = msg + return `${wrap(mapper(level), level)} ${message} ${ rest && Object.keys(rest).length ? JSONSerializer(rest) : '' }` + } }