├── .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 | [![NPM Package Version](https://img.shields.io/npm/v/pino-pretty)](https://www.npmjs.com/package/pino-pretty) 5 | [![Build Status](https://img.shields.io/github/actions/workflow/status/pinojs/pino-pretty/ci.yml?branch=master)](https://github.com/pinojs/pino-pretty/actions?query=workflow%3ACI) 6 | [![Coverage Status](https://img.shields.io/coveralls/github/pinojs/pino-pretty)](https://coveralls.io/github/pinojs/pino-pretty?branch=master) 7 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](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 | ![demo](demo.png) 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 | --------------------------------------------------------------------------------