├── .gitattributes ├── .github ├── .stale.yml ├── dependabot.yml ├── tests_checker.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── benchmark ├── format-date.js └── format-message.js ├── eslint.config.js ├── index.js ├── lib ├── formatDate.js └── messageFormatFactory.js ├── package.json ├── test ├── fastify.test.js ├── formatDate.test.js ├── helpers.js └── index.test.js └── types ├── index.d.ts └── index.test-d.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically convert line endings 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/.stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 15 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - "discussion" 8 | - "feature request" 9 | - "bug" 10 | - "help wanted" 11 | - "plugin suggestion" 12 | - "good first issue" 13 | # Label to use when marking an issue as stale 14 | staleLabel: stale 15 | # Comment to post when marking an issue as stale. Set to `false` to disable 16 | markComment: > 17 | This issue has been automatically marked as stale because it has not had 18 | recent activity. It will be closed if no further activity occurs. Thank you 19 | for your contributions. 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /.github/tests_checker.yml: -------------------------------------------------------------------------------- 1 | comment: | 2 | Hello! Thank you for contributing! 3 | It appears that you have changed the code, but the tests that verify your change are missing. Could you please add them? 4 | fileExtensions: 5 | - '.ts' 6 | - '.js' 7 | 8 | testDir: 'test' -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | - 'v*' 9 | paths-ignore: 10 | - 'docs/**' 11 | - '*.md' 12 | pull_request: 13 | paths-ignore: 14 | - 'docs/**' 15 | - '*.md' 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | test: 22 | permissions: 23 | contents: write 24 | pull-requests: write 25 | uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5 26 | with: 27 | license-check: true 28 | lint: true 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # Vim swap files 133 | *.swp 134 | 135 | # macOS files 136 | .DS_Store 137 | 138 | # Clinic 139 | .clinic 140 | 141 | # lock files 142 | bun.lockb 143 | package-lock.json 144 | pnpm-lock.yaml 145 | yarn.lock 146 | 147 | # editor files 148 | .vscode 149 | .idea 150 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Fastify collaborators 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # @fastify/one-line-logger 3 | 4 | 5 | [![CI](https://github.com/fastify/one-line-logger/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/one-line-logger/actions/workflows/ci.yml) 6 | [![NPM version](https://img.shields.io/npm/v/@fastify/one-line-logger.svg?style=flat)](https://www.npmjs.com/package/@fastify/one-line-logger) 7 | [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard) 8 | 9 | `@fastify/one-line-logger` helps you format fastify's log into a nice one-line message: 10 | 11 | ``` 12 | YYYY-MM-dd HH:mm:ss.SSSTZ - - - 13 | ``` 14 | 15 | A standard incoming request log line like: 16 | 17 | ``` 18 | {"level": 30,"time": 1660151282194,"pid": 1557,"hostname": "foo","reqId": "req-1","req": {"method": "GET","url": "/path","hostname": "localhost:8080","remoteAddress": "127.0.0.1"},"msg": "incoming request"} 19 | ``` 20 | 21 | Will format to: 22 | 23 | ``` 24 | 2022-08-11 01:08:02.194+0100 - info - GET / - incoming request 25 | ``` 26 | 27 | 28 | ## Install 29 | 30 | ``` 31 | npm i @fastify/one-line-logger 32 | ``` 33 | 34 | 35 | ## Getting started 36 | 37 | ```js 38 | const server = fastify({ 39 | logger: { 40 | transport: { 41 | target: "@fastify/one-line-logger", 42 | }, 43 | }, 44 | }); 45 | ``` 46 | 47 | ## Colors 48 | 49 | 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). 50 | 51 | ```js 52 | const server = fastify({ 53 | logger: { 54 | transport: { 55 | target: "@fastify/one-line-logger", 56 | colorize: false, 57 | }, 58 | }, 59 | }); 60 | ``` 61 | 62 | 63 | ## Custom levels 64 | 65 | Custom levels could be used by passing it into logger opts 66 | 67 | ```js 68 | const server = fastify({ 69 | logger: { 70 | transport: { 71 | target: "@fastify/one-line-logger", 72 | }, 73 | customLevels: { 74 | foo: 35, 75 | bar: 45, 76 | }, 77 | }, 78 | }); 79 | 80 | server.get("/", (request, reply) => { 81 | request.log.info("time to foobar"); 82 | request.log.foo("FOO!"); 83 | request.log.bar("BAR!"); 84 | reply.send({ foobar: true }); 85 | }); 86 | ``` 87 | 88 | ## Custom level colors 89 | 90 | Custom level colors can be used by passing it into logger opts. They can also overwrite the default level's colors. Check all the supported colors [here](https://github.com/jorgebucaran/colorette?tab=readme-ov-file#supported-colors). 91 | 92 | ```js 93 | const server = fastify({ 94 | logger: { 95 | transport: { 96 | target: "@fastify/one-line-logger", 97 | colors: { 98 | 35: "bgYellow", 99 | 45: "magenta", 100 | 60: "bgRedBright" // overwriting the `fatal` log color 101 | } 102 | }, 103 | customLevels: { 104 | foo: 35, 105 | bar: 45, 106 | }, 107 | }, 108 | }); 109 | 110 | server.get("/", (request, reply) => { 111 | request.log.fatal("An error occured"); 112 | request.log.foo("FOO!"); 113 | request.log.bar("BAR!"); 114 | reply.send({ foobar: true }); 115 | }); 116 | ``` 117 | 118 | 119 | ## License 120 | 121 | Licensed under [MIT](./LICENSE). 122 | -------------------------------------------------------------------------------- /benchmark/format-date.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const benchmark = require('benchmark') 4 | const formatDate = require('../lib/formatDate') 5 | 6 | const now = Date.now() 7 | 8 | new benchmark.Suite() 9 | .add('formatDate', function () { formatDate(now) }, { minSamples: 100 }) 10 | .on('cycle', function onCycle (event) { console.log(String(event.target)) }) 11 | .run() 12 | -------------------------------------------------------------------------------- /benchmark/format-message.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const benchmark = require('benchmark') 4 | const messageFormatFactory = require('../lib/messageFormatFactory') 5 | 6 | const colors = { 7 | 60: 'red', 8 | 50: 'red', 9 | 40: 'yellow', 10 | 30: 'green', 11 | 20: 'blue', 12 | 10: 'cyan' 13 | } 14 | const formatMessageColorized = messageFormatFactory(true, colors, true) 15 | const log = { 16 | time: Date.now(), 17 | level: 30, 18 | message: 'oneLineLogger', 19 | req: { 20 | method: 'GET', 21 | url: 'http://localhost' 22 | } 23 | } 24 | 25 | new benchmark.Suite() 26 | .add('formatMessageColorized', function () { formatMessageColorized(log, 'message') }, { minSamples: 100 }) 27 | .on('cycle', function onCycle (event) { console.log(String(event.target)) }) 28 | .run() 29 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('neostandard')({ 4 | ignores: require('neostandard').resolveIgnoresFromGitignore(), 5 | ts: true 6 | }) 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const pretty = require('pino-pretty') 4 | const messageFormatFactory = require('./lib/messageFormatFactory') 5 | 6 | const oneLineLogger = (opts = {}) => { 7 | const { levels, colors, ...rest } = opts 8 | 9 | const messageFormat = messageFormatFactory( 10 | levels, 11 | colors, 12 | opts.colorize ?? pretty.isColorSupported 13 | ) 14 | 15 | return pretty({ 16 | messageFormat, 17 | ignore: 'pid,hostname,time,level', 18 | hideObject: true, 19 | colorize: false, 20 | ...rest 21 | }) 22 | } 23 | 24 | module.exports = oneLineLogger 25 | module.exports.default = oneLineLogger 26 | module.exports.oneLineLogger = oneLineLogger 27 | 28 | module.exports.messageFormatFactory = messageFormatFactory 29 | -------------------------------------------------------------------------------- /lib/formatDate.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const TWO_PADDED_NUMBERS = new Array(60).fill(0).map((_v, i) => i.toString().padStart(2, '0')) 4 | const THREE_PADDED_NUMBERS = new Array(1000).fill(0).map((_v, i) => i.toString().padStart(3, '0')) 5 | 6 | const MONTH_LOOKUP = new Array(12).fill(0).map((_v, i) => (i + 1).toString().padStart(2, '0')) 7 | const TZ_OFFSET_LOOKUP = {} 8 | 9 | for (let i = -900; i < 900; i += 15) { 10 | TZ_OFFSET_LOOKUP[i] = (i > 0 ? '-' : '+') + ('' + (Math.floor(Math.abs(i) / 60) * 100 + (Math.abs(i) % 60))).padStart(4, '0') 11 | } 12 | 13 | function formatDate (timestamp) { 14 | const date = new Date(timestamp) 15 | return date.getFullYear() + '-' + 16 | MONTH_LOOKUP[date.getMonth()] + '-' + 17 | TWO_PADDED_NUMBERS[date.getDate()] + ' ' + 18 | TWO_PADDED_NUMBERS[date.getHours()] + ':' + 19 | TWO_PADDED_NUMBERS[date.getMinutes()] + ':' + 20 | TWO_PADDED_NUMBERS[date.getSeconds()] + '.' + 21 | THREE_PADDED_NUMBERS[date.getMilliseconds()] + 22 | TZ_OFFSET_LOOKUP[date.getTimezoneOffset()] 23 | } 24 | 25 | module.exports = formatDate 26 | -------------------------------------------------------------------------------- /lib/messageFormatFactory.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const formatDate = require('./formatDate') 3 | const colorizerFactory = require('pino-pretty').colorizerFactory 4 | 5 | const messageFormatFactory = (levels, colors, useColors) => { 6 | let customColors 7 | if (colors != null) { 8 | customColors = Object.entries(colors) 9 | } 10 | const colorizer = colorizerFactory(useColors, customColors) 11 | 12 | const levelLookUp = { 13 | 60: colorizer('fatal').toLowerCase(), 14 | 50: colorizer('error').toLowerCase(), 15 | 40: colorizer('warn').toLowerCase(), 16 | 30: colorizer('info').toLowerCase(), 17 | 20: colorizer('debug').toLowerCase(), 18 | 10: colorizer('trace').toLowerCase() 19 | } 20 | 21 | const shouldAddCustomLevels = !!levels 22 | if (shouldAddCustomLevels) { 23 | Object.entries(levels).forEach(([name, level]) => { 24 | const customLevels = { [level]: name } 25 | const customLevelNames = { [name]: level } 26 | levelLookUp[level] = colorizer(name, { 27 | customLevelNames, 28 | customLevels 29 | }).toLowerCase() 30 | }) 31 | } 32 | const colorizeMessage = colorizer.message 33 | 34 | const messageFormat = (log, messageKey) => { 35 | const time = formatDate(log.time) 36 | const level = levelLookUp[log.level] 37 | 38 | return log.req 39 | ? `${time} - ${level} - ${log.req.method} ${ 40 | log.req.url 41 | } - ${colorizeMessage(log[messageKey])}` 42 | : `${time} - ${level} - ${colorizeMessage(log[messageKey])}` 43 | } 44 | 45 | return messageFormat 46 | } 47 | 48 | module.exports = messageFormatFactory 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fastify/one-line-logger", 3 | "version": "2.0.2", 4 | "description": "A simple formatter for fastify's logs", 5 | "main": "index.js", 6 | "type": "commonjs", 7 | "types": "types/index.d.ts", 8 | "directories": { 9 | "doc": "docs", 10 | "example": "examples", 11 | "test": "test" 12 | }, 13 | "dependencies": { 14 | "pino-pretty": "^13.0.0" 15 | }, 16 | "devDependencies": { 17 | "benchmark": "^2.1.4", 18 | "eslint": "^9.17.0", 19 | "fastify": "^5.0.0", 20 | "neostandard": "^0.12.0", 21 | "tsd": "^0.32.0" 22 | }, 23 | "resolutions": { 24 | "@types/node": "^20.12.7" 25 | }, 26 | "scripts": { 27 | "lint": "eslint", 28 | "lint:fix": "eslint --fix", 29 | "test": "npm run test:unit && npm run test:typescript", 30 | "test:typescript": "tsd", 31 | "test:unit": "node --test --experimental-test-coverage", 32 | "test:watch": "node --test --watch" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/fastify/one-line-logger.git" 37 | }, 38 | "bugs": { 39 | "url": "https://github.com/fastify/one-line-logger/issues" 40 | }, 41 | "homepage": "https://github.com/fastify/one-line-logger#readme", 42 | "funding": [ 43 | { 44 | "type": "github", 45 | "url": "https://github.com/sponsors/fastify" 46 | }, 47 | { 48 | "type": "opencollective", 49 | "url": "https://opencollective.com/fastify" 50 | } 51 | ], 52 | "keywords": [ 53 | "fastify", 54 | "logger", 55 | "pino", 56 | "pino-pretty", 57 | "one-line-logger" 58 | ], 59 | "author": "Matteo Collina ", 60 | "contributors": [ 61 | { 62 | "name": "James Sumners", 63 | "url": "https://james.sumners.info" 64 | }, 65 | { 66 | "name": "Manuel Spigolon", 67 | "email": "behemoth89@gmail.com" 68 | }, 69 | { 70 | "name": "Aras Abbasi", 71 | "email": "aras.abbasi@gmail.com" 72 | }, 73 | { 74 | "name": "Frazer Smith", 75 | "email": "frazer.dev@icloud.com", 76 | "url": "https://github.com/fdawgs" 77 | } 78 | ], 79 | "license": "MIT" 80 | } 81 | -------------------------------------------------------------------------------- /test/fastify.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { serverFactory, TIME, unmockTime, mockTime } = require('./helpers') 4 | const pretty = require('pino-pretty') 5 | const { before, beforeEach, after, test } = require('node:test') 6 | 7 | const messages = [] 8 | let server = serverFactory(messages, { colorize: false }) 9 | 10 | before(() => { 11 | mockTime() 12 | }) 13 | 14 | after(() => { 15 | unmockTime() 16 | }) 17 | 18 | beforeEach(() => { 19 | // empty messages array 20 | messages.splice(0, messages.length) 21 | 22 | server = serverFactory(messages) 23 | }) 24 | 25 | test('should log server started messages', async (t) => { 26 | t.beforeEach(async () => { 27 | await server.listen({ port: 63995 }) 28 | t.afterEach(async () => await server.close()) 29 | }) 30 | 31 | await t.test('colors supported in TTY', { skip: !pretty.isColorSupported }, (t) => { 32 | const messagesExpected = [ 33 | `${TIME} - \x1B[32minfo\x1B[39m - \x1B[36mServer listening at http://127.0.0.1:63995\x1B[39m\n`, 34 | `${TIME} - \x1B[32minfo\x1B[39m - \x1B[36mServer listening at http://[::1]:63995\x1B[39m\n` 35 | ] 36 | 37 | // sort because the order of the messages is not guaranteed 38 | t.assert.deepStrictEqual(messages.sort(), messagesExpected.sort()) 39 | }) 40 | 41 | await t.test( 42 | 'colors not supported in TTY', 43 | { skip: pretty.isColorSupported }, 44 | (t) => { 45 | const messagesExpected = [ 46 | `${TIME} - info - Server listening at http://127.0.0.1:63995\n`, 47 | `${TIME} - info - Server listening at http://[::1]:63995\n` 48 | ] 49 | 50 | // sort because the order of the messages is not guaranteed 51 | t.assert.deepStrictEqual(messages.sort(), messagesExpected.sort()) 52 | } 53 | ) 54 | }) 55 | 56 | const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'] 57 | methods.forEach((method) => { 58 | test('should log request and response messages for %p', async (t) => { 59 | t.beforeEach(async () => { 60 | const serverMethod = method === 'HEAD' ? 'GET' : method 61 | server[serverMethod.toLowerCase()]('/path', (_, req) => { 62 | req.send() 63 | }) 64 | 65 | await server.inject({ 66 | method, 67 | url: '/path' 68 | }) 69 | }) 70 | 71 | await t.test( 72 | 'colors supported in TTY', 73 | { skip: !pretty.isColorSupported }, 74 | (t) => { 75 | const messagesExpected = [ 76 | `${TIME} - \x1B[32minfo\x1B[39m - ${method} /path - \x1B[36mincoming request\x1B[39m\n`, 77 | `${TIME} - \x1B[32minfo\x1B[39m - \x1B[36mrequest completed\x1B[39m\n` 78 | ] 79 | t.assert.deepEqual(messages, messagesExpected) 80 | } 81 | ) 82 | 83 | await t.test( 84 | 'colors not supported in TTY', 85 | { skip: pretty.isColorSupported }, 86 | (t) => { 87 | const messagesExpected = [ 88 | `${TIME} - info - ${method} /path - incoming request\n`, 89 | `${TIME} - info - request completed\n` 90 | ] 91 | t.assert.deepEqual(messages, messagesExpected) 92 | } 93 | ) 94 | }) 95 | }) 96 | 97 | test('should handle user defined log', async (t) => { 98 | t.beforeEach(async () => { 99 | server = serverFactory(messages, { minimumLevel: 'trace' }) 100 | 101 | server.get('/a-path-with-user-defined-log', (res, req) => { 102 | res.log.fatal('a user defined fatal log') 103 | res.log.error('a user defined error log') 104 | res.log.warn('a user defined warn log') 105 | res.log.info('a user defined info log') 106 | res.log.debug('a user defined debug log') 107 | res.log.trace('a user defined trace log') 108 | 109 | req.send() 110 | }) 111 | 112 | await server.inject('/a-path-with-user-defined-log') 113 | }) 114 | 115 | await t.test('colors supported in TTY', { skip: !pretty.isColorSupported }, (t) => { 116 | const messagesExpected = [ 117 | `${TIME} - \x1B[32minfo\x1B[39m - GET /a-path-with-user-defined-log - \x1B[36mincoming request\x1B[39m\n`, 118 | `${TIME} - \x1B[41mfatal\x1B[49m - \x1B[36ma user defined fatal log\x1B[39m\n`, 119 | `${TIME} - \x1B[31merror\x1B[39m - \x1B[36ma user defined error log\x1B[39m\n`, 120 | `${TIME} - \x1B[33mwarn\x1B[39m - \x1B[36ma user defined warn log\x1B[39m\n`, 121 | `${TIME} - \x1B[32minfo\x1B[39m - \x1B[36ma user defined info log\x1B[39m\n`, 122 | `${TIME} - \x1B[34mdebug\x1B[39m - \x1B[36ma user defined debug log\x1B[39m\n`, 123 | `${TIME} - \x1B[90mtrace\x1B[39m - \x1B[36ma user defined trace log\x1B[39m\n`, 124 | `${TIME} - \x1B[32minfo\x1B[39m - \x1B[36mrequest completed\x1B[39m\n` 125 | ] 126 | t.assert.deepStrictEqual(messages, messagesExpected) 127 | }) 128 | 129 | await t.test( 130 | 'colors not supported in TTY', 131 | { skip: pretty.isColorSupported }, 132 | (t) => { 133 | const messagesExpected = [ 134 | `${TIME} - info - GET /a-path-with-user-defined-log - incoming request\n`, 135 | `${TIME} - fatal - a user defined fatal log\n`, 136 | `${TIME} - error - a user defined error log\n`, 137 | `${TIME} - warn - a user defined warn log\n`, 138 | `${TIME} - info - a user defined info log\n`, 139 | `${TIME} - debug - a user defined debug log\n`, 140 | `${TIME} - trace - a user defined trace log\n`, 141 | `${TIME} - info - request completed\n` 142 | ] 143 | t.assert.deepStrictEqual(messages, messagesExpected) 144 | } 145 | ) 146 | }) 147 | -------------------------------------------------------------------------------- /test/formatDate.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const formatDate = require('../lib/formatDate') 4 | const { test } = require('node:test') 5 | 6 | const timeFormatRE = /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d[+-]\d\d\d\d$/ 7 | 8 | test('should generate valid formatted time', (t) => { 9 | const iterations = 1e5 10 | const maxTimestamp = 2 ** 31 * 1000 11 | t.plan(iterations) 12 | 13 | for (let i = 0; i < iterations; ++i) { 14 | const randomInt = Math.floor(Math.random() * maxTimestamp) 15 | t.assert.ok(timeFormatRE.test(formatDate(randomInt))) 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Writable } = require('node:stream') 4 | const fastify = require('fastify') 5 | const pino = require('pino') 6 | const target = require('..') 7 | 8 | const HOUR = 20 9 | const TIME = `2017-02-14 ${HOUR}:51:48.000+0800` 10 | const EPOCH = 1487076708000 11 | const TIMEZONE_OFFSET = -8 * 60 12 | const MESSAGE_KEY = 'message' 13 | 14 | const pinoFactory = (opts) => { 15 | const level = opts.minimumLevel || 'info' 16 | 17 | return pino( 18 | { level }, 19 | target({ 20 | ...opts 21 | }) 22 | ) 23 | } 24 | 25 | const serverFactory = (messages, opts, fastifyOpts) => { 26 | const destination = new Writable({ 27 | write (chunk, _enc, cb) { 28 | messages.push(chunk.toString()) 29 | 30 | process.nextTick(cb) 31 | } 32 | }) 33 | 34 | const pinoLogger = pinoFactory({ destination, ...opts }) 35 | 36 | return fastify({ 37 | loggerInstance: pinoLogger, 38 | ...fastifyOpts 39 | }) 40 | } 41 | 42 | const dateOriginalNow = Date.now 43 | const dateGetTimezoneOffset = Date.prototype.getTimezoneOffset 44 | const dateOriginalGetHours = Date.prototype.getHours 45 | 46 | const mockTime = () => { 47 | Date.now = () => EPOCH 48 | 49 | // eslint-disable-next-line 50 | Date.prototype.getTimezoneOffset = () => TIMEZONE_OFFSET; 51 | 52 | // eslint-disable-next-line 53 | Date.prototype.getHours = () => HOUR; 54 | } 55 | 56 | const unmockTime = () => { 57 | Date.now = dateOriginalNow 58 | // eslint-disable-next-line 59 | Date.prototype.getTimezoneOffset = dateGetTimezoneOffset; 60 | // eslint-disable-next-line 61 | Date.prototype.getHours = dateOriginalGetHours; 62 | } 63 | 64 | module.exports = { 65 | TIME, 66 | EPOCH, 67 | MESSAGE_KEY, 68 | TIMEZONE_OFFSET, 69 | pinoFactory, 70 | serverFactory, 71 | mockTime, 72 | unmockTime 73 | } 74 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { EPOCH, TIME, MESSAGE_KEY, mockTime, unmockTime } = require('./helpers') 4 | const target = require('..') 5 | const { before, after, test } = require('node:test') 6 | const pretty = require('pino-pretty') 7 | const { messageFormatFactory } = target 8 | 9 | const messageFormat = messageFormatFactory( 10 | undefined, 11 | undefined, 12 | pretty.isColorSupported 13 | ) 14 | 15 | before(() => { 16 | mockTime() 17 | }) 18 | 19 | after(() => { 20 | unmockTime() 21 | }) 22 | 23 | test('able to instantiate target without arguments', () => { 24 | target() 25 | }) 26 | 27 | test('format log correctly with different logDescriptor', async (t) => { 28 | const logDescriptorLogPairs = [ 29 | [ 30 | { time: EPOCH, level: 10, [MESSAGE_KEY]: 'basic log' }, 31 | `${TIME} - \x1B[90mtrace\x1B[39m - \x1B[36mbasic log\x1B[39m`, 32 | `${TIME} - trace - basic log` 33 | ], 34 | [ 35 | { 36 | time: EPOCH, 37 | level: 30, 38 | [MESSAGE_KEY]: 'basic incoming request log', 39 | req: { 40 | method: 'GET', 41 | url: '/path' 42 | } 43 | }, 44 | `${TIME} - \x1B[32minfo\x1B[39m - GET /path - \x1B[36mbasic incoming request log\x1B[39m`, 45 | `${TIME} - info - GET /path - basic incoming request log` 46 | ] 47 | ] 48 | 49 | await Promise.all(logDescriptorLogPairs.map( 50 | async ([logDescriptor, expectedLogColored, expectedLogUncolored]) => { 51 | await t.test( 52 | 'colors supported in TTY', 53 | { skip: !pretty.isColorSupported }, 54 | (t) => { 55 | const log = messageFormat(logDescriptor, MESSAGE_KEY) 56 | t.assert.strictEqual(log, expectedLogColored) 57 | } 58 | ) 59 | 60 | await t.test( 61 | 'colors not supported in TTY', 62 | { skip: pretty.isColorSupported }, 63 | (t) => { 64 | const log = messageFormat(logDescriptor, MESSAGE_KEY) 65 | t.assert.strictEqual(log, expectedLogUncolored) 66 | } 67 | ) 68 | } 69 | )) 70 | }) 71 | 72 | test('colorize log correctly with different logDescriptor', async (t) => { 73 | const logDescriptorColorizedLogPairs = [ 74 | [ 75 | { time: EPOCH, level: 10, [MESSAGE_KEY]: 'basic log' }, 76 | `${TIME} - \u001B[90mtrace\u001B[39m - \u001B[36mbasic log\u001B[39m`, 77 | `${TIME} - trace - basic log` 78 | ], 79 | [ 80 | { 81 | time: EPOCH, 82 | level: 30, 83 | [MESSAGE_KEY]: 'basic incoming request log', 84 | req: { 85 | method: 'GET', 86 | url: '/path' 87 | } 88 | }, 89 | `${TIME} - \u001B[32minfo\u001B[39m - GET /path - \u001B[36mbasic incoming request log\u001B[39m`, 90 | `${TIME} - info - GET /path - basic incoming request log` 91 | ] 92 | ] 93 | 94 | await Promise.all(logDescriptorColorizedLogPairs.map( 95 | async ([logDescriptor, expectedLogColored, expectedLogUncolored]) => { 96 | await t.test( 97 | 'colors supported in TTY', 98 | { skip: !pretty.isColorSupported }, 99 | (t) => { 100 | const log = messageFormat(logDescriptor, MESSAGE_KEY) 101 | t.assert.strictEqual(log, expectedLogColored) 102 | } 103 | ) 104 | 105 | await t.test( 106 | 'colors not supported in TTY', 107 | { skip: pretty.isColorSupported }, 108 | (t) => { 109 | const log = messageFormat(logDescriptor, MESSAGE_KEY) 110 | t.assert.strictEqual(log, expectedLogUncolored) 111 | } 112 | ) 113 | } 114 | )) 115 | }) 116 | 117 | test('format log correctly with custom levels', async (t) => { 118 | const levels = { 119 | foo: 35, 120 | bar: 45 121 | } 122 | const messageFormat = messageFormatFactory( 123 | levels, 124 | undefined, 125 | pretty.isColorSupported 126 | ) 127 | 128 | const logCustomLevelsLogPairs = [ 129 | [ 130 | { time: EPOCH, level: 35, [MESSAGE_KEY]: 'basic foo log' }, 131 | `${TIME} - \u001b[37mfoo\u001b[39m - \u001B[36mbasic foo log\u001B[39m`, 132 | `${TIME} - foo - basic foo log` 133 | ], 134 | [ 135 | { 136 | time: EPOCH, 137 | level: 45, 138 | [MESSAGE_KEY]: 'basic incoming request bar log', 139 | req: { 140 | method: 'GET', 141 | url: '/bar' 142 | } 143 | }, 144 | `${TIME} - \u001b[37mbar\u001b[39m - GET /bar - \u001B[36mbasic incoming request bar log\u001B[39m`, 145 | `${TIME} - bar - GET /bar - basic incoming request bar log` 146 | ] 147 | ] 148 | 149 | await Promise.all(logCustomLevelsLogPairs.map( 150 | async ([logDescriptor, expectedLogColored, expectedLogUncolored]) => { 151 | await t.test( 152 | 'colors supported in TTY', 153 | { skip: !pretty.isColorSupported }, 154 | (t) => { 155 | const log = messageFormat(logDescriptor, MESSAGE_KEY) 156 | t.assert.strictEqual(log, expectedLogColored) 157 | } 158 | ) 159 | 160 | await t.test( 161 | 'colors not supported in TTY', 162 | { skip: pretty.isColorSupported }, 163 | (t) => { 164 | const log = messageFormat(logDescriptor, MESSAGE_KEY) 165 | t.assert.strictEqual(log, expectedLogUncolored) 166 | } 167 | ) 168 | } 169 | )) 170 | }) 171 | 172 | test('format log correctly with custom colors per level', async (t) => { 173 | const levels = { 174 | foo: 35, 175 | bar: 45 176 | } 177 | const messageFormat = messageFormatFactory( 178 | levels, 179 | { 180 | 35: 'bgCyanBright', 181 | 45: 'yellow' 182 | }, 183 | pretty.isColorSupported 184 | ) 185 | 186 | const logCustomLevelsLogPairs = [ 187 | [ 188 | { time: EPOCH, level: 35, [MESSAGE_KEY]: 'basic foo log' }, 189 | `${TIME} - \u001B[106mfoo\u001B[49m - \u001B[36mbasic foo log\u001B[39m`, 190 | `${TIME} - foo - basic foo log` 191 | ], 192 | [ 193 | { 194 | time: EPOCH, 195 | level: 45, 196 | [MESSAGE_KEY]: 'basic incoming request bar log', 197 | req: { 198 | method: 'GET', 199 | url: '/bar' 200 | } 201 | }, 202 | `${TIME} - \u001B[33mbar\u001B[39m - GET /bar - \u001B[36mbasic incoming request bar log\u001B[39m`, 203 | `${TIME} - bar - GET /bar - basic incoming request bar log` 204 | ] 205 | ] 206 | 207 | await Promise.all(logCustomLevelsLogPairs.map( 208 | async ([logDescriptor, expectedLogColored, expectedLogUncolored]) => { 209 | await t.test( 210 | 'colors supported in TTY', 211 | { skip: !pretty.isColorSupported }, 212 | (t) => { 213 | const log = messageFormat(logDescriptor, MESSAGE_KEY) 214 | t.assert.strictEqual(log, expectedLogColored) 215 | } 216 | ) 217 | 218 | await t.test( 219 | 'colors not supported in TTY', 220 | { skip: pretty.isColorSupported }, 221 | (t) => { 222 | const log = messageFormat(logDescriptor, MESSAGE_KEY) 223 | t.assert.strictEqual(log, expectedLogUncolored) 224 | } 225 | ) 226 | } 227 | )) 228 | }) 229 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { PinoPretty } from 'pino-pretty' 2 | 3 | type OneLineLogger = typeof PinoPretty 4 | 5 | declare namespace oneLineLogger { 6 | export interface Request { 7 | method: string; 8 | url: string; 9 | } 10 | 11 | export interface CustomColor { 12 | [key: number]: string; 13 | } 14 | 15 | export type LogDescriptor = Record & { 16 | time: number; 17 | level: number; 18 | colors?: CustomColor; 19 | req?: Request; 20 | } 21 | export const messageFormatFactory: (colorize: boolean, levels: Record, colors?: CustomColor) => (log: LogDescriptor, messageKey: string) => string 22 | 23 | export const oneLineLogger: OneLineLogger 24 | export { oneLineLogger as default } 25 | } 26 | 27 | declare function oneLineLogger (...params: Parameters): ReturnType 28 | export = oneLineLogger 29 | -------------------------------------------------------------------------------- /types/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import pretty from 'pino-pretty' 2 | import { expectType, expectAssignable } from 'tsd' 3 | import oneLineLogger, { 4 | CustomColor, 5 | LogDescriptor, 6 | Request, 7 | messageFormatFactory, 8 | oneLineLogger as oneLineLoggerNamed 9 | } from '..' 10 | 11 | expectType(({} as Request).method) 12 | expectType(({} as Request).url) 13 | 14 | expectType(({} as LogDescriptor).level) 15 | expectType(({} as LogDescriptor).time) 16 | expectType(({} as LogDescriptor).req) 17 | 18 | expectType<(colorize: boolean, levels: Record, colors?: CustomColor) => (log: LogDescriptor, messageKey: string) => string>(messageFormatFactory) 19 | 20 | expectType(oneLineLoggerNamed) 21 | expectAssignable<(opts?: pretty.PrettyOptions) => pretty.PrettyStream>(oneLineLogger) 22 | --------------------------------------------------------------------------------