├── .borp.yaml
├── .editorconfig
├── .eslintrc
├── .github
├── dependabot.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── .taprc.yaml
├── LICENSE
├── Readme.md
├── benchmark.js
├── bin.js
├── coverage-map.js
├── demo.png
├── docs
└── help.md
├── help
└── help.txt
├── index.d.ts
├── index.js
├── lib
├── colors.js
├── colors.test.js
├── constants.js
├── pretty.js
└── utils
│ ├── build-safe-sonic-boom.js
│ ├── build-safe-sonic-boom.test.js
│ ├── create-date.js
│ ├── create-date.test.js
│ ├── delete-log-property.js
│ ├── delete-log-property.test.js
│ ├── filter-log.js
│ ├── filter-log.test.js
│ ├── format-time.js
│ ├── format-time.test.js
│ ├── get-level-label-data.js
│ ├── get-property-value.js
│ ├── get-property-value.test.js
│ ├── handle-custom-levels-names-opts.js
│ ├── handle-custom-levels-names-opts.test.js
│ ├── handle-custom-levels-opts.js
│ ├── handle-custom-levels-opts.test.js
│ ├── index.js
│ ├── index.test.js
│ ├── interpret-conditionals.js
│ ├── interpret-conditionals.test.js
│ ├── is-object.js
│ ├── is-object.test.js
│ ├── is-valid-date.js
│ ├── is-valid-date.test.js
│ ├── join-lines-with-indentation.js
│ ├── join-lines-with-indentation.test.js
│ ├── noop.js
│ ├── noop.test.js
│ ├── parse-factory-options.js
│ ├── prettify-error-log.js
│ ├── prettify-error-log.test.js
│ ├── prettify-error.js
│ ├── prettify-error.test.js
│ ├── prettify-level.js
│ ├── prettify-level.test.js
│ ├── prettify-message.js
│ ├── prettify-message.test.js
│ ├── prettify-metadata.js
│ ├── prettify-metadata.test.js
│ ├── prettify-object.js
│ ├── prettify-object.test.js
│ ├── prettify-time.js
│ ├── prettify-time.test.js
│ ├── split-property-key.js
│ └── split-property-key.test.js
├── package.json
├── test
├── basic.test.js
├── cli-rc.test.js
├── cli.test.js
├── crlf.test.js
├── error-objects.test.js
├── example
│ └── example.js
└── types
│ └── pino-pretty.test-d.ts
└── tsconfig.json
/.borp.yaml:
--------------------------------------------------------------------------------
1 | reporters:
2 | - '@jsumners/line-reporter'
3 |
4 | files:
5 | - 'lib/**/*.test.js'
6 | - 'test/**/*.test.js'
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 |
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | insert_final_newline = true
8 | indent_style = space
9 | indent_size = 2
10 | trim_trailing_whitespace = true
11 |
12 | # [*.md]
13 | # trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "standard"
4 | ],
5 | "rules": {
6 | "no-var": "off"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.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/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | paths-ignore:
6 | - 'docs/**'
7 | - '*.md'
8 | pull_request:
9 | paths-ignore:
10 | - 'docs/**'
11 | - '*.md'
12 |
13 | # This allows a subsequently queued workflow run to interrupt previous runs
14 | concurrency:
15 | group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
16 | cancel-in-progress: true
17 |
18 | jobs:
19 | dependency-review:
20 | name: Dependency Review
21 | if: github.event_name == 'pull_request'
22 | runs-on: ubuntu-latest
23 | permissions:
24 | contents: read
25 | steps:
26 | - name: Check out repo
27 | uses: actions/checkout@v4
28 | with:
29 | persist-credentials: false
30 |
31 | - name: Dependency review
32 | uses: actions/dependency-review-action@v4
33 |
34 | test:
35 | name: Test
36 | runs-on: ${{ matrix.os }}
37 | outputs:
38 | COVERALLS: ${{ steps.coveralls-trigger.outputs.COVERALLS_TRIGGER }}
39 | permissions:
40 | contents: read
41 | strategy:
42 | matrix:
43 | node-version: [20, 22, 24]
44 | os: [ubuntu-latest]
45 | pino-version: [^9.0.0]
46 | steps:
47 | - name: Check out repo
48 | uses: actions/checkout@v4
49 | with:
50 | persist-credentials: false
51 |
52 | - name: Setup Node ${{ matrix.node-version }}
53 | uses: actions/setup-node@v4
54 | with:
55 | node-version: ${{ matrix.node-version }}
56 |
57 | - name: Restore cached dependencies
58 | uses: actions/cache@v4
59 | with:
60 | path: node_modules
61 | key: node-modules-${{ hashFiles('package.json') }}
62 |
63 | - name: Install dependencies
64 | run: npm i --ignore-scripts
65 |
66 | - name: Install pino ${{ matrix.pino-version }}
67 | run: npm i --no-save pino@${{ matrix.pino-version }}
68 |
69 | - name: Run tests
70 | run: npm run ci
71 |
72 | automerge:
73 | name: Automerge Dependabot PRs
74 | if: >
75 | github.event_name == 'pull_request' &&
76 | github.event.pull_request.user.login == 'dependabot[bot]'
77 | needs: test
78 | permissions:
79 | pull-requests: write
80 | contents: write
81 | runs-on: ubuntu-latest
82 | steps:
83 | - uses: fastify/github-action-merge-dependabot@v3
84 | with:
85 | github-token: ${{ secrets.GITHUB_TOKEN }}
86 |
--------------------------------------------------------------------------------
/.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 | # editor files
139 | .vscode
140 | .idea
141 |
142 | # lock files
143 | package-lock.json
144 | pnpm-lock.yaml
145 | yarn.lock
146 |
147 | # 0x
148 | .__browserify*
149 | profile-*
150 |
151 | # Run Configuration
152 | test/.tmp*
153 |
154 | # Local History
155 | .history
156 |
--------------------------------------------------------------------------------
/.taprc.yaml:
--------------------------------------------------------------------------------
1 | coverage: true
2 | coverage-map: 'coverage-map.js'
3 |
4 | reporter: terse
5 |
6 | files:
7 | - 'lib/**/*.test.js'
8 | - 'test/**/*.test.js'
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 the Pino team listed at https://github.com/pinojs/pino#the-team
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 | # pino-pretty
3 |
4 | [](https://www.npmjs.com/package/pino-pretty)
5 | [](https://github.com/pinojs/pino-pretty/actions?query=workflow%3ACI)
6 | [](https://coveralls.io/github/pinojs/pino-pretty?branch=master)
7 | [](https://standardjs.com/)
8 |
9 | This module provides a basic [ndjson](https://github.com/ndjson/ndjson-spec) formatter to be used in __development__. If an
10 | incoming line looks like it could be a log line from an ndjson logger, in
11 | particular the [Pino](https://getpino.io/) logging library, then it will apply
12 | extra formatting by considering things like the log level and timestamp.
13 |
14 | A standard Pino log line like:
15 |
16 | ```
17 | {"level":30,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo","v":1}
18 | ```
19 |
20 | Will format to:
21 |
22 | ```
23 | [17:35:28.992] INFO (42): hello world
24 | ```
25 |
26 | If you landed on this page due to the deprecation of the `prettyPrint` option
27 | of `pino`, read the [Programmatic Integration](#integration) section.
28 |
29 |
30 | ## Example
31 |
32 | Using the [example script][exscript] from the Pino module, we can see what the
33 | prettified logs will look like:
34 |
35 | 
36 |
37 | [exscript]: https://github.com/pinojs/pino/blob/25ba61f40ea5a1a753c85002812426d765da52a4/examples/basic.js
38 |
39 |
40 | ## Install
41 |
42 | ```sh
43 | npm install -g pino-pretty
44 | ```
45 |
46 |
47 | ## Usage
48 |
49 | It is recommended to use `pino-pretty` with `pino`
50 | by piping output to the CLI tool:
51 |
52 | ```sh
53 | node app.js | pino-pretty
54 | ```
55 |
56 |
57 | ### CLI Arguments
58 |
59 | - `--colorize` (`-c`): Adds terminal color escape sequences to the output.
60 | - `--no-colorizeObjects`: Suppress colorization of objects.
61 | - `--crlf` (`-f`): Appends carriage return and line feed, instead of just a line
62 | feed, to the formatted log line.
63 | - `--errorProps` (`-e`): When formatting an error object, display this list
64 | of properties. The list should be a comma-separated list of properties Default: `''`.
65 | Do not use this option if logging from pino@7. Support will be removed from future versions.
66 | - `--levelFirst` (`-l`): Display the log level name before the logged date and time.
67 | - `--errorLikeObjectKeys` (`-k`): Define the log keys that are associated with
68 | error like objects. Default: `err,error`.
69 | - `--messageKey` (`-m`): Define the key that contains the main log message.
70 | Default: `msg`.
71 | - `--levelKey` (`--levelKey`): Define the key that contains the level of the log. Nested keys are supported with each property delimited by a dot character (`.`).
72 | Keys may be escaped to target property names that contains the delimiter itself:
73 | (`--levelKey tags\\.level`).
74 | Default: `level`.
75 | - `--levelLabel` (`-b`): Output the log level using the specified label.
76 | Default: `levelLabel`.
77 | - `--minimumLevel` (`-L`): Hide messages below the specified log level. Accepts a number, `trace`, `debug`, `info`, `warn`, `error`, or `fatal`. If any more filtering is required, consider using [`jq`](https://stedolan.github.io/jq/).
78 | - `--customLevels` (`-x`): Override default levels with custom levels, e.g. `-x err:99,info:1`
79 | - `--customColors` (`-X`): Override default colors with custom colors, e.g. `-X err:red,info:blue`
80 | - `--useOnlyCustomProps` (`-U`): Only use custom levels and colors (if provided) (default: true); else fallback to default levels and colors, e.g. `-U false`
81 | - `--messageFormat` (`-o`): Format output of message, e.g. `{levelLabel} - {pid} - url:{req.url}` will output message: `INFO - 1123 - url:localhost:3000/test`
82 | Default: `false`
83 | - `--timestampKey` (`-a`): Define the key that contains the log timestamp.
84 | Default: `time`.
85 | - `--translateTime` (`-t`): Translate the epoch time value into a human-readable
86 | date and time string. This flag also can set the format string to apply when
87 | translating the date to a human-readable format. For a list of available pattern
88 | letters, see the [`dateformat` documentation](https://www.npmjs.com/package/dateformat).
89 | - The default format is `HH:MM:ss.l` in the local timezone.
90 | - Require a `UTC:` prefix to translate time to UTC, e.g. `UTC:yyyy-mm-dd HH:MM:ss.l o`.
91 | - Require a `SYS:` prefix to translate time to the local system's time zone. A
92 | shortcut `SYS:standard` to translate time to `yyyy-mm-dd HH:MM:ss.l o` in
93 | system time zone.
94 | - `--ignore` (`-i`): Ignore one or several keys, nested keys are supported with each property delimited by a dot character (`.`),
95 | keys may be escaped to target property names that contains the delimiter itself:
96 | (`-i time,hostname,req.headers,log\\.domain\\.corp/foo`).
97 | The `--ignore` option would be ignored, if both `--ignore` and `--include` are passed.
98 | Default: `hostname`.
99 | - `--include` (`-I`): The opposite of `--ignore`. Include one or several keys.
100 | - `--hideObject` (`-H`): Hide objects from output (but not error object)
101 | - `--singleLine` (`-S`): Print each log message on a single line (errors will still be multi-line)
102 | - `--config`: Specify a path to a config file containing the pino-pretty options. pino-pretty will attempt to read from a `.pino-prettyrc` in your current directory (`process.cwd`) if not specified
103 |
104 |
105 | ## Programmatic Integration
106 |
107 | We recommend against using `pino-pretty` in production and highly
108 | recommend installing `pino-pretty` as a development dependency.
109 |
110 | Install `pino-pretty` alongside `pino` and set the transport target to `'pino-pretty'`:
111 |
112 | ```js
113 | const pino = require('pino')
114 | const logger = pino({
115 | transport: {
116 | target: 'pino-pretty'
117 | },
118 | })
119 |
120 | logger.info('hi')
121 | ```
122 |
123 | The transport option can also have an options object containing `pino-pretty` options:
124 |
125 | ```js
126 | const pino = require('pino')
127 | const logger = pino({
128 | transport: {
129 | target: 'pino-pretty',
130 | options: {
131 | colorize: true
132 | }
133 | }
134 | })
135 |
136 | logger.info('hi')
137 | ```
138 |
139 | Use it as a stream:
140 |
141 | ```js
142 | const pino = require('pino')
143 | const pretty = require('pino-pretty')
144 | const logger = pino(pretty())
145 |
146 | logger.info('hi')
147 | ```
148 |
149 | Options are also supported:
150 |
151 | ```js
152 | const pino = require('pino')
153 | const pretty = require('pino-pretty')
154 | const stream = pretty({
155 | colorize: true
156 | })
157 | const logger = pino(stream)
158 |
159 | logger.info('hi')
160 | ```
161 |
162 | See the [Options](#options) section for all possible options.
163 |
164 |
165 | ### Usage as a stream
166 |
167 | If you are using `pino-pretty` as a stream and you need to provide options to `pino`,
168 | pass the options as the first argument and `pino-pretty` as second argument:
169 |
170 | ```js
171 | const pino = require('pino')
172 | const pretty = require('pino-pretty')
173 | const stream = pretty({
174 | colorize: true
175 | })
176 | const logger = pino({ level: 'info' }, stream)
177 |
178 | // Nothing is printed
179 | logger.debug('hi')
180 | ```
181 |
182 | ### Usage with Jest
183 |
184 | Logging with Jest is _problematic_, as the test framework requires no asynchronous operation to
185 | continue after the test has finished. The following is the only supported way to use this module
186 | with Jest:
187 |
188 | ```js
189 | import pino from 'pino'
190 | import pretty from 'pino-pretty'
191 |
192 | test('test pino-pretty', () => {
193 | const logger = pino(pretty({ sync: true }));
194 | logger.info('Info');
195 | logger.error('Error');
196 | });
197 | ```
198 |
199 | ### Handling non-serializable options
200 |
201 | Using the new [pino v7+
202 | transports](https://getpino.io/#/docs/transports?id=v7-transports) not all
203 | options are serializable, for example if you want to use `messageFormat` as a
204 | function you will need to wrap `pino-pretty` in a custom module.
205 |
206 | Executing `main.js` below will log a colorized `hello world` message using a
207 | custom function `messageFormat`:
208 |
209 | ```js
210 | // main.js
211 | const pino = require('pino')
212 |
213 | const logger = pino({
214 | transport: {
215 | target: './pino-pretty-transport',
216 | options: {
217 | colorize: true
218 | }
219 | },
220 | })
221 |
222 | logger.info('world')
223 | ```
224 |
225 | ```js
226 | // pino-pretty-transport.js
227 | module.exports = opts => require('pino-pretty')({
228 | ...opts,
229 | messageFormat: (log, messageKey) => `hello ${log[messageKey]}`
230 | })
231 | ```
232 |
233 | ### Checking color support in TTY
234 |
235 | This boolean returns whether the currently used TTY supports colorizing the logs.
236 |
237 | ```js
238 | import pretty from 'pino-pretty'
239 |
240 | if (pretty.isColorSupported) {
241 | ...
242 | }
243 |
244 | ```
245 |
246 |
247 | ### Options
248 |
249 | The options accepted have keys corresponding to the options described in [CLI Arguments](#cliargs):
250 |
251 | ```js
252 | {
253 | colorize: colorette.isColorSupported, // --colorize
254 | colorizeObjects: true, //--colorizeObjects
255 | crlf: false, // --crlf
256 | errorLikeObjectKeys: ['err', 'error'], // --errorLikeObjectKeys (not required to match custom errorKey with pino >=8.21.0)
257 | errorProps: '', // --errorProps
258 | levelFirst: false, // --levelFirst
259 | messageKey: 'msg', // --messageKey (not required with pino >=8.21.0)
260 | levelKey: 'level', // --levelKey
261 | messageFormat: false, // --messageFormat
262 | timestampKey: 'time', // --timestampKey
263 | translateTime: false, // --translateTime
264 | ignore: 'pid,hostname', // --ignore
265 | include: 'level,time', // --include
266 | hideObject: false, // --hideObject
267 | singleLine: false, // --singleLine
268 | customColors: 'err:red,info:blue', // --customColors
269 | customLevels: 'err:99,info:1', // --customLevels (not required with pino >=8.21.0)
270 | levelLabel: 'levelLabel', // --levelLabel
271 | minimumLevel: 'info', // --minimumLevel
272 | useOnlyCustomProps: true, // --useOnlyCustomProps
273 | // The file or file descriptor (1 is stdout) to write to
274 | destination: 1,
275 |
276 | // Alternatively, pass a `sonic-boom` instance (allowing more flexibility):
277 | // destination: new SonicBoom({ dest: 'a/file', mkdir: true })
278 |
279 | // You can also configure some SonicBoom options directly
280 | sync: false, // by default we write asynchronously
281 | append: true, // the file is opened with the 'a' flag
282 | mkdir: true, // create the target destination
283 |
284 |
285 | customPrettifiers: {}
286 | }
287 | ```
288 |
289 | The `colorize` default follows
290 | [`colorette.isColorSupported`](https://github.com/jorgebucaran/colorette#iscolorsupported).
291 |
292 | The defaults for `sync`, `append`, `mkdir` inherit from
293 | [`SonicBoom(opts)`](https://github.com/pinojs/sonic-boom#API).
294 |
295 | `customPrettifiers` option provides the ability to add a custom prettify function
296 | for specific log properties. `customPrettifiers` is an object, where keys are
297 | log properties that will be prettified and value is the prettify function itself.
298 | For example, if a log line contains a `query` property,
299 | you can specify a prettifier for it:
300 |
301 | ```js
302 | {
303 | customPrettifiers: {
304 | query: prettifyQuery
305 | }
306 | }
307 | //...
308 | const prettifyQuery = value => {
309 | // do some prettify magic
310 | }
311 | ```
312 |
313 | All prettifiers use this function signature:
314 |
315 | ```js
316 | ['logObjKey']: (output, keyName, logObj, extras) => string
317 | ```
318 |
319 | * `logObjKey` - name of the key of the property in the log object that should have this function applied to it
320 | * `output` - the value of the property in the log object
321 | * `keyName` - the name of the property (useful for `level` and `message` when `levelKey` or `messageKey` is used)
322 | * `logObj` - the full log object, for context
323 | * `extras` - an object containing **additional** data/functions created in the context of this pino-pretty logger or specific to the key (see `level` prettifying below)
324 | * All `extras` objects contain `colors` which is a [Colorette](https://github.com/jorgebucaran/colorette?tab=readme-ov-file#supported-colors) object containing color functions. Colors are enabled based on `colorize` provided to pino-pretty or `colorette.isColorSupported` if `colorize` was not provided.
325 |
326 | Additionally, `customPrettifiers` can be used to format the `time`, `hostname`,
327 | `pid`, `name`, `caller` and `level` outputs AS WELL AS any arbitrary key-value that exists on a given log object.
328 |
329 | An example usage of `customPrettifiers` using all parameters from the function signature:
330 |
331 | ```js
332 | {
333 | customPrettifiers: {
334 | // The argument for this function will be the same
335 | // string that's at the start of the log-line by default:
336 | time: timestamp => `🕰 ${timestamp}`,
337 |
338 | // The argument for the level-prettifier may vary depending
339 | // on if the levelKey option is used or not.
340 | // By default this will be the same numerics as the Pino default:
341 | level: logLevel => `LEVEL: ${logLevel}`,
342 | // level provides additional data in `extras`:
343 | // * label => derived level label string
344 | // * labelColorized => derived level label string with colorette colors applied based on customColors and whether colors are supported
345 | level: (logLevel, key, log, { label, labelColorized, colors }) => `LEVEL: ${logLevel} LABEL: ${levelLabel} COLORIZED LABEL: ${labelColorized}`,
346 |
347 | // other prettifiers can be used for the other keys if needed, for example
348 | hostname: hostname => `MY HOST: ${hostname}`,
349 | pid: pid => pid,
350 | name: (name, key, log, { colors }) => `${colors.blue(name)}`,
351 | caller: (caller, key, log, { colors }) => `${colors.greenBright(caller)}`,
352 | myCustomLogProp: (value, key, log, { colors }) => `My Prop -> ${colors.bold(value)} <--`
353 | }
354 | }
355 | ```
356 |
357 | `messageFormat` option allows you to customize the message output.
358 | A template `string` like this can define the format:
359 |
360 | ```js
361 | {
362 | messageFormat: '{levelLabel} - {pid} - url:{req.url}'
363 | }
364 | ```
365 |
366 | In addition to this, if / end statement blocks can also be specified.
367 | Else statements and nested conditions are not supported.
368 |
369 | ```js
370 | {
371 | messageFormat: '{levelLabel} - {if pid}{pid} - {end}url:{req.url}'
372 | }
373 | ```
374 |
375 | This option can also be defined as a `function` with this function signature:
376 |
377 | ```js
378 | {
379 | messageFormat: (log, messageKey, levelLabel, { colors }) => {
380 | // do some log message customization
381 | //
382 | // `colors` is a Colorette object with colors enabled based on `colorize` option
383 | return `This is a ${colors.red('colorized')}, custom message: ${log[messageKey]}`;
384 | }
385 | }
386 | ```
387 |
388 | ## Limitations
389 |
390 | Because `pino-pretty` uses stdout redirection, in some cases the command may
391 | terminate with an error due to shell limitations.
392 |
393 | For example, currently, mingw64 based shells (e.g. Bash as supplied by [git for
394 | Windows](https://gitforwindows.org)) are affected and terminate the process with
395 | a `stdout is not a tty` error message.
396 |
397 | Any PRs are welcomed!
398 |
399 |
400 | ## License
401 |
402 | MIT License
403 |
--------------------------------------------------------------------------------
/benchmark.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | // We do not expect amazing numbers from `pino-pretty` as the whole purpose
4 | // of the module is a very slow operation. However, this benchmark should give
5 | // us some guidance on how features, or code changes, will affect the
6 | // performance of the module.
7 |
8 | const bench = require('fastbench')
9 | const {
10 | prettyFactory
11 | } = require('./index')
12 |
13 | const max = 10
14 | const tstampMillis = 1693401358754
15 |
16 | /* eslint-disable no-var */
17 | const run = bench([
18 | function basicLog (cb) {
19 | const pretty = prettyFactory({})
20 | const input = `{"time":${tstampMillis},"pid":1,"hostname":"foo","msg":"benchmark","foo":"foo","bar":{"bar":"bar"}}\n`
21 | for (var i = 0; i < max; i += 1) {
22 | pretty(input)
23 | }
24 | setImmediate(cb)
25 | },
26 |
27 | function objectLog (cb) {
28 | const pretty = prettyFactory({})
29 | const input = {
30 | time: tstampMillis,
31 | pid: 1,
32 | hostname: 'foo',
33 | msg: 'benchmark',
34 | foo: 'foo',
35 | bar: { bar: 'bar' }
36 | }
37 | for (var i = 0; i < max; i += 1) {
38 | pretty(input)
39 | }
40 | setImmediate(cb)
41 | },
42 |
43 | function coloredLog (cb) {
44 | const pretty = prettyFactory({ colorize: true })
45 | const input = `{"time":${tstampMillis},"pid":1,"hostname":"foo","msg":"benchmark","foo":"foo","bar":{"bar":"bar"}}\n`
46 | for (var i = 0; i < max; i += 1) {
47 | pretty(input)
48 | }
49 | setImmediate(cb)
50 | },
51 |
52 | function customPrettifiers (cb) {
53 | const pretty = prettyFactory({
54 | customPrettifiers: {
55 | time (tstamp) {
56 | return tstamp
57 | },
58 | pid () {
59 | return ''
60 | }
61 | }
62 | })
63 | const input = `{"time":${tstampMillis},"pid":1,"hostname":"foo","msg":"benchmark","foo":"foo","bar":{"bar":"bar"}}\n`
64 | for (var i = 0; i < max; i += 1) {
65 | pretty(input)
66 | }
67 | setImmediate(cb)
68 | },
69 |
70 | function logWithErrorObject (cb) {
71 | const pretty = prettyFactory({})
72 | const err = Error('boom')
73 | const input = `{"time":${tstampMillis},"pid":1,"hostname":"foo","msg":"benchmark","foo":"foo","bar":{"bar":"bar"},"err":{"message":"${err.message}","stack":"${err.stack}"}}\n`
74 | for (var i = 0; i < max; i += 1) {
75 | pretty(input)
76 | }
77 | setImmediate(cb)
78 | },
79 |
80 | function logRemappedMsgErrKeys (cb) {
81 | const pretty = prettyFactory({
82 | messageKey: 'message',
83 | errorLikeObjectKeys: ['myError']
84 | })
85 | const err = Error('boom')
86 | const input = `{"time":${tstampMillis},"pid":1,"hostname":"foo","message":"benchmark","foo":"foo","bar":{"bar":"bar"},"myError":{"message":"${err.message}","stack":"${err.stack}"}}\n`
87 | for (var i = 0; i < max; i += 1) {
88 | pretty(input)
89 | }
90 | setImmediate(cb)
91 | },
92 |
93 | function messageFormatString (cb) {
94 | const pretty = prettyFactory({
95 | messageFormat: '{levelLabel}{if pid} {pid} - {end}{msg}'
96 | })
97 | const input = `{"time":${tstampMillis},"pid":1,"hostname":"foo","msg":"benchmark","foo":"foo","bar":{"bar":"bar"}}\n`
98 | for (var i = 0; i < max; i += 1) {
99 | pretty(input)
100 | }
101 | setImmediate(cb)
102 | }
103 | ], 10000)
104 |
105 | run(run)
106 |
--------------------------------------------------------------------------------
/bin.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | 'use strict'
4 |
5 | const fs = require('node:fs')
6 | const path = require('node:path')
7 | const help = require('help-me')({
8 | dir: path.join(__dirname, 'help'),
9 | ext: '.txt'
10 | })
11 | const pump = require('pump')
12 | const sjp = require('secure-json-parse')
13 | const JoyCon = require('joycon')
14 | const stripJsonComments = require('strip-json-comments')
15 |
16 | const build = require('./')
17 | const CONSTANTS = require('./lib/constants')
18 | const { isObject } = require('./lib/utils')
19 | const minimist = require('minimist')
20 |
21 | const parseJSON = input => {
22 | return sjp.parse(stripJsonComments(input), { protoAction: 'remove' })
23 | }
24 |
25 | const joycon = new JoyCon({
26 | parseJSON,
27 | files: [
28 | 'pino-pretty.config.cjs',
29 | 'pino-pretty.config.js',
30 | '.pino-prettyrc',
31 | '.pino-prettyrc.json'
32 | ],
33 | stopDir: path.dirname(process.cwd())
34 | })
35 |
36 | const cmd = minimist(process.argv.slice(2))
37 |
38 | if (cmd.h || cmd.help) {
39 | help.toStdout()
40 | } else {
41 | const DEFAULT_VALUE = '\0default'
42 |
43 | let opts = minimist(process.argv, {
44 | alias: {
45 | colorize: 'c',
46 | crlf: 'f',
47 | errorProps: 'e',
48 | levelFirst: 'l',
49 | minimumLevel: 'L',
50 | customLevels: 'x',
51 | customColors: 'X',
52 | useOnlyCustomProps: 'U',
53 | errorLikeObjectKeys: 'k',
54 | messageKey: 'm',
55 | levelKey: CONSTANTS.LEVEL_KEY,
56 | levelLabel: 'b',
57 | messageFormat: 'o',
58 | timestampKey: 'a',
59 | translateTime: 't',
60 | ignore: 'i',
61 | include: 'I',
62 | hideObject: 'H',
63 | singleLine: 'S'
64 | },
65 | default: {
66 | messageKey: DEFAULT_VALUE,
67 | minimumLevel: DEFAULT_VALUE,
68 | levelKey: DEFAULT_VALUE,
69 | timestampKey: DEFAULT_VALUE
70 | }
71 | })
72 |
73 | // Remove default values
74 | opts = filter(opts, value => value !== DEFAULT_VALUE)
75 | const config = loadConfig(opts.config)
76 | // Override config with cli options
77 | opts = Object.assign({}, config, opts)
78 | // set defaults
79 | opts.errorLikeObjectKeys = opts.errorLikeObjectKeys || 'err,error'
80 | opts.errorProps = opts.errorProps || ''
81 |
82 | const res = build(opts)
83 | pump(process.stdin, res)
84 |
85 | // https://github.com/pinojs/pino/pull/358
86 | /* istanbul ignore next */
87 | if (!process.stdin.isTTY && !fs.fstatSync(process.stdin.fd).isFile()) {
88 | process.once('SIGINT', function noOp () {})
89 | }
90 |
91 | function loadConfig (configPath) {
92 | const files = configPath ? [path.resolve(configPath)] : undefined
93 | const result = joycon.loadSync(files)
94 | if (result.path && !isObject(result.data)) {
95 | configPath = configPath || path.basename(result.path)
96 | throw new Error(`Invalid runtime configuration file: ${configPath}`)
97 | }
98 | if (configPath && !result.data) {
99 | throw new Error(`Failed to load runtime configuration file: ${configPath}`)
100 | }
101 | return result.data
102 | }
103 |
104 | function filter (obj, cb) {
105 | return Object.keys(obj).reduce((acc, key) => {
106 | const value = obj[key]
107 | if (cb(value, key)) {
108 | acc[key] = value
109 | }
110 | return acc
111 | }, {})
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/coverage-map.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = testFile => {
4 | // Ignore coverage on files that do not have a direct corollary.
5 | if (testFile.startsWith('test/')) return false
6 |
7 | // Indicate the matching name, sans '.test.js', should be checked for coverage.
8 | return testFile.replace(/\.test\.js$/, '.js')
9 | }
10 |
--------------------------------------------------------------------------------
/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pinojs/pino-pretty/2537c2b50ee8a4a6fee903ad4ece1dd3b0fb057f/demo.png
--------------------------------------------------------------------------------
/docs/help.md:
--------------------------------------------------------------------------------
1 |
2 | ## Systemd example
3 |
4 | If you run your Node.js process via [Systemd](https://www.freedesktop.org/wiki/Software/systemd/) and you examine your logs with [journalctl](https://www.freedesktop.org/software/systemd/man/journalctl.html) some data will be duplicated. You can use a combination of `journalctl` options and `pino-pretty` options to shape the output.
5 |
6 | For example viewing the prettified logs of a process named `monitor` with `journalctl -u monitor -f | pino-pretty`, might output something like this:
7 |
8 | ```
9 | Apr 24 07:40:01 nanopi node[6080]: {"level":30,"time":1587706801902,"pid":6080,"hostname":"nanopi","msg":"TT
10 | 21","v":1}
11 | ```
12 | As you can see, the timestamp, hostname, and pid are duplicated.
13 | If you just want the bare prettified Pino logs you can strip out the duplicate items from the `journalctl` output with the `-o cat` option of `journalctl`:
14 | ```
15 | journalctl -u monitor -f -o cat | pino-pretty
16 | ```
17 | the output now looks something like this:
18 | ```
19 | [1587706801902] INFO (6080 on nanopi): TT 21
20 | ```
21 | Make the output even more human readable by using the pino-pretty options `-t` to format the timestamp and `-i pid, hostname` to filter out hostname and pid:
22 | ```
23 | [2020-04-24 05:42:24.836 +0000] INFO : TT 21
24 | ```
25 |
--------------------------------------------------------------------------------
/help/help.txt:
--------------------------------------------------------------------------------
1 | Usage: pino-pretty [options] [command]
2 |
3 | Commands:
4 | help Display help
5 | version Display version
6 |
7 | Options:
8 | -c, --colorize Force adding color sequences to the output
9 | -C, --config specify a path to a json file containing the pino-pretty options
10 | -f, --crlf Append CRLF instead of LF to formatted lines
11 | -X, --customColors Override default colors using names from https://www.npmjs.com/package/colorette (`-X err:red,info:blue`)
12 | -x, --customLevels Override default levels (`-x err:99,info:1`)
13 | -k, --errorLikeObjectKeys Define which keys contain error objects (`-k err,error`) (defaults to `err,error`)
14 | -e, --errorProps Comma separated list of properties on error objects to show (`*` for all properties) (defaults to ``)
15 | -h, --help Output usage information
16 | -H, --hideObject Hide objects from output (but not error object)
17 | -i, --ignore Ignore one or several keys: (`-i time,hostname`)
18 | -I, --include The opposite of `--ignore`, only include one or several keys: (`-I level,time`)
19 | -l, --levelFirst Display the log level as the first output field
20 | -L, --levelKey [value] Detect the log level under the specified key (defaults to "level")
21 | -b, --levelLabel [value] Output the log level using the specified label (defaults to "levelLabel")
22 | -o, --messageFormat Format output of message
23 | -m, --messageKey [value] Highlight the message under the specified key (defaults to "msg")
24 | -L, --minimumLevel Hide messages below the specified log level
25 | -S, --singleLine Print all non-error objects on a single line
26 | -a, --timestampKey [value] Display the timestamp from the specified key (defaults to "time")
27 | -t, --translateTime Display epoch timestamps as UTC ISO format or according to an optional format string (default ISO 8601)
28 | -U, --useOnlyCustomProps Only use custom levels and colors (if provided); don't fallback to default levels and colors (-U false)
29 | -v, --version Output the version number
30 |
31 | Examples:
32 | - To prettify logs, simply pipe a log file through
33 | $ cat log | pino-pretty
34 |
35 | - To highlight a string at a key other than 'msg'
36 | $ cat log | pino-pretty -m fooMessage
37 |
38 | - To detect the log level at a key other than 'level'
39 | $ cat log | pino-pretty --levelKey fooLevel
40 |
41 | - To output the log level label using a key other than 'levelLabel'
42 | $ cat log | pino-pretty --levelLabel LVL -o "{LVL}"
43 |
44 | - To display timestamp from a key other than 'time'
45 | $ cat log | pino-pretty -a fooTimestamp
46 |
47 | - To convert Epoch timestamps to ISO timestamps use the -t option
48 | $ cat log | pino-pretty -t
49 |
50 | - To convert Epoch timestamps to local timezone format use the -t option with "SYS:" prefixed format string
51 | $ cat log | pino-pretty -t "SYS:yyyy-mm-dd HH:MM:ss"
52 |
53 | - To flip level and time/date in standard output use the -l option
54 | $ cat log | pino-pretty -l
55 |
56 | - Only prints messages with a minimum log level of info
57 | $ cat log | pino-pretty -L info
58 |
59 | - Prettify logs but don't print pid and hostname
60 | $ cat log | pino-pretty -i pid,hostname
61 |
62 | - Prettify logs but only print time and level
63 | $ cat log | pino-pretty -I time,level
64 |
65 | - Loads options from a config file
66 | $ cat log | pino-pretty --config=/path/to/config.json
67 |
68 |
69 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | // Type definitions for pino-pretty 7.0
2 | // Project: https://github.com/pinojs/pino-pretty#readme
3 | // Definitions by: Adam Vigneaux
4 | // tearwyx
5 | // Minimum TypeScript Version: 3.0
6 |
7 | ///
8 |
9 | import { Transform } from 'node:stream';
10 | import { OnUnknown } from 'pino-abstract-transport';
11 | // @ts-ignore fall back to any if pino is not available, i.e. when running pino tests
12 | import { DestinationStream, Level } from 'pino';
13 | import * as Colorette from "colorette";
14 |
15 | type LogDescriptor = Record;
16 |
17 | declare function PinoPretty(options?: PinoPretty.PrettyOptions): PinoPretty.PrettyStream;
18 | declare namespace PinoPretty {
19 |
20 | function colorizerFactory(
21 | useColors?: boolean,
22 | customColors?: [number, string][],
23 | useOnlyCustomProps?: boolean,
24 | ): {
25 | (
26 | level?: number | string,
27 | opts?: {
28 | customLevels?: { [level: number]: string };
29 | customLevelNames?: { [name: string]: number };
30 | },
31 | ): string,
32 | message: (input: string | number) => string,
33 | greyMessage: (input: string | number) => string,
34 | }
35 |
36 | function prettyFactory(options: PrettyOptions): (inputData: any) => string
37 |
38 | interface PrettyOptions {
39 | /**
40 | * Hide objects from output (but not error object).
41 | * @default false
42 | */
43 | hideObject?: boolean;
44 | /**
45 | * Translate the epoch time value into a human readable date and time string. This flag also can set the format
46 | * string to apply when translating the date to human readable format. For a list of available pattern letters
47 | * see the {@link https://www.npmjs.com/package/dateformat|dateformat documentation}.
48 | * - The default format is `yyyy-mm-dd HH:MM:ss.l o` in UTC.
49 | * - Requires a `SYS:` prefix to translate time to the local system's timezone. Use the shortcut `SYS:standard`
50 | * to translate time to `yyyy-mm-dd HH:MM:ss.l o` in system timezone.
51 | * @default false
52 | */
53 | translateTime?: boolean | string;
54 | /**
55 | * If set to true, it will print the name of the log level as the first field in the log line.
56 | * @default false
57 | */
58 | levelFirst?: boolean;
59 | /**
60 | * Define the key that contains the level of the log.
61 | * @default "level"
62 | */
63 | levelKey?: string;
64 | /**
65 | * Output the log level using the specified label.
66 | * @default "levelLabel"
67 | */
68 | levelLabel?: string;
69 | /**
70 | * The key in the JSON object to use as the highlighted message.
71 | * @default "msg"
72 | *
73 | * Not required when used with pino >= 8.21.0
74 | */
75 | messageKey?: string;
76 | /**
77 | * Print each log message on a single line (errors will still be multi-line).
78 | * @default false
79 | */
80 | singleLine?: boolean;
81 | /**
82 | * The key in the JSON object to use for timestamp display.
83 | * @default "time"
84 | */
85 | timestampKey?: string;
86 | /**
87 | * The minimum log level to include in the output.
88 | * @default "trace"
89 | */
90 | minimumLevel?: Level;
91 | /**
92 | * Format output of message, e.g. {level} - {pid} will output message: INFO - 1123
93 | * @default false
94 | *
95 | * @example
96 | * ```typescript
97 | * {
98 | * messageFormat: (log, messageKey) => {
99 | * const message = log[messageKey];
100 | * if (log.requestId) return `[${log.requestId}] ${message}`;
101 | * return message;
102 | * }
103 | * }
104 | * ```
105 | */
106 | messageFormat?: false | string | MessageFormatFunc;
107 | /**
108 | * If set to true, will add color information to the formatted output message.
109 | * @default false
110 | */
111 | colorize?: boolean;
112 | /**
113 | * If set to false while `colorize` is `true`, will output JSON objects without color.
114 | * @default true
115 | */
116 | colorizeObjects?: boolean;
117 | /**
118 | * Appends carriage return and line feed, instead of just a line feed, to the formatted log line.
119 | * @default false
120 | */
121 | crlf?: boolean;
122 | /**
123 | * Define the log keys that are associated with error like objects.
124 | * @default ["err", "error"]
125 | *
126 | * Not required to handle custom errorKey when used with pino >= 8.21.0
127 | */
128 | errorLikeObjectKeys?: string[];
129 | /**
130 | * When formatting an error object, display this list of properties.
131 | * The list should be a comma separated list of properties.
132 | * @default ""
133 | */
134 | errorProps?: string;
135 | /**
136 | * Ignore one or several keys.
137 | * Will be overridden by the option include if include is presented.
138 | * @example "time,hostname"
139 | */
140 | ignore?: string;
141 | /**
142 | * Include one or several keys.
143 | * @example "time,level"
144 | */
145 | include?: string;
146 | /**
147 | * Makes messaging synchronous.
148 | * @default false
149 | */
150 | sync?: boolean;
151 | /**
152 | * The file, file descriptor, or stream to write to. Defaults to 1 (stdout).
153 | * @default 1
154 | */
155 | destination?: string | number | DestinationStream | NodeJS.WritableStream;
156 | /**
157 | * Opens the file with the 'a' flag.
158 | * @default true
159 | */
160 | append?: boolean;
161 | /**
162 | * Ensure directory for destination file exists.
163 | * @default false
164 | */
165 | mkdir?: boolean;
166 | /**
167 | * Provides the ability to add a custom prettify function for specific log properties.
168 | * `customPrettifiers` is an object, where keys are log properties that will be prettified
169 | * and value is the prettify function itself.
170 | * For example, if a log line contains a query property, you can specify a prettifier for it:
171 | * @default {}
172 | *
173 | * @example
174 | * ```typescript
175 | * {
176 | * customPrettifiers: {
177 | * query: prettifyQuery
178 | * }
179 | * }
180 | * //...
181 | * const prettifyQuery = value => {
182 | * // do some prettify magic
183 | * }
184 | * ```
185 | */
186 | customPrettifiers?: Record &
187 | {
188 | level?: Prettifier
189 | };
190 | /**
191 | * Change the level names and values to an user custom preset.
192 | *
193 | * Can be a CSV string in 'level_name:level_value' format or an object.
194 | *
195 | * @example ( CSV ) customLevels: 'info:10,some_level:40'
196 | * @example ( Object ) customLevels: { info: 10, some_level: 40 }
197 | *
198 | * Not required when used with pino >= 8.21.0
199 | */
200 | customLevels?: string|object;
201 | /**
202 | * Change the level colors to an user custom preset.
203 | *
204 | * Can be a CSV string in 'level_name:color_value' format or an object.
205 | * Also supports 'default' as level_name for fallback color.
206 | *
207 | * @example ( CSV ) customColors: 'info:white,some_level:red'
208 | * @example ( Object ) customColors: { info: 'white', some_level: 'red' }
209 | */
210 | customColors?: string|object;
211 | /**
212 | * Only use custom levels and colors (if provided); else fallback to default levels and colors.
213 | *
214 | * @default true
215 | */
216 | useOnlyCustomProps?: boolean;
217 | }
218 |
219 | function build(options: PrettyOptions): PrettyStream;
220 |
221 | type Prettifier = (inputData: string | object, key: string, log: object, extras: PrettifierExtras) => string;
222 | type PrettifierExtras = {colors: Colorette.Colorette} & T;
223 | type LevelPrettifierExtras = {label: string, labelColorized: string}
224 | type MessageFormatFunc = (log: LogDescriptor, messageKey: string, levelLabel: string, extras: PrettifierExtras) => string;
225 | type PrettyStream = Transform & OnUnknown;
226 | type ColorizerFactory = typeof colorizerFactory;
227 | type PrettyFactory = typeof prettyFactory;
228 | type Build = typeof build;
229 | type isColorSupported = typeof Colorette.isColorSupported;
230 |
231 | export { build, PinoPretty, PrettyOptions, PrettyStream, colorizerFactory, prettyFactory, isColorSupported };
232 | }
233 |
234 | export = PinoPretty;
235 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { isColorSupported } = require('colorette')
4 | const pump = require('pump')
5 | const { Transform } = require('node:stream')
6 | const abstractTransport = require('pino-abstract-transport')
7 | const colors = require('./lib/colors')
8 | const {
9 | ERROR_LIKE_KEYS,
10 | LEVEL_KEY,
11 | LEVEL_LABEL,
12 | MESSAGE_KEY,
13 | TIMESTAMP_KEY
14 | } = require('./lib/constants')
15 | const {
16 | buildSafeSonicBoom,
17 | parseFactoryOptions
18 | } = require('./lib/utils')
19 | const pretty = require('./lib/pretty')
20 |
21 | /**
22 | * @typedef {object} PinoPrettyOptions
23 | * @property {boolean} [colorize] Indicates if colors should be used when
24 | * prettifying. The default will be determined by the terminal capabilities at
25 | * run time.
26 | * @property {boolean} [colorizeObjects=true] Apply coloring to rendered objects
27 | * when coloring is enabled.
28 | * @property {boolean} [crlf=false] End lines with `\r\n` instead of `\n`.
29 | * @property {string|null} [customColors=null] A comma separated list of colors
30 | * to use for specific level labels, e.g. `err:red,info:blue`.
31 | * @property {string|null} [customLevels=null] A comma separated list of user
32 | * defined level names and numbers, e.g. `err:99,info:1`.
33 | * @property {CustomPrettifiers} [customPrettifiers={}] A set of prettifier
34 | * functions to apply to keys defined in this object.
35 | * @property {K_ERROR_LIKE_KEYS} [errorLikeObjectKeys] A list of string property
36 | * names to consider as error objects.
37 | * @property {string} [errorProps=''] A comma separated list of properties on
38 | * error objects to include in the output.
39 | * @property {boolean} [hideObject=false] When `true`, data objects will be
40 | * omitted from the output (except for error objects).
41 | * @property {string} [ignore='hostname'] A comma separated list of log keys
42 | * to omit when outputting the prettified log information.
43 | * @property {undefined|string} [include=undefined] A comma separated list of
44 | * log keys to include in the prettified log information. Only the keys in this
45 | * list will be included in the output.
46 | * @property {boolean} [levelFirst=false] When true, the log level will be the
47 | * first field in the prettified output.
48 | * @property {string} [levelKey='level'] The key name in the log data that
49 | * contains the level value for the log.
50 | * @property {string} [levelLabel='levelLabel'] Token name to use in
51 | * `messageFormat` to represent the name of the logged level.
52 | * @property {null|MessageFormatString|MessageFormatFunction} [messageFormat=null]
53 | * When a string, defines how the prettified line should be formatted according
54 | * to defined tokens. When a function, a synchronous function that returns a
55 | * formatted string.
56 | * @property {string} [messageKey='msg'] Defines the key in incoming logs that
57 | * contains the message of the log, if present.
58 | * @property {undefined|string|number} [minimumLevel=undefined] The minimum
59 | * level for logs that should be processed. Any logs below this level will
60 | * be omitted.
61 | * @property {object} [outputStream=process.stdout] The stream to write
62 | * prettified log lines to.
63 | * @property {boolean} [singleLine=false] When `true` any objects, except error
64 | * objects, in the log data will be printed as a single line instead as multiple
65 | * lines.
66 | * @property {string} [timestampKey='time'] Defines the key in incoming logs
67 | * that contains the timestamp of the log, if present.
68 | * @property {boolean|string} [translateTime=true] When true, will translate a
69 | * JavaScript date integer into a human-readable string. If set to a string,
70 | * it must be a format string.
71 | * @property {boolean} [useOnlyCustomProps=true] When true, only custom levels
72 | * and colors will be used if they have been provided.
73 | */
74 |
75 | /**
76 | * The default options that will be used when prettifying log lines.
77 | *
78 | * @type {PinoPrettyOptions}
79 | */
80 | const defaultOptions = {
81 | colorize: isColorSupported,
82 | colorizeObjects: true,
83 | crlf: false,
84 | customColors: null,
85 | customLevels: null,
86 | customPrettifiers: {},
87 | errorLikeObjectKeys: ERROR_LIKE_KEYS,
88 | errorProps: '',
89 | hideObject: false,
90 | ignore: 'hostname',
91 | include: undefined,
92 | levelFirst: false,
93 | levelKey: LEVEL_KEY,
94 | levelLabel: LEVEL_LABEL,
95 | messageFormat: null,
96 | messageKey: MESSAGE_KEY,
97 | minimumLevel: undefined,
98 | outputStream: process.stdout,
99 | singleLine: false,
100 | timestampKey: TIMESTAMP_KEY,
101 | translateTime: true,
102 | useOnlyCustomProps: true
103 | }
104 |
105 | /**
106 | * Processes the supplied options and returns a function that accepts log data
107 | * and produces a prettified log string.
108 | *
109 | * @param {PinoPrettyOptions} options Configuration for the prettifier.
110 | * @returns {LogPrettifierFunc}
111 | */
112 | function prettyFactory (options) {
113 | const context = parseFactoryOptions(Object.assign({}, defaultOptions, options))
114 | return pretty.bind({ ...context, context })
115 | }
116 |
117 | /**
118 | * @typedef {PinoPrettyOptions} BuildStreamOpts
119 | * @property {object|number|string} [destination] A destination stream, file
120 | * descriptor, or target path to a file.
121 | * @property {boolean} [append]
122 | * @property {boolean} [mkdir]
123 | * @property {boolean} [sync=false]
124 | */
125 |
126 | /**
127 | * Constructs a {@link LogPrettifierFunc} and a stream to which the produced
128 | * prettified log data will be written.
129 | *
130 | * @param {BuildStreamOpts} opts
131 | * @returns {Transform | (Transform & OnUnknown)}
132 | */
133 | function build (opts = {}) {
134 | let pretty = prettyFactory(opts)
135 | let destination
136 | return abstractTransport(function (source) {
137 | source.on('message', function pinoConfigListener (message) {
138 | if (!message || message.code !== 'PINO_CONFIG') return
139 | Object.assign(opts, {
140 | messageKey: message.config.messageKey,
141 | errorLikeObjectKeys: Array.from(new Set([...(opts.errorLikeObjectKeys || ERROR_LIKE_KEYS), message.config.errorKey])),
142 | customLevels: message.config.levels.values
143 | })
144 | pretty = prettyFactory(opts)
145 | source.off('message', pinoConfigListener)
146 | })
147 | const stream = new Transform({
148 | objectMode: true,
149 | autoDestroy: true,
150 | transform (chunk, enc, cb) {
151 | const line = pretty(chunk)
152 | cb(null, line)
153 | }
154 | })
155 |
156 | if (typeof opts.destination === 'object' && typeof opts.destination.write === 'function') {
157 | destination = opts.destination
158 | } else {
159 | destination = buildSafeSonicBoom({
160 | dest: opts.destination || 1,
161 | append: opts.append,
162 | mkdir: opts.mkdir,
163 | sync: opts.sync // by default sonic will be async
164 | })
165 | }
166 |
167 | source.on('unknown', function (line) {
168 | destination.write(line + '\n')
169 | })
170 |
171 | pump(source, stream, destination)
172 | return stream
173 | }, {
174 | parse: 'lines',
175 | close (err, cb) {
176 | destination.on('close', () => {
177 | cb(err)
178 | })
179 | }
180 | })
181 | }
182 |
183 | module.exports = build
184 | module.exports.build = build
185 | module.exports.PinoPretty = build
186 | module.exports.prettyFactory = prettyFactory
187 | module.exports.colorizerFactory = colors
188 | module.exports.isColorSupported = isColorSupported
189 | module.exports.default = build
190 |
--------------------------------------------------------------------------------
/lib/colors.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const nocolor = input => input
4 | const plain = {
5 | default: nocolor,
6 | 60: nocolor,
7 | 50: nocolor,
8 | 40: nocolor,
9 | 30: nocolor,
10 | 20: nocolor,
11 | 10: nocolor,
12 | message: nocolor,
13 | greyMessage: nocolor,
14 | property: nocolor
15 | }
16 |
17 | const { createColors } = require('colorette')
18 | const getLevelLabelData = require('./utils/get-level-label-data')
19 | const availableColors = createColors({ useColor: true })
20 | const { white, bgRed, red, yellow, green, blue, gray, cyan, magenta } = availableColors
21 |
22 | const colored = {
23 | default: white,
24 | 60: bgRed,
25 | 50: red,
26 | 40: yellow,
27 | 30: green,
28 | 20: blue,
29 | 10: gray,
30 | message: cyan,
31 | greyMessage: gray,
32 | property: magenta
33 | }
34 |
35 | function resolveCustomColoredColorizer (customColors) {
36 | return customColors.reduce(
37 | function (agg, [level, color]) {
38 | agg[level] = typeof availableColors[color] === 'function' ? availableColors[color] : white
39 |
40 | return agg
41 | },
42 | { default: white, message: cyan, greyMessage: gray }
43 | )
44 | }
45 |
46 | function colorizeLevel (useOnlyCustomProps) {
47 | return function (level, colorizer, { customLevels, customLevelNames } = {}) {
48 | const [levelStr, levelNum] = getLevelLabelData(useOnlyCustomProps, customLevels, customLevelNames)(level)
49 |
50 | return Object.prototype.hasOwnProperty.call(colorizer, levelNum) ? colorizer[levelNum](levelStr) : colorizer.default(levelStr)
51 | }
52 | }
53 |
54 | function plainColorizer (useOnlyCustomProps) {
55 | const newPlainColorizer = colorizeLevel(useOnlyCustomProps)
56 | const customColoredColorizer = function (level, opts) {
57 | return newPlainColorizer(level, plain, opts)
58 | }
59 | customColoredColorizer.message = plain.message
60 | customColoredColorizer.greyMessage = plain.greyMessage
61 | customColoredColorizer.property = plain.property
62 | customColoredColorizer.colors = createColors({ useColor: false })
63 | return customColoredColorizer
64 | }
65 |
66 | function coloredColorizer (useOnlyCustomProps) {
67 | const newColoredColorizer = colorizeLevel(useOnlyCustomProps)
68 | const customColoredColorizer = function (level, opts) {
69 | return newColoredColorizer(level, colored, opts)
70 | }
71 | customColoredColorizer.message = colored.message
72 | customColoredColorizer.property = colored.property
73 | customColoredColorizer.greyMessage = colored.greyMessage
74 | customColoredColorizer.colors = availableColors
75 | return customColoredColorizer
76 | }
77 |
78 | function customColoredColorizerFactory (customColors, useOnlyCustomProps) {
79 | const onlyCustomColored = resolveCustomColoredColorizer(customColors)
80 | const customColored = useOnlyCustomProps ? onlyCustomColored : Object.assign({}, colored, onlyCustomColored)
81 | const colorizeLevelCustom = colorizeLevel(useOnlyCustomProps)
82 |
83 | const customColoredColorizer = function (level, opts) {
84 | return colorizeLevelCustom(level, customColored, opts)
85 | }
86 | customColoredColorizer.colors = availableColors
87 | customColoredColorizer.message = customColoredColorizer.message || customColored.message
88 | customColoredColorizer.greyMessage = customColoredColorizer.greyMessage || customColored.greyMessage
89 |
90 | return customColoredColorizer
91 | }
92 |
93 | /**
94 | * Applies colorization, if possible, to a string representing the passed in
95 | * `level`. For example, the default colorizer will return a "green" colored
96 | * string for the "info" level.
97 | *
98 | * @typedef {function} ColorizerFunc
99 | * @param {string|number} level In either case, the input will map to a color
100 | * for the specified level or to the color for `USERLVL` if the level is not
101 | * recognized.
102 | * @property {function} message Accepts one string parameter that will be
103 | * colorized to a predefined color.
104 | * @property {Colorette.Colorette} colors Available color functions based on `useColor` (or `colorize`) context
105 | */
106 |
107 | /**
108 | * Factory function get a function to colorized levels. The returned function
109 | * also includes a `.message(str)` method to colorize strings.
110 | *
111 | * @param {boolean} [useColors=false] When `true` a function that applies standard
112 | * terminal colors is returned.
113 | * @param {array[]} [customColors] Tuple where first item of each array is the
114 | * level index and the second item is the color
115 | * @param {boolean} [useOnlyCustomProps] When `true`, only use the provided
116 | * custom colors provided and not fallback to default
117 | *
118 | * @returns {ColorizerFunc} `function (level) {}` has a `.message(str)` method to
119 | * apply colorization to a string. The core function accepts either an integer
120 | * `level` or a `string` level. The integer level will map to a known level
121 | * string or to `USERLVL` if not known. The string `level` will map to the same
122 | * colors as the integer `level` and will also default to `USERLVL` if the given
123 | * string is not a recognized level name.
124 | */
125 | module.exports = function getColorizer (useColors = false, customColors, useOnlyCustomProps) {
126 | if (useColors && customColors !== undefined) {
127 | return customColoredColorizerFactory(customColors, useOnlyCustomProps)
128 | } else if (useColors) {
129 | return coloredColorizer(useOnlyCustomProps)
130 | }
131 |
132 | return plainColorizer(useOnlyCustomProps)
133 | }
134 |
--------------------------------------------------------------------------------
/lib/colors.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { test } = require('node:test')
4 | const getColorizer = require('./colors')
5 |
6 | const testDefaultColorizer = getColorizer => t => {
7 | const colorizer = getColorizer()
8 | let colorized = colorizer(10)
9 | t.assert.strictEqual(colorized, 'TRACE')
10 |
11 | colorized = colorizer(20)
12 | t.assert.strictEqual(colorized, 'DEBUG')
13 |
14 | colorized = colorizer(30)
15 | t.assert.strictEqual(colorized, 'INFO')
16 |
17 | colorized = colorizer(40)
18 | t.assert.strictEqual(colorized, 'WARN')
19 |
20 | colorized = colorizer(50)
21 | t.assert.strictEqual(colorized, 'ERROR')
22 |
23 | colorized = colorizer(60)
24 | t.assert.strictEqual(colorized, 'FATAL')
25 |
26 | colorized = colorizer(900)
27 | t.assert.strictEqual(colorized, 'USERLVL')
28 |
29 | colorized = colorizer('info')
30 | t.assert.strictEqual(colorized, 'INFO')
31 |
32 | colorized = colorizer('use-default')
33 | t.assert.strictEqual(colorized, 'USERLVL')
34 |
35 | colorized = colorizer.message('foo')
36 | t.assert.strictEqual(colorized, 'foo')
37 |
38 | colorized = colorizer.greyMessage('foo')
39 | t.assert.strictEqual(colorized, 'foo')
40 | }
41 |
42 | const testColoringColorizer = getColorizer => t => {
43 | const colorizer = getColorizer(true)
44 | let colorized = colorizer(10)
45 | t.assert.strictEqual(colorized, '\u001B[90mTRACE\u001B[39m')
46 |
47 | colorized = colorizer(20)
48 | t.assert.strictEqual(colorized, '\u001B[34mDEBUG\u001B[39m')
49 |
50 | colorized = colorizer(30)
51 | t.assert.strictEqual(colorized, '\u001B[32mINFO\u001B[39m')
52 |
53 | colorized = colorizer(40)
54 | t.assert.strictEqual(colorized, '\u001B[33mWARN\u001B[39m')
55 |
56 | colorized = colorizer(50)
57 | t.assert.strictEqual(colorized, '\u001B[31mERROR\u001B[39m')
58 |
59 | colorized = colorizer(60)
60 | t.assert.strictEqual(colorized, '\u001B[41mFATAL\u001B[49m')
61 |
62 | colorized = colorizer(900)
63 | t.assert.strictEqual(colorized, '\u001B[37mUSERLVL\u001B[39m')
64 |
65 | colorized = colorizer('info')
66 | t.assert.strictEqual(colorized, '\u001B[32mINFO\u001B[39m')
67 |
68 | colorized = colorizer('use-default')
69 | t.assert.strictEqual(colorized, '\u001B[37mUSERLVL\u001B[39m')
70 |
71 | colorized = colorizer.message('foo')
72 | t.assert.strictEqual(colorized, '\u001B[36mfoo\u001B[39m')
73 |
74 | colorized = colorizer.greyMessage('foo')
75 | t.assert.strictEqual(colorized, '\u001B[90mfoo\u001B[39m')
76 | }
77 |
78 | const testCustomColoringColorizer = getColorizer => t => {
79 | const customLevels = {
80 | 0: 'INFO',
81 | 1: 'ERR',
82 | default: 'USERLVL'
83 | }
84 | const customLevelNames = {
85 | info: 0,
86 | err: 1
87 | }
88 | const customColors = [
89 | [0, 'not-a-color'],
90 | [1, 'red']
91 | ]
92 | const opts = {
93 | customLevels,
94 | customLevelNames
95 | }
96 |
97 | const colorizer = getColorizer(true, customColors)
98 | const colorizerWithCustomPropUse = getColorizer(true, customColors, true)
99 | let colorized = colorizer(1, opts)
100 | t.assert.strictEqual(colorized, '\u001B[31mERR\u001B[39m')
101 |
102 | colorized = colorizer(0, opts)
103 | t.assert.strictEqual(colorized, '\u001B[37mINFO\u001B[39m')
104 |
105 | colorized = colorizer(900)
106 | t.assert.strictEqual(colorized, '\u001B[37mUSERLVL\u001B[39m')
107 |
108 | colorized = colorizer('err', opts)
109 | t.assert.strictEqual(colorized, '\u001B[31mERR\u001B[39m')
110 |
111 | colorized = colorizer('info', opts)
112 | t.assert.strictEqual(colorized, '\u001B[37mINFO\u001B[39m')
113 |
114 | colorized = colorizer('use-default')
115 | t.assert.strictEqual(colorized, '\u001B[37mUSERLVL\u001B[39m')
116 |
117 | colorized = colorizer(40, opts)
118 | t.assert.strictEqual(colorized, '\u001B[33mWARN\u001B[39m')
119 |
120 | colorized = colorizerWithCustomPropUse(50, opts)
121 | t.assert.strictEqual(colorized, '\u001B[37mUSERLVL\u001B[39m')
122 | }
123 |
124 | test('returns default colorizer - private export', testDefaultColorizer(getColorizer))
125 | test('returns colorizing colorizer - private export', testColoringColorizer(getColorizer))
126 | test('returns custom colorizing colorizer - private export', testCustomColoringColorizer(getColorizer))
127 |
128 | test('custom props defaults to standard levels', t => {
129 | const colorizer = getColorizer(true, [], true)
130 | const colorized = colorizer('info')
131 | t.assert.strictEqual(colorized, '\u001B[37mINFO\u001B[39m')
132 | })
133 |
--------------------------------------------------------------------------------
/lib/constants.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /**
4 | * A set of property names that indicate the value represents an error object.
5 | *
6 | * @typedef {string[]} K_ERROR_LIKE_KEYS
7 | */
8 |
9 | module.exports = {
10 | DATE_FORMAT: 'yyyy-mm-dd HH:MM:ss.l o',
11 | DATE_FORMAT_SIMPLE: 'HH:MM:ss.l',
12 |
13 | /**
14 | * @type {K_ERROR_LIKE_KEYS}
15 | */
16 | ERROR_LIKE_KEYS: ['err', 'error'],
17 |
18 | MESSAGE_KEY: 'msg',
19 |
20 | LEVEL_KEY: 'level',
21 |
22 | LEVEL_LABEL: 'levelLabel',
23 |
24 | TIMESTAMP_KEY: 'time',
25 |
26 | LEVELS: {
27 | default: 'USERLVL',
28 | 60: 'FATAL',
29 | 50: 'ERROR',
30 | 40: 'WARN',
31 | 30: 'INFO',
32 | 20: 'DEBUG',
33 | 10: 'TRACE'
34 | },
35 |
36 | LEVEL_NAMES: {
37 | fatal: 60,
38 | error: 50,
39 | warn: 40,
40 | info: 30,
41 | debug: 20,
42 | trace: 10
43 | },
44 |
45 | // Object keys that probably came from a logger like Pino or Bunyan.
46 | LOGGER_KEYS: [
47 | 'pid',
48 | 'hostname',
49 | 'name',
50 | 'level',
51 | 'time',
52 | 'timestamp',
53 | 'caller'
54 | ]
55 | }
56 |
--------------------------------------------------------------------------------
/lib/pretty.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = pretty
4 |
5 | const sjs = require('secure-json-parse')
6 |
7 | const isObject = require('./utils/is-object')
8 | const prettifyErrorLog = require('./utils/prettify-error-log')
9 | const prettifyLevel = require('./utils/prettify-level')
10 | const prettifyMessage = require('./utils/prettify-message')
11 | const prettifyMetadata = require('./utils/prettify-metadata')
12 | const prettifyObject = require('./utils/prettify-object')
13 | const prettifyTime = require('./utils/prettify-time')
14 | const filterLog = require('./utils/filter-log')
15 |
16 | const {
17 | LEVELS,
18 | LEVEL_KEY,
19 | LEVEL_NAMES
20 | } = require('./constants')
21 |
22 | const jsonParser = input => {
23 | try {
24 | return { value: sjs.parse(input, { protoAction: 'remove' }) }
25 | } catch (err) {
26 | return { err }
27 | }
28 | }
29 |
30 | /**
31 | * Orchestrates processing the received log data according to the provided
32 | * configuration and returns a prettified log string.
33 | *
34 | * @typedef {function} LogPrettifierFunc
35 | * @param {string|object} inputData A log string or a log-like object.
36 | * @returns {string} A string that represents the prettified log data.
37 | */
38 | function pretty (inputData) {
39 | let log
40 | if (!isObject(inputData)) {
41 | const parsed = jsonParser(inputData)
42 | if (parsed.err || !isObject(parsed.value)) {
43 | // pass through
44 | return inputData + this.EOL
45 | }
46 | log = parsed.value
47 | } else {
48 | log = inputData
49 | }
50 |
51 | if (this.minimumLevel) {
52 | // We need to figure out if the custom levels has the desired minimum
53 | // level & use that one if found. If not, determine if the level exists
54 | // in the standard levels. In both cases, make sure we have the level
55 | // number instead of the level name.
56 | let condition
57 | if (this.useOnlyCustomProps) {
58 | condition = this.customLevels
59 | } else {
60 | condition = this.customLevelNames[this.minimumLevel] !== undefined
61 | }
62 | let minimum
63 | if (condition) {
64 | minimum = this.customLevelNames[this.minimumLevel]
65 | } else {
66 | minimum = LEVEL_NAMES[this.minimumLevel]
67 | }
68 | if (!minimum) {
69 | minimum = typeof this.minimumLevel === 'string'
70 | ? LEVEL_NAMES[this.minimumLevel]
71 | : LEVEL_NAMES[LEVELS[this.minimumLevel].toLowerCase()]
72 | }
73 |
74 | const level = log[this.levelKey === undefined ? LEVEL_KEY : this.levelKey]
75 | if (level < minimum) return
76 | }
77 |
78 | const prettifiedMessage = prettifyMessage({ log, context: this.context })
79 |
80 | if (this.ignoreKeys || this.includeKeys) {
81 | log = filterLog({ log, context: this.context })
82 | }
83 |
84 | const prettifiedLevel = prettifyLevel({
85 | log,
86 | context: {
87 | ...this.context,
88 | // This is odd. The colorizer ends up relying on the value of
89 | // `customProperties` instead of the original `customLevels` and
90 | // `customLevelNames`.
91 | ...this.context.customProperties
92 | }
93 | })
94 | const prettifiedMetadata = prettifyMetadata({ log, context: this.context })
95 | const prettifiedTime = prettifyTime({ log, context: this.context })
96 |
97 | let line = ''
98 | if (this.levelFirst && prettifiedLevel) {
99 | line = `${prettifiedLevel}`
100 | }
101 |
102 | if (prettifiedTime && line === '') {
103 | line = `${prettifiedTime}`
104 | } else if (prettifiedTime) {
105 | line = `${line} ${prettifiedTime}`
106 | }
107 |
108 | if (!this.levelFirst && prettifiedLevel) {
109 | if (line.length > 0) {
110 | line = `${line} ${prettifiedLevel}`
111 | } else {
112 | line = prettifiedLevel
113 | }
114 | }
115 |
116 | if (prettifiedMetadata) {
117 | if (line.length > 0) {
118 | line = `${line} ${prettifiedMetadata}:`
119 | } else {
120 | line = prettifiedMetadata
121 | }
122 | }
123 |
124 | if (line.endsWith(':') === false && line !== '') {
125 | line += ':'
126 | }
127 |
128 | if (prettifiedMessage !== undefined) {
129 | if (line.length > 0) {
130 | line = `${line} ${prettifiedMessage}`
131 | } else {
132 | line = prettifiedMessage
133 | }
134 | }
135 |
136 | if (line.length > 0 && !this.singleLine) {
137 | line += this.EOL
138 | }
139 |
140 | // pino@7+ does not log this anymore
141 | if (log.type === 'Error' && typeof log.stack === 'string') {
142 | const prettifiedErrorLog = prettifyErrorLog({ log, context: this.context })
143 | if (this.singleLine) line += this.EOL
144 | line += prettifiedErrorLog
145 | } else if (this.hideObject === false) {
146 | const skipKeys = [
147 | this.messageKey,
148 | this.levelKey,
149 | this.timestampKey
150 | ]
151 | .map((key) => key.replaceAll(/\\/g, ''))
152 | .filter(key => {
153 | return typeof log[key] === 'string' ||
154 | typeof log[key] === 'number' ||
155 | typeof log[key] === 'boolean'
156 | })
157 | const prettifiedObject = prettifyObject({
158 | log,
159 | skipKeys,
160 | context: this.context
161 | })
162 |
163 | // In single line mode, include a space only if prettified version isn't empty
164 | if (this.singleLine && !/^\s$/.test(prettifiedObject)) {
165 | line += ' '
166 | }
167 | line += prettifiedObject
168 | }
169 |
170 | return line
171 | }
172 |
--------------------------------------------------------------------------------
/lib/utils/build-safe-sonic-boom.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = buildSafeSonicBoom
4 |
5 | const { isMainThread } = require('node:worker_threads')
6 | const SonicBoom = require('sonic-boom')
7 | const noop = require('./noop')
8 |
9 | /**
10 | * Creates a safe SonicBoom instance
11 | *
12 | * @param {object} opts Options for SonicBoom
13 | *
14 | * @returns {object} A new SonicBoom stream
15 | */
16 | function buildSafeSonicBoom (opts) {
17 | const stream = new SonicBoom(opts)
18 | stream.on('error', filterBrokenPipe)
19 | // if we are sync: false, we must flush on exit
20 | // NODE_V8_COVERAGE must breaks everything
21 | // https://github.com/nodejs/node/issues/49344
22 | if (!process.env.NODE_V8_COVERAGE && !opts.sync && isMainThread) {
23 | setupOnExit(stream)
24 | }
25 | return stream
26 |
27 | function filterBrokenPipe (err) {
28 | if (err.code === 'EPIPE') {
29 | stream.write = noop
30 | stream.end = noop
31 | stream.flushSync = noop
32 | stream.destroy = noop
33 | return
34 | }
35 | stream.removeListener('error', filterBrokenPipe)
36 | }
37 | }
38 |
39 | function setupOnExit (stream) {
40 | /* istanbul ignore next */
41 | if (global.WeakRef && global.WeakMap && global.FinalizationRegistry) {
42 | // This is leak free, it does not leave event handlers
43 | const onExit = require('on-exit-leak-free')
44 |
45 | onExit.register(stream, autoEnd)
46 |
47 | stream.on('close', function () {
48 | onExit.unregister(stream)
49 | })
50 | }
51 | }
52 |
53 | /* istanbul ignore next */
54 | function autoEnd (stream, eventName) {
55 | // This check is needed only on some platforms
56 |
57 | if (stream.destroyed) {
58 | return
59 | }
60 |
61 | if (eventName === 'beforeExit') {
62 | // We still have an event loop, let's use it
63 | stream.flush()
64 | stream.on('drain', function () {
65 | stream.end()
66 | })
67 | } else {
68 | // We do not have an event loop, so flush synchronously
69 | stream.flushSync()
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/lib/utils/build-safe-sonic-boom.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const tap = require('tap')
4 | const { rimraf } = require('rimraf')
5 | const fs = require('node:fs')
6 | const { join } = require('node:path')
7 |
8 | const buildSafeSonicBoom = require('./build-safe-sonic-boom')
9 |
10 | const file = () => {
11 | const dest = join(__dirname, `${process.pid}-${process.hrtime().toString()}`)
12 | const fd = fs.openSync(dest, 'w')
13 | return { dest, fd }
14 | }
15 |
16 | tap.test('should not write when error emitted and code is "EPIPE"', async t => {
17 | t.plan(1)
18 |
19 | const { fd, dest } = file()
20 | const stream = buildSafeSonicBoom({ sync: true, fd, mkdir: true })
21 | t.teardown(() => rimraf(dest))
22 |
23 | stream.emit('error', { code: 'EPIPE' })
24 | stream.write('will not work')
25 |
26 | const dataFile = fs.readFileSync(dest)
27 | t.equal(dataFile.length, 0)
28 | })
29 |
30 | tap.test('should stream.write works when error code is not "EPIPE"', async t => {
31 | t.plan(3)
32 | const { fd, dest } = file()
33 | const stream = buildSafeSonicBoom({ sync: true, fd, mkdir: true })
34 |
35 | t.teardown(() => rimraf(dest))
36 |
37 | stream.on('error', () => t.pass('error emitted'))
38 |
39 | stream.emit('error', 'fake error description')
40 |
41 | t.ok(stream.write('will work'))
42 |
43 | const dataFile = fs.readFileSync(dest)
44 | t.equal(dataFile.toString(), 'will work')
45 | })
46 |
47 | tap.test('cover setupOnExit', async t => {
48 | t.plan(3)
49 | const { fd, dest } = file()
50 | const stream = buildSafeSonicBoom({ sync: false, fd, mkdir: true })
51 |
52 | t.teardown(() => rimraf(dest))
53 |
54 | stream.on('error', () => t.pass('error emitted'))
55 | stream.emit('error', 'fake error description')
56 |
57 | t.ok(stream.write('will work'))
58 |
59 | await watchFileCreated(dest)
60 |
61 | const dataFile = fs.readFileSync(dest)
62 | t.equal(dataFile.toString(), 'will work')
63 | })
64 |
65 | function watchFileCreated (filename) {
66 | return new Promise((resolve, reject) => {
67 | const TIMEOUT = 2000
68 | const INTERVAL = 100
69 | const threshold = TIMEOUT / INTERVAL
70 | let counter = 0
71 | const interval = setInterval(() => {
72 | // On some CI runs file is created but not filled
73 | if (fs.existsSync(filename) && fs.statSync(filename).size !== 0) {
74 | clearInterval(interval)
75 | resolve()
76 | } else if (counter <= threshold) {
77 | counter++
78 | } else {
79 | clearInterval(interval)
80 | reject(new Error(`${filename} was not created.`))
81 | }
82 | }, INTERVAL)
83 | })
84 | }
85 |
--------------------------------------------------------------------------------
/lib/utils/create-date.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = createDate
4 |
5 | const isValidDate = require('./is-valid-date')
6 |
7 | /**
8 | * Constructs a JS Date from a number or string. Accepts any single number
9 | * or single string argument that is valid for the Date() constructor,
10 | * or an epoch as a string.
11 | *
12 | * @param {string|number} epoch The representation of the Date.
13 | *
14 | * @returns {Date} The constructed Date.
15 | */
16 | function createDate (epoch) {
17 | // If epoch is already a valid argument, return the valid Date
18 | let date = new Date(epoch)
19 | if (isValidDate(date)) {
20 | return date
21 | }
22 |
23 | // Convert to a number to permit epoch as a string
24 | date = new Date(+epoch)
25 | return date
26 | }
27 |
--------------------------------------------------------------------------------
/lib/utils/create-date.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const tap = require('tap')
4 | const createDate = require('./create-date')
5 |
6 | const wanted = 1624450038567
7 |
8 | tap.test('accepts arguments the Date constructor would accept', async t => {
9 | t.plan(2)
10 | t.same(createDate(1624450038567).getTime(), wanted)
11 | t.same(createDate('2021-06-23T12:07:18.567Z').getTime(), wanted)
12 | })
13 |
14 | tap.test('accepts epoch as a string', async t => {
15 | // If Date() accepts this argument, the createDate function is not needed
16 | // and can be replaced with Date()
17 | t.plan(2)
18 | t.notSame(new Date('16244500385-67').getTime(), wanted)
19 | t.same(createDate('1624450038567').getTime(), wanted)
20 | })
21 |
--------------------------------------------------------------------------------
/lib/utils/delete-log-property.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = deleteLogProperty
4 |
5 | const getPropertyValue = require('./get-property-value')
6 | const splitPropertyKey = require('./split-property-key')
7 |
8 | /**
9 | * Deletes a specified property from a log object if it exists.
10 | * This function mutates the passed in `log` object.
11 | *
12 | * @param {object} log The log object to be modified.
13 | * @param {string} property A string identifying the property to be deleted from
14 | * the log object. Accepts nested properties delimited by a `.`
15 | * Delimiter can be escaped to preserve property names that contain the delimiter.
16 | * e.g. `'prop1.prop2'` or `'prop2\.domain\.corp.prop2'`
17 | */
18 | function deleteLogProperty (log, property) {
19 | const props = splitPropertyKey(property)
20 | const propToDelete = props.pop()
21 |
22 | log = getPropertyValue(log, props)
23 |
24 | /* istanbul ignore else */
25 | if (log !== null && typeof log === 'object' && Object.prototype.hasOwnProperty.call(log, propToDelete)) {
26 | delete log[propToDelete]
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/lib/utils/delete-log-property.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const tap = require('tap')
4 | const { createCopier } = require('fast-copy')
5 | const fastCopy = createCopier({})
6 | const deleteLogProperty = require('./delete-log-property')
7 |
8 | const logData = {
9 | level: 30,
10 | data1: {
11 | data2: { 'data-3': 'bar' }
12 | }
13 | }
14 |
15 | tap.test('deleteLogProperty deletes property of depth 1', async t => {
16 | const log = fastCopy(logData)
17 | deleteLogProperty(log, 'data1')
18 | t.same(log, { level: 30 })
19 | })
20 |
21 | tap.test('deleteLogProperty deletes property of depth 2', async t => {
22 | const log = fastCopy(logData)
23 | deleteLogProperty(log, 'data1.data2')
24 | t.same(log, { level: 30, data1: { } })
25 | })
26 |
27 | tap.test('deleteLogProperty deletes property of depth 3', async t => {
28 | const log = fastCopy(logData)
29 | deleteLogProperty(log, 'data1.data2.data-3')
30 | t.same(log, { level: 30, data1: { data2: { } } })
31 | })
32 |
--------------------------------------------------------------------------------
/lib/utils/filter-log.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = filterLog
4 |
5 | const { createCopier } = require('fast-copy')
6 | const fastCopy = createCopier({})
7 |
8 | const deleteLogProperty = require('./delete-log-property')
9 |
10 | /**
11 | * @typedef {object} FilterLogParams
12 | * @property {object} log The log object to be modified.
13 | * @property {PrettyContext} context The context object built from parsing
14 | * the options.
15 | */
16 |
17 | /**
18 | * Filter a log object by removing or including keys accordingly.
19 | * When `includeKeys` is passed, `ignoredKeys` will be ignored.
20 | * One of ignoreKeys or includeKeys must be pass in.
21 | *
22 | * @param {FilterLogParams} input
23 | *
24 | * @returns {object} A new `log` object instance that
25 | * either only includes the keys in ignoreKeys
26 | * or does not include those in ignoredKeys.
27 | */
28 | function filterLog ({ log, context }) {
29 | const { ignoreKeys, includeKeys } = context
30 | const logCopy = fastCopy(log)
31 |
32 | if (includeKeys) {
33 | const logIncluded = {}
34 |
35 | includeKeys.forEach((key) => {
36 | logIncluded[key] = logCopy[key]
37 | })
38 | return logIncluded
39 | }
40 |
41 | ignoreKeys.forEach((ignoreKey) => {
42 | deleteLogProperty(logCopy, ignoreKey)
43 | })
44 | return logCopy
45 | }
46 |
--------------------------------------------------------------------------------
/lib/utils/filter-log.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const tap = require('tap')
4 | const filterLog = require('./filter-log')
5 |
6 | const context = {
7 | includeKeys: undefined,
8 | ignoreKeys: undefined
9 | }
10 | const logData = {
11 | level: 30,
12 | time: 1522431328992,
13 | data1: {
14 | data2: { 'data-3': 'bar' },
15 | error: new Error('test')
16 | }
17 | }
18 | const logData2 = Object.assign({
19 | 'logging.domain.corp/operation': {
20 | id: 'foo',
21 | producer: 'bar'
22 | }
23 | }, logData)
24 |
25 | tap.test('#filterLog with an ignoreKeys option', t => {
26 | t.test('filterLog removes single entry', async t => {
27 | const result = filterLog({
28 | log: logData,
29 | context: {
30 | ...context,
31 | ignoreKeys: ['data1.data2.data-3']
32 | }
33 | })
34 | t.same(result, { level: 30, time: 1522431328992, data1: { data2: { }, error: new Error('test') } })
35 | })
36 |
37 | t.test('filterLog removes multiple entries', async t => {
38 | const result = filterLog({
39 | log: logData,
40 | context: {
41 | ...context,
42 | ignoreKeys: ['time', 'data1']
43 | }
44 | })
45 | t.same(result, { level: 30 })
46 | })
47 |
48 | t.test('filterLog keeps error instance', async t => {
49 | const result = filterLog({
50 | log: logData,
51 | context: {
52 | ...context,
53 | ignoreKeys: []
54 | }
55 | })
56 | t.equal(logData.data1.error, result.data1.error)
57 | })
58 |
59 | t.test('filterLog removes entry with escape sequence', async t => {
60 | const result = filterLog({
61 | log: logData2,
62 | context: {
63 | ...context,
64 | ignoreKeys: ['data1', 'logging\\.domain\\.corp/operation']
65 | }
66 | })
67 | t.same(result, { level: 30, time: 1522431328992 })
68 | })
69 |
70 | t.test('filterLog removes entry with escape sequence nested', async t => {
71 | const result = filterLog({
72 | log: logData2,
73 | context: {
74 | ...context,
75 | ignoreKeys: ['data1', 'logging\\.domain\\.corp/operation.producer']
76 | }
77 | })
78 | t.same(result, { level: 30, time: 1522431328992, 'logging.domain.corp/operation': { id: 'foo' } })
79 | })
80 |
81 | t.end()
82 | })
83 |
84 | const ignoreKeysArray = [
85 | undefined,
86 | ['level'],
87 | ['level', 'data1.data2.data-3']
88 | ]
89 | ignoreKeysArray.forEach(ignoreKeys => {
90 | tap.test(`#filterLog with an includeKeys option when the ignoreKeys being ${ignoreKeys}`, t => {
91 | t.test('filterLog include nothing', async t => {
92 | const result = filterLog({
93 | log: logData,
94 | context: {
95 | ...context,
96 | ignoreKeys,
97 | includeKeys: []
98 | }
99 | })
100 | t.same(result, {})
101 | })
102 |
103 | t.test('filterLog include single entry', async t => {
104 | const result = filterLog({
105 | log: logData,
106 | context: {
107 | ...context,
108 | ignoreKeys,
109 | includeKeys: ['time']
110 | }
111 | })
112 | t.same(result, { time: 1522431328992 })
113 | })
114 |
115 | t.test('filterLog include multiple entries', async t => {
116 | const result = filterLog({
117 | log: logData,
118 | context: {
119 | ...context,
120 | ignoreKeys,
121 | includeKeys: ['time', 'data1']
122 | }
123 | })
124 | t.same(result, {
125 | time: 1522431328992,
126 | data1: {
127 | data2: { 'data-3': 'bar' },
128 | error: new Error('test')
129 | }
130 | })
131 | })
132 |
133 | t.end()
134 | })
135 | })
136 |
137 | tap.test('#filterLog with circular references', t => {
138 | const logData = {
139 | level: 30,
140 | time: 1522431328992,
141 | data1: 'test'
142 | }
143 | logData.circular = logData
144 |
145 | t.test('filterLog removes single entry', async t => {
146 | const result = filterLog({
147 | log: logData,
148 | context: {
149 | ...context,
150 | ignoreKeys: ['data1']
151 | }
152 | })
153 |
154 | t.same(result.circular.level, result.level)
155 | t.same(result.circular.time, result.time)
156 |
157 | delete result.circular
158 | t.same(result, { level: 30, time: 1522431328992 })
159 | })
160 |
161 | t.test('filterLog includes single entry', async t => {
162 | const result = filterLog({
163 | log: logData,
164 | context: {
165 | ...context,
166 | includeKeys: ['data1']
167 | }
168 | })
169 |
170 | t.same(result, { data1: 'test' })
171 | })
172 |
173 | t.test('filterLog includes circular keys', async t => {
174 | const result = filterLog({
175 | log: logData,
176 | context: {
177 | ...context,
178 | includeKeys: ['level', 'circular']
179 | }
180 | })
181 |
182 | t.same(result.circular.level, logData.level)
183 | t.same(result.circular.time, logData.time)
184 |
185 | delete result.circular
186 | t.same(result, { level: 30 })
187 | })
188 |
189 | t.end()
190 | })
191 |
--------------------------------------------------------------------------------
/lib/utils/format-time.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = formatTime
4 |
5 | const {
6 | DATE_FORMAT,
7 | DATE_FORMAT_SIMPLE
8 | } = require('../constants')
9 |
10 | const dateformat = require('dateformat')
11 | const createDate = require('./create-date')
12 | const isValidDate = require('./is-valid-date')
13 |
14 | /**
15 | * Converts a given `epoch` to a desired display format.
16 | *
17 | * @param {number|string} epoch The time to convert. May be any value that is
18 | * valid for `new Date()`.
19 | * @param {boolean|string} [translateTime=false] When `false`, the given `epoch`
20 | * will simply be returned. When `true`, the given `epoch` will be converted
21 | * to a string at UTC using the `DATE_FORMAT` constant. If `translateTime` is
22 | * a string, the following rules are available:
23 | *
24 | * - ``: The string is a literal format string. This format
25 | * string will be used to interpret the `epoch` and return a display string
26 | * at UTC.
27 | * - `SYS:STANDARD`: The returned display string will follow the `DATE_FORMAT`
28 | * constant at the system's local timezone.
29 | * - `SYS:`: The returned display string will follow the given
30 | * `` at the system's local timezone.
31 | * - `UTC:`: The returned display string will follow the given
32 | * `` at UTC.
33 | *
34 | * @returns {number|string} The formatted time.
35 | */
36 | function formatTime (epoch, translateTime = false) {
37 | if (translateTime === false) {
38 | return epoch
39 | }
40 |
41 | const instant = createDate(epoch)
42 |
43 | // If the Date is invalid, do not attempt to format
44 | if (!isValidDate(instant)) {
45 | return epoch
46 | }
47 |
48 | if (translateTime === true) {
49 | return dateformat(instant, DATE_FORMAT_SIMPLE)
50 | }
51 |
52 | const upperFormat = translateTime.toUpperCase()
53 | if (upperFormat === 'SYS:STANDARD') {
54 | return dateformat(instant, DATE_FORMAT)
55 | }
56 |
57 | const prefix = upperFormat.substr(0, 4)
58 | if (prefix === 'SYS:' || prefix === 'UTC:') {
59 | if (prefix === 'UTC:') {
60 | return dateformat(instant, translateTime)
61 | }
62 | return dateformat(instant, translateTime.slice(4))
63 | }
64 |
65 | return dateformat(instant, `UTC:${translateTime}`)
66 | }
67 |
--------------------------------------------------------------------------------
/lib/utils/format-time.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.TZ = 'UTC'
4 |
5 | const tap = require('tap')
6 | const formatTime = require('./format-time')
7 |
8 | const dateStr = '2019-04-06T13:30:00.000-04:00'
9 | const epoch = new Date(dateStr)
10 | const epochMS = epoch.getTime()
11 |
12 | tap.test('passes through epoch if `translateTime` is `false`', async t => {
13 | const formattedTime = formatTime(epochMS)
14 | t.equal(formattedTime, epochMS)
15 | })
16 |
17 | tap.test('passes through epoch if date is invalid', async t => {
18 | const input = 'this is not a date'
19 | const formattedTime = formatTime(input, true)
20 | t.equal(formattedTime, input)
21 | })
22 |
23 | tap.test('translates epoch milliseconds if `translateTime` is `true`', async t => {
24 | const formattedTime = formatTime(epochMS, true)
25 | t.equal(formattedTime, '17:30:00.000')
26 | })
27 |
28 | tap.test('translates epoch milliseconds to UTC string given format', async t => {
29 | const formattedTime = formatTime(epochMS, 'd mmm yyyy H:MM')
30 | t.equal(formattedTime, '6 Apr 2019 17:30')
31 | })
32 |
33 | tap.test('translates epoch milliseconds to SYS:STANDARD', async t => {
34 | const formattedTime = formatTime(epochMS, 'SYS:STANDARD')
35 | t.match(formattedTime, /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [-+]?\d{4}/)
36 | })
37 |
38 | tap.test('translates epoch milliseconds to SYS:', async t => {
39 | const formattedTime = formatTime(epochMS, 'SYS:d mmm yyyy H:MM')
40 | t.match(formattedTime, /\d{1} \w{3} \d{4} \d{1,2}:\d{2}/)
41 | })
42 |
43 | tap.test('passes through date string if `translateTime` is `false`', async t => {
44 | const formattedTime = formatTime(dateStr)
45 | t.equal(formattedTime, dateStr)
46 | })
47 |
48 | tap.test('translates date string if `translateTime` is `true`', async t => {
49 | const formattedTime = formatTime(dateStr, true)
50 | t.equal(formattedTime, '17:30:00.000')
51 | })
52 |
53 | tap.test('translates date string to UTC string given format', async t => {
54 | const formattedTime = formatTime(dateStr, 'd mmm yyyy H:MM')
55 | t.equal(formattedTime, '6 Apr 2019 17:30')
56 | })
57 |
58 | tap.test('translates date string to SYS:STANDARD', async t => {
59 | const formattedTime = formatTime(dateStr, 'SYS:STANDARD')
60 | t.match(formattedTime, /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [-+]?\d{4}/)
61 | })
62 |
63 | tap.test('translates date string to UTC:', async t => {
64 | const formattedTime = formatTime(dateStr, 'UTC:d mmm yyyy H:MM')
65 | t.equal(formattedTime, '6 Apr 2019 17:30')
66 | })
67 |
68 | tap.test('translates date string to SYS:', async t => {
69 | const formattedTime = formatTime(dateStr, 'SYS:d mmm yyyy H:MM')
70 | t.match(formattedTime, /\d{1} \w{3} \d{4} \d{1,2}:\d{2}/)
71 | })
72 |
--------------------------------------------------------------------------------
/lib/utils/get-level-label-data.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = getLevelLabelData
4 | const { LEVELS, LEVEL_NAMES } = require('../constants')
5 |
6 | /**
7 | * Given initial settings for custom levels/names and use of only custom props
8 | * get the level label that corresponds with a given level number
9 | *
10 | * @param {boolean} useOnlyCustomProps
11 | * @param {object} customLevels
12 | * @param {object} customLevelNames
13 | *
14 | * @returns {function} A function that takes a number level and returns the level's label string
15 | */
16 | function getLevelLabelData (useOnlyCustomProps, customLevels, customLevelNames) {
17 | const levels = useOnlyCustomProps ? customLevels || LEVELS : Object.assign({}, LEVELS, customLevels)
18 | const levelNames = useOnlyCustomProps ? customLevelNames || LEVEL_NAMES : Object.assign({}, LEVEL_NAMES, customLevelNames)
19 | return function (level) {
20 | let levelNum = 'default'
21 | if (Number.isInteger(+level)) {
22 | levelNum = Object.prototype.hasOwnProperty.call(levels, level) ? level : levelNum
23 | } else {
24 | levelNum = Object.prototype.hasOwnProperty.call(levelNames, level.toLowerCase()) ? levelNames[level.toLowerCase()] : levelNum
25 | }
26 |
27 | return [levels[levelNum], levelNum]
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib/utils/get-property-value.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = getPropertyValue
4 |
5 | const splitPropertyKey = require('./split-property-key')
6 |
7 | /**
8 | * Gets a specified property from an object if it exists.
9 | *
10 | * @param {object} obj The object to be searched.
11 | * @param {string|string[]} property A string, or an array of strings, identifying
12 | * the property to be retrieved from the object.
13 | * Accepts nested properties delimited by a `.`.
14 | * Delimiter can be escaped to preserve property names that contain the delimiter.
15 | * e.g. `'prop1.prop2'` or `'prop2\.domain\.corp.prop2'`.
16 | *
17 | * @returns {*}
18 | */
19 | function getPropertyValue (obj, property) {
20 | const props = Array.isArray(property) ? property : splitPropertyKey(property)
21 |
22 | for (const prop of props) {
23 | if (!Object.prototype.hasOwnProperty.call(obj, prop)) {
24 | return
25 | }
26 | obj = obj[prop]
27 | }
28 |
29 | return obj
30 | }
31 |
--------------------------------------------------------------------------------
/lib/utils/get-property-value.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const tap = require('tap')
4 | const getPropertyValue = require('./get-property-value')
5 |
6 | tap.test('getPropertyValue returns the value of the property', async t => {
7 | const result = getPropertyValue({
8 | foo: 'bar'
9 | }, 'foo')
10 | t.same(result, 'bar')
11 | })
12 |
13 | tap.test('getPropertyValue returns the value of the nested property', async t => {
14 | const result = getPropertyValue({ extra: { foo: { value: 'bar' } } }, 'extra.foo.value')
15 | t.same(result, 'bar')
16 | })
17 |
18 | tap.test('getPropertyValue returns the value of the nested property using the array of nested property keys', async t => {
19 | const result = getPropertyValue({ extra: { foo: { value: 'bar' } } }, ['extra', 'foo', 'value'])
20 | t.same(result, 'bar')
21 | })
22 |
23 | tap.test('getPropertyValue returns undefined for non-existing properties', async t => {
24 | const result = getPropertyValue({ extra: { foo: { value: 'bar' } } }, 'extra.foo.value-2')
25 | t.same(result, undefined)
26 | })
27 |
28 | tap.test('getPropertyValue returns undefined for non-existing properties using the array of nested property keys', async t => {
29 | const result = getPropertyValue({ extra: { foo: { value: 'bar' } } }, ['extra', 'foo', 'value-2'])
30 | t.same(result, undefined)
31 | })
32 |
--------------------------------------------------------------------------------
/lib/utils/handle-custom-levels-names-opts.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = handleCustomLevelsNamesOpts
4 |
5 | /**
6 | * Parse a CSV string or options object that maps level
7 | * labels to level values.
8 | *
9 | * @param {string|object} cLevels An object mapping level
10 | * names to level values, e.g. `{ info: 30, debug: 65 }`, or a
11 | * CSV string in the format `level_name:level_value`, e.g.
12 | * `info:30,debug:65`.
13 | *
14 | * @returns {object} An object mapping levels names to level values
15 | * e.g. `{ info: 30, debug: 65 }`.
16 | */
17 | function handleCustomLevelsNamesOpts (cLevels) {
18 | if (!cLevels) return {}
19 |
20 | if (typeof cLevels === 'string') {
21 | return cLevels
22 | .split(',')
23 | .reduce((agg, value, idx) => {
24 | const [levelName, levelNum = idx] = value.split(':')
25 | agg[levelName.toLowerCase()] = levelNum
26 | return agg
27 | }, {})
28 | } else if (Object.prototype.toString.call(cLevels) === '[object Object]') {
29 | return Object
30 | .keys(cLevels)
31 | .reduce((agg, levelName) => {
32 | agg[levelName.toLowerCase()] = cLevels[levelName]
33 | return agg
34 | }, {})
35 | } else {
36 | return {}
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/utils/handle-custom-levels-names-opts.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const tap = require('tap')
4 | const handleCustomLevelsNamesOpts = require('./handle-custom-levels-names-opts')
5 |
6 | tap.test('returns a empty object `{}` for undefined parameter', async t => {
7 | const handledCustomLevelNames = handleCustomLevelsNamesOpts()
8 | t.same(handledCustomLevelNames, {})
9 | })
10 |
11 | tap.test('returns a empty object `{}` for unknown parameter', async t => {
12 | const handledCustomLevelNames = handleCustomLevelsNamesOpts(123)
13 | t.same(handledCustomLevelNames, {})
14 | })
15 |
16 | tap.test('returns a filled object for string parameter', async t => {
17 | const handledCustomLevelNames = handleCustomLevelsNamesOpts('ok:10,warn:20,error:35')
18 | t.same(handledCustomLevelNames, {
19 | ok: 10,
20 | warn: 20,
21 | error: 35
22 | })
23 | })
24 |
25 | tap.test('returns a filled object for object parameter', async t => {
26 | const handledCustomLevelNames = handleCustomLevelsNamesOpts({
27 | ok: 10,
28 | warn: 20,
29 | error: 35
30 | })
31 | t.same(handledCustomLevelNames, {
32 | ok: 10,
33 | warn: 20,
34 | error: 35
35 | })
36 | })
37 |
38 | tap.test('defaults missing level num to first index', async t => {
39 | const result = handleCustomLevelsNamesOpts('ok:10,info')
40 | t.same(result, {
41 | ok: 10,
42 | info: 1
43 | })
44 | })
45 |
--------------------------------------------------------------------------------
/lib/utils/handle-custom-levels-opts.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = handleCustomLevelsOpts
4 |
5 | /**
6 | * Parse a CSV string or options object that specifies
7 | * configuration for custom levels.
8 | *
9 | * @param {string|object} cLevels An object mapping level
10 | * names to values, e.g. `{ info: 30, debug: 65 }`, or a
11 | * CSV string in the format `level_name:level_value`, e.g.
12 | * `info:30,debug:65`.
13 | *
14 | * @returns {object} An object mapping levels to labels that
15 | * appear in logs, e.g. `{ '30': 'INFO', '65': 'DEBUG' }`.
16 | */
17 | function handleCustomLevelsOpts (cLevels) {
18 | if (!cLevels) return {}
19 |
20 | if (typeof cLevels === 'string') {
21 | return cLevels
22 | .split(',')
23 | .reduce((agg, value, idx) => {
24 | const [levelName, levelNum = idx] = value.split(':')
25 | agg[levelNum] = levelName.toUpperCase()
26 | return agg
27 | },
28 | { default: 'USERLVL' })
29 | } else if (Object.prototype.toString.call(cLevels) === '[object Object]') {
30 | return Object
31 | .keys(cLevels)
32 | .reduce((agg, levelName) => {
33 | agg[cLevels[levelName]] = levelName.toUpperCase()
34 | return agg
35 | }, { default: 'USERLVL' })
36 | } else {
37 | return {}
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/lib/utils/handle-custom-levels-opts.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const tap = require('tap')
4 | const handleCustomLevelsOpts = require('./handle-custom-levels-opts')
5 |
6 | tap.test('returns a empty object `{}` for undefined parameter', async t => {
7 | const handledCustomLevel = handleCustomLevelsOpts()
8 | t.same(handledCustomLevel, {})
9 | })
10 |
11 | tap.test('returns a empty object `{}` for unknown parameter', async t => {
12 | const handledCustomLevel = handleCustomLevelsOpts(123)
13 | t.same(handledCustomLevel, {})
14 | })
15 |
16 | tap.test('returns a filled object for string parameter', async t => {
17 | const handledCustomLevel = handleCustomLevelsOpts('ok:10,warn:20,error:35')
18 | t.same(handledCustomLevel, {
19 | 10: 'OK',
20 | 20: 'WARN',
21 | 35: 'ERROR',
22 | default: 'USERLVL'
23 | })
24 | })
25 |
26 | tap.test('returns a filled object for object parameter', async t => {
27 | const handledCustomLevel = handleCustomLevelsOpts({
28 | ok: 10,
29 | warn: 20,
30 | error: 35
31 | })
32 | t.same(handledCustomLevel, {
33 | 10: 'OK',
34 | 20: 'WARN',
35 | 35: 'ERROR',
36 | default: 'USERLVL'
37 | })
38 | })
39 |
40 | tap.test('defaults missing level num to first index', async t => {
41 | const result = handleCustomLevelsOpts('ok:10,info')
42 | t.same(result, {
43 | 10: 'OK',
44 | 1: 'INFO',
45 | default: 'USERLVL'
46 | })
47 | })
48 |
--------------------------------------------------------------------------------
/lib/utils/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = {
4 | buildSafeSonicBoom: require('./build-safe-sonic-boom.js'),
5 | createDate: require('./create-date.js'),
6 | deleteLogProperty: require('./delete-log-property.js'),
7 | filterLog: require('./filter-log.js'),
8 | formatTime: require('./format-time.js'),
9 | getPropertyValue: require('./get-property-value.js'),
10 | handleCustomLevelsNamesOpts: require('./handle-custom-levels-names-opts.js'),
11 | handleCustomLevelsOpts: require('./handle-custom-levels-opts.js'),
12 | interpretConditionals: require('./interpret-conditionals.js'),
13 | isObject: require('./is-object.js'),
14 | isValidDate: require('./is-valid-date.js'),
15 | joinLinesWithIndentation: require('./join-lines-with-indentation.js'),
16 | noop: require('./noop.js'),
17 | parseFactoryOptions: require('./parse-factory-options.js'),
18 | prettifyErrorLog: require('./prettify-error-log.js'),
19 | prettifyError: require('./prettify-error.js'),
20 | prettifyLevel: require('./prettify-level.js'),
21 | prettifyMessage: require('./prettify-message.js'),
22 | prettifyMetadata: require('./prettify-metadata.js'),
23 | prettifyObject: require('./prettify-object.js'),
24 | prettifyTime: require('./prettify-time.js'),
25 | splitPropertyKey: require('./split-property-key.js'),
26 | getLevelLabelData: require('./get-level-label-data')
27 | }
28 |
29 | // The remainder of this file consists of jsdoc blocks that are difficult to
30 | // determine a more appropriate "home" for. As an example, the blocks associated
31 | // with custom prettifiers could live in either the `prettify-level`,
32 | // `prettify-metadata`, or `prettify-time` files since they are the primary
33 | // files where such code is used. But we want a central place to define common
34 | // doc blocks, so we are picking this file as the answer.
35 |
36 | /**
37 | * A hash of log property names mapped to prettifier functions. When the
38 | * incoming log data is being processed for prettification, any key on the log
39 | * that matches a key in a custom prettifiers hash will be prettified using
40 | * that matching custom prettifier. The value passed to the custom prettifier
41 | * will the value associated with the corresponding log key.
42 | *
43 | * The hash may contain any arbitrary keys for arbitrary log properties, but it
44 | * may also contain a set of predefined key names that map to well-known log
45 | * properties. These keys are:
46 | *
47 | * + `time` (for the timestamp field)
48 | * + `level` (for the level label field; value may be a level number instead
49 | * of a level label)
50 | * + `hostname`
51 | * + `pid`
52 | * + `name`
53 | * + `caller`
54 | *
55 | * @typedef {Object.} CustomPrettifiers
56 | */
57 |
58 | /**
59 | * A synchronous function to be used for prettifying a log property. It must
60 | * return a string.
61 | *
62 | * @typedef {function} CustomPrettifierFunc
63 | * @param {any} value The value to be prettified for the key associated with
64 | * the prettifier.
65 | * @returns {string}
66 | */
67 |
68 | /**
69 | * A tokenized string that indicates how the prettified log line should be
70 | * formatted. Tokens are either log properties enclosed in curly braces, e.g.
71 | * `{levelLabel}`, `{pid}`, or `{req.url}`, or conditional directives in curly
72 | * braces. The only conditional directives supported are `if` and `end`, e.g.
73 | * `{if pid}{pid}{end}`; every `if` must have a matching `end`. Nested
74 | * conditions are not supported.
75 | *
76 | * @typedef {string} MessageFormatString
77 | *
78 | * @example
79 | * `{levelLabel} - {if pid}{pid} - {end}url:{req.url}`
80 | */
81 |
82 | /**
83 | * @typedef {object} PrettifyMessageExtras
84 | * @property {object} colors Available color functions based on `useColor` (or `colorize`) context
85 | * the options.
86 | */
87 |
88 | /**
89 | * A function that accepts a log object, name of the message key, and name of
90 | * the level label key and returns a formatted log line.
91 | *
92 | * Note: this function must be synchronous.
93 | *
94 | * @typedef {function} MessageFormatFunction
95 | * @param {object} log The log object to be processed.
96 | * @param {string} messageKey The name of the key in the `log` object that
97 | * contains the log message.
98 | * @param {string} levelLabel The name of the key in the `log` object that
99 | * contains the log level name.
100 | * @param {PrettifyMessageExtras} extras Additional data available for message context
101 | * @returns {string}
102 | *
103 | * @example
104 | * function (log, messageKey, levelLabel) {
105 | * return `${log[levelLabel]} - ${log[messageKey]}`
106 | * }
107 | */
108 |
--------------------------------------------------------------------------------
/lib/utils/index.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { test } = require('node:test')
4 | const index = require('./index.js')
5 | const { readdirSync } = require('node:fs')
6 | const { basename } = require('node:path')
7 |
8 | test(
9 | 'index exports exactly all non-test files excluding itself',
10 | t => {
11 | // Read all files in the `util` directory
12 | const files = readdirSync(__dirname)
13 |
14 | for (const file of files) {
15 | const kebabName = basename(file, '.js')
16 | const snakeName = kebabName.split('-').map((part, idx) => {
17 | if (idx === 0) return part
18 | return part[0].toUpperCase() + part.slice(1)
19 | }).join('')
20 |
21 | if (file.endsWith('.test.js') === false && file !== 'index.js') {
22 | // We expect all files to be exported except…
23 | t.assert.ok(index[snakeName], `exports ${snakeName}`)
24 | } else {
25 | // …test files and the index file itself – those must not be exported
26 | t.assert.ok(!index[snakeName], `does not export ${snakeName}`)
27 | }
28 |
29 | // Remove the exported file from the index object
30 | delete index[snakeName]
31 | }
32 |
33 | // Now the index is expected to be empty, as nothing else should be
34 | // exported from it
35 | t.assert.deepStrictEqual(index, {}, 'does not export anything else')
36 | }
37 | )
38 |
--------------------------------------------------------------------------------
/lib/utils/interpret-conditionals.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = interpretConditionals
4 |
5 | const getPropertyValue = require('./get-property-value')
6 |
7 | /**
8 | * Translates all conditional blocks from within the messageFormat. Translates
9 | * any matching {if key}{key}{end} statements and returns everything between
10 | * if and else blocks if the key provided was found in log.
11 | *
12 | * @param {MessageFormatString|MessageFormatFunction} messageFormat A format
13 | * string or function that defines how the logged message should be
14 | * conditionally formatted.
15 | * @param {object} log The log object to be modified.
16 | *
17 | * @returns {string} The parsed messageFormat.
18 | */
19 | function interpretConditionals (messageFormat, log) {
20 | messageFormat = messageFormat.replace(/{if (.*?)}(.*?){end}/g, replacer)
21 |
22 | // Remove non-terminated if blocks
23 | messageFormat = messageFormat.replace(/{if (.*?)}/g, '')
24 | // Remove floating end blocks
25 | messageFormat = messageFormat.replace(/{end}/g, '')
26 |
27 | return messageFormat.replace(/\s+/g, ' ').trim()
28 |
29 | function replacer (_, key, value) {
30 | const propertyValue = getPropertyValue(log, key)
31 | if (propertyValue && value.includes(key)) {
32 | return value.replace(new RegExp('{' + key + '}', 'g'), propertyValue)
33 | } else {
34 | return ''
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/lib/utils/interpret-conditionals.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const tap = require('tap')
4 | const { createCopier } = require('fast-copy')
5 | const fastCopy = createCopier({})
6 | const interpretConditionals = require('./interpret-conditionals')
7 |
8 | const logData = {
9 | level: 30,
10 | data1: {
11 | data2: 'bar'
12 | },
13 | msg: 'foo'
14 | }
15 |
16 | tap.test('interpretConditionals translates if / else statement to found property value', async t => {
17 | const log = fastCopy(logData)
18 | t.equal(interpretConditionals('{level} - {if data1.data2}{data1.data2}{end}', log), '{level} - bar')
19 | })
20 |
21 | tap.test('interpretConditionals translates if / else statement to found property value and leave unmatched property key untouched', async t => {
22 | const log = fastCopy(logData)
23 | t.equal(interpretConditionals('{level} - {if data1.data2}{data1.data2} ({msg}){end}', log), '{level} - bar ({msg})')
24 | })
25 |
26 | tap.test('interpretConditionals removes non-terminated if statements', async t => {
27 | const log = fastCopy(logData)
28 | t.equal(interpretConditionals('{level} - {if data1.data2}{data1.data2}', log), '{level} - {data1.data2}')
29 | })
30 |
31 | tap.test('interpretConditionals removes floating end statements', async t => {
32 | const log = fastCopy(logData)
33 | t.equal(interpretConditionals('{level} - {data1.data2}{end}', log), '{level} - {data1.data2}')
34 | })
35 |
36 | tap.test('interpretConditionals removes floating end statements within translated if / end statements', async t => {
37 | const log = fastCopy(logData)
38 | t.equal(interpretConditionals('{level} - {if msg}({msg}){end}{end}', log), '{level} - (foo)')
39 | })
40 |
41 | tap.test('interpretConditionals removes if / end blocks if existent condition key does not match existent property key', async t => {
42 | const log = fastCopy(logData)
43 | t.equal(interpretConditionals('{level}{if msg}{data1.data2}{end}', log), '{level}')
44 | })
45 |
46 | tap.test('interpretConditionals removes if / end blocks if non-existent condition key does not match existent property key', async t => {
47 | const log = fastCopy(logData)
48 | t.equal(interpretConditionals('{level}{if foo}{msg}{end}', log), '{level}')
49 | })
50 |
51 | tap.test('interpretConditionals removes if / end blocks if existent condition key does not match non-existent property key', async t => {
52 | const log = fastCopy(logData)
53 | t.equal(interpretConditionals('{level}{if msg}{foo}{end}', log), '{level}')
54 | })
55 |
56 | tap.test('interpretConditionals removes if / end blocks if non-existent condition key does not match non-existent property key', async t => {
57 | const log = fastCopy(logData)
58 | t.equal(interpretConditionals('{level}{if foo}{bar}{end}', log), '{level}')
59 | })
60 |
61 | tap.test('interpretConditionals removes if / end blocks if nested condition key does not match property key', async t => {
62 | const log = fastCopy(logData)
63 | t.equal(interpretConditionals('{level}{if data1.msg}{data1.data2}{end}', log), '{level}')
64 | })
65 |
66 | tap.test('interpretConditionals removes nested if / end statement blocks', async t => {
67 | const log = fastCopy(logData)
68 | t.equal(interpretConditionals('{if msg}{if data1.data2}{msg}{data1.data2}{end}{end}', log), 'foo{data1.data2}')
69 | })
70 |
--------------------------------------------------------------------------------
/lib/utils/is-object.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = isObject
4 |
5 | function isObject (input) {
6 | return Object.prototype.toString.apply(input) === '[object Object]'
7 | }
8 |
--------------------------------------------------------------------------------
/lib/utils/is-object.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const tap = require('tap')
4 | const isObject = require('./is-object')
5 |
6 | tap.test('returns correct answer', async t => {
7 | t.equal(isObject({}), true)
8 | t.equal(isObject([]), false)
9 | t.equal(isObject(42), false)
10 | })
11 |
--------------------------------------------------------------------------------
/lib/utils/is-valid-date.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = isValidDate
4 |
5 | /**
6 | * Checks if the argument is a JS Date and not 'Invalid Date'.
7 | *
8 | * @param {Date} date The date to check.
9 | *
10 | * @returns {boolean} true if the argument is a JS Date and not 'Invalid Date'.
11 | */
12 | function isValidDate (date) {
13 | return date instanceof Date && !Number.isNaN(date.getTime())
14 | }
15 |
--------------------------------------------------------------------------------
/lib/utils/is-valid-date.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.TZ = 'UTC'
4 |
5 | const tap = require('tap')
6 | const isValidDate = require('./is-valid-date')
7 |
8 | tap.test('returns true for valid dates', async t => {
9 | t.same(isValidDate(new Date()), true)
10 | })
11 |
12 | tap.test('returns false for non-dates and invalid dates', async t => {
13 | t.plan(2)
14 | t.same(isValidDate('20210621'), false)
15 | t.same(isValidDate(new Date('2021-41-99')), false)
16 | })
17 |
--------------------------------------------------------------------------------
/lib/utils/join-lines-with-indentation.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = joinLinesWithIndentation
4 |
5 | /**
6 | * @typedef {object} JoinLinesWithIndentationParams
7 | * @property {string} input The string to split and reformat.
8 | * @property {string} [ident] The indentation string. Default: ` ` (4 spaces).
9 | * @property {string} [eol] The end of line sequence to use when rejoining
10 | * the lines. Default: `'\n'`.
11 | */
12 |
13 | /**
14 | * Given a string with line separators, either `\r\n` or `\n`, add indentation
15 | * to all lines subsequent to the first line and rejoin the lines using an
16 | * end of line sequence.
17 | *
18 | * @param {JoinLinesWithIndentationParams} input
19 | *
20 | * @returns {string} A string with lines subsequent to the first indented
21 | * with the given indentation sequence.
22 | */
23 | function joinLinesWithIndentation ({ input, ident = ' ', eol = '\n' }) {
24 | const lines = input.split(/\r?\n/)
25 | for (let i = 1; i < lines.length; i += 1) {
26 | lines[i] = ident + lines[i]
27 | }
28 | return lines.join(eol)
29 | }
30 |
--------------------------------------------------------------------------------
/lib/utils/join-lines-with-indentation.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const tap = require('tap')
4 | const joinLinesWithIndentation = require('./join-lines-with-indentation')
5 |
6 | tap.test('joinLinesWithIndentation adds indentation to beginning of subsequent lines', async t => {
7 | const input = 'foo\nbar\nbaz'
8 | const result = joinLinesWithIndentation({ input })
9 | t.equal(result, 'foo\n bar\n baz')
10 | })
11 |
12 | tap.test('joinLinesWithIndentation accepts custom indentation, line breaks, and eol', async t => {
13 | const input = 'foo\nbar\r\nbaz'
14 | const result = joinLinesWithIndentation({ input, ident: ' ', eol: '^' })
15 | t.equal(result, 'foo^ bar^ baz')
16 | })
17 |
--------------------------------------------------------------------------------
/lib/utils/noop.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = function noop () {}
4 |
--------------------------------------------------------------------------------
/lib/utils/noop.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const tap = require('tap')
4 | const noop = require('./noop')
5 |
6 | tap.test('is a function', async t => {
7 | t.type(noop, Function)
8 | })
9 |
10 | tap.test('does nothing', async t => {
11 | t.equal(noop('stuff'), undefined)
12 | })
13 |
--------------------------------------------------------------------------------
/lib/utils/parse-factory-options.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = parseFactoryOptions
4 |
5 | const {
6 | LEVEL_NAMES
7 | } = require('../constants')
8 | const colors = require('../colors')
9 | const handleCustomLevelsOpts = require('./handle-custom-levels-opts')
10 | const handleCustomLevelsNamesOpts = require('./handle-custom-levels-names-opts')
11 | const handleLevelLabelData = require('./get-level-label-data')
12 |
13 | /**
14 | * A `PrettyContext` is an object to be used by the various functions that
15 | * process log data. It is derived from the provided {@link PinoPrettyOptions}.
16 | * It may be used as a `this` context.
17 | *
18 | * @typedef {object} PrettyContext
19 | * @property {string} EOL The escape sequence chosen as the line terminator.
20 | * @property {string} IDENT The string to use as the indentation sequence.
21 | * @property {ColorizerFunc} colorizer A configured colorizer function.
22 | * @property {Array[Array]} customColors A set of custom color
23 | * names associated with level numbers.
24 | * @property {object} customLevelNames A hash of level numbers to level names,
25 | * e.g. `{ 30: "info" }`.
26 | * @property {object} customLevels A hash of level names to level numbers,
27 | * e.g. `{ info: 30 }`.
28 | * @property {CustomPrettifiers} customPrettifiers A hash of custom prettifier
29 | * functions.
30 | * @property {object} customProperties Comprised of `customLevels` and
31 | * `customLevelNames` if such options are provided.
32 | * @property {string[]} errorLikeObjectKeys The key names in the log data that
33 | * should be considered as holding error objects.
34 | * @property {string[]} errorProps A list of error object keys that should be
35 | * included in the output.
36 | * @property {function} getLevelLabelData Pass a numeric level to return [levelLabelString,levelNum]
37 | * @property {boolean} hideObject Indicates the prettifier should omit objects
38 | * in the output.
39 | * @property {string[]} ignoreKeys Set of log data keys to omit.
40 | * @property {string[]} includeKeys Opposite of `ignoreKeys`.
41 | * @property {boolean} levelFirst Indicates the level should be printed first.
42 | * @property {string} levelKey Name of the key in the log data that contains
43 | * the message.
44 | * @property {string} levelLabel Format token to represent the position of the
45 | * level name in the output string.
46 | * @property {MessageFormatString|MessageFormatFunction} messageFormat
47 | * @property {string} messageKey Name of the key in the log data that contains
48 | * the message.
49 | * @property {string|number} minimumLevel The minimum log level to process
50 | * and output.
51 | * @property {ColorizerFunc} objectColorizer
52 | * @property {boolean} singleLine Indicates objects should be printed on a
53 | * single output line.
54 | * @property {string} timestampKey The name of the key in the log data that
55 | * contains the log timestamp.
56 | * @property {boolean} translateTime Indicates if timestamps should be
57 | * translated to a human-readable string.
58 | * @property {boolean} useOnlyCustomProps
59 | */
60 |
61 | /**
62 | * @param {PinoPrettyOptions} options The user supplied object of options.
63 | *
64 | * @returns {PrettyContext}
65 | */
66 | function parseFactoryOptions (options) {
67 | const EOL = options.crlf ? '\r\n' : '\n'
68 | const IDENT = ' '
69 | const {
70 | customPrettifiers,
71 | errorLikeObjectKeys,
72 | hideObject,
73 | levelFirst,
74 | levelKey,
75 | levelLabel,
76 | messageFormat,
77 | messageKey,
78 | minimumLevel,
79 | singleLine,
80 | timestampKey,
81 | translateTime
82 | } = options
83 | const errorProps = options.errorProps.split(',')
84 | const useOnlyCustomProps = typeof options.useOnlyCustomProps === 'boolean'
85 | ? options.useOnlyCustomProps
86 | : (options.useOnlyCustomProps === 'true')
87 | const customLevels = handleCustomLevelsOpts(options.customLevels)
88 | const customLevelNames = handleCustomLevelsNamesOpts(options.customLevels)
89 | const getLevelLabelData = handleLevelLabelData(useOnlyCustomProps, customLevels, customLevelNames)
90 |
91 | let customColors
92 | if (options.customColors) {
93 | if (typeof options.customColors === 'string') {
94 | customColors = options.customColors.split(',').reduce((agg, value) => {
95 | const [level, color] = value.split(':')
96 | const condition = useOnlyCustomProps
97 | ? options.customLevels
98 | : customLevelNames[level] !== undefined
99 | const levelNum = condition
100 | ? customLevelNames[level]
101 | : LEVEL_NAMES[level]
102 | const colorIdx = levelNum !== undefined
103 | ? levelNum
104 | : level
105 | agg.push([colorIdx, color])
106 | return agg
107 | }, [])
108 | } else if (typeof options.customColors === 'object') {
109 | customColors = Object.keys(options.customColors).reduce((agg, value) => {
110 | const [level, color] = [value, options.customColors[value]]
111 | const condition = useOnlyCustomProps
112 | ? options.customLevels
113 | : customLevelNames[level] !== undefined
114 | const levelNum = condition
115 | ? customLevelNames[level]
116 | : LEVEL_NAMES[level]
117 | const colorIdx = levelNum !== undefined
118 | ? levelNum
119 | : level
120 | agg.push([colorIdx, color])
121 | return agg
122 | }, [])
123 | } else {
124 | throw new Error('options.customColors must be of type string or object.')
125 | }
126 | }
127 |
128 | const customProperties = { customLevels, customLevelNames }
129 | if (useOnlyCustomProps === true && !options.customLevels) {
130 | customProperties.customLevels = undefined
131 | customProperties.customLevelNames = undefined
132 | }
133 |
134 | const includeKeys = options.include !== undefined
135 | ? new Set(options.include.split(','))
136 | : undefined
137 | const ignoreKeys = (!includeKeys && options.ignore)
138 | ? new Set(options.ignore.split(','))
139 | : undefined
140 |
141 | const colorizer = colors(options.colorize, customColors, useOnlyCustomProps)
142 | const objectColorizer = options.colorizeObjects
143 | ? colorizer
144 | : colors(false, [], false)
145 |
146 | return {
147 | EOL,
148 | IDENT,
149 | colorizer,
150 | customColors,
151 | customLevelNames,
152 | customLevels,
153 | customPrettifiers,
154 | customProperties,
155 | errorLikeObjectKeys,
156 | errorProps,
157 | getLevelLabelData,
158 | hideObject,
159 | ignoreKeys,
160 | includeKeys,
161 | levelFirst,
162 | levelKey,
163 | levelLabel,
164 | messageFormat,
165 | messageKey,
166 | minimumLevel,
167 | objectColorizer,
168 | singleLine,
169 | timestampKey,
170 | translateTime,
171 | useOnlyCustomProps
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/lib/utils/prettify-error-log.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = prettifyErrorLog
4 |
5 | const {
6 | LOGGER_KEYS
7 | } = require('../constants')
8 |
9 | const isObject = require('./is-object')
10 | const joinLinesWithIndentation = require('./join-lines-with-indentation')
11 | const prettifyObject = require('./prettify-object')
12 |
13 | /**
14 | * @typedef {object} PrettifyErrorLogParams
15 | * @property {object} log The error log to prettify.
16 | * @property {PrettyContext} context The context object built from parsing
17 | * the options.
18 | */
19 |
20 | /**
21 | * Given a log object that has a `type: 'Error'` key, prettify the object and
22 | * return the result. In other
23 | *
24 | * @param {PrettifyErrorLogParams} input
25 | *
26 | * @returns {string} A string that represents the prettified error log.
27 | */
28 | function prettifyErrorLog ({ log, context }) {
29 | const {
30 | EOL: eol,
31 | IDENT: ident,
32 | errorProps: errorProperties,
33 | messageKey
34 | } = context
35 | const stack = log.stack
36 | const joinedLines = joinLinesWithIndentation({ input: stack, ident, eol })
37 | let result = `${ident}${joinedLines}${eol}`
38 |
39 | if (errorProperties.length > 0) {
40 | const excludeProperties = LOGGER_KEYS.concat(messageKey, 'type', 'stack')
41 | let propertiesToPrint
42 | if (errorProperties[0] === '*') {
43 | // Print all sibling properties except for the standard exclusions.
44 | propertiesToPrint = Object.keys(log).filter(k => excludeProperties.includes(k) === false)
45 | } else {
46 | // Print only specified properties unless the property is a standard exclusion.
47 | propertiesToPrint = errorProperties.filter(k => excludeProperties.includes(k) === false)
48 | }
49 |
50 | for (let i = 0; i < propertiesToPrint.length; i += 1) {
51 | const key = propertiesToPrint[i]
52 | if (key in log === false) continue
53 | if (isObject(log[key])) {
54 | // The nested object may have "logger" type keys but since they are not
55 | // at the root level of the object being processed, we want to print them.
56 | // Thus, we invoke with `excludeLoggerKeys: false`.
57 | const prettifiedObject = prettifyObject({
58 | log: log[key],
59 | excludeLoggerKeys: false,
60 | context: {
61 | ...context,
62 | IDENT: ident + ident
63 | }
64 | })
65 | result = `${result}${ident}${key}: {${eol}${prettifiedObject}${ident}}${eol}`
66 | continue
67 | }
68 | result = `${result}${ident}${key}: ${log[key]}${eol}`
69 | }
70 | }
71 |
72 | return result
73 | }
74 |
--------------------------------------------------------------------------------
/lib/utils/prettify-error-log.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const tap = require('tap')
4 | const prettifyErrorLog = require('./prettify-error-log')
5 | const colors = require('../colors')
6 | const {
7 | ERROR_LIKE_KEYS,
8 | MESSAGE_KEY
9 | } = require('../constants')
10 |
11 | const context = {
12 | EOL: '\n',
13 | IDENT: ' ',
14 | customPrettifiers: {},
15 | errorLikeObjectKeys: ERROR_LIKE_KEYS,
16 | errorProps: [],
17 | messageKey: MESSAGE_KEY,
18 | objectColorizer: colors()
19 | }
20 |
21 | tap.test('returns string with default settings', async t => {
22 | const err = Error('Something went wrong')
23 | const str = prettifyErrorLog({ log: err, context })
24 | t.ok(str.startsWith(' Error: Something went wrong'))
25 | })
26 |
27 | tap.test('returns string with custom ident', async t => {
28 | const err = Error('Something went wrong')
29 | const str = prettifyErrorLog({
30 | log: err,
31 | context: {
32 | ...context,
33 | IDENT: ' '
34 | }
35 | })
36 | t.ok(str.startsWith(' Error: Something went wrong'))
37 | })
38 |
39 | tap.test('returns string with custom eol', async t => {
40 | const err = Error('Something went wrong')
41 | const str = prettifyErrorLog({
42 | log: err,
43 | context: {
44 | ...context,
45 | EOL: '\r\n'
46 | }
47 | })
48 | t.ok(str.startsWith(' Error: Something went wrong\r\n'))
49 | })
50 |
51 | tap.test('errorProperties', t => {
52 | t.test('excludes all for wildcard', async t => {
53 | const err = Error('boom')
54 | err.foo = 'foo'
55 | const str = prettifyErrorLog({
56 | log: err,
57 | context: {
58 | ...context,
59 | errorProps: ['*']
60 | }
61 | })
62 | t.ok(str.startsWith(' Error: boom'))
63 | t.equal(str.includes('foo: "foo"'), false)
64 | })
65 |
66 | t.test('excludes only selected properties', async t => {
67 | const err = Error('boom')
68 | err.foo = 'foo'
69 | const str = prettifyErrorLog({
70 | log: err,
71 | context: {
72 | ...context,
73 | errorProps: ['foo']
74 | }
75 | })
76 | t.ok(str.startsWith(' Error: boom'))
77 | t.equal(str.includes('foo: foo'), true)
78 | })
79 |
80 | t.test('ignores specified properties if not present', async t => {
81 | const err = Error('boom')
82 | err.foo = 'foo'
83 | const str = prettifyErrorLog({
84 | log: err,
85 | context: {
86 | ...context,
87 | errorProps: ['foo', 'bar']
88 | }
89 | })
90 | t.ok(str.startsWith(' Error: boom'))
91 | t.equal(str.includes('foo: foo'), true)
92 | t.equal(str.includes('bar'), false)
93 | })
94 |
95 | t.test('processes nested objects', async t => {
96 | const err = Error('boom')
97 | err.foo = { bar: 'bar', message: 'included' }
98 | const str = prettifyErrorLog({
99 | log: err,
100 | context: {
101 | ...context,
102 | errorProps: ['foo']
103 | }
104 | })
105 | t.ok(str.startsWith(' Error: boom'))
106 | t.equal(str.includes('foo: {'), true)
107 | t.equal(str.includes('bar: "bar"'), true)
108 | t.equal(str.includes('message: "included"'), true)
109 | })
110 |
111 | t.end()
112 | })
113 |
--------------------------------------------------------------------------------
/lib/utils/prettify-error.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = prettifyError
4 |
5 | const joinLinesWithIndentation = require('./join-lines-with-indentation')
6 |
7 | /**
8 | * @typedef {object} PrettifyErrorParams
9 | * @property {string} keyName The key assigned to this error in the log object.
10 | * @property {string} lines The STRINGIFIED error. If the error field has a
11 | * custom prettifier, that should be pre-applied as well.
12 | * @property {string} ident The indentation sequence to use.
13 | * @property {string} eol The EOL sequence to use.
14 | */
15 |
16 | /**
17 | * Prettifies an error string into a multi-line format.
18 | *
19 | * @param {PrettifyErrorParams} input
20 | *
21 | * @returns {string}
22 | */
23 | function prettifyError ({ keyName, lines, eol, ident }) {
24 | let result = ''
25 | const joinedLines = joinLinesWithIndentation({ input: lines, ident, eol })
26 | const splitLines = `${ident}${keyName}: ${joinedLines}${eol}`.split(eol)
27 |
28 | for (let j = 0; j < splitLines.length; j += 1) {
29 | if (j !== 0) result += eol
30 |
31 | const line = splitLines[j]
32 | if (/^\s*"stack"/.test(line)) {
33 | const matches = /^(\s*"stack":)\s*(".*"),?$/.exec(line)
34 | /* istanbul ignore else */
35 | if (matches && matches.length === 3) {
36 | const indentSize = /^\s*/.exec(line)[0].length + 4
37 | const indentation = ' '.repeat(indentSize)
38 | const stackMessage = matches[2]
39 | result += matches[1] + eol + indentation + JSON.parse(stackMessage).replace(/\n/g, eol + indentation)
40 | } else {
41 | result += line
42 | }
43 | } else {
44 | result += line
45 | }
46 | }
47 |
48 | return result
49 | }
50 |
--------------------------------------------------------------------------------
/lib/utils/prettify-error.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const tap = require('tap')
4 | const stringifySafe = require('fast-safe-stringify')
5 | const prettifyError = require('./prettify-error')
6 |
7 | tap.test('prettifies error', t => {
8 | const error = Error('Bad error!')
9 | const lines = stringifySafe(error, Object.getOwnPropertyNames(error), 2)
10 |
11 | const prettyError = prettifyError({ keyName: 'errorKey', lines, ident: ' ', eol: '\n' })
12 | t.match(prettyError, /\s*errorKey: {\n\s*"stack":[\s\S]*"message": "Bad error!"/)
13 | t.end()
14 | })
15 |
--------------------------------------------------------------------------------
/lib/utils/prettify-level.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = prettifyLevel
4 |
5 | const getPropertyValue = require('./get-property-value')
6 |
7 | /**
8 | * @typedef {object} PrettifyLevelParams
9 | * @property {object} log The log object.
10 | * @property {PrettyContext} context The context object built from parsing
11 | * the options.
12 | */
13 |
14 | /**
15 | * Checks if the passed in log has a `level` value and returns a prettified
16 | * string for that level if so.
17 | *
18 | * @param {PrettifyLevelParams} input
19 | *
20 | * @returns {undefined|string} If `log` does not have a `level` property then
21 | * `undefined` will be returned. Otherwise, a string from the specified
22 | * `colorizer` is returned.
23 | */
24 | function prettifyLevel ({ log, context }) {
25 | const {
26 | colorizer,
27 | customLevels,
28 | customLevelNames,
29 | levelKey,
30 | getLevelLabelData
31 | } = context
32 | const prettifier = context.customPrettifiers?.level
33 | const output = getPropertyValue(log, levelKey)
34 | if (output === undefined) return undefined
35 | const labelColorized = colorizer(output, { customLevels, customLevelNames })
36 | if (prettifier) {
37 | const [label] = getLevelLabelData(output)
38 | return prettifier(output, levelKey, log, { label, labelColorized, colors: colorizer.colors })
39 | }
40 | return labelColorized
41 | }
42 |
--------------------------------------------------------------------------------
/lib/utils/prettify-level.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const tap = require('tap')
4 | const prettifyLevel = require('./prettify-level')
5 | const getColorizer = require('../colors')
6 | const getLevelLabelData = require('./get-level-label-data')
7 | const {
8 | LEVEL_KEY
9 | } = require('../constants')
10 |
11 | const context = {
12 | colorizer: getColorizer(),
13 | customLevelNames: undefined,
14 | customLevels: undefined,
15 | levelKey: LEVEL_KEY,
16 | customPrettifiers: undefined,
17 | getLevelLabelData: getLevelLabelData(false, {}, {})
18 | }
19 |
20 | tap.test('returns `undefined` for unknown level', async t => {
21 | const colorized = prettifyLevel({
22 | log: {},
23 | context: {
24 | ...context
25 | }
26 | })
27 | t.equal(colorized, undefined)
28 | })
29 |
30 | tap.test('returns non-colorized value for default colorizer', async t => {
31 | const log = {
32 | level: 30
33 | }
34 | const colorized = prettifyLevel({
35 | log,
36 | context: {
37 | ...context
38 | }
39 | })
40 | t.equal(colorized, 'INFO')
41 | })
42 |
43 | tap.test('returns colorized value for color colorizer', async t => {
44 | const log = {
45 | level: 30
46 | }
47 | const colorizer = getColorizer(true)
48 | const colorized = prettifyLevel({
49 | log,
50 | context: {
51 | ...context,
52 | colorizer
53 | }
54 | })
55 | t.equal(colorized, '\u001B[32mINFO\u001B[39m')
56 | })
57 |
58 | tap.test('passes output through provided prettifier', async t => {
59 | const log = {
60 | level: 30
61 | }
62 | const colorized = prettifyLevel({
63 | log,
64 | context: {
65 | ...context,
66 | customPrettifiers: { level () { return 'modified' } }
67 | }
68 | })
69 | t.equal(colorized, 'modified')
70 | })
71 |
--------------------------------------------------------------------------------
/lib/utils/prettify-message.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = prettifyMessage
4 |
5 | const {
6 | LEVELS
7 | } = require('../constants')
8 |
9 | const getPropertyValue = require('./get-property-value')
10 | const interpretConditionals = require('./interpret-conditionals')
11 |
12 | /**
13 | * @typedef {object} PrettifyMessageParams
14 | * @property {object} log The log object with the message to colorize.
15 | * @property {PrettyContext} context The context object built from parsing
16 | * the options.
17 | */
18 |
19 | /**
20 | * Prettifies a message string if the given `log` has a message property.
21 | *
22 | * @param {PrettifyMessageParams} input
23 | *
24 | * @returns {undefined|string} If the message key is not found, or the message
25 | * key is not a string, then `undefined` will be returned. Otherwise, a string
26 | * that is the prettified message.
27 | */
28 | function prettifyMessage ({ log, context }) {
29 | const {
30 | colorizer,
31 | customLevels,
32 | levelKey,
33 | levelLabel,
34 | messageFormat,
35 | messageKey,
36 | useOnlyCustomProps
37 | } = context
38 | if (messageFormat && typeof messageFormat === 'string') {
39 | const parsedMessageFormat = interpretConditionals(messageFormat, log)
40 |
41 | const message = String(parsedMessageFormat).replace(
42 | /{([^{}]+)}/g,
43 | function (match, p1) {
44 | // return log level as string instead of int
45 | let level
46 | if (p1 === levelLabel && (level = getPropertyValue(log, levelKey)) !== undefined) {
47 | const condition = useOnlyCustomProps ? customLevels === undefined : customLevels[level] === undefined
48 | return condition ? LEVELS[level] : customLevels[level]
49 | }
50 |
51 | // Parse nested key access, e.g. `{keyA.subKeyB}`.
52 | return getPropertyValue(log, p1) || ''
53 | })
54 | return colorizer.message(message)
55 | }
56 | if (messageFormat && typeof messageFormat === 'function') {
57 | const msg = messageFormat(log, messageKey, levelLabel, { colors: colorizer.colors })
58 | return colorizer.message(msg)
59 | }
60 | if (messageKey in log === false) return undefined
61 | if (typeof log[messageKey] !== 'string' && typeof log[messageKey] !== 'number' && typeof log[messageKey] !== 'boolean') return undefined
62 | return colorizer.message(log[messageKey])
63 | }
64 |
--------------------------------------------------------------------------------
/lib/utils/prettify-message.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const tap = require('tap')
4 | const prettifyMessage = require('./prettify-message')
5 | const getColorizer = require('../colors')
6 | const {
7 | LEVEL_KEY,
8 | LEVEL_LABEL
9 | } = require('../constants')
10 | const context = {
11 | colorizer: getColorizer(),
12 | levelKey: LEVEL_KEY,
13 | levelLabel: LEVEL_LABEL,
14 | messageKey: 'msg'
15 | }
16 |
17 | tap.test('returns `undefined` if `messageKey` not found', async t => {
18 | const str = prettifyMessage({ log: {}, context })
19 | t.equal(str, undefined)
20 | })
21 |
22 | tap.test('returns `undefined` if `messageKey` not string', async t => {
23 | const str = prettifyMessage({ log: { msg: {} }, context })
24 | t.equal(str, undefined)
25 | })
26 |
27 | tap.test('returns non-colorized value for default colorizer', async t => {
28 | const colorizer = getColorizer()
29 | const str = prettifyMessage({
30 | log: { msg: 'foo' },
31 | context: { ...context, colorizer }
32 | })
33 | t.equal(str, 'foo')
34 | })
35 |
36 | tap.test('returns non-colorized value for alternate `messageKey`', async t => {
37 | const str = prettifyMessage({
38 | log: { message: 'foo' },
39 | context: { ...context, messageKey: 'message' }
40 | })
41 | t.equal(str, 'foo')
42 | })
43 |
44 | tap.test('returns colorized value for color colorizer', async t => {
45 | const colorizer = getColorizer(true)
46 | const str = prettifyMessage({
47 | log: { msg: 'foo' },
48 | context: { ...context, colorizer }
49 | })
50 | t.equal(str, '\u001B[36mfoo\u001B[39m')
51 | })
52 |
53 | tap.test('returns colorized value for color colorizer for alternate `messageKey`', async t => {
54 | const colorizer = getColorizer(true)
55 | const str = prettifyMessage({
56 | log: { message: 'foo' },
57 | context: { ...context, messageKey: 'message', colorizer }
58 | })
59 | t.equal(str, '\u001B[36mfoo\u001B[39m')
60 | })
61 |
62 | tap.test('returns message formatted by `messageFormat` option', async t => {
63 | const str = prettifyMessage({
64 | log: { msg: 'foo', context: 'appModule' },
65 | context: { ...context, messageFormat: '{context} - {msg}' }
66 | })
67 | t.equal(str, 'appModule - foo')
68 | })
69 |
70 | tap.test('returns message formatted by `messageFormat` option - missing prop', async t => {
71 | const str = prettifyMessage({
72 | log: { context: 'appModule' },
73 | context: { ...context, messageFormat: '{context} - {msg}' }
74 | })
75 | t.equal(str, 'appModule - ')
76 | })
77 |
78 | tap.test('returns message formatted by `messageFormat` option - levelLabel & useOnlyCustomProps false', async t => {
79 | const str = prettifyMessage({
80 | log: { msg: 'foo', context: 'appModule', level: 30 },
81 | context: {
82 | ...context,
83 | messageFormat: '[{level}] {levelLabel} {context} - {msg}',
84 | customLevels: {}
85 | }
86 | })
87 | t.equal(str, '[30] INFO appModule - foo')
88 | })
89 |
90 | tap.test('returns message formatted by `messageFormat` option - levelLabel & useOnlyCustomProps true', async t => {
91 | const str = prettifyMessage({
92 | log: { msg: 'foo', context: 'appModule', level: 30 },
93 | context: {
94 | ...context,
95 | messageFormat: '[{level}] {levelLabel} {context} - {msg}',
96 | customLevels: { 30: 'CHECK' },
97 | useOnlyCustomProps: true
98 | }
99 | })
100 | t.equal(str, '[30] CHECK appModule - foo')
101 | })
102 |
103 | tap.test('returns message formatted by `messageFormat` option - levelLabel & customLevels', async t => {
104 | const str = prettifyMessage({
105 | log: { msg: 'foo', context: 'appModule', level: 123 },
106 | context: {
107 | ...context,
108 | messageFormat: '[{level}] {levelLabel} {context} - {msg}',
109 | customLevels: { 123: 'CUSTOM' }
110 | }
111 | })
112 | t.equal(str, '[123] CUSTOM appModule - foo')
113 | })
114 |
115 | tap.test('returns message formatted by `messageFormat` option - levelLabel, customLevels & useOnlyCustomProps', async t => {
116 | const str = prettifyMessage({
117 | log: { msg: 'foo', context: 'appModule', level: 123 },
118 | context: {
119 | ...context,
120 | messageFormat: '[{level}] {levelLabel} {context} - {msg}',
121 | customLevels: { 123: 'CUSTOM' },
122 | useOnlyCustomProps: true
123 | }
124 | })
125 | t.equal(str, '[123] CUSTOM appModule - foo')
126 | })
127 |
128 | tap.test('returns message formatted by `messageFormat` option - levelLabel, customLevels & useOnlyCustomProps false', async t => {
129 | const str = prettifyMessage({
130 | log: { msg: 'foo', context: 'appModule', level: 40 },
131 | context: {
132 | ...context,
133 | messageFormat: '[{level}] {levelLabel} {context} - {msg}',
134 | customLevels: { 123: 'CUSTOM' },
135 | useOnlyCustomProps: false
136 | }
137 | })
138 | t.equal(str, '[40] WARN appModule - foo')
139 | })
140 |
141 | tap.test('`messageFormat` supports nested curly brackets', async t => {
142 | const str = prettifyMessage({
143 | log: { level: 30 },
144 | context: {
145 | ...context,
146 | messageFormat: '{{level}}-{level}-{{level}-{level}}'
147 | }
148 | })
149 | t.equal(str, '{30}-30-{30-30}')
150 | })
151 |
152 | tap.test('`messageFormat` supports nested object', async t => {
153 | const str = prettifyMessage({
154 | log: { level: 30, request: { url: 'localhost/test' }, msg: 'foo' },
155 | context: {
156 | ...context,
157 | messageFormat: '{request.url} - param: {request.params.process} - {msg}'
158 | }
159 | })
160 | t.equal(str, 'localhost/test - param: - foo')
161 | })
162 |
163 | tap.test('`messageFormat` supports conditional blocks', async t => {
164 | const str = prettifyMessage({
165 | log: { level: 30, req: { id: 'foo' } },
166 | context: {
167 | ...context,
168 | messageFormat: '{level} | {if req.id}({req.id}){end}{if msg}{msg}{end}'
169 | }
170 | })
171 | t.equal(str, '30 | (foo)')
172 | })
173 |
174 | tap.test('`messageFormat` supports function definition', async t => {
175 | const str = prettifyMessage({
176 | log: { level: 30, request: { url: 'localhost/test' }, msg: 'incoming request' },
177 | context: {
178 | ...context,
179 | messageFormat: (log, messageKey, levelLabel) => {
180 | let msg = log[messageKey]
181 | if (msg === 'incoming request') msg = `--> ${log.request.url}`
182 | return msg
183 | }
184 | }
185 | })
186 | t.equal(str, '--> localhost/test')
187 | })
188 |
189 | tap.test('`messageFormat` supports function definition with colorizer object', async t => {
190 | const colorizer = getColorizer(true)
191 | const str = prettifyMessage({
192 | log: { level: 30, request: { url: 'localhost/test' }, msg: 'incoming request' },
193 | context: {
194 | ...context,
195 | colorizer,
196 | messageFormat: (log, messageKey, levelLabel, { colors }) => {
197 | let msg = log[messageKey]
198 | if (msg === 'incoming request') msg = `--> ${colors.red(log.request.url)}`
199 | return msg
200 | }
201 | }
202 | })
203 | t.equal(str, '\u001B[36m--> \u001B[31mlocalhost/test\u001B[36m\u001B[39m')
204 | })
205 |
206 | tap.test('`messageFormat` supports function definition with colorizer object when using custom colors', async t => {
207 | const colorizer = getColorizer(true, [[30, 'brightGreen']], false)
208 | const str = prettifyMessage({
209 | log: { level: 30, request: { url: 'localhost/test' }, msg: 'incoming request' },
210 | context: {
211 | ...context,
212 | colorizer,
213 | messageFormat: (log, messageKey, levelLabel, { colors }) => {
214 | let msg = log[messageKey]
215 | if (msg === 'incoming request') msg = `--> ${colors.red(log.request.url)}`
216 | return msg
217 | }
218 | }
219 | })
220 | t.equal(str, '\u001B[36m--> \u001B[31mlocalhost/test\u001B[36m\u001B[39m')
221 | })
222 |
223 | tap.test('`messageFormat` supports function definition with colorizer object when no color is supported', async t => {
224 | const colorizer = getColorizer(false)
225 | const str = prettifyMessage({
226 | log: { level: 30, request: { url: 'localhost/test' }, msg: 'incoming request' },
227 | context: {
228 | ...context,
229 | colorizer,
230 | messageFormat: (log, messageKey, levelLabel, { colors }) => {
231 | let msg = log[messageKey]
232 | if (msg === 'incoming request') msg = `--> ${colors.red(log.request.url)}`
233 | return msg
234 | }
235 | }
236 | })
237 | t.equal(str, '--> localhost/test')
238 | })
239 |
--------------------------------------------------------------------------------
/lib/utils/prettify-metadata.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = prettifyMetadata
4 |
5 | /**
6 | * @typedef {object} PrettifyMetadataParams
7 | * @property {object} log The log that may or may not contain metadata to
8 | * be prettified.
9 | * @property {PrettyContext} context The context object built from parsing
10 | * the options.
11 | */
12 |
13 | /**
14 | * Prettifies metadata that is usually present in a Pino log line. It looks for
15 | * fields `name`, `pid`, `hostname`, and `caller` and returns a formatted string using
16 | * the fields it finds.
17 | *
18 | * @param {PrettifyMetadataParams} input
19 | *
20 | * @returns {undefined|string} If no metadata is found then `undefined` is
21 | * returned. Otherwise, a string of prettified metadata is returned.
22 | */
23 | function prettifyMetadata ({ log, context }) {
24 | const { customPrettifiers: prettifiers, colorizer } = context
25 | let line = ''
26 |
27 | if (log.name || log.pid || log.hostname) {
28 | line += '('
29 |
30 | if (log.name) {
31 | line += prettifiers.name
32 | ? prettifiers.name(log.name, 'name', log, { colors: colorizer.colors })
33 | : log.name
34 | }
35 |
36 | if (log.pid) {
37 | const prettyPid = prettifiers.pid
38 | ? prettifiers.pid(log.pid, 'pid', log, { colors: colorizer.colors })
39 | : log.pid
40 | if (log.name && log.pid) {
41 | line += '/' + prettyPid
42 | } else {
43 | line += prettyPid
44 | }
45 | }
46 |
47 | if (log.hostname) {
48 | // If `pid` and `name` were in the ignore keys list then we don't need
49 | // the leading space.
50 | const prettyHostname = prettifiers.hostname
51 | ? prettifiers.hostname(log.hostname, 'hostname', log, { colors: colorizer.colors })
52 | : log.hostname
53 |
54 | line += `${line === '(' ? 'on' : ' on'} ${prettyHostname}`
55 | }
56 |
57 | line += ')'
58 | }
59 |
60 | if (log.caller) {
61 | const prettyCaller = prettifiers.caller
62 | ? prettifiers.caller(log.caller, 'caller', log, { colors: colorizer.colors })
63 | : log.caller
64 |
65 | line += `${line === '' ? '' : ' '}<${prettyCaller}>`
66 | }
67 |
68 | if (line === '') {
69 | return undefined
70 | } else {
71 | return line
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/lib/utils/prettify-metadata.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const tap = require('tap')
4 | const prettifyMetadata = require('./prettify-metadata')
5 | const getColorizer = require('../colors')
6 | const context = {
7 | customPrettifiers: {},
8 | colorizer: {
9 | colors: {}
10 | }
11 | }
12 |
13 | tap.test('returns `undefined` if no metadata present', async t => {
14 | const str = prettifyMetadata({ log: {}, context })
15 | t.equal(str, undefined)
16 | })
17 |
18 | tap.test('works with only `name` present', async t => {
19 | const str = prettifyMetadata({ log: { name: 'foo' }, context })
20 | t.equal(str, '(foo)')
21 | })
22 |
23 | tap.test('works with only `pid` present', async t => {
24 | const str = prettifyMetadata({ log: { pid: '1234' }, context })
25 | t.equal(str, '(1234)')
26 | })
27 |
28 | tap.test('works with only `hostname` present', async t => {
29 | const str = prettifyMetadata({ log: { hostname: 'bar' }, context })
30 | t.equal(str, '(on bar)')
31 | })
32 |
33 | tap.test('works with only `name` & `pid` present', async t => {
34 | const str = prettifyMetadata({ log: { name: 'foo', pid: '1234' }, context })
35 | t.equal(str, '(foo/1234)')
36 | })
37 |
38 | tap.test('works with only `name` & `hostname` present', async t => {
39 | const str = prettifyMetadata({ log: { name: 'foo', hostname: 'bar' }, context })
40 | t.equal(str, '(foo on bar)')
41 | })
42 |
43 | tap.test('works with only `pid` & `hostname` present', async t => {
44 | const str = prettifyMetadata({ log: { pid: '1234', hostname: 'bar' }, context })
45 | t.equal(str, '(1234 on bar)')
46 | })
47 |
48 | tap.test('works with only `name`, `pid`, & `hostname` present', async t => {
49 | const str = prettifyMetadata({ log: { name: 'foo', pid: '1234', hostname: 'bar' }, context })
50 | t.equal(str, '(foo/1234 on bar)')
51 | })
52 |
53 | tap.test('works with only `name` & `caller` present', async t => {
54 | const str = prettifyMetadata({ log: { name: 'foo', caller: 'baz' }, context })
55 | t.equal(str, '(foo) ')
56 | })
57 |
58 | tap.test('works with only `pid` & `caller` present', async t => {
59 | const str = prettifyMetadata({ log: { pid: '1234', caller: 'baz' }, context })
60 | t.equal(str, '(1234) ')
61 | })
62 |
63 | tap.test('works with only `hostname` & `caller` present', async t => {
64 | const str = prettifyMetadata({ log: { hostname: 'bar', caller: 'baz' }, context })
65 | t.equal(str, '(on bar) ')
66 | })
67 |
68 | tap.test('works with only `name`, `pid`, & `caller` present', async t => {
69 | const str = prettifyMetadata({ log: { name: 'foo', pid: '1234', caller: 'baz' }, context })
70 | t.equal(str, '(foo/1234) ')
71 | })
72 |
73 | tap.test('works with only `name`, `hostname`, & `caller` present', async t => {
74 | const str = prettifyMetadata({ log: { name: 'foo', hostname: 'bar', caller: 'baz' }, context })
75 | t.equal(str, '(foo on bar) ')
76 | })
77 |
78 | tap.test('works with only `caller` present', async t => {
79 | const str = prettifyMetadata({ log: { caller: 'baz' }, context })
80 | t.equal(str, '')
81 | })
82 |
83 | tap.test('works with only `pid`, `hostname`, & `caller` present', async t => {
84 | const str = prettifyMetadata({ log: { pid: '1234', hostname: 'bar', caller: 'baz' }, context })
85 | t.equal(str, '(1234 on bar) ')
86 | })
87 |
88 | tap.test('works with all four present', async t => {
89 | const str = prettifyMetadata({ log: { name: 'foo', pid: '1234', hostname: 'bar', caller: 'baz' }, context })
90 | t.equal(str, '(foo/1234 on bar) ')
91 | })
92 |
93 | tap.test('uses prettifiers from passed prettifiers object', async t => {
94 | const prettifiers = {
95 | name (input) {
96 | return input.toUpperCase()
97 | },
98 | pid (input) {
99 | return input + '__'
100 | },
101 | hostname (input) {
102 | return input.toUpperCase()
103 | },
104 | caller (input) {
105 | return input.toUpperCase()
106 | }
107 | }
108 | const str = prettifyMetadata({
109 | log: { pid: '1234', hostname: 'bar', caller: 'baz', name: 'joe' },
110 | context: {
111 | customPrettifiers: prettifiers,
112 | colorizer: { colors: {} }
113 | }
114 | })
115 | t.equal(str, '(JOE/1234__ on BAR) ')
116 | })
117 |
118 | tap.test('uses colorizer from passed context to colorize metadata', async t => {
119 | const prettifiers = {
120 | name (input, _key, _log, { colors }) {
121 | return colors.blue(input)
122 | },
123 | pid (input, _key, _log, { colors }) {
124 | return colors.red(input)
125 | },
126 | hostname (input, _key, _log, { colors }) {
127 | return colors.green(input)
128 | },
129 | caller (input, _key, _log, { colors }) {
130 | return colors.cyan(input)
131 | }
132 | }
133 | const log = { name: 'foo', pid: '1234', hostname: 'bar', caller: 'baz' }
134 | const colorizer = getColorizer(true)
135 | const context = {
136 | customPrettifiers: prettifiers,
137 | colorizer
138 | }
139 |
140 | const result = prettifyMetadata({ log, context })
141 |
142 | const colorizedName = colorizer.colors.blue(log.name)
143 | const colorizedPid = colorizer.colors.red(log.pid)
144 | const colorizedHostname = colorizer.colors.green(log.hostname)
145 | const colorizedCaller = colorizer.colors.cyan(log.caller)
146 | const expected = `(${colorizedName}/${colorizedPid} on ${colorizedHostname}) <${colorizedCaller}>`
147 |
148 | t.equal(result, expected)
149 | })
150 |
--------------------------------------------------------------------------------
/lib/utils/prettify-object.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = prettifyObject
4 |
5 | const {
6 | LOGGER_KEYS
7 | } = require('../constants')
8 |
9 | const stringifySafe = require('fast-safe-stringify')
10 | const joinLinesWithIndentation = require('./join-lines-with-indentation')
11 | const prettifyError = require('./prettify-error')
12 |
13 | /**
14 | * @typedef {object} PrettifyObjectParams
15 | * @property {object} log The object to prettify.
16 | * @property {boolean} [excludeLoggerKeys] Indicates if known logger specific
17 | * keys should be excluded from prettification. Default: `true`.
18 | * @property {string[]} [skipKeys] A set of object keys to exclude from the
19 | * * prettified result. Default: `[]`.
20 | * @property {PrettyContext} context The context object built from parsing
21 | * the options.
22 | */
23 |
24 | /**
25 | * Prettifies a standard object. Special care is taken when processing the object
26 | * to handle child objects that are attached to keys known to contain error
27 | * objects.
28 | *
29 | * @param {PrettifyObjectParams} input
30 | *
31 | * @returns {string} The prettified string. This can be as little as `''` if
32 | * there was nothing to prettify.
33 | */
34 | function prettifyObject ({
35 | log,
36 | excludeLoggerKeys = true,
37 | skipKeys = [],
38 | context
39 | }) {
40 | const {
41 | EOL: eol,
42 | IDENT: ident,
43 | customPrettifiers,
44 | errorLikeObjectKeys: errorLikeKeys,
45 | objectColorizer,
46 | singleLine,
47 | colorizer
48 | } = context
49 | const keysToIgnore = [].concat(skipKeys)
50 |
51 | /* istanbul ignore else */
52 | if (excludeLoggerKeys === true) Array.prototype.push.apply(keysToIgnore, LOGGER_KEYS)
53 |
54 | let result = ''
55 |
56 | // Split object keys into two categories: error and non-error
57 | const { plain, errors } = Object.entries(log).reduce(({ plain, errors }, [k, v]) => {
58 | if (keysToIgnore.includes(k) === false) {
59 | // Pre-apply custom prettifiers, because all 3 cases below will need this
60 | const pretty = typeof customPrettifiers[k] === 'function'
61 | ? customPrettifiers[k](v, k, log, { colors: colorizer.colors })
62 | : v
63 | if (errorLikeKeys.includes(k)) {
64 | errors[k] = pretty
65 | } else {
66 | plain[k] = pretty
67 | }
68 | }
69 | return { plain, errors }
70 | }, { plain: {}, errors: {} })
71 |
72 | if (singleLine) {
73 | // Stringify the entire object as a single JSON line
74 | /* istanbul ignore else */
75 | if (Object.keys(plain).length > 0) {
76 | result += objectColorizer.greyMessage(stringifySafe(plain))
77 | }
78 | result += eol
79 | // Avoid printing the escape character on escaped backslashes.
80 | result = result.replace(/\\\\/gi, '\\')
81 | } else {
82 | // Put each object entry on its own line
83 | Object.entries(plain).forEach(([keyName, keyValue]) => {
84 | // custom prettifiers are already applied above, so we can skip it now
85 | let lines = typeof customPrettifiers[keyName] === 'function'
86 | ? keyValue
87 | : stringifySafe(keyValue, null, 2)
88 |
89 | if (lines === undefined) return
90 |
91 | // Avoid printing the escape character on escaped backslashes.
92 | lines = lines.replace(/\\\\/gi, '\\')
93 |
94 | const joinedLines = joinLinesWithIndentation({ input: lines, ident, eol })
95 | result += `${ident}${objectColorizer.property(keyName)}:${joinedLines.startsWith(eol) ? '' : ' '}${joinedLines}${eol}`
96 | })
97 | }
98 |
99 | // Errors
100 | Object.entries(errors).forEach(([keyName, keyValue]) => {
101 | // custom prettifiers are already applied above, so we can skip it now
102 | const lines = typeof customPrettifiers[keyName] === 'function'
103 | ? keyValue
104 | : stringifySafe(keyValue, null, 2)
105 |
106 | if (lines === undefined) return
107 |
108 | result += prettifyError({ keyName, lines, eol, ident })
109 | })
110 |
111 | return result
112 | }
113 |
--------------------------------------------------------------------------------
/lib/utils/prettify-object.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const tap = require('tap')
4 | const colors = require('../colors')
5 | const prettifyObject = require('./prettify-object')
6 | const {
7 | ERROR_LIKE_KEYS
8 | } = require('../constants')
9 |
10 | const context = {
11 | EOL: '\n',
12 | IDENT: ' ',
13 | customPrettifiers: {},
14 | errorLikeObjectKeys: ERROR_LIKE_KEYS,
15 | objectColorizer: colors(),
16 | singleLine: false,
17 | colorizer: colors()
18 | }
19 |
20 | tap.test('returns empty string if no properties present', async t => {
21 | const str = prettifyObject({ log: {}, context })
22 | t.equal(str, '')
23 | })
24 |
25 | tap.test('works with single level properties', async t => {
26 | const str = prettifyObject({ log: { foo: 'bar' }, context })
27 | t.equal(str, ' foo: "bar"\n')
28 | })
29 |
30 | tap.test('works with multiple level properties', async t => {
31 | const str = prettifyObject({ log: { foo: { bar: 'baz' } }, context })
32 | t.equal(str, ' foo: {\n "bar": "baz"\n }\n')
33 | })
34 |
35 | tap.test('skips specified keys', async t => {
36 | const str = prettifyObject({
37 | log: { foo: 'bar', hello: 'world' },
38 | skipKeys: ['foo'],
39 | context
40 | })
41 | t.equal(str, ' hello: "world"\n')
42 | })
43 |
44 | tap.test('ignores predefined keys', async t => {
45 | const str = prettifyObject({ log: { foo: 'bar', pid: 12345 }, context })
46 | t.equal(str, ' foo: "bar"\n')
47 | })
48 |
49 | tap.test('ignores escaped backslashes in string values', async t => {
50 | const str = prettifyObject({ log: { foo_regexp: '\\[^\\w\\s]\\' }, context })
51 | t.equal(str, ' foo_regexp: "\\[^\\w\\s]\\"\n')
52 | })
53 |
54 | tap.test('ignores escaped backslashes in string values (singleLine option)', async t => {
55 | const str = prettifyObject({
56 | log: { foo_regexp: '\\[^\\w\\s]\\' },
57 | context: {
58 | ...context,
59 | singleLine: true
60 | }
61 | })
62 | t.equal(str, '{"foo_regexp":"\\[^\\w\\s]\\"}\n')
63 | })
64 |
65 | tap.test('works with error props', async t => {
66 | const err = Error('Something went wrong')
67 | const serializedError = {
68 | message: err.message,
69 | stack: err.stack
70 | }
71 | const str = prettifyObject({ log: { error: serializedError }, context })
72 | t.ok(str.startsWith(' error:'))
73 | t.ok(str.includes(' "message": "Something went wrong",'))
74 | t.ok(str.includes(' Error: Something went wrong'))
75 | })
76 |
77 | tap.test('customPrettifiers gets applied', async t => {
78 | const customPrettifiers = {
79 | foo: v => v.toUpperCase()
80 | }
81 | const str = prettifyObject({
82 | log: { foo: 'foo' },
83 | context: {
84 | ...context,
85 | customPrettifiers
86 | }
87 | })
88 | t.equal(str.startsWith(' foo: FOO'), true)
89 | })
90 |
91 | tap.test('skips lines omitted by customPrettifiers', async t => {
92 | const customPrettifiers = {
93 | foo: () => { return undefined }
94 | }
95 | const str = prettifyObject({
96 | log: { foo: 'foo', bar: 'bar' },
97 | context: {
98 | ...context,
99 | customPrettifiers
100 | }
101 | })
102 | t.equal(str.includes('bar: "bar"'), true)
103 | t.equal(str.includes('foo: "foo"'), false)
104 | })
105 |
106 | tap.test('joined lines omits starting eol', async t => {
107 | const str = prettifyObject({
108 | log: { msg: 'doing work', calls: ['step 1', 'step 2', 'step 3'], level: 30 },
109 | context: {
110 | ...context,
111 | IDENT: '',
112 | customPrettifiers: {
113 | calls: val => '\n' + val.map(it => ' ' + it).join('\n')
114 | }
115 | }
116 | })
117 | t.equal(str, [
118 | 'msg: "doing work"',
119 | 'calls:',
120 | ' step 1',
121 | ' step 2',
122 | ' step 3',
123 | ''
124 | ].join('\n'))
125 | })
126 |
127 | tap.test('errors skips prettifiers', async t => {
128 | const customPrettifiers = {
129 | err: () => { return 'is_err' }
130 | }
131 | const str = prettifyObject({
132 | log: { err: Error('boom') },
133 | context: {
134 | ...context,
135 | customPrettifiers
136 | }
137 | })
138 | t.equal(str.includes('err: is_err'), true)
139 | })
140 |
141 | tap.test('errors skips prettifying if no lines are present', async t => {
142 | const customPrettifiers = {
143 | err: () => { return undefined }
144 | }
145 | const str = prettifyObject({
146 | log: { err: Error('boom') },
147 | context: {
148 | ...context,
149 | customPrettifiers
150 | }
151 | })
152 | t.equal(str, '')
153 | })
154 |
155 | tap.test('works with single level properties', async t => {
156 | const colorizer = colors(true)
157 | const str = prettifyObject({
158 | log: { foo: 'bar' },
159 | context: {
160 | ...context,
161 | objectColorizer: colorizer,
162 | colorizer
163 | }
164 | })
165 | t.equal(str, ` ${colorizer.colors.magenta('foo')}: "bar"\n`)
166 | })
167 |
--------------------------------------------------------------------------------
/lib/utils/prettify-time.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = prettifyTime
4 |
5 | const formatTime = require('./format-time')
6 |
7 | /**
8 | * @typedef {object} PrettifyTimeParams
9 | * @property {object} log The log object with the timestamp to be prettified.
10 | * @property {PrettyContext} context The context object built from parsing
11 | * the options.
12 | */
13 |
14 | /**
15 | * Prettifies a timestamp if the given `log` has either `time`, `timestamp` or custom specified timestamp
16 | * property.
17 | *
18 | * @param {PrettifyTimeParams} input
19 | *
20 | * @returns {undefined|string} If a timestamp property cannot be found then
21 | * `undefined` is returned. Otherwise, the prettified time is returned as a
22 | * string.
23 | */
24 | function prettifyTime ({ log, context }) {
25 | const {
26 | timestampKey,
27 | translateTime: translateFormat
28 | } = context
29 | const prettifier = context.customPrettifiers?.time
30 | let time = null
31 |
32 | if (timestampKey in log) {
33 | time = log[timestampKey]
34 | } else if ('timestamp' in log) {
35 | time = log.timestamp
36 | }
37 |
38 | if (time === null) return undefined
39 | const output = translateFormat ? formatTime(time, translateFormat) : time
40 |
41 | return prettifier ? prettifier(output) : `[${output}]`
42 | }
43 |
--------------------------------------------------------------------------------
/lib/utils/prettify-time.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.TZ = 'UTC'
4 |
5 | const { test } = require('node:test')
6 | const prettifyTime = require('./prettify-time')
7 | const {
8 | TIMESTAMP_KEY
9 | } = require('../constants')
10 | const context = {
11 | timestampKey: TIMESTAMP_KEY,
12 | translateTime: true,
13 | customPrettifiers: {}
14 | }
15 |
16 | test('returns `undefined` if `time` or `timestamp` not in log', t => {
17 | const str = prettifyTime({ log: {}, context })
18 | t.assert.strictEqual(str, undefined)
19 | })
20 |
21 | test('returns prettified formatted time from custom field', t => {
22 | const log = { customtime: 1554642900000 }
23 | let str = prettifyTime({
24 | log,
25 | context: {
26 | ...context,
27 | timestampKey: 'customtime'
28 | }
29 | })
30 | t.assert.strictEqual(str, '[13:15:00.000]')
31 |
32 | str = prettifyTime({
33 | log,
34 | context: {
35 | ...context,
36 | translateTime: false,
37 | timestampKey: 'customtime'
38 | }
39 | })
40 | t.assert.strictEqual(str, '[1554642900000]')
41 | })
42 |
43 | test('returns prettified formatted time', t => {
44 | let log = { time: 1554642900000 }
45 | let str = prettifyTime({
46 | log,
47 | context: {
48 | ...context
49 | }
50 | })
51 | t.assert.strictEqual(str, '[13:15:00.000]')
52 |
53 | log = { timestamp: 1554642900000 }
54 | str = prettifyTime({
55 | log,
56 | context: {
57 | ...context
58 | }
59 | })
60 | t.assert.strictEqual(str, '[13:15:00.000]')
61 |
62 | log = { time: '2019-04-07T09:15:00.000-04:00' }
63 | str = prettifyTime({
64 | log,
65 | context: {
66 | ...context
67 | }
68 | })
69 | t.assert.strictEqual(str, '[13:15:00.000]')
70 |
71 | log = { timestamp: '2019-04-07T09:15:00.000-04:00' }
72 | str = prettifyTime({
73 | log,
74 | context: {
75 | ...context
76 | }
77 | })
78 | t.assert.strictEqual(str, '[13:15:00.000]')
79 |
80 | log = { time: 1554642900000 }
81 | str = prettifyTime({
82 | log,
83 | context: {
84 | ...context,
85 | translateTime: 'd mmm yyyy H:MM'
86 | }
87 | })
88 | t.assert.strictEqual(str, '[7 Apr 2019 13:15]')
89 |
90 | log = { timestamp: 1554642900000 }
91 | str = prettifyTime({
92 | log,
93 | context: {
94 | ...context,
95 | translateTime: 'd mmm yyyy H:MM'
96 | }
97 | })
98 | t.assert.strictEqual(str, '[7 Apr 2019 13:15]')
99 |
100 | log = { time: '2019-04-07T09:15:00.000-04:00' }
101 | str = prettifyTime({
102 | log,
103 | context: {
104 | ...context,
105 | translateTime: 'd mmm yyyy H:MM'
106 | }
107 | })
108 | t.assert.strictEqual(str, '[7 Apr 2019 13:15]')
109 |
110 | log = { timestamp: '2019-04-07T09:15:00.000-04:00' }
111 | str = prettifyTime({
112 | log,
113 | context: {
114 | ...context,
115 | translateTime: 'd mmm yyyy H:MM'
116 | }
117 | })
118 | t.assert.strictEqual(str, '[7 Apr 2019 13:15]')
119 | })
120 |
121 | test('passes through value', t => {
122 | let log = { time: 1554642900000 }
123 | let str = prettifyTime({
124 | log,
125 | context: {
126 | ...context,
127 | translateTime: undefined
128 | }
129 | })
130 | t.assert.strictEqual(str, '[1554642900000]')
131 |
132 | log = { timestamp: 1554642900000 }
133 | str = prettifyTime({
134 | log,
135 | context: {
136 | ...context,
137 | translateTime: undefined
138 | }
139 | })
140 | t.assert.strictEqual(str, '[1554642900000]')
141 |
142 | log = { time: '2019-04-07T09:15:00.000-04:00' }
143 | str = prettifyTime({
144 | log,
145 | context: {
146 | ...context,
147 | translateTime: undefined
148 | }
149 | })
150 | t.assert.strictEqual(str, '[2019-04-07T09:15:00.000-04:00]')
151 |
152 | log = { timestamp: '2019-04-07T09:15:00.000-04:00' }
153 | str = prettifyTime({
154 | log,
155 | context: {
156 | ...context,
157 | translateTime: undefined
158 | }
159 | })
160 | t.assert.strictEqual(str, '[2019-04-07T09:15:00.000-04:00]')
161 | })
162 |
163 | test('handles the 0 timestamp', t => {
164 | let log = { time: 0 }
165 | let str = prettifyTime({
166 | log,
167 | context: {
168 | ...context,
169 | translateTime: undefined
170 | }
171 | })
172 | t.assert.strictEqual(str, '[0]')
173 |
174 | log = { timestamp: 0 }
175 | str = prettifyTime({
176 | log,
177 | context: {
178 | ...context,
179 | translateTime: undefined
180 | }
181 | })
182 | t.assert.strictEqual(str, '[0]')
183 | })
184 |
185 | test('works with epoch as a number or string', (t) => {
186 | t.plan(3)
187 | const epoch = 1522431328992
188 | const asNumber = prettifyTime({
189 | log: { time: epoch, msg: 'foo' },
190 | context: {
191 | ...context,
192 | translateTime: true
193 | }
194 | })
195 | const asString = prettifyTime({
196 | log: { time: `${epoch}`, msg: 'foo' },
197 | context: {
198 | ...context,
199 | translateTime: true
200 | }
201 | })
202 | const invalid = prettifyTime({
203 | log: { time: '2 days ago', msg: 'foo' },
204 | context: {
205 | ...context,
206 | translateTime: true
207 | }
208 | })
209 | t.assert.strictEqual(asString, '[17:35:28.992]')
210 | t.assert.strictEqual(asNumber, '[17:35:28.992]')
211 | t.assert.strictEqual(invalid, '[2 days ago]')
212 | })
213 |
214 | test('uses custom prettifier', t => {
215 | const str = prettifyTime({
216 | log: { time: 0 },
217 | context: {
218 | ...context,
219 | customPrettifiers: {
220 | time () {
221 | return 'done'
222 | }
223 | }
224 | }
225 | })
226 | t.assert.strictEqual(str, 'done')
227 | })
228 |
--------------------------------------------------------------------------------
/lib/utils/split-property-key.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = splitPropertyKey
4 |
5 | /**
6 | * Splits the property key delimited by a dot character but not when it is preceded
7 | * by a backslash.
8 | *
9 | * @param {string} key A string identifying the property.
10 | *
11 | * @returns {string[]} Returns a list of string containing each delimited property.
12 | * e.g. `'prop2\.domain\.corp.prop2'` should return [ 'prop2.domain.com', 'prop2' ]
13 | */
14 | function splitPropertyKey (key) {
15 | const result = []
16 | let backslash = false
17 | let segment = ''
18 |
19 | for (let i = 0; i < key.length; i++) {
20 | const c = key.charAt(i)
21 |
22 | if (c === '\\') {
23 | backslash = true
24 | continue
25 | }
26 |
27 | if (backslash) {
28 | backslash = false
29 | segment += c
30 | continue
31 | }
32 |
33 | /* Non-escaped dot, push to result */
34 | if (c === '.') {
35 | result.push(segment)
36 | segment = ''
37 | continue
38 | }
39 |
40 | segment += c
41 | }
42 |
43 | /* Push last entry to result */
44 | if (segment.length) {
45 | result.push(segment)
46 | }
47 |
48 | return result
49 | }
50 |
--------------------------------------------------------------------------------
/lib/utils/split-property-key.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const tap = require('tap')
4 | const splitPropertyKey = require('./split-property-key')
5 |
6 | tap.test('splitPropertyKey does not change key', async t => {
7 | const result = splitPropertyKey('data1')
8 | t.same(result, ['data1'])
9 | })
10 |
11 | tap.test('splitPropertyKey splits nested key', async t => {
12 | const result = splitPropertyKey('data1.data2.data-3')
13 | t.same(result, ['data1', 'data2', 'data-3'])
14 | })
15 |
16 | tap.test('splitPropertyKey splits nested keys ending with a dot', async t => {
17 | const result = splitPropertyKey('data1.data2.data-3.')
18 | t.same(result, ['data1', 'data2', 'data-3'])
19 | })
20 |
21 | tap.test('splitPropertyKey splits nested escaped key', async t => {
22 | const result = splitPropertyKey('logging\\.domain\\.corp/operation.foo.bar-2')
23 | t.same(result, ['logging.domain.corp/operation', 'foo', 'bar-2'])
24 | })
25 |
26 | tap.test('splitPropertyKey splits nested escaped key with special characters', async t => {
27 | const result = splitPropertyKey('logging\\.domain\\.corp/operation.!\t@#$%^&*()_+=-<>.bar\\.2')
28 | t.same(result, ['logging.domain.corp/operation', '!\t@#$%^&*()_+=-<>', 'bar.2'])
29 | })
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pino-pretty",
3 | "version": "13.0.0",
4 | "description": "Prettifier for Pino log lines",
5 | "type": "commonjs",
6 | "main": "index.js",
7 | "types": "index.d.ts",
8 | "bin": {
9 | "pino-pretty": "./bin.js"
10 | },
11 | "scripts": {
12 | "ci": "standard && borp --check-coverage && npm run test-types",
13 | "lint": "standard | snazzy",
14 | "test": "borp",
15 | "test-types": "tsc && tsd && attw --pack .",
16 | "test:watch": "borp -w --reporter gh",
17 | "test:report": "c8 --reporter html borp"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "git+ssh://git@github.com/pinojs/pino-pretty.git"
22 | },
23 | "keywords": [
24 | "pino"
25 | ],
26 | "author": "James Sumners ",
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/pinojs/pino-pretty/issues"
30 | },
31 | "homepage": "https://github.com/pinojs/pino-pretty#readme",
32 | "precommit": [
33 | "lint",
34 | "test"
35 | ],
36 | "dependencies": {
37 | "colorette": "^2.0.7",
38 | "dateformat": "^4.6.3",
39 | "fast-copy": "^3.0.2",
40 | "fast-safe-stringify": "^2.1.1",
41 | "help-me": "^5.0.0",
42 | "joycon": "^3.1.1",
43 | "minimist": "^1.2.6",
44 | "on-exit-leak-free": "^2.1.0",
45 | "pino-abstract-transport": "^2.0.0",
46 | "pump": "^3.0.0",
47 | "secure-json-parse": "^4.0.0",
48 | "sonic-boom": "^4.0.1",
49 | "strip-json-comments": "^3.1.1"
50 | },
51 | "devDependencies": {
52 | "@arethetypeswrong/cli": "^0.18.1",
53 | "@jsumners/line-reporter": "^1.0.1",
54 | "@types/node": "^22.0.0",
55 | "borp": "^0.20.0",
56 | "fastbench": "^1.0.1",
57 | "pino": "^9.0.0",
58 | "pre-commit": "^1.2.2",
59 | "rimraf": "^6.0.1",
60 | "semver": "^7.6.0",
61 | "snazzy": "^9.0.0",
62 | "standard": "^17.0.0",
63 | "tap": "^16.0.0",
64 | "tsd": "^0.32.0",
65 | "typescript": "~5.8.2"
66 | },
67 | "tsd": {
68 | "directory": "./test/types"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/test/cli-rc.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.TZ = 'UTC'
4 |
5 | const path = require('node:path')
6 | const { spawn } = require('node:child_process')
7 | const { test } = require('tap')
8 | const fs = require('node:fs')
9 | const { rimraf } = require('rimraf')
10 |
11 | const bin = require.resolve('../bin')
12 | const logLine = '{"level":30,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n'
13 |
14 | test('cli', (t) => {
15 | const tmpDir = path.join(__dirname, '.tmp_' + Date.now())
16 | fs.mkdirSync(tmpDir)
17 |
18 | t.teardown(() => rimraf(tmpDir))
19 |
20 | t.test('loads and applies default config file: pino-pretty.config.js', (t) => {
21 | t.plan(1)
22 | // Set translateTime: true on run configuration
23 | const configFile = path.join(tmpDir, 'pino-pretty.config.js')
24 | fs.writeFileSync(configFile, 'module.exports = { translateTime: true }')
25 | const env = { TERM: 'dumb', TZ: 'UTC' }
26 | const child = spawn(process.argv[0], [bin], { env, cwd: tmpDir })
27 | // Validate that the time has been translated
28 | child.on('error', t.threw)
29 | child.stdout.on('data', (data) => {
30 | t.equal(data.toString(), '[17:35:28.992] INFO (42): hello world\n')
31 | })
32 | child.stdin.write(logLine)
33 | t.teardown(() => {
34 | fs.unlinkSync(configFile)
35 | child.kill()
36 | })
37 | })
38 |
39 | t.test('loads and applies default config file: pino-pretty.config.cjs', (t) => {
40 | t.plan(1)
41 | // Set translateTime: true on run configuration
42 | const configFile = path.join(tmpDir, 'pino-pretty.config.cjs')
43 | fs.writeFileSync(configFile, 'module.exports = { translateTime: true }')
44 | // Tell the loader to expect ESM modules
45 | const packageJsonFile = path.join(tmpDir, 'package.json')
46 | fs.writeFileSync(packageJsonFile, JSON.stringify({ type: 'module' }, null, 4))
47 | const env = { TERM: ' dumb', TZ: 'UTC' }
48 | const child = spawn(process.argv[0], [bin], { env, cwd: tmpDir })
49 | // Validate that the time has been translated
50 | child.on('error', t.threw)
51 | child.stdout.on('data', (data) => {
52 | t.equal(data.toString(), '[17:35:28.992] INFO (42): hello world\n')
53 | })
54 | child.stdin.write(logLine)
55 | t.teardown(() => {
56 | fs.unlinkSync(configFile)
57 | fs.unlinkSync(packageJsonFile)
58 | child.kill()
59 | })
60 | })
61 |
62 | t.test('loads and applies default config file: .pino-prettyrc', (t) => {
63 | t.plan(1)
64 | // Set translateTime: true on run configuration
65 | const configFile = path.join(tmpDir, '.pino-prettyrc')
66 | fs.writeFileSync(configFile, JSON.stringify({ translateTime: true }, null, 4))
67 | const env = { TERM: ' dumb', TZ: 'UTC' }
68 | const child = spawn(process.argv[0], [bin], { env, cwd: tmpDir })
69 | // Validate that the time has been translated
70 | child.on('error', t.threw)
71 | child.stdout.on('data', (data) => {
72 | t.equal(data.toString(), '[17:35:28.992] INFO (42): hello world\n')
73 | })
74 | child.stdin.write(logLine)
75 | t.teardown(() => {
76 | fs.unlinkSync(configFile)
77 | child.kill()
78 | })
79 | })
80 |
81 | t.test('loads and applies default config file: .pino-prettyrc.json', (t) => {
82 | t.plan(1)
83 | // Set translateTime: true on run configuration
84 | const configFile = path.join(tmpDir, '.pino-prettyrc.json')
85 | fs.writeFileSync(configFile, JSON.stringify({ translateTime: true }, null, 4))
86 | const env = { TERM: ' dumb', TZ: 'UTC' }
87 | const child = spawn(process.argv[0], [bin], { env, cwd: tmpDir })
88 | // Validate that the time has been translated
89 | child.on('error', t.threw)
90 | child.stdout.on('data', (data) => {
91 | t.equal(data.toString(), '[17:35:28.992] INFO (42): hello world\n')
92 | })
93 | child.stdin.write(logLine)
94 | t.teardown(() => {
95 | fs.unlinkSync(configFile)
96 | child.kill()
97 | })
98 | })
99 |
100 | t.test('loads and applies custom config file: pino-pretty.config.test.json', (t) => {
101 | t.plan(1)
102 | // Set translateTime: true on run configuration
103 | const configFile = path.join(tmpDir, 'pino-pretty.config.test.json')
104 | fs.writeFileSync(configFile, JSON.stringify({ translateTime: true }, null, 4))
105 | const env = { TERM: ' dumb', TZ: 'UTC' }
106 | const child = spawn(process.argv[0], [bin, '--config', configFile], { env, cwd: tmpDir })
107 | // Validate that the time has been translated
108 | child.on('error', t.threw)
109 | child.stdout.on('data', (data) => {
110 | t.equal(data.toString(), '[17:35:28.992] INFO (42): hello world\n')
111 | })
112 | child.stdin.write(logLine)
113 | t.teardown(() => child.kill())
114 | })
115 |
116 | t.test('loads and applies custom config file: pino-pretty.config.test.js', (t) => {
117 | t.plan(1)
118 | // Set translateTime: true on run configuration
119 | const configFile = path.join(tmpDir, 'pino-pretty.config.test.js')
120 | fs.writeFileSync(configFile, 'module.exports = { translateTime: true }')
121 | const env = { TERM: ' dumb', TZ: 'UTC' }
122 | const child = spawn(process.argv[0], [bin, '--config', configFile], { env, cwd: tmpDir })
123 | // Validate that the time has been translated
124 | child.on('error', t.threw)
125 | child.stdout.on('data', (data) => {
126 | t.equal(data.toString(), '[17:35:28.992] INFO (42): hello world\n')
127 | })
128 | child.stdin.write(logLine)
129 | t.teardown(() => child.kill())
130 | })
131 |
132 | ;['--messageKey', '-m'].forEach((optionName) => {
133 | t.test(`cli options override config options via ${optionName}`, (t) => {
134 | t.plan(1)
135 | // Set translateTime: true on run configuration
136 | const configFile = path.join(tmpDir, 'pino-pretty.config.js')
137 | fs.writeFileSync(configFile, `
138 | module.exports = {
139 | translateTime: true,
140 | messageKey: 'custom_msg'
141 | }
142 | `.trim())
143 | // Set messageKey: 'new_msg' using command line option
144 | const env = { TERM: ' dumb', TZ: 'UTC' }
145 | const child = spawn(process.argv[0], [bin, optionName, 'new_msg'], { env, cwd: tmpDir })
146 | // Validate that the time has been translated and correct message key has been used
147 | child.on('error', t.threw)
148 | child.stdout.on('data', (data) => {
149 | t.equal(data.toString(), '[17:35:28.992] INFO (42): hello world\n')
150 | })
151 | child.stdin.write(logLine.replace(/"msg"/, '"new_msg"'))
152 | t.teardown(() => {
153 | fs.unlinkSync(configFile)
154 | child.kill()
155 | })
156 | })
157 | })
158 |
159 | t.test('cli options with defaults can be overridden by config', (t) => {
160 | t.plan(1)
161 | // Set errorProps: '*' on run configuration
162 | const configFile = path.join(tmpDir, 'pino-pretty.config.js')
163 | fs.writeFileSync(configFile, `
164 | module.exports = {
165 | errorProps: '*'
166 | }
167 | `.trim())
168 | // Set messageKey: 'new_msg' using command line option
169 | const env = { TERM: ' dumb', TZ: 'UTC' }
170 | const child = spawn(process.argv[0], [bin], { env, cwd: tmpDir })
171 | // Validate that the time has been translated and correct message key has been used
172 | child.on('error', t.threw)
173 | child.stdout.on('data', (data) => {
174 | t.equal(data.toString(), '[21:31:36.006] FATAL: There was an error starting the process.\n QueryError: Error during sql query: syntax error at or near SELECTT\n at /home/me/projects/example/sql.js\n at /home/me/projects/example/index.js\n querySql: SELECTT * FROM "test" WHERE id = $1;\n queryArgs: 12\n')
175 | })
176 | child.stdin.write('{"level":60,"time":1594416696006,"msg":"There was an error starting the process.","type":"Error","stack":"QueryError: Error during sql query: syntax error at or near SELECTT\\n at /home/me/projects/example/sql.js\\n at /home/me/projects/example/index.js","querySql":"SELECTT * FROM \\"test\\" WHERE id = $1;","queryArgs":[12]}\n')
177 | t.teardown(() => {
178 | fs.unlinkSync(configFile)
179 | child.kill()
180 | })
181 | })
182 |
183 | t.test('throws on missing config file', (t) => {
184 | t.plan(2)
185 | const args = [bin, '--config', 'pino-pretty.config.missing.json']
186 | const env = { TERM: ' dumb', TZ: 'UTC' }
187 | const child = spawn(process.argv[0], args, { env, cwd: tmpDir })
188 | child.on('close', (code) => t.equal(code, 1))
189 | child.stdout.pipe(process.stdout)
190 | child.stderr.setEncoding('utf8')
191 | let data = ''
192 | child.stderr.on('data', (chunk) => {
193 | data += chunk
194 | })
195 | child.on('close', function () {
196 | t.match(
197 | data.toString(), 'Error: Failed to load runtime configuration file: pino-pretty.config.missing.json')
198 | })
199 | t.teardown(() => child.kill())
200 | })
201 |
202 | t.test('throws on invalid default config file', (t) => {
203 | t.plan(2)
204 | const configFile = path.join(tmpDir, 'pino-pretty.config.js')
205 | fs.writeFileSync(configFile, 'module.exports = () => {}')
206 | const env = { TERM: ' dumb', TZ: 'UTC' }
207 | const child = spawn(process.argv[0], [bin], { env, cwd: tmpDir })
208 | child.on('close', (code) => t.equal(code, 1))
209 | child.stdout.pipe(process.stdout)
210 | child.stderr.setEncoding('utf8')
211 | let data = ''
212 | child.stderr.on('data', (chunk) => {
213 | data += chunk
214 | })
215 | child.on('close', function () {
216 | t.match(data, 'Error: Invalid runtime configuration file: pino-pretty.config.js')
217 | })
218 | t.teardown(() => child.kill())
219 | })
220 |
221 | t.test('throws on invalid custom config file', (t) => {
222 | t.plan(2)
223 | const configFile = path.join(tmpDir, 'pino-pretty.config.invalid.js')
224 | fs.writeFileSync(configFile, 'module.exports = () => {}')
225 | const args = [bin, '--config', path.relative(tmpDir, configFile)]
226 | const env = { TERM: ' dumb', TZ: 'UTC' }
227 | const child = spawn(process.argv[0], args, { env, cwd: tmpDir })
228 | child.on('close', (code) => t.equal(code, 1))
229 | child.stdout.pipe(process.stdout)
230 | child.stderr.setEncoding('utf8')
231 | let data = ''
232 | child.stderr.on('data', (chunk) => {
233 | data += chunk
234 | })
235 | child.on('close', function () {
236 | t.match(data, 'Error: Invalid runtime configuration file: pino-pretty.config.invalid.js')
237 | })
238 | t.teardown(() => child.kill())
239 | })
240 |
241 | t.test('test help', (t) => {
242 | t.plan(1)
243 | const env = { TERM: ' dumb', TZ: 'UTC' }
244 | const child = spawn(process.argv[0], [bin, '--help'], { env })
245 | const file = fs.readFileSync('help/help.txt').toString()
246 | child.on('error', t.threw)
247 | child.stdout.on('data', (data) => {
248 | t.equal(data.toString(), file)
249 | })
250 | t.teardown(() => child.kill())
251 | })
252 |
253 | t.end()
254 | })
255 |
--------------------------------------------------------------------------------
/test/cli.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.TZ = 'UTC'
4 |
5 | const path = require('node:path')
6 | const { spawn } = require('node:child_process')
7 | const { test } = require('tap')
8 |
9 | const bin = require.resolve(path.join(__dirname, '..', 'bin.js'))
10 | const epoch = 1522431328992
11 | const logLine = '{"level":30,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n'
12 | const env = { TERM: 'dumb', TZ: 'UTC' }
13 | const formattedEpoch = '17:35:28.992'
14 |
15 | test('cli', (t) => {
16 | t.test('does basic reformatting', (t) => {
17 | t.plan(1)
18 | const child = spawn(process.argv[0], [bin], { env })
19 | child.on('error', t.threw)
20 | child.stdout.on('data', (data) => {
21 | t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
22 | })
23 | child.stdin.write(logLine)
24 | t.teardown(() => child.kill())
25 | })
26 |
27 | ;['--levelFirst', '-l'].forEach((optionName) => {
28 | t.test(`flips epoch and level via ${optionName}`, (t) => {
29 | t.plan(1)
30 | const child = spawn(process.argv[0], [bin, optionName], { env })
31 | child.on('error', t.threw)
32 | child.stdout.on('data', (data) => {
33 | t.equal(data.toString(), `INFO [${formattedEpoch}] (42): hello world\n`)
34 | })
35 | child.stdin.write(logLine)
36 | t.teardown(() => child.kill())
37 | })
38 | })
39 |
40 | ;['--translateTime', '-t'].forEach((optionName) => {
41 | t.test(`translates time to default format via ${optionName}`, (t) => {
42 | t.plan(1)
43 | const child = spawn(process.argv[0], [bin, optionName], { env })
44 | child.on('error', t.threw)
45 | child.stdout.on('data', (data) => {
46 | t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
47 | })
48 | child.stdin.write(logLine)
49 | t.teardown(() => child.kill())
50 | })
51 | })
52 |
53 | ;['--ignore', '-i'].forEach((optionName) => {
54 | t.test('does ignore multiple keys', (t) => {
55 | t.plan(1)
56 | const child = spawn(process.argv[0], [bin, optionName, 'pid,hostname'], { env })
57 | child.on('error', t.threw)
58 | child.stdout.on('data', (data) => {
59 | t.equal(data.toString(), `[${formattedEpoch}] INFO: hello world\n`)
60 | })
61 | child.stdin.write(logLine)
62 | t.teardown(() => child.kill())
63 | })
64 | })
65 |
66 | ;['--customLevels', '-x'].forEach((optionName) => {
67 | t.test(`customize levels via ${optionName}`, (t) => {
68 | t.plan(1)
69 | const logLine = '{"level":1,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n'
70 | const child = spawn(process.argv[0], [bin, optionName, 'err:99,info:1'], { env })
71 | child.on('error', t.threw)
72 | child.stdout.on('data', (data) => {
73 | t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
74 | })
75 | child.stdin.write(logLine)
76 | t.teardown(() => child.kill())
77 | })
78 |
79 | t.test(`customize levels via ${optionName} without index`, (t) => {
80 | t.plan(1)
81 | const logLine = '{"level":1,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n'
82 | const child = spawn(process.argv[0], [bin, optionName, 'err:99,info'], { env })
83 | child.on('error', t.threw)
84 | child.stdout.on('data', (data) => {
85 | t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
86 | })
87 | child.stdin.write(logLine)
88 | t.teardown(() => child.kill())
89 | })
90 |
91 | t.test(`customize levels via ${optionName} with minimumLevel`, (t) => {
92 | t.plan(1)
93 | const child = spawn(process.argv[0], [bin, '--minimumLevel', 'err', optionName, 'err:99,info:1'], { env })
94 | child.on('error', t.threw)
95 | child.stdout.on('data', (data) => {
96 | t.equal(data.toString(), `[${formattedEpoch}] ERR (42): hello world\n`)
97 | })
98 | child.stdin.write('{"level":1,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n')
99 | child.stdin.write('{"level":99,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n')
100 | t.teardown(() => child.kill())
101 | })
102 |
103 | t.test(`customize levels via ${optionName} with minimumLevel, customLevels and useOnlyCustomProps false`, (t) => {
104 | t.plan(1)
105 | const child = spawn(process.argv[0], [bin, '--minimumLevel', 'custom', '--useOnlyCustomProps', 'false', optionName, 'custom:99,info:1'], { env })
106 | child.on('error', t.threw)
107 | child.stdout.on('data', (data) => {
108 | t.equal(data.toString(), `[${formattedEpoch}] CUSTOM (42): hello world\n`)
109 | })
110 | child.stdin.write('{"level":1,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n')
111 | child.stdin.write('{"level":99,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n')
112 | t.teardown(() => child.kill())
113 | })
114 |
115 | t.test(`customize levels via ${optionName} with minimumLevel, customLevels and useOnlyCustomProps true`, (t) => {
116 | t.plan(1)
117 | const child = spawn(process.argv[0], [bin, '--minimumLevel', 'custom', '--useOnlyCustomProps', 'true', optionName, 'custom:99,info:1'], { env })
118 | child.on('error', t.threw)
119 | child.stdout.on('data', (data) => {
120 | t.equal(data.toString(), `[${formattedEpoch}] CUSTOM (42): hello world\n`)
121 | })
122 | child.stdin.write('{"level":1,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n')
123 | child.stdin.write('{"level":99,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n')
124 | t.teardown(() => child.kill())
125 | })
126 | })
127 |
128 | ;['--customColors', '-X'].forEach((optionName) => {
129 | t.test(`customize levels via ${optionName}`, (t) => {
130 | t.plan(1)
131 | const child = spawn(process.argv[0], [bin, optionName, 'info:blue,message:red'], { env })
132 | child.on('error', t.threw)
133 | child.stdout.on('data', (data) => {
134 | t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
135 | })
136 | child.stdin.write(logLine)
137 | t.teardown(() => child.kill())
138 | })
139 |
140 | t.test(`customize levels via ${optionName} with customLevels`, (t) => {
141 | t.plan(1)
142 | const logLine = '{"level":1,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n'
143 | const child = spawn(process.argv[0], [bin, '--customLevels', 'err:99,info', optionName, 'info:blue,message:red'], { env })
144 | child.on('error', t.threw)
145 | child.stdout.on('data', (data) => {
146 | t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
147 | })
148 | child.stdin.write(logLine)
149 | t.teardown(() => child.kill())
150 | })
151 | })
152 |
153 | ;['--useOnlyCustomProps', '-U'].forEach((optionName) => {
154 | t.test(`customize levels via ${optionName} false and customColors`, (t) => {
155 | t.plan(1)
156 | const child = spawn(process.argv[0], [bin, '--customColors', 'err:blue,info:red', optionName, 'false'], { env })
157 | child.on('error', t.threw)
158 | child.stdout.on('data', (data) => {
159 | t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
160 | })
161 | child.stdin.write(logLine)
162 | t.teardown(() => child.kill())
163 | })
164 |
165 | t.test(`customize levels via ${optionName} true and customColors`, (t) => {
166 | t.plan(1)
167 | const child = spawn(process.argv[0], [bin, '--customColors', 'err:blue,info:red', optionName, 'true'], { env })
168 | child.on('error', t.threw)
169 | child.stdout.on('data', (data) => {
170 | t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
171 | })
172 | child.stdin.write(logLine)
173 | t.teardown(() => child.kill())
174 | })
175 |
176 | t.test(`customize levels via ${optionName} true and customLevels`, (t) => {
177 | t.plan(1)
178 | const child = spawn(process.argv[0], [bin, '--customLevels', 'err:99,custom:30', optionName, 'true'], { env })
179 | child.on('error', t.threw)
180 | child.stdout.on('data', (data) => {
181 | t.equal(data.toString(), `[${formattedEpoch}] CUSTOM (42): hello world\n`)
182 | })
183 | child.stdin.write(logLine)
184 | t.teardown(() => child.kill())
185 | })
186 |
187 | t.test(`customize levels via ${optionName} true and no customLevels`, (t) => {
188 | t.plan(1)
189 | const child = spawn(process.argv[0], [bin, optionName, 'true'], { env })
190 | child.on('error', t.threw)
191 | child.stdout.on('data', (data) => {
192 | t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
193 | })
194 | child.stdin.write(logLine)
195 | t.teardown(() => child.kill())
196 | })
197 |
198 | t.test(`customize levels via ${optionName} false and customLevels`, (t) => {
199 | t.plan(1)
200 | const child = spawn(process.argv[0], [bin, '--customLevels', 'err:99,custom:25', optionName, 'false'], { env })
201 | child.on('error', t.threw)
202 | child.stdout.on('data', (data) => {
203 | t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
204 | })
205 | child.stdin.write(logLine)
206 | t.teardown(() => child.kill())
207 | })
208 |
209 | t.test(`customize levels via ${optionName} false and no customLevels`, (t) => {
210 | t.plan(1)
211 | const child = spawn(process.argv[0], [bin, optionName, 'false'], { env })
212 | child.on('error', t.threw)
213 | child.stdout.on('data', (data) => {
214 | t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
215 | })
216 | child.stdin.write(logLine)
217 | t.teardown(() => child.kill())
218 | })
219 | })
220 |
221 | t.test('does ignore escaped keys', (t) => {
222 | t.plan(1)
223 | const child = spawn(process.argv[0], [bin, '-i', 'log\\.domain\\.corp/foo'], { env })
224 | child.on('error', t.threw)
225 | child.stdout.on('data', (data) => {
226 | t.equal(data.toString(), `[${formattedEpoch}] INFO: hello world\n`)
227 | })
228 | const logLine = '{"level":30,"time":1522431328992,"msg":"hello world","log.domain.corp/foo":"bar"}\n'
229 | child.stdin.write(logLine)
230 | t.teardown(() => child.kill())
231 | })
232 |
233 | t.test('passes through stringified date as string', (t) => {
234 | t.plan(1)
235 | const child = spawn(process.argv[0], [bin], { env })
236 | child.on('error', t.threw)
237 |
238 | const date = JSON.stringify(new Date(epoch))
239 |
240 | child.stdout.on('data', (data) => {
241 | t.equal(data.toString(), date + '\n')
242 | })
243 |
244 | child.stdin.write(date)
245 | child.stdin.write('\n')
246 |
247 | t.teardown(() => child.kill())
248 | })
249 |
250 | t.test('end stdin does not end the destination', (t) => {
251 | t.plan(2)
252 | const child = spawn(process.argv[0], [bin], { env })
253 | child.on('error', t.threw)
254 |
255 | child.stdout.on('data', (data) => {
256 | t.equal(data.toString(), 'aaa\n')
257 | })
258 |
259 | child.stdin.end('aaa\n')
260 | child.on('exit', function (code) {
261 | t.equal(code, 0)
262 | })
263 |
264 | t.teardown(() => child.kill())
265 | })
266 |
267 | ;['--timestampKey', '-a'].forEach((optionName) => {
268 | t.test(`uses specified timestamp key via ${optionName}`, (t) => {
269 | t.plan(1)
270 | const child = spawn(process.argv[0], [bin, optionName, '@timestamp'], { env })
271 | child.on('error', t.threw)
272 | child.stdout.on('data', (data) => {
273 | t.equal(data.toString(), `[${formattedEpoch}] INFO: hello world\n`)
274 | })
275 | const logLine = '{"level":30,"@timestamp":1522431328992,"msg":"hello world"}\n'
276 | child.stdin.write(logLine)
277 | t.teardown(() => child.kill())
278 | })
279 | })
280 |
281 | ;['--singleLine', '-S'].forEach((optionName) => {
282 | t.test(`singleLine=true via ${optionName}`, (t) => {
283 | t.plan(1)
284 | const logLineWithExtra = JSON.stringify(Object.assign(JSON.parse(logLine), {
285 | extra: {
286 | foo: 'bar',
287 | number: 42
288 | }
289 | })) + '\n'
290 |
291 | const child = spawn(process.argv[0], [bin, optionName], { env })
292 | child.on('error', t.threw)
293 | child.stdout.on('data', (data) => {
294 | t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world {"extra":{"foo":"bar","number":42}}\n`)
295 | })
296 | child.stdin.write(logLineWithExtra)
297 | t.teardown(() => child.kill())
298 | })
299 | })
300 |
301 | t.test('does ignore nested keys', (t) => {
302 | t.plan(1)
303 |
304 | const logLineNested = JSON.stringify(Object.assign(JSON.parse(logLine), {
305 | extra: {
306 | foo: 'bar',
307 | number: 42,
308 | nested: {
309 | foo2: 'bar2'
310 | }
311 | }
312 | })) + '\n'
313 |
314 | const child = spawn(process.argv[0], [bin, '-S', '-i', 'extra.foo,extra.nested,extra.nested.miss'], { env })
315 | child.on('error', t.threw)
316 | child.stdout.on('data', (data) => {
317 | t.equal(data.toString(), `[${formattedEpoch}] INFO (42 on foo): hello world {"extra":{"number":42}}\n`)
318 | })
319 | child.stdin.write(logLineNested)
320 | t.teardown(() => child.kill())
321 | })
322 |
323 | t.test('change TZ', (t) => {
324 | t.plan(1)
325 | const child = spawn(process.argv[0], [bin], { env: { ...env, TZ: 'Europe/Amsterdam' } })
326 | child.on('error', t.threw)
327 | child.stdout.on('data', (data) => {
328 | t.equal(data.toString(), '[19:35:28.992] INFO (42): hello world\n')
329 | })
330 | child.stdin.write(logLine)
331 | t.teardown(() => child.kill())
332 | })
333 |
334 | t.end()
335 | })
336 |
--------------------------------------------------------------------------------
/test/crlf.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.TZ = 'UTC'
4 |
5 | const { test } = require('tap')
6 | const _prettyFactory = require('../').prettyFactory
7 |
8 | function prettyFactory (opts) {
9 | if (!opts) {
10 | opts = { colorize: false }
11 | } else if (!Object.prototype.hasOwnProperty.call(opts, 'colorize')) {
12 | opts.colorize = false
13 | }
14 | return _prettyFactory(opts)
15 | }
16 |
17 | const logLine = '{"level":30,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n'
18 |
19 | test('crlf', (t) => {
20 | t.test('uses LF by default', (t) => {
21 | t.plan(1)
22 | const pretty = prettyFactory()
23 | const formatted = pretty(logLine)
24 | t.equal(formatted.substr(-2), 'd\n')
25 | })
26 |
27 | t.test('can use CRLF', (t) => {
28 | t.plan(1)
29 | const pretty = prettyFactory({ crlf: true })
30 | const formatted = pretty(logLine)
31 | t.equal(formatted.substr(-3), 'd\r\n')
32 | })
33 |
34 | t.end()
35 | })
36 |
--------------------------------------------------------------------------------
/test/error-objects.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.TZ = 'UTC'
4 |
5 | const { Writable } = require('node:stream')
6 | const { test } = require('tap')
7 | const pino = require('pino')
8 | const semver = require('semver')
9 | const serializers = pino.stdSerializers
10 | const pinoPretty = require('../')
11 | const _prettyFactory = pinoPretty.prettyFactory
12 |
13 | function prettyFactory (opts) {
14 | if (!opts) {
15 | opts = { colorize: false }
16 | } else if (!Object.prototype.hasOwnProperty.call(opts, 'colorize')) {
17 | opts.colorize = false
18 | }
19 | return _prettyFactory(opts)
20 | }
21 |
22 | // All dates are computed from 'Fri, 30 Mar 2018 17:35:28 GMT'
23 | const epoch = 1522431328992
24 | const formattedEpoch = '17:35:28.992'
25 | const pid = process.pid
26 |
27 | test('error like objects tests', (t) => {
28 | t.beforeEach(() => {
29 | Date.originalNow = Date.now
30 | Date.now = () => epoch
31 | })
32 | t.afterEach(() => {
33 | Date.now = Date.originalNow
34 | delete Date.originalNow
35 | })
36 |
37 | t.test('pino transform prettifies Error', (t) => {
38 | t.plan(2)
39 | const pretty = prettyFactory()
40 | const err = Error('hello world')
41 | const expected = err.stack.split('\n')
42 | expected.unshift(err.message)
43 |
44 | const log = pino({}, new Writable({
45 | write (chunk, enc, cb) {
46 | const formatted = pretty(chunk.toString())
47 | const lines = formatted.split('\n')
48 | t.equal(lines.length, expected.length + 6)
49 | t.equal(lines[0], `[${formattedEpoch}] INFO (${pid}): hello world`)
50 | cb()
51 | }
52 | }))
53 |
54 | log.info(err)
55 | })
56 |
57 | t.test('errorProps recognizes user specified properties', (t) => {
58 | t.plan(3)
59 | const pretty = prettyFactory({ errorProps: 'statusCode,originalStack' })
60 | const log = pino({}, new Writable({
61 | write (chunk, enc, cb) {
62 | const formatted = pretty(chunk.toString())
63 | t.match(formatted, /\s{4}error stack/)
64 | t.match(formatted, /"statusCode": 500/)
65 | t.match(formatted, /"originalStack": "original stack"/)
66 | cb()
67 | }
68 | }))
69 |
70 | const error = Error('error message')
71 | error.stack = 'error stack'
72 | error.statusCode = 500
73 | error.originalStack = 'original stack'
74 |
75 | log.error(error)
76 | })
77 |
78 | t.test('prettifies ignores undefined errorLikeObject', (t) => {
79 | const pretty = prettyFactory()
80 | pretty({ err: undefined })
81 | pretty({ error: undefined })
82 | t.end()
83 | })
84 |
85 | t.test('prettifies Error in property within errorLikeObjectKeys', (t) => {
86 | t.plan(8)
87 | const pretty = prettyFactory({
88 | errorLikeObjectKeys: ['err']
89 | })
90 |
91 | const err = Error('hello world')
92 | const expected = err.stack.split('\n')
93 | expected.unshift(err.message)
94 |
95 | const log = pino({ serializers: { err: serializers.err } }, new Writable({
96 | write (chunk, enc, cb) {
97 | const formatted = pretty(chunk.toString())
98 | const lines = formatted.split('\n')
99 | t.equal(lines.length, expected.length + 6)
100 | t.equal(lines[0], `[${formattedEpoch}] INFO (${pid}): hello world`)
101 | t.match(lines[1], /\s{4}err: {/)
102 | t.match(lines[2], /\s{6}"type": "Error",/)
103 | t.match(lines[3], /\s{6}"message": "hello world",/)
104 | t.match(lines[4], /\s{6}"stack":/)
105 | t.match(lines[5], /\s{6}Error: hello world/)
106 | // Node 12 labels the test ``
107 | t.match(lines[6], /\s{10}(at Test.t.test|at Test.)/)
108 | cb()
109 | }
110 | }))
111 |
112 | log.info({ err })
113 | })
114 |
115 | t.test('prettifies Error in property with singleLine=true', (t) => {
116 | // singleLine=true doesn't apply to errors
117 | t.plan(8)
118 | const pretty = prettyFactory({
119 | singleLine: true,
120 | errorLikeObjectKeys: ['err']
121 | })
122 |
123 | const err = Error('hello world')
124 | const expected = [
125 | '{"extra":{"a":1,"b":2}}',
126 | err.message,
127 | ...err.stack.split('\n')
128 | ]
129 |
130 | const log = pino({ serializers: { err: serializers.err } }, new Writable({
131 | write (chunk, enc, cb) {
132 | const formatted = pretty(chunk.toString())
133 | const lines = formatted.split('\n')
134 | t.equal(lines.length, expected.length + 5)
135 | t.equal(lines[0], `[${formattedEpoch}] INFO (${pid}): hello world {"extra":{"a":1,"b":2}}`)
136 | t.match(lines[1], /\s{4}err: {/)
137 | t.match(lines[2], /\s{6}"type": "Error",/)
138 | t.match(lines[3], /\s{6}"message": "hello world",/)
139 | t.match(lines[4], /\s{6}"stack":/)
140 | t.match(lines[5], /\s{6}Error: hello world/)
141 | // Node 12 labels the test ``
142 | t.match(lines[6], /\s{10}(at Test.t.test|at Test.)/)
143 | cb()
144 | }
145 | }))
146 |
147 | log.info({ err, extra: { a: 1, b: 2 } })
148 | })
149 |
150 | t.test('prettifies Error in property within errorLikeObjectKeys with custom function', (t) => {
151 | t.plan(4)
152 | const pretty = prettyFactory({
153 | errorLikeObjectKeys: ['err'],
154 | customPrettifiers: {
155 | err: val => `error is ${val.message}`
156 | }
157 | })
158 |
159 | const err = Error('hello world')
160 | err.stack = 'Error: hello world\n at anonymous (C:\\project\\node_modules\\example\\index.js)'
161 | const expected = err.stack.split('\n')
162 | expected.unshift(err.message)
163 |
164 | const log = pino({ serializers: { err: serializers.err } }, new Writable({
165 | write (chunk, enc, cb) {
166 | const formatted = pretty(chunk.toString())
167 | const lines = formatted.split('\n')
168 | t.equal(lines.length, 3)
169 | t.equal(lines[0], `[${formattedEpoch}] INFO (${pid}): hello world`)
170 | t.equal(lines[1], ' err: error is hello world')
171 | t.equal(lines[2], '')
172 |
173 | cb()
174 | }
175 | }))
176 |
177 | log.info({ err })
178 | })
179 |
180 | t.test('prettifies Error in property within errorLikeObjectKeys when stack has escaped characters', (t) => {
181 | t.plan(8)
182 | const pretty = prettyFactory({
183 | errorLikeObjectKeys: ['err']
184 | })
185 |
186 | const err = Error('hello world')
187 | err.stack = 'Error: hello world\n at anonymous (C:\\project\\node_modules\\example\\index.js)'
188 | const expected = err.stack.split('\n')
189 | expected.unshift(err.message)
190 |
191 | const log = pino({ serializers: { err: serializers.err } }, new Writable({
192 | write (chunk, enc, cb) {
193 | const formatted = pretty(chunk.toString())
194 | const lines = formatted.split('\n')
195 | t.equal(lines.length, expected.length + 6)
196 | t.equal(lines[0], `[${formattedEpoch}] INFO (${pid}): hello world`)
197 | t.match(lines[1], /\s{4}err: {$/)
198 | t.match(lines[2], /\s{6}"type": "Error",$/)
199 | t.match(lines[3], /\s{6}"message": "hello world",$/)
200 | t.match(lines[4], /\s{6}"stack":$/)
201 | t.match(lines[5], /\s{10}Error: hello world$/)
202 | t.match(lines[6], /\s{10}at anonymous \(C:\\project\\node_modules\\example\\index.js\)$/)
203 | cb()
204 | }
205 | }))
206 |
207 | log.info({ err })
208 | })
209 |
210 | t.test('prettifies Error in property within errorLikeObjectKeys when stack is not the last property', (t) => {
211 | t.plan(9)
212 | const pretty = prettyFactory({
213 | errorLikeObjectKeys: ['err']
214 | })
215 |
216 | const err = Error('hello world')
217 | err.anotherField = 'dummy value'
218 | const expected = err.stack.split('\n')
219 | expected.unshift(err.message)
220 |
221 | const log = pino({ serializers: { err: serializers.err } }, new Writable({
222 | write (chunk, enc, cb) {
223 | const formatted = pretty(chunk.toString())
224 | const lines = formatted.split('\n')
225 | t.equal(lines.length, expected.length + 7)
226 | t.equal(lines[0], `[${formattedEpoch}] INFO (${pid}): hello world`)
227 | t.match(lines[1], /\s{4}err: {/)
228 | t.match(lines[2], /\s{6}"type": "Error",/)
229 | t.match(lines[3], /\s{6}"message": "hello world",/)
230 | t.match(lines[4], /\s{6}"stack":/)
231 | t.match(lines[5], /\s{6}Error: hello world/)
232 | // Node 12 labels the test ``
233 | t.match(lines[6], /\s{10}(at Test.t.test|at Test.)/)
234 | t.match(lines[lines.length - 3], /\s{6}"anotherField": "dummy value"/)
235 | cb()
236 | }
237 | }))
238 |
239 | log.info({ err })
240 | })
241 |
242 | t.test('errorProps flag with "*" (print all nested props)', function (t) {
243 | const pretty = prettyFactory({ errorProps: '*' })
244 | const expectedLines = [
245 | ' err: {',
246 | ' "type": "Error",',
247 | ' "message": "error message",',
248 | ' "stack":',
249 | ' error stack',
250 | ' "statusCode": 500,',
251 | ' "originalStack": "original stack",',
252 | ' "dataBaseSpecificError": {',
253 | ' "erroMessage": "some database error message",',
254 | ' "evenMoreSpecificStuff": {',
255 | ' "someErrorRelatedObject": "error"',
256 | ' }',
257 | ' }',
258 | ' }'
259 | ]
260 | t.plan(expectedLines.length)
261 | const log = pino({}, new Writable({
262 | write (chunk, enc, cb) {
263 | const formatted = pretty(chunk.toString())
264 | const lines = formatted.split('\n')
265 | lines.shift(); lines.pop()
266 | for (let i = 0; i < lines.length; i += 1) {
267 | t.equal(lines[i], expectedLines[i])
268 | }
269 | cb()
270 | }
271 | }))
272 |
273 | const error = Error('error message')
274 | error.stack = 'error stack'
275 | error.statusCode = 500
276 | error.originalStack = 'original stack'
277 | error.dataBaseSpecificError = {
278 | erroMessage: 'some database error message',
279 | evenMoreSpecificStuff: {
280 | someErrorRelatedObject: 'error'
281 | }
282 | }
283 |
284 | log.error(error)
285 | })
286 |
287 | t.test('prettifies legacy error object at top level when singleLine=true', function (t) {
288 | t.plan(4)
289 | const pretty = prettyFactory({ singleLine: true })
290 | const err = Error('hello world')
291 | const expected = err.stack.split('\n')
292 | expected.unshift(err.message)
293 |
294 | const log = pino({}, new Writable({
295 | write (chunk, enc, cb) {
296 | const formatted = pretty(chunk.toString())
297 | const lines = formatted.split('\n')
298 | t.equal(lines.length, expected.length + 1)
299 | t.equal(lines[0], `[${formattedEpoch}] INFO (${pid}): ${expected[0]}`)
300 | t.equal(lines[1], ` ${expected[1]}`)
301 | t.equal(lines[2], ` ${expected[2]}`)
302 | cb()
303 | }
304 | }))
305 |
306 | log.info({ type: 'Error', stack: err.stack, msg: err.message })
307 | })
308 |
309 | t.test('errorProps: legacy error object at top level', function (t) {
310 | const pretty = prettyFactory({ errorProps: '*' })
311 | const expectedLines = [
312 | 'INFO:',
313 | ' error stack',
314 | ' message: hello message',
315 | ' statusCode: 500',
316 | ' originalStack: original stack',
317 | ' dataBaseSpecificError: {',
318 | ' errorMessage: "some database error message"',
319 | ' evenMoreSpecificStuff: {',
320 | ' "someErrorRelatedObject": "error"',
321 | ' }',
322 | ' }',
323 | ''
324 | ]
325 |
326 | t.plan(expectedLines.length)
327 |
328 | const error = {}
329 | error.level = 30
330 | error.message = 'hello message'
331 | error.type = 'Error'
332 | error.stack = 'error stack'
333 | error.statusCode = 500
334 | error.originalStack = 'original stack'
335 | error.dataBaseSpecificError = {
336 | errorMessage: 'some database error message',
337 | evenMoreSpecificStuff: {
338 | someErrorRelatedObject: 'error'
339 | }
340 | }
341 |
342 | const formatted = pretty(JSON.stringify(error))
343 | const lines = formatted.split('\n')
344 | for (let i = 0; i < lines.length; i += 1) {
345 | t.equal(lines[i], expectedLines[i])
346 | }
347 | })
348 |
349 | t.test('errorProps flag with a single property', function (t) {
350 | const pretty = prettyFactory({ errorProps: 'originalStack' })
351 | const expectedLines = [
352 | 'INFO:',
353 | ' error stack',
354 | ' originalStack: original stack',
355 | ''
356 | ]
357 | t.plan(expectedLines.length)
358 |
359 | const error = {}
360 | error.level = 30
361 | error.message = 'hello message'
362 | error.type = 'Error'
363 | error.stack = 'error stack'
364 | error.statusCode = 500
365 | error.originalStack = 'original stack'
366 | error.dataBaseSpecificError = {
367 | erroMessage: 'some database error message',
368 | evenMoreSpecificStuff: {
369 | someErrorRelatedObject: 'error'
370 | }
371 | }
372 |
373 | const formatted = pretty(JSON.stringify(error))
374 | const lines = formatted.split('\n')
375 | for (let i = 0; i < lines.length; i += 1) {
376 | t.equal(lines[i], expectedLines[i])
377 | }
378 | })
379 |
380 | t.test('errorProps flag with a single property non existent', function (t) {
381 | const pretty = prettyFactory({ errorProps: 'originalStackABC' })
382 | const expectedLines = [
383 | 'INFO:',
384 | ' error stack',
385 | ''
386 | ]
387 | t.plan(expectedLines.length)
388 |
389 | const error = {}
390 | error.level = 30
391 | error.message = 'hello message'
392 | error.type = 'Error'
393 | error.stack = 'error stack'
394 | error.statusCode = 500
395 | error.originalStack = 'original stack'
396 | error.dataBaseSpecificError = {
397 | erroMessage: 'some database error message',
398 | evenMoreSpecificStuff: {
399 | someErrorRelatedObject: 'error'
400 | }
401 | }
402 |
403 | const formatted = pretty(JSON.stringify(error))
404 | const lines = formatted.split('\n')
405 | for (let i = 0; i < lines.length; i += 1) {
406 | t.equal(lines[i], expectedLines[i])
407 | }
408 | })
409 |
410 | t.test('handles errors with a null stack', (t) => {
411 | t.plan(2)
412 | const pretty = prettyFactory()
413 | const log = pino({}, new Writable({
414 | write (chunk, enc, cb) {
415 | const formatted = pretty(chunk.toString())
416 | t.match(formatted, /\s{4}message: "foo"/)
417 | t.match(formatted, /\s{4}stack: null/)
418 | cb()
419 | }
420 | }))
421 |
422 | const error = { message: 'foo', stack: null }
423 | log.error(error)
424 | })
425 |
426 | t.test('handles errors with a null stack for Error object', (t) => {
427 | const pretty = prettyFactory()
428 | const expectedLines = [
429 | ' "type": "Error",',
430 | ' "message": "error message",',
431 | ' "stack":',
432 | ' ',
433 | ' "some": "property"'
434 | ]
435 | t.plan(expectedLines.length)
436 | const log = pino({}, new Writable({
437 | write (chunk, enc, cb) {
438 | const formatted = pretty(chunk.toString())
439 | const lines = formatted.split('\n')
440 | lines.shift(); lines.shift(); lines.pop(); lines.pop()
441 | for (let i = 0; i < lines.length; i += 1) {
442 | t.ok(lines[i].includes(expectedLines[i]))
443 | }
444 | cb()
445 | }
446 | }))
447 |
448 | const error = Error('error message')
449 | error.stack = null
450 | error.some = 'property'
451 |
452 | log.error(error)
453 | })
454 |
455 | t.end()
456 | })
457 |
458 | if (semver.gte(pino.version, '8.21.0')) {
459 | test('using pino config', (t) => {
460 | t.beforeEach(() => {
461 | Date.originalNow = Date.now
462 | Date.now = () => epoch
463 | })
464 | t.afterEach(() => {
465 | Date.now = Date.originalNow
466 | delete Date.originalNow
467 | })
468 |
469 | t.test('prettifies Error in custom errorKey', (t) => {
470 | t.plan(8)
471 | const destination = new Writable({
472 | write (chunk, enc, cb) {
473 | const formatted = chunk.toString()
474 | const lines = formatted.split('\n')
475 | t.equal(lines.length, expected.length + 7)
476 | t.equal(lines[0], `[${formattedEpoch}] INFO (${pid}): hello world`)
477 | t.match(lines[1], /\s{4}customErrorKey: {/)
478 | t.match(lines[2], /\s{6}"type": "Error",/)
479 | t.match(lines[3], /\s{6}"message": "hello world",/)
480 | t.match(lines[4], /\s{6}"stack":/)
481 | t.match(lines[5], /\s{6}Error: hello world/)
482 | // Node 12 labels the test ``
483 | t.match(lines[6], /\s{10}(at Test.t.test|at Test.)/)
484 | cb()
485 | }
486 | })
487 | const pretty = pinoPretty({
488 | destination,
489 | colorize: false
490 | })
491 | const log = pino({ errorKey: 'customErrorKey' }, pretty)
492 | const err = Error('hello world')
493 | const expected = err.stack.split('\n')
494 | log.info({ customErrorKey: err })
495 | })
496 |
497 | t.end()
498 | })
499 | }
500 |
--------------------------------------------------------------------------------
/test/example/example.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | // Run this to see how colouring works
4 |
5 | const _prettyFactory = require('../../')
6 | const pino = require('pino')
7 | const { Writable } = require('node:stream')
8 |
9 | function prettyFactory () {
10 | return _prettyFactory({
11 | colorize: true
12 | })
13 | }
14 |
15 | const pretty = prettyFactory()
16 | const formatted = pretty('this is not json\nit\'s just regular output\n')
17 | console.log(formatted)
18 |
19 | const opts = {
20 | base: {
21 | hostname: 'localhost',
22 | pid: process.pid
23 | }
24 | }
25 | const log = pino(opts, new Writable({
26 | write (chunk, enc, cb) {
27 | const formatted = pretty(chunk.toString())
28 | console.log(formatted)
29 | cb()
30 | }
31 | }))
32 |
33 | log.info('foobar')
34 |
--------------------------------------------------------------------------------
/test/types/pino-pretty.test-d.ts:
--------------------------------------------------------------------------------
1 | import { expectType } from "tsd";
2 |
3 | import pretty from "../../";
4 | import PinoPretty, {
5 | PinoPretty as PinoPrettyNamed,
6 | PrettyOptions,
7 | colorizerFactory,
8 | prettyFactory
9 | } from "../../";
10 | import PinoPrettyDefault from "../../";
11 | import * as PinoPrettyStar from "../../";
12 | import PinoPrettyCjsImport = require("../../");
13 | import PrettyStream = PinoPretty.PrettyStream;
14 | const PinoPrettyCjs = require("../../");
15 |
16 | const options: PinoPretty.PrettyOptions = {
17 | colorize: true,
18 | crlf: false,
19 | errorLikeObjectKeys: ["err", "error"],
20 | errorProps: "",
21 | hideObject: true,
22 | levelKey: "level",
23 | levelLabel: "foo",
24 | messageFormat: false,
25 | ignore: "",
26 | levelFirst: false,
27 | messageKey: "msg",
28 | timestampKey: "timestamp",
29 | minimumLevel: "trace",
30 | translateTime: "UTC:h:MM:ss TT Z",
31 | singleLine: false,
32 | customPrettifiers: {
33 | key: (value) => {
34 | return value.toString().toUpperCase();
35 | },
36 | level: (level, label, colorized) => {
37 | return level.toString();
38 | }
39 | },
40 | customLevels: 'verbose:5',
41 | customColors: 'default:white,verbose:gray',
42 | sync: false,
43 | destination: 2,
44 | append: true,
45 | mkdir: true,
46 | useOnlyCustomProps: false,
47 | };
48 |
49 | expectType(pretty()); // #326
50 | expectType(pretty(options));
51 | expectType(PinoPrettyNamed(options));
52 | expectType(PinoPrettyDefault(options));
53 | expectType(PinoPrettyStar.PinoPretty(options));
54 | expectType(PinoPrettyCjsImport.PinoPretty(options));
55 | expectType(PinoPrettyCjs(options));
56 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "lib": [ "es2015" ],
5 | "module": "commonjs",
6 | "noEmit": true,
7 | "strict": true
8 | },
9 | "include": [
10 | "./test/types/pino-pretty.test.d.ts",
11 | "./index.d.ts"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------