diff --git a/README.md b/README.md index 8115333..66b36bd 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,22 @@ const server = fastify({ }); ``` +## Colors + +Colors are enabled by default when supported. To manually disable the colors you need to set the `transport.colorize` option to `false`. For more options check the `colorette` [docs](https://github.com/jorgebucaran/colorette?tab=readme-ov-file#environment). + +```js +const server = fastify({ + logger: { + transport: { + target: "@fastify/one-line-logger", + colorize: false, + }, + }, +}); +``` + + ## Custom levels Custom levels could be used by passing it into logger opts diff --git a/index.js b/index.js index f6f3ea1..033c83b 100644 --- a/index.js +++ b/index.js @@ -4,9 +4,13 @@ const pretty = require('pino-pretty') const messageFormatFactory = require('./lib/messageFormatFactory') const oneLineLogger = (opts = {}) => { - const { colorize, levels, colors, ...rest } = opts + const { levels, colors, ...rest } = opts - const messageFormat = messageFormatFactory(colorize, levels, colors) + const messageFormat = messageFormatFactory( + levels, + colors, + opts.colorize ?? pretty.isColorSupported + ) return pretty({ messageFormat, diff --git a/lib/messageFormatFactory.js b/lib/messageFormatFactory.js index b5e31c8..8714304 100644 --- a/lib/messageFormatFactory.js +++ b/lib/messageFormatFactory.js @@ -2,13 +2,14 @@ const formatDate = require('./formatDate') const colorizerFactory = require('pino-pretty').colorizerFactory -const messageFormatFactory = (colorize, levels, colors) => { - const customColors = colors != null - ? Object.entries(colors).reduce((colors, [level, color]) => { - return [...colors, [level, color]] - }, []) - : undefined - const colorizer = colorizerFactory(colorize === true, customColors) +const messageFormatFactory = (levels, colors, useColors) => { + const customColors = + colors != null + ? Object.entries(colors).reduce((colors, [level, color]) => { + return [...colors, [level, color]] + }, []) + : undefined + const colorizer = colorizerFactory(useColors, customColors) const levelLookUp = { 60: colorizer('fatal').toLowerCase(), @@ -24,10 +25,12 @@ const messageFormatFactory = (colorize, levels, colors) => { Object.entries(levels).forEach(([name, level]) => { const customLevels = { [level]: name } const customLevelNames = { [name]: level } - levelLookUp[level] = colorizer(name, { customLevelNames, customLevels }).toLowerCase() + levelLookUp[level] = colorizer(name, { + customLevelNames, + customLevels + }).toLowerCase() }) } - const colorizeMessage = colorizer.message const messageFormat = (log, messageKey) => { @@ -35,7 +38,9 @@ const messageFormatFactory = (colorize, levels, colors) => { const level = levelLookUp[log.level] return log.req - ? `${time} - ${level} - ${log.req.method} ${log.req.url} - ${colorizeMessage(log[messageKey])}` + ? `${time} - ${level} - ${log.req.method} ${ + log.req.url + } - ${colorizeMessage(log[messageKey])}` : `${time} - ${level} - ${colorizeMessage(log[messageKey])}` } diff --git a/package.json b/package.json index 051fab3..f202ecf 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "test": "test" }, "dependencies": { - "pino-pretty": "^11.0.0" + "pino-pretty": "^11.1.0" }, "devDependencies": { "benchmark": "^2.1.4", diff --git a/test/fastify.test.js b/test/fastify.test.js index ab088c1..e32da61 100644 --- a/test/fastify.test.js +++ b/test/fastify.test.js @@ -1,6 +1,7 @@ 'use strict' const { serverFactory, TIME, unmockTime, mockTime } = require('./helpers') +const pretty = require('pino-pretty') const tap = require('tap') const { test } = tap @@ -24,69 +25,130 @@ tap.beforeEach(() => { }) test('should log server started messages', async (t) => { - await server.listen({ port: 63995 }) + t.beforeEach(async () => { + await server.listen({ port: 63995 }) + t.teardown(async () => await server.close()) + }) + + t.test('colors supported in TTY', { skip: !pretty.isColorSupported }, (t) => { + const messagesExpected = [ + `${TIME} - \x1B[32minfo\x1B[39m - \x1B[36mServer listening at http://127.0.0.1:63995\x1B[39m\n`, + `${TIME} - \x1B[32minfo\x1B[39m - \x1B[36mServer listening at http://[::1]:63995\x1B[39m\n` + ] - const messagesExpected = [ - `${TIME} - info - Server listening at http://127.0.0.1:63995\n`, - `${TIME} - info - Server listening at http://[::1]:63995\n` - ] + // sort because the order of the messages is not guaranteed + t.same(messages.sort(), messagesExpected.sort()) + t.end() + }) - // sort because the order of the messages is not guaranteed - t.same(messages.sort(), messagesExpected.sort()) - await server.close() - t.end() + t.test( + 'colors not supported in TTY', + { skip: pretty.isColorSupported }, + (t) => { + const messagesExpected = [ + `${TIME} - info - Server listening at http://127.0.0.1:63995\n`, + `${TIME} - info - Server listening at http://[::1]:63995\n` + ] + + // sort because the order of the messages is not guaranteed + t.same(messages.sort(), messagesExpected.sort()) + t.end() + } + ) }) const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'] methods.forEach((method) => { test('should log request and response messages for %p', async (t) => { - const serverMethod = method === 'HEAD' ? 'GET' : method - server[serverMethod.toLowerCase()]('/path', (_, req) => { - req.send() - }) - - await server.inject({ - method, - url: '/path' + t.beforeEach(async () => { + const serverMethod = method === 'HEAD' ? 'GET' : method + server[serverMethod.toLowerCase()]('/path', (_, req) => { + req.send() + }) + + await server.inject({ + method, + url: '/path' + }) }) - const messagesExpected = [ - `${TIME} - info - ${method} /path - incoming request\n`, - `${TIME} - info - request completed\n` - ] - - t.same(messages, messagesExpected) - t.end() + t.test( + 'colors supported in TTY', + { skip: !pretty.isColorSupported }, + (t) => { + const messagesExpected = [ + `${TIME} - \x1B[32minfo\x1B[39m - ${method} /path - \x1B[36mincoming request\x1B[39m\n`, + `${TIME} - \x1B[32minfo\x1B[39m - \x1B[36mrequest completed\x1B[39m\n` + ] + t.same(messages, messagesExpected) + t.end() + } + ) + + t.test( + 'colors not supported in TTY', + { skip: pretty.isColorSupported }, + (t) => { + const messagesExpected = [ + `${TIME} - info - ${method} /path - incoming request\n`, + `${TIME} - info - request completed\n` + ] + t.same(messages, messagesExpected) + t.end() + } + ) }) }) test('should handle user defined log', async (t) => { - server = serverFactory(messages, { minimumLevel: 'trace' }) + t.beforeEach(async () => { + server = serverFactory(messages, { minimumLevel: 'trace' }) + + server.get('/a-path-with-user-defined-log', (res, req) => { + res.log.fatal('a user defined fatal log') + res.log.error('a user defined error log') + res.log.warn('a user defined warn log') + res.log.info('a user defined info log') + res.log.debug('a user defined debug log') + res.log.trace('a user defined trace log') - server.get('/a-path-with-user-defined-log', (res, req) => { - res.log.fatal('a user defined fatal log') - res.log.error('a user defined error log') - res.log.warn('a user defined warn log') - res.log.info('a user defined info log') - res.log.debug('a user defined debug log') - res.log.trace('a user defined trace log') + req.send() + }) - req.send() + await server.inject('/a-path-with-user-defined-log') + }) + + t.test('colors supported in TTY', { skip: !pretty.isColorSupported }, (t) => { + const messagesExpected = [ + `${TIME} - \x1B[32minfo\x1B[39m - GET /a-path-with-user-defined-log - \x1B[36mincoming request\x1B[39m\n`, + `${TIME} - \x1B[41mfatal\x1B[49m - \x1B[36ma user defined fatal log\x1B[39m\n`, + `${TIME} - \x1B[31merror\x1B[39m - \x1B[36ma user defined error log\x1B[39m\n`, + `${TIME} - \x1B[33mwarn\x1B[39m - \x1B[36ma user defined warn log\x1B[39m\n`, + `${TIME} - \x1B[32minfo\x1B[39m - \x1B[36ma user defined info log\x1B[39m\n`, + `${TIME} - \x1B[34mdebug\x1B[39m - \x1B[36ma user defined debug log\x1B[39m\n`, + `${TIME} - \x1B[90mtrace\x1B[39m - \x1B[36ma user defined trace log\x1B[39m\n`, + `${TIME} - \x1B[32minfo\x1B[39m - \x1B[36mrequest completed\x1B[39m\n` + ] + t.same(messages, messagesExpected) + t.end() }) - await server.inject('/a-path-with-user-defined-log') - - const messagesExpected = [ - `${TIME} - info - GET /a-path-with-user-defined-log - incoming request\n`, - `${TIME} - fatal - a user defined fatal log\n`, - `${TIME} - error - a user defined error log\n`, - `${TIME} - warn - a user defined warn log\n`, - `${TIME} - info - a user defined info log\n`, - `${TIME} - debug - a user defined debug log\n`, - `${TIME} - trace - a user defined trace log\n`, - `${TIME} - info - request completed\n` - ] - - t.same(messages, messagesExpected) - t.end() + t.test( + 'colors not supported in TTY', + { skip: pretty.isColorSupported }, + (t) => { + const messagesExpected = [ + `${TIME} - info - GET /a-path-with-user-defined-log - incoming request\n`, + `${TIME} - fatal - a user defined fatal log\n`, + `${TIME} - error - a user defined error log\n`, + `${TIME} - warn - a user defined warn log\n`, + `${TIME} - info - a user defined info log\n`, + `${TIME} - debug - a user defined debug log\n`, + `${TIME} - trace - a user defined trace log\n`, + `${TIME} - info - request completed\n` + ] + t.same(messages, messagesExpected) + t.end() + } + ) }) diff --git a/test/helpers.js b/test/helpers.js index d5eea6a..8363c50 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -17,7 +17,6 @@ const pinoFactory = (opts) => { return pino( { level }, target({ - colorize: false, ...opts }) ) @@ -57,9 +56,9 @@ const mockTime = () => { const unmockTime = () => { Date.now = dateOriginalNow // eslint-disable-next-line - Date.prototype.getTimezoneOffset = dateGetTimezoneOffset + Date.prototype.getTimezoneOffset = dateGetTimezoneOffset; // eslint-disable-next-line - Date.prototype.getHours = dateOriginalGetHours + Date.prototype.getHours = dateOriginalGetHours; } module.exports = { diff --git a/test/index.test.js b/test/index.test.js index 904fbf2..f5732a0 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -3,13 +3,16 @@ const { EPOCH, TIME, MESSAGE_KEY, mockTime, unmockTime } = require('./helpers') const target = require('..') const tap = require('tap') - +const pretty = require('pino-pretty') const { messageFormatFactory } = target const { test } = tap -const messageFormat = messageFormatFactory(false) -const messageFormatColorized = messageFormatFactory(true) +const messageFormat = messageFormatFactory( + undefined, + undefined, + pretty.isColorSupported +) tap.before(() => { mockTime() @@ -25,70 +28,115 @@ test('able to instantiate target without arguments', (t) => { t.end() }) -const logDescriptorLogPairs = [ - [ - { time: EPOCH, level: 10, [MESSAGE_KEY]: 'basic log' }, - `${TIME} - trace - basic log` - ], - [ - { - time: EPOCH, - level: 30, - [MESSAGE_KEY]: 'basic incoming request log', - req: { - method: 'GET', - url: '/path' - } - }, - `${TIME} - info - GET /path - basic incoming request log` +test('format log correctly with different logDescriptor', async (t) => { + const logDescriptorLogPairs = [ + [ + { time: EPOCH, level: 10, [MESSAGE_KEY]: 'basic log' }, + `${TIME} - \x1B[90mtrace\x1B[39m - \x1B[36mbasic log\x1B[39m`, + `${TIME} - trace - basic log` + ], + [ + { + time: EPOCH, + level: 30, + [MESSAGE_KEY]: 'basic incoming request log', + req: { + method: 'GET', + url: '/path' + } + }, + `${TIME} - \x1B[32minfo\x1B[39m - GET /path - \x1B[36mbasic incoming request log\x1B[39m`, + `${TIME} - info - GET /path - basic incoming request log` + ] ] -] -logDescriptorLogPairs.forEach(([logDescriptor, expectedLog]) => { - test('format log correctly with different logDescriptor', (t) => { - const log = messageFormat(logDescriptor, MESSAGE_KEY) - - t.equal(log, expectedLog) - t.end() - }) + + await logDescriptorLogPairs.forEach( + async ([logDescriptor, expectedLogColored, expectedLogUncolored]) => { + await t.test( + 'colors supported in TTY', + { skip: !pretty.isColorSupported }, + (t) => { + const log = messageFormat(logDescriptor, MESSAGE_KEY) + t.equal(log, expectedLogColored) + t.end() + } + ) + + await t.test( + 'colors not supported in TTY', + { skip: pretty.isColorSupported }, + (t) => { + const log = messageFormat(logDescriptor, MESSAGE_KEY) + t.equal(log, expectedLogUncolored) + t.end() + } + ) + } + ) }) -const logDescriptorColorizedLogPairs = [ - [ - { time: EPOCH, level: 10, [MESSAGE_KEY]: 'basic log' }, - `${TIME} - \u001B[90mtrace\u001B[39m - \u001B[36mbasic log\u001B[39m` - ], - [ - { - time: EPOCH, - level: 30, - [MESSAGE_KEY]: 'basic incoming request log', - req: { - method: 'GET', - url: '/path' - } - }, - `${TIME} - \u001B[32minfo\u001B[39m - GET /path - \u001B[36mbasic incoming request log\u001B[39m` +test('colorize log correctly with different logDescriptor', async (t) => { + const logDescriptorColorizedLogPairs = [ + [ + { time: EPOCH, level: 10, [MESSAGE_KEY]: 'basic log' }, + `${TIME} - \u001B[90mtrace\u001B[39m - \u001B[36mbasic log\u001B[39m`, + `${TIME} - trace - basic log` + ], + [ + { + time: EPOCH, + level: 30, + [MESSAGE_KEY]: 'basic incoming request log', + req: { + method: 'GET', + url: '/path' + } + }, + `${TIME} - \u001B[32minfo\u001B[39m - GET /path - \u001B[36mbasic incoming request log\u001B[39m`, + `${TIME} - info - GET /path - basic incoming request log` + ] ] -] -logDescriptorColorizedLogPairs.forEach(([logDescriptor, logColorized]) => { - test('colorize log correctly with different logDescriptor', (t) => { - const log = messageFormatColorized(logDescriptor, MESSAGE_KEY) - - t.equal(log, logColorized) - t.end() - }) + + await logDescriptorColorizedLogPairs.forEach( + async ([logDescriptor, expectedLogColored, expectedLogUncolored]) => { + await t.test( + 'colors supported in TTY', + { skip: !pretty.isColorSupported }, + (t) => { + const log = messageFormat(logDescriptor, MESSAGE_KEY) + t.equal(log, expectedLogColored) + t.end() + } + ) + + await t.test( + 'colors not supported in TTY', + { skip: pretty.isColorSupported }, + (t) => { + const log = messageFormat(logDescriptor, MESSAGE_KEY) + t.equal(log, expectedLogUncolored) + t.end() + } + ) + } + ) }) -{ +test('format log correctly with custom levels', async (t) => { const levels = { foo: 35, bar: 45 } - const messageFormat = messageFormatFactory(false, levels) + const messageFormat = messageFormatFactory( + levels, + undefined, + pretty.isColorSupported + ) const logCustomLevelsLogPairs = [ [ { time: EPOCH, level: 35, [MESSAGE_KEY]: 'basic foo log' }, + `${TIME} - \u001b[37mfoo\u001b[39m - \u001B[36mbasic foo log\u001B[39m`, `${TIME} - foo - basic foo log` ], [ @@ -101,30 +149,55 @@ logDescriptorColorizedLogPairs.forEach(([logDescriptor, logColorized]) => { url: '/bar' } }, + `${TIME} - \u001b[37mbar\u001b[39m - GET /bar - \u001B[36mbasic incoming request bar log\u001B[39m`, `${TIME} - bar - GET /bar - basic incoming request bar log` ] ] - logCustomLevelsLogPairs.forEach(([logDescriptor, expectedLog]) => { - test('format log correctly with custom levels', (t) => { - const log = messageFormat(logDescriptor, MESSAGE_KEY) - t.equal(log, expectedLog) - t.end() - }) - }) -} + await logCustomLevelsLogPairs.forEach( + async ([logDescriptor, expectedLogColored, expectedLogUncolored]) => { + await t.test( + 'colors supported in TTY', + { skip: !pretty.isColorSupported }, + (t) => { + const log = messageFormat(logDescriptor, MESSAGE_KEY) + t.equal(log, expectedLogColored) + t.end() + } + ) + + await t.test( + 'colors not supported in TTY', + { skip: pretty.isColorSupported }, + (t) => { + const log = messageFormat(logDescriptor, MESSAGE_KEY) + t.equal(log, expectedLogUncolored) + t.end() + } + ) + } + ) +}) -{ +test('format log correctly with custom colors per level', async (t) => { const levels = { foo: 35, bar: 45 } - const messageFormat = messageFormatFactory(true, levels, { 35: 'bgCyanBright', 45: 'yellow' }) + const messageFormat = messageFormatFactory( + levels, + { + 35: 'bgCyanBright', + 45: 'yellow' + }, + pretty.isColorSupported + ) const logCustomLevelsLogPairs = [ [ { time: EPOCH, level: 35, [MESSAGE_KEY]: 'basic foo log' }, - `${TIME} - \u001B[106mfoo\u001B[49m - \u001B[36mbasic foo log\u001B[39m` + `${TIME} - \u001B[106mfoo\u001B[49m - \u001B[36mbasic foo log\u001B[39m`, + `${TIME} - foo - basic foo log` ], [ { @@ -136,15 +209,32 @@ logDescriptorColorizedLogPairs.forEach(([logDescriptor, logColorized]) => { url: '/bar' } }, - `${TIME} - \u001B[33mbar\u001B[39m - GET /bar - \u001B[36mbasic incoming request bar log\u001B[39m` + `${TIME} - \u001B[33mbar\u001B[39m - GET /bar - \u001B[36mbasic incoming request bar log\u001B[39m`, + `${TIME} - bar - GET /bar - basic incoming request bar log` ] ] - logCustomLevelsLogPairs.forEach(([logDescriptor, expectedLog]) => { - test('format log correctly with custom colors per level', (t) => { - const log = messageFormat(logDescriptor, MESSAGE_KEY) - - t.equal(log, expectedLog) - t.end() - }) - }) -} + + await logCustomLevelsLogPairs.forEach( + async ([logDescriptor, expectedLogColored, expectedLogUncolored]) => { + await t.test( + 'colors supported in TTY', + { skip: !pretty.isColorSupported }, + (t) => { + const log = messageFormat(logDescriptor, MESSAGE_KEY) + t.equal(log, expectedLogColored) + t.end() + } + ) + + await t.test( + 'colors not supported in TTY', + { skip: pretty.isColorSupported }, + (t) => { + const log = messageFormat(logDescriptor, MESSAGE_KEY) + t.equal(log, expectedLogUncolored) + t.end() + } + ) + } + ) +})