├── .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 | [](https://github.com/fastify/one-line-logger/actions/workflows/ci.yml)
6 | [](https://www.npmjs.com/package/@fastify/one-line-logger)
7 | [](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 |
--------------------------------------------------------------------------------