dev-api: add developer portal, OpenAPI spec, and Flatenbadet case study

- New /developers/ page with API docs, SDKs, pricing, use cases
- OpenAPI 3.0 spec for Orders, Missions, Photos, Analytics
- Case study: Glasskiosken i Flatenbadet — complete ROI analysis
- Updated /order/ with recurring missions and frequency dropdown
This commit is contained in:
Bernt
2026-07-14 15:27:33 +00:00
parent 3c522e39f0
commit 1a12fb870b
6296 changed files with 911440 additions and 55607 deletions
+13
View File
@@ -0,0 +1,13 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
# [*.md]
# trim_trailing_whitespace = false
+8
View File
@@ -0,0 +1,8 @@
{
"extends": [
"standard"
],
"rules": {
"no-var": "off"
}
}
@@ -0,0 +1,13 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 10
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
@@ -0,0 +1,81 @@
name: CI
on:
push:
paths-ignore:
- 'docs/**'
- '*.md'
pull_request:
paths-ignore:
- 'docs/**'
- '*.md'
# This allows a subsequently queued workflow run to interrupt previous runs
concurrency:
group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
cancel-in-progress: true
jobs:
dependency-review:
name: Dependency Review
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Check out repo
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Dependency review
uses: actions/dependency-review-action@v3
test:
name: Test
runs-on: ${{ matrix.os }}
outputs:
COVERALLS: ${{ steps.coveralls-trigger.outputs.COVERALLS_TRIGGER }}
permissions:
contents: read
strategy:
matrix:
node-version: [14, 16, 18]
os: [ubuntu-latest]
steps:
- name: Check out repo
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup Node ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Restore cached dependencies
uses: actions/cache@v3
with:
path: node_modules
key: node-modules-${{ hashFiles('package.json') }}
- name: Install dependencies
run: npm i --ignore-scripts
- name: Run tests
run: npm run ci
automerge:
name: Automerge Dependabot PRs
if: >
github.event_name == 'pull_request' &&
github.event.pull_request.user.login == 'dependabot[bot]'
needs: test
permissions:
pull-requests: write
contents: write
runs-on: ubuntu-latest
steps:
- uses: fastify/github-action-merge-dependabot@v3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
+8
View File
@@ -0,0 +1,8 @@
coverage: true
coverage-map: 'coverage-map.js'
reporter: terse
files:
- 'lib/**/*.test.js'
- 'test/**/*.test.js'
+23
View File
@@ -0,0 +1,23 @@
The MIT License (MIT)
Copyright (c) 2019 the Pino team
Pino team listed at https://github.com/pinojs/pino#the-team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+380
View File
@@ -0,0 +1,380 @@
<a id="intro"></a>
# pino-pretty
[![NPM Package Version](https://img.shields.io/npm/v/pino-pretty)](https://www.npmjs.com/package/pino-pretty)
[![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)
[![Coverage Status](https://img.shields.io/coveralls/github/pinojs/pino-pretty)](https://coveralls.io/github/pinojs/pino-pretty?branch=master)
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/)
This module provides a basic [ndjson](http://ndjson.org/) formatter to be used in __development__. If an
incoming line looks like it could be a log line from an ndjson logger, in
particular the [Pino](https://getpino.io/) logging library, then it will apply
extra formatting by considering things like the log level and timestamp.
A standard Pino log line like:
```
{"level":30,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo","v":1}
```
Will format to:
```
[17:35:28.992] INFO (42): hello world
```
If you landed on this page due to the deprecation of the `prettyPrint` option
of `pino`, read the [Programmatic Integration](#integration) section.
<a id="example"></a>
## Example
Using the [example script][exscript] from the Pino module, we can see what the
prettified logs will look like:
![demo](demo.png)
[exscript]: https://github.com/pinojs/pino/blob/25ba61f40ea5a1a753c85002812426d765da52a4/examples/basic.js
<a id="install"></a>
## Install
```sh
$ npm install -g pino-pretty
```
<a id="usage"></a>
## Usage
It is recommended to use `pino-pretty` with `pino`
by piping output to the CLI tool:
```sh
node app.js | pino-pretty
```
<a id="cliargs"></a>
### CLI Arguments
- `--colorize` (`-c`): Adds terminal color escape sequences to the output.
- `--colorizeObjects` (`-C`): Allows suppressing colorization of objects when set to `false`. In combination with `--singleLine`, this ensures that the end of each line is parsable JSON.
- `--crlf` (`-f`): Appends carriage return and line feed, instead of just a line
feed, to the formatted log line.
- `--errorProps` (`-e`): When formatting an error object, display this list
of properties. The list should be a comma-separated list of properties Default: `''`.
Do not use this option if logging from pino@7. Support will be removed from future verions.
- `--levelFirst` (`-l`): Display the log level name before the logged date and time.
- `--errorLikeObjectKeys` (`-k`): Define the log keys that are associated with
error like objects. Default: `err,error`.
- `--messageKey` (`-m`): Define the key that contains the main log message.
Default: `msg`.
- `--levelKey` (`--levelKey`): Define the key that contains the level of the log. Nested keys are supported with each property delimited by a dot character (`.`).
Keys may be escaped to target property names that contains the delimiter itself:
(`--levelKey tags\\.level`).
Default: `level`.
- `--levelLabel` (`-b`): Output the log level using the specified label.
Default: `levelLabel`.
- `--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/).
- `--customLevels` (`-x`): Override default levels with custom levels, e.g. `-x err:99,info:1`
- `--customColors` (`-X`): Override default colors with custom colors, e.g. `-X err:red,info:blue`
- `--useOnlyCustomProps` (`-U`): Only use custom levels and colors (if provided) (default: true); else fallback to default levels and colors, e.g. `-U false`
- `--messageFormat` (`-o`): Format output of message, e.g. `{levelLabel} - {pid} - url:{req.url}` will output message: `INFO - 1123 - url:localhost:3000/test`
Default: `false`
- `--timestampKey` (`-a`): Define the key that contains the log timestamp.
Default: `time`.
- `--translateTime` (`-t`): Translate the epoch time value into a human-readable
date and time string. This flag also can set the format string to apply when
translating the date to a human-readable format. For a list of available pattern
letters, see the [`dateformat` documentation](https://www.npmjs.com/package/dateformat).
- The default format is `HH:MM:ss.l` in the local timezone.
- Require a `UTC:` prefix to translate time to UTC, e.g. `UTC:yyyy-mm-dd HH:MM:ss.l o`.
- Require a `SYS:` prefix to translate time to the local system's time zone. A
shortcut `SYS:standard` to translate time to `yyyy-mm-dd HH:MM:ss.l o` in
system time zone.
- `--ignore` (`-i`): Ignore one or several keys, nested keys are supported with each property delimited by a dot character (`.`),
keys may be escaped to target property names that contains the delimiter itself:
(`-i time,hostname,req.headers,log\\.domain\\.corp/foo`).
The `--ignore` option would be ignored, if both `--ignore` and `--include` are passed.
Default: `hostname`.
- `--include` (`-I`): The opposite of `--ignore`. Include one or several keys.
- `--hideObject` (`-H`): Hide objects from output (but not error object)
- `--singleLine` (`-S`): Print each log message on a single line (errors will still be multi-line)
- `--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
<a id="integration"></a>
## Programmatic Integration
We recommend against using `pino-pretty` in production and highly
recommend installing `pino-pretty` as a development dependency.
Install `pino-pretty` alongside `pino` and set the transport target to `'pino-pretty'`:
```js
const pino = require('pino')
const logger = pino({
transport: {
target: 'pino-pretty'
},
})
logger.info('hi')
```
The transport option can also have an options object containing `pino-pretty` options:
```js
const pino = require('pino')
const logger = pino({
transport: {
target: 'pino-pretty',
options: {
colorize: true
}
}
})
logger.info('hi')
```
Use it as a stream:
```js
const pino = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())
logger.info('hi')
```
Options are also supported:
```js
const pino = require('pino')
const pretty = require('pino-pretty')
const stream = pretty({
colorize: true
})
const logger = pino(stream)
logger.info('hi')
```
See the [Options](#options) section for all possible options.
### Usage as a stream
If you are using `pino-pretty` as a stream and you need to provide options to `pino`,
pass the options as the first argument and `pino-pretty` as second argument:
```js
const pino = require('pino')
const pretty = require('pino-pretty')
const stream = pretty({
colorize: true
})
const logger = pino({ level: 'info' }, stream)
// Nothing is printed
logger.debug('hi')
```
### Usage with Jest
Logging with Jest is _problematic_, as the test framework requires no asynchronous operation to
continue after the test has finished. The following is the only supported way to use this module
with Jest:
```js
import pino from 'pino'
import pretty from 'pino-pretty'
test('test pino-pretty', () => {
const logger = pino(pretty({ sync: true }));
logger.info('Info');
logger.error('Error');
});
```
### Handling non-serializable options
Using the new [pino v7+
transports](https://getpino.io/#/docs/transports?id=v7-transports) not all
options are serializable, for example if you want to use `messageFormat` as a
function you will need to wrap `pino-pretty` in a custom module.
Executing `main.js` below will log a colorized `hello world` message using a
custom function `messageFormat`:
```js
// main.js
const pino = require('pino')
const logger = pino({
transport: {
target: './pino-pretty-transport',
options: {
colorize: true
}
},
})
logger.info('world')
```
```js
// pino-pretty-transport.js
module.exports = opts => require('pino-pretty')({
...opts,
messageFormat: (log, messageKey) => `hello ${log[messageKey]}`
})
```
<a id="options"></a>
### Options
The options accepted have keys corresponding to the options described in [CLI Arguments](#cliargs):
```js
{
colorize: colorette.isColorSupported, // --colorize
colorizeObjects: true, //--colorizeObjects
crlf: false, // --crlf
errorLikeObjectKeys: ['err', 'error'], // --errorLikeObjectKeys
errorProps: '', // --errorProps
levelFirst: false, // --levelFirst
messageKey: 'msg', // --messageKey
levelKey: 'level', // --levelKey
messageFormat: false, // --messageFormat
timestampKey: 'time', // --timestampKey
translateTime: false, // --translateTime
ignore: 'pid,hostname', // --ignore
include: 'level,time', // --include
hideObject: false, // --hideObject
singleLine: false, // --singleLine
customColors: 'err:red,info:blue', // --customColors
customLevels: 'err:99,info:1', // --customLevels
levelLabel: 'levelLabel', // --levelLabel
minimumLevel: 'info', // --minimumLevel
useOnlyCustomProps: true, // --useOnlyCustomProps
// The file or file descriptor (1 is stdout) to write to
destination: 1,
// Alternatively, pass a `sonic-boom` instance (allowing more flexibility):
// destination: new SonicBoom({ dest: 'a/file', mkdir: true })
// You can also configure some SonicBoom options directly
sync: false, // by default we write asynchronously
append: true, // the file is opened with the 'a' flag
mkdir: true, // create the target destination
customPrettifiers: {}
}
```
The `colorize` default follows
[`colorette.isColorSupported`](https://github.com/jorgebucaran/colorette#iscolorsupported).
The defaults for `sync`, `append`, `mkdir` inherit from
[`SonicBoom(opts)`](https://github.com/pinojs/sonic-boom#API).
`customPrettifiers` option provides the ability to add a custom prettify function
for specific log properties. `customPrettifiers` is an object, where keys are
log properties that will be prettified and value is the prettify function itself.
For example, if a log line contains a `query` property,
you can specify a prettifier for it:
```js
{
customPrettifiers: {
query: prettifyQuery
}
}
//...
const prettifyQuery = value => {
// do some prettify magic
}
```
Additionally, `customPrettifiers` can be used to format the `time`, `hostname`,
`pid`, `name`, `caller` and `level` outputs:
```js
{
customPrettifiers: {
// The argument for this function will be the same
// string that's at the start of the log-line by default:
time: timestamp => `🕰 ${timestamp}`,
// The argument for the level-prettifier may vary depending
// on if the levelKey option is used or not.
// By default this will be the same numerics as the Pino default:
level: logLevel => `LEVEL: ${logLevel}`,
// other prettifiers can be used for the other keys if needed, for example
hostname: hostname => colorGreen(hostname),
pid: pid => colorRed(pid),
name: name => colorBlue(name),
caller: caller => colorCyan(caller)
}
}
```
Note that prettifiers do not include any coloring, if the stock coloring on
`level` is desired, it can be accomplished using the following:
```js
const { colorizerFactory } = require('pino-pretty')
const levelColorize = colorizerFactory(true)
const levelPrettifier = logLevel => `LEVEL: ${levelColorize(logLevel)}`
//...
{
customPrettifiers: { level: levelPrettifier }
}
```
`messageFormat` option allows you to customize the message output.
A template `string` like this can define the format:
```js
{
messageFormat: '{levelLabel} - {pid} - url:{req.url}'
}
```
In addition to this, if / end statement blocks can also be specified.
Else statements and nested conditions are not supported.
```js
{
messageFormat: '{levelLabel} - {if pid}{pid} - {end}url:{req.url}'
}
```
This option can also be defined as a `function` with this prototype:
```js
{
messageFormat: (log, messageKey, levelLabel) => {
// do some log message customization
return customized_message;
}
}
```
## Limitations
Because `pino-pretty` uses stdout redirection, in some cases the command may
terminate with an error due to shell limitations.
For example, currently, mingw64 based shells (e.g. Bash as supplied by [git for
Windows](https://gitforwindows.org)) are affected and terminate the process with
a `stdout is not a tty` error message.
Any PRs are welcomed!
<a id="license"></a>
## License
MIT License
+105
View File
@@ -0,0 +1,105 @@
'use strict'
// We do not expect amazing numbers from `pino-pretty` as the whole purpose
// of the module is a very slow operation. However, this benchmark should give
// us some guidance on how features, or code changes, will affect the
// performance of the module.
const bench = require('fastbench')
const {
prettyFactory
} = require('./index')
const max = 10
const tstampMillis = 1693401358754
/* eslint-disable no-var */
const run = bench([
function basicLog (cb) {
const pretty = prettyFactory({})
const input = `{"time":${tstampMillis},"pid":1,"hostname":"foo","msg":"benchmark","foo":"foo","bar":{"bar":"bar"}}\n`
for (var i = 0; i < max; i += 1) {
pretty(input)
}
setImmediate(cb)
},
function objectLog (cb) {
const pretty = prettyFactory({})
const input = {
time: tstampMillis,
pid: 1,
hostname: 'foo',
msg: 'benchmark',
foo: 'foo',
bar: { bar: 'bar' }
}
for (var i = 0; i < max; i += 1) {
pretty(input)
}
setImmediate(cb)
},
function coloredLog (cb) {
const pretty = prettyFactory({ colorize: true })
const input = `{"time":${tstampMillis},"pid":1,"hostname":"foo","msg":"benchmark","foo":"foo","bar":{"bar":"bar"}}\n`
for (var i = 0; i < max; i += 1) {
pretty(input)
}
setImmediate(cb)
},
function customPrettifiers (cb) {
const pretty = prettyFactory({
customPrettifiers: {
time (tstamp) {
return tstamp
},
pid () {
return ''
}
}
})
const input = `{"time":${tstampMillis},"pid":1,"hostname":"foo","msg":"benchmark","foo":"foo","bar":{"bar":"bar"}}\n`
for (var i = 0; i < max; i += 1) {
pretty(input)
}
setImmediate(cb)
},
function logWithErrorObject (cb) {
const pretty = prettyFactory({})
const err = Error('boom')
const input = `{"time":${tstampMillis},"pid":1,"hostname":"foo","msg":"benchmark","foo":"foo","bar":{"bar":"bar"},"err":{"message":"${err.message}","stack":"${err.stack}"}}\n`
for (var i = 0; i < max; i += 1) {
pretty(input)
}
setImmediate(cb)
},
function logRemappedMsgErrKeys (cb) {
const pretty = prettyFactory({
messageKey: 'message',
errorLikeObjectKeys: ['myError']
})
const err = Error('boom')
const input = `{"time":${tstampMillis},"pid":1,"hostname":"foo","message":"benchmark","foo":"foo","bar":{"bar":"bar"},"myError":{"message":"${err.message}","stack":"${err.stack}"}}\n`
for (var i = 0; i < max; i += 1) {
pretty(input)
}
setImmediate(cb)
},
function messageFormatString (cb) {
const pretty = prettyFactory({
messageFormat: '{levelLabel}{if pid} {pid} - {end}{msg}'
})
const input = `{"time":${tstampMillis},"pid":1,"hostname":"foo","msg":"benchmark","foo":"foo","bar":{"bar":"bar"}}\n`
for (var i = 0; i < max; i += 1) {
pretty(input)
}
setImmediate(cb)
}
], 10000)
run(run)
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env node
const fs = require('fs')
const path = require('path')
const help = require('help-me')({
dir: path.join(__dirname, 'help'),
ext: '.txt'
})
const pump = require('pump')
const sjp = require('secure-json-parse')
const JoyCon = require('joycon')
const stripJsonComments = require('strip-json-comments')
const build = require('./')
const CONSTANTS = require('./lib/constants')
const { isObject } = require('./lib/utils')
const minimist = require('minimist')
const parseJSON = input => {
return sjp.parse(stripJsonComments(input), { protoAction: 'remove' })
}
const joycon = new JoyCon({
parseJSON,
files: [
'pino-pretty.config.cjs',
'pino-pretty.config.js',
'.pino-prettyrc',
'.pino-prettyrc.json'
],
stopDir: path.dirname(process.cwd())
})
const cmd = minimist(process.argv.slice(2))
if (cmd.h || cmd.help) {
help.toStdout()
} else {
const DEFAULT_VALUE = '\0default'
let opts = minimist(process.argv, {
alias: {
colorize: 'c',
colorizeObjects: 'C',
crlf: 'f',
errorProps: 'e',
levelFirst: 'l',
minimumLevel: 'L',
customLevels: 'x',
customColors: 'X',
useOnlyCustomProps: 'U',
errorLikeObjectKeys: 'k',
messageKey: 'm',
levelKey: CONSTANTS.LEVEL_KEY,
levelLabel: 'b',
messageFormat: 'o',
timestampKey: 'a',
translateTime: 't',
ignore: 'i',
include: 'I',
hideObject: 'H',
singleLine: 'S'
},
default: {
messageKey: DEFAULT_VALUE,
minimumLevel: DEFAULT_VALUE,
levelKey: DEFAULT_VALUE,
timestampKey: DEFAULT_VALUE
}
})
// Remove default values
opts = filter(opts, value => value !== DEFAULT_VALUE)
const config = loadConfig(opts.config)
// Override config with cli options
opts = Object.assign({}, config, opts)
// set defaults
opts.errorLikeObjectKeys = opts.errorLikeObjectKeys || 'err,error'
opts.errorProps = opts.errorProps || ''
const res = build(opts)
pump(process.stdin, res)
// https://github.com/pinojs/pino/pull/358
/* istanbul ignore next */
if (!process.stdin.isTTY && !fs.fstatSync(process.stdin.fd).isFile()) {
process.once('SIGINT', function noOp () {})
}
function loadConfig (configPath) {
const files = configPath ? [path.resolve(configPath)] : undefined
const result = joycon.loadSync(files)
if (result.path && !isObject(result.data)) {
configPath = configPath || path.basename(result.path)
throw new Error(`Invalid runtime configuration file: ${configPath}`)
}
if (configPath && !result.data) {
throw new Error(`Failed to load runtime configuration file: ${configPath}`)
}
return result.data
}
function filter (obj, cb) {
return Object.keys(obj).reduce((acc, key) => {
const value = obj[key]
if (cb(value, key)) {
acc[key] = value
}
return acc
}, {})
}
}
+9
View File
@@ -0,0 +1,9 @@
'use strict'
module.exports = testFile => {
// Ignore coverage on files that do not have a direct corollary.
if (testFile.startsWith('test/')) return false
// Indicate the matching name, sans '.test.js', should be checked for coverage.
return testFile.replace(/\.test\.js$/, '.js')
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+24
View File
@@ -0,0 +1,24 @@
<a id="Systemd"></a>
## Systemd example
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.
For example viewing the prettified logs of a process named `monitor` with `journalctl -u monitor -f | pino-pretty`, might output something like this:
```
Apr 24 07:40:01 nanopi node[6080]: {"level":30,"time":1587706801902,"pid":6080,"hostname":"nanopi","msg":"TT
21","v":1}
```
As you can see, the timestamp, hostname, and pid are duplicated.
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`:
```
journalctl -u monitor -f -o cat | pino-pretty
```
the output now looks something like this:
```
[1587706801902] INFO (6080 on nanopi): TT 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:
```
[2020-04-24 05:42:24.836 +0000] INFO : TT 21
```
+68
View File
@@ -0,0 +1,68 @@
Usage: pino pretty [options] [command]
Commands:
help Display help
version Display version
Options:
-c, --colorize Force adding color sequences to the output
-C, --config specify a path to a json file containing the pino-pretty options
-f, --crlf Append CRLF instead of LF to formatted lines
-X, --customColors Override default colors using names from https://www.npmjs.com/package/colorette (`-X err:red,info:blue`)
-x, --customLevels Override default levels (`-x err:99,info:1`)
-k, --errorLikeObjectKeys Define which keys contain error objects (`-k err,error`) (defaults to `err,error`)
-e, --errorProps Comma separated list of properties on error objects to show (`*` for all properties) (defaults to ``)
-h, --help Output usage information
-H, --hideObject Hide objects from output (but not error object)
-i, --ignore Ignore one or several keys: (`-i time,hostname`)
-I, --include The opposite of `--ignore`, only include one or several keys: (`-I level,time`)
-l, --levelFirst Display the log level as the first output field
-L, --levelKey [value] Detect the log level under the specified key (defaults to "level")
-b, --levelLabel [value] Output the log level using the specified label (defaults to "levelLabel")
-o, --messageFormat Format output of message
-m, --messageKey [value] Highlight the message under the specified key (defaults to "msg")
-L, --minimumLevel Hide messages below the specified log level
-S, --singleLine Print all non-error objects on a single line
-a, --timestampKey [value] Display the timestamp from the specified key (defaults to "time")
-t, --translateTime Display epoch timestamps as UTC ISO format or according to an optional format string (default ISO 8601)
-U, --useOnlyCustomProps Only use custom levels and colors (if provided); don't fallback to default levels and colors (-U false)
-v, --version Output the version number
Examples:
- To prettify logs, simply pipe a log file through
$ cat log | pino-pretty
- To highlight a string at a key other than 'msg'
$ cat log | pino-pretty -m fooMessage
- To detect the log level at a key other than 'level'
$ cat log | pino-pretty --levelKey fooLevel
- To output the log level label using a key other than 'levelLabel'
$ cat log | pino-pretty --levelLabel LVL -o "{LVL}"
- To display timestamp from a key other than 'time'
$ cat log | pino-pretty -a fooTimestamp
- To convert Epoch timestamps to ISO timestamps use the -t option
$ cat log | pino-pretty -t
- To convert Epoch timestamps to local timezone format use the -t option with "SYS:" prefixed format string
$ cat log | pino-pretty -t "SYS:yyyy-mm-dd HH:MM:ss"
- To flip level and time/date in standard output use the -l option
$ cat log | pino-pretty -l
- Only prints messages with a minimum log level of info
$ cat log | pino-pretty -L info
- Prettify logs but don't print pid and hostname
$ cat log | pino-pretty -i pid,hostname
- Prettify logs but only print time and level
$ cat log | pino-pretty -I time,level
- Loads options from a config file
$ cat log | pino-pretty --config=/path/to/config.json
+217
View File
@@ -0,0 +1,217 @@
// Type definitions for pino-pretty 7.0
// Project: https://github.com/pinojs/pino-pretty#readme
// Definitions by: Adam Vigneaux <https://github.com/AdamVig>
// tearwyx <https://github.com/tearwyx>
// Minimum TypeScript Version: 3.0
/// <reference types="node" />
import { Transform } from 'stream';
import { OnUnknown } from 'pino-abstract-transport';
// @ts-ignore fall back to any if pino is not available, i.e. when running pino tests
import { DestinationStream, Level } from 'pino';
type LogDescriptor = Record<string, unknown>;
declare function PinoPretty(options?: PrettyOptions_): PinoPretty.PrettyStream;
declare function colorizerFactory(
useColors?: boolean,
customColors?: [number, string][],
useOnlyCustomProps?: boolean,
): {
(
level?: number | string,
opts?: {
customLevels?: { [level: number]: string };
customLevelNames?: { [name: string]: number };
},
): string,
message: (input: string | number) => string,
greyMessage: (input: string | number) => string,
}
declare function prettyFactory(
options: PrettyOptions_,
): (inputData: any) => string
interface PrettyOptions_ {
/**
* Hide objects from output (but not error object).
* @default false
*/
hideObject?: boolean;
/**
* Translate the epoch time value into a human readable date and time string. This flag also can set the format
* string to apply when translating the date to human readable format. For a list of available pattern letters
* see the {@link https://www.npmjs.com/package/dateformat|dateformat documentation}.
* - The default format is `yyyy-mm-dd HH:MM:ss.l o` in UTC.
* - Requires a `SYS:` prefix to translate time to the local system's timezone. Use the shortcut `SYS:standard`
* to translate time to `yyyy-mm-dd HH:MM:ss.l o` in system timezone.
* @default false
*/
translateTime?: boolean | string;
/**
* If set to true, it will print the name of the log level as the first field in the log line.
* @default false
*/
levelFirst?: boolean;
/**
* Define the key that contains the level of the log.
* @default "level"
*/
levelKey?: string;
/**
* Output the log level using the specified label.
* @default "levelLabel"
*/
levelLabel?: string;
/**
* The key in the JSON object to use as the highlighted message.
* @default "msg"
*/
messageKey?: string;
/**
* Print each log message on a single line (errors will still be multi-line).
* @default false
*/
singleLine?: boolean;
/**
* The key in the JSON object to use for timestamp display.
* @default "time"
*/
timestampKey?: string;
/**
* The minimum log level to include in the output.
* @default "trace"
*/
minimumLevel?: Level;
/**
* Format output of message, e.g. {level} - {pid} will output message: INFO - 1123
* @default false
*
* @example
* ```typescript
* {
* messageFormat: (log, messageKey) => {
* const message = log[messageKey];
* if (log.requestId) return `[${log.requestId}] ${message}`;
* return message;
* }
* }
* ```
*/
messageFormat?: false | string | PinoPretty.MessageFormatFunc;
/**
* If set to true, will add color information to the formatted output message.
* @default false
*/
colorize?: boolean;
/**
* If set to false while `colorize` is `true`, will output JSON objects without color.
* @default true
*/
colorizeObjects?: boolean;
/**
* Appends carriage return and line feed, instead of just a line feed, to the formatted log line.
* @default false
*/
crlf?: boolean;
/**
* Define the log keys that are associated with error like objects.
* @default ["err", "error"]
*/
errorLikeObjectKeys?: string[];
/**
* When formatting an error object, display this list of properties.
* The list should be a comma separated list of properties.
* @default ""
*/
errorProps?: string;
/**
* Ignore one or several keys.
* Will be overridden by the option include if include is presented.
* @example "time,hostname"
*/
ignore?: string;
/**
* Include one or several keys.
* @example "time,level"
*/
include?: string;
/**
* Makes messaging synchronous.
* @default false
*/
sync?: boolean;
/**
* The file, file descriptor, or stream to write to. Defaults to 1 (stdout).
* @default 1
*/
destination?: string | number | DestinationStream | NodeJS.WritableStream;
/**
* Opens the file with the 'a' flag.
* @default true
*/
append?: boolean;
/**
* Ensure directory for destination file exists.
* @default false
*/
mkdir?: boolean;
/**
* Provides the ability to add a custom prettify function for specific log properties.
* `customPrettifiers` is an object, where keys are log properties that will be prettified
* and value is the prettify function itself.
* For example, if a log line contains a query property, you can specify a prettifier for it:
* @default {}
*
* @example
* ```typescript
* {
* customPrettifiers: {
* query: prettifyQuery
* }
* }
* //...
* const prettifyQuery = value => {
* // do some prettify magic
* }
* ```
*/
customPrettifiers?: Record<string, PinoPretty.Prettifier>;
/**
* Change the level names and values to an user custom preset.
*
* Can be a CSV string in 'level_name:level_value' format or an object.
*
* @example ( CSV ) customLevels: 'info:10,some_level:40'
* @example ( Object ) customLevels: { info: 10, some_level: 40 }
*/
customLevels?: string|object;
/**
* Change the level colors to an user custom preset.
*
* Can be a CSV string in 'level_name:color_value' format or an object.
* Also supports 'default' as level_name for fallback color.
*
* @example ( CSV ) customColors: 'info:white,some_level:red'
* @example ( Object ) customColors: { info: 'white', some_level: 'red' }
*/
customColors?: string|object;
}
declare function build(options: PrettyOptions_): PinoPretty.PrettyStream;
declare namespace PinoPretty {
type Prettifier = (inputData: string | object) => string;
type MessageFormatFunc = (log: LogDescriptor, messageKey: string, levelLabel: string) => string;
type PrettyOptions = PrettyOptions_;
type PrettyStream = Transform & OnUnknown;
type ColorizerFactory = typeof colorizerFactory;
type PrettyFactory = typeof prettyFactory;
type Build = typeof build;
}
export default PinoPretty;
export { build, PinoPretty, PrettyOptions_ as PrettyOptions, colorizerFactory, prettyFactory };
+171
View File
@@ -0,0 +1,171 @@
'use strict'
const { isColorSupported } = require('colorette')
const pump = require('pump')
const { Transform } = require('readable-stream')
const abstractTransport = require('pino-abstract-transport')
const colors = require('./lib/colors')
const {
ERROR_LIKE_KEYS,
LEVEL_KEY,
LEVEL_LABEL,
MESSAGE_KEY,
TIMESTAMP_KEY
} = require('./lib/constants')
const {
buildSafeSonicBoom,
parseFactoryOptions
} = require('./lib/utils')
const pretty = require('./lib/pretty')
/**
* @typedef {object} PinoPrettyOptions
* @property {boolean} [colorize] Indicates if colors should be used when
* prettifying. The default will be determined by the terminal capabilities at
* run time.
* @property {boolean} [colorizeObjects=true] Apply coloring to rendered objects
* when coloring is enabled.
* @property {boolean} [crlf=false] End lines with `\r\n` instead of `\n`.
* @property {string|null} [customColors=null] A comma separated list of colors
* to use for specific level labels, e.g. `err:red,info:blue`.
* @property {string|null} [customLevels=null] A comma separated list of user
* defined level names and numbers, e.g. `err:99,info:1`.
* @property {CustomPrettifiers} [customPrettifiers={}] A set of prettifier
* functions to apply to keys defined in this object.
* @property {K_ERROR_LIKE_KEYS} [errorLikeObjectKeys] A list of string property
* names to consider as error objects.
* @property {string} [errorProps=''] A comma separated list of properties on
* error objects to include in the output.
* @property {boolean} [hideObject=false] When `true`, data objects will be
* omitted from the output (except for error objects).
* @property {string} [ignore='hostname'] A comma separated list of log keys
* to omit when outputting the prettified log information.
* @property {undefined|string} [include=undefined] A comma separated list of
* log keys to include in the prettified log information. Only the keys in this
* list will be included in the output.
* @property {boolean} [levelFirst=false] When true, the log level will be the
* first field in the prettified output.
* @property {string} [levelKey='level'] The key name in the log data that
* contains the level value for the log.
* @property {string} [levelLabel='levelLabel'] Token name to use in
* `messageFormat` to represent the name of the logged level.
* @property {null|MessageFormatString|MessageFormatFunction} [messageFormat=null]
* When a string, defines how the prettified line should be formatted according
* to defined tokens. When a function, a synchronous function that returns a
* formatted string.
* @property {string} [messageKey='msg'] Defines the key in incoming logs that
* contains the message of the log, if present.
* @property {undefined|string|number} [minimumLevel=undefined] The minimum
* level for logs that should be processed. Any logs below this level will
* be omitted.
* @property {object} [outputStream=process.stdout] The stream to write
* prettified log lines to.
* @property {boolean} [singleLine=false] When `true` any objects, except error
* objects, in the log data will be printed as a single line instead as multiple
* lines.
* @property {string} [timestampKey='time'] Defines the key in incoming logs
* that contains the timestamp of the log, if present.
* @property {boolean|string} [translateTime=true] When true, will translate a
* JavaScript date integer into a human-readable string. If set to a string,
* it must be a format string.
* @property {boolean} [useOnlyCustomProps=true] When true, only custom levels
* and colors will be used if they have been provided.
*/
/**
* The default options that will be used when prettifying log lines.
*
* @type {PinoPrettyOptions}
*/
const defaultOptions = {
colorize: isColorSupported,
colorizeObjects: true,
crlf: false,
customColors: null,
customLevels: null,
customPrettifiers: {},
errorLikeObjectKeys: ERROR_LIKE_KEYS,
errorProps: '',
hideObject: false,
ignore: 'hostname',
include: undefined,
levelFirst: false,
levelKey: LEVEL_KEY,
levelLabel: LEVEL_LABEL,
messageFormat: null,
messageKey: MESSAGE_KEY,
minimumLevel: undefined,
outputStream: process.stdout,
singleLine: false,
timestampKey: TIMESTAMP_KEY,
translateTime: true,
useOnlyCustomProps: true
}
/**
* Processes the supplied options and returns a function that accepts log data
* and produces a prettified log string.
*
* @param {PinoPrettyOptions} options Configuration for the prettifier.
* @returns {LogPrettifierFunc}
*/
function prettyFactory (options) {
const context = parseFactoryOptions(Object.assign({}, defaultOptions, options))
return pretty.bind({ ...context, context })
}
/**
* @typedef {PinoPrettyOptions} BuildStreamOpts
* @property {object|number|string} [destination] A destination stream, file
* descriptor, or target path to a file.
* @property {boolean} [append]
* @property {boolean} [mkdir]
* @property {boolean} [sync=false]
*/
/**
* Constructs a {@link LogPrettifierFunc} and a stream to which the produced
* prettified log data will be written.
*
* @param {BuildStreamOpts} opts
* @returns {Transform | (Transform & OnUnknown)}
*/
function build (opts = {}) {
const pretty = prettyFactory(opts)
return abstractTransport(function (source) {
const stream = new Transform({
objectMode: true,
autoDestroy: true,
transform (chunk, enc, cb) {
const line = pretty(chunk)
cb(null, line)
}
})
let destination
if (typeof opts.destination === 'object' && typeof opts.destination.write === 'function') {
destination = opts.destination
} else {
destination = buildSafeSonicBoom({
dest: opts.destination || 1,
append: opts.append,
mkdir: opts.mkdir,
sync: opts.sync // by default sonic will be async
})
}
source.on('unknown', function (line) {
destination.write(line + '\n')
})
pump(source, stream, destination)
return stream
}, { parse: 'lines' })
}
module.exports = build
module.exports.build = build
module.exports.prettyFactory = prettyFactory
module.exports.colorizerFactory = colors
module.exports.default = build
+136
View File
@@ -0,0 +1,136 @@
'use strict'
const { LEVELS, LEVEL_NAMES } = require('./constants')
const nocolor = input => input
const plain = {
default: nocolor,
60: nocolor,
50: nocolor,
40: nocolor,
30: nocolor,
20: nocolor,
10: nocolor,
message: nocolor,
greyMessage: nocolor
}
const { createColors } = require('colorette')
const availableColors = createColors({ useColor: true })
const { white, bgRed, red, yellow, green, blue, gray, cyan } = availableColors
const colored = {
default: white,
60: bgRed,
50: red,
40: yellow,
30: green,
20: blue,
10: gray,
message: cyan,
greyMessage: gray
}
function resolveCustomColoredColorizer (customColors) {
return customColors.reduce(
function (agg, [level, color]) {
agg[level] = typeof availableColors[color] === 'function' ? availableColors[color] : white
return agg
},
{ default: white, message: cyan, greyMessage: gray }
)
}
function colorizeLevel (useOnlyCustomProps) {
return function (level, colorizer, { customLevels, customLevelNames } = {}) {
const levels = useOnlyCustomProps ? customLevels || LEVELS : Object.assign({}, LEVELS, customLevels)
const levelNames = useOnlyCustomProps ? customLevelNames || LEVEL_NAMES : Object.assign({}, LEVEL_NAMES, customLevelNames)
let levelNum = 'default'
if (Number.isInteger(+level)) {
levelNum = Object.prototype.hasOwnProperty.call(levels, level) ? level : levelNum
} else {
levelNum = Object.prototype.hasOwnProperty.call(levelNames, level.toLowerCase()) ? levelNames[level.toLowerCase()] : levelNum
}
const levelStr = levels[levelNum]
return Object.prototype.hasOwnProperty.call(colorizer, levelNum) ? colorizer[levelNum](levelStr) : colorizer.default(levelStr)
}
}
function plainColorizer (useOnlyCustomProps) {
const newPlainColorizer = colorizeLevel(useOnlyCustomProps)
const customColoredColorizer = function (level, opts) {
return newPlainColorizer(level, plain, opts)
}
customColoredColorizer.message = plain.message
customColoredColorizer.greyMessage = plain.greyMessage
return customColoredColorizer
}
function coloredColorizer (useOnlyCustomProps) {
const newColoredColorizer = colorizeLevel(useOnlyCustomProps)
const customColoredColorizer = function (level, opts) {
return newColoredColorizer(level, colored, opts)
}
customColoredColorizer.message = colored.message
customColoredColorizer.greyMessage = colored.greyMessage
return customColoredColorizer
}
function customColoredColorizerFactory (customColors, useOnlyCustomProps) {
const onlyCustomColored = resolveCustomColoredColorizer(customColors)
const customColored = useOnlyCustomProps ? onlyCustomColored : Object.assign({}, colored, onlyCustomColored)
const colorizeLevelCustom = colorizeLevel(useOnlyCustomProps)
const customColoredColorizer = function (level, opts) {
return colorizeLevelCustom(level, customColored, opts)
}
customColoredColorizer.message = customColoredColorizer.message || customColored.message
customColoredColorizer.greyMessage = customColoredColorizer.greyMessage || customColored.greyMessage
return customColoredColorizer
}
/**
* Applies colorization, if possible, to a string representing the passed in
* `level`. For example, the default colorizer will return a "green" colored
* string for the "info" level.
*
* @typedef {function} ColorizerFunc
* @param {string|number} level In either case, the input will map to a color
* for the specified level or to the color for `USERLVL` if the level is not
* recognized.
* @property {function} message Accepts one string parameter that will be
* colorized to a predefined color.
*/
/**
* Factory function get a function to colorized levels. The returned function
* also includes a `.message(str)` method to colorize strings.
*
* @param {boolean} [useColors=false] When `true` a function that applies standard
* terminal colors is returned.
* @param {array[]} [customColors] Tuple where first item of each array is the
* level index and the second item is the color
* @param {boolean} [useOnlyCustomProps] When `true`, only use the provided
* custom colors provided and not fallback to default
*
* @returns {ColorizerFunc} `function (level) {}` has a `.message(str)` method to
* apply colorization to a string. The core function accepts either an integer
* `level` or a `string` level. The integer level will map to a known level
* string or to `USERLVL` if not known. The string `level` will map to the same
* colors as the integer `level` and will also default to `USERLVL` if the given
* string is not a recognized level name.
*/
module.exports = function getColorizer (useColors = false, customColors, useOnlyCustomProps) {
if (useColors && customColors !== undefined) {
return customColoredColorizerFactory(customColors, useOnlyCustomProps)
} else if (useColors) {
return coloredColorizer(useOnlyCustomProps)
}
return plainColorizer(useOnlyCustomProps)
}
+132
View File
@@ -0,0 +1,132 @@
'use strict'
const { test } = require('tap')
const getColorizer = require('./colors')
const testDefaultColorizer = getColorizer => async t => {
const colorizer = getColorizer()
let colorized = colorizer(10)
t.equal(colorized, 'TRACE')
colorized = colorizer(20)
t.equal(colorized, 'DEBUG')
colorized = colorizer(30)
t.equal(colorized, 'INFO')
colorized = colorizer(40)
t.equal(colorized, 'WARN')
colorized = colorizer(50)
t.equal(colorized, 'ERROR')
colorized = colorizer(60)
t.equal(colorized, 'FATAL')
colorized = colorizer(900)
t.equal(colorized, 'USERLVL')
colorized = colorizer('info')
t.equal(colorized, 'INFO')
colorized = colorizer('use-default')
t.equal(colorized, 'USERLVL')
colorized = colorizer.message('foo')
t.equal(colorized, 'foo')
colorized = colorizer.greyMessage('foo')
t.equal(colorized, 'foo')
}
const testColoringColorizer = getColorizer => async t => {
const colorizer = getColorizer(true)
let colorized = colorizer(10)
t.equal(colorized, '\u001B[90mTRACE\u001B[39m')
colorized = colorizer(20)
t.equal(colorized, '\u001B[34mDEBUG\u001B[39m')
colorized = colorizer(30)
t.equal(colorized, '\u001B[32mINFO\u001B[39m')
colorized = colorizer(40)
t.equal(colorized, '\u001B[33mWARN\u001B[39m')
colorized = colorizer(50)
t.equal(colorized, '\u001B[31mERROR\u001B[39m')
colorized = colorizer(60)
t.equal(colorized, '\u001B[41mFATAL\u001B[49m')
colorized = colorizer(900)
t.equal(colorized, '\u001B[37mUSERLVL\u001B[39m')
colorized = colorizer('info')
t.equal(colorized, '\u001B[32mINFO\u001B[39m')
colorized = colorizer('use-default')
t.equal(colorized, '\u001B[37mUSERLVL\u001B[39m')
colorized = colorizer.message('foo')
t.equal(colorized, '\u001B[36mfoo\u001B[39m')
colorized = colorizer.greyMessage('foo')
t.equal(colorized, '\u001B[90mfoo\u001B[39m')
}
const testCustomColoringColorizer = getColorizer => async t => {
const customLevels = {
0: 'INFO',
1: 'ERR',
default: 'USERLVL'
}
const customLevelNames = {
info: 0,
err: 1
}
const customColors = [
[0, 'not-a-color'],
[1, 'red']
]
const opts = {
customLevels,
customLevelNames
}
const colorizer = getColorizer(true, customColors)
const colorizerWithCustomPropUse = getColorizer(true, customColors, true)
let colorized = colorizer(1, opts)
t.equal(colorized, '\u001B[31mERR\u001B[39m')
colorized = colorizer(0, opts)
t.equal(colorized, '\u001B[37mINFO\u001B[39m')
colorized = colorizer(900)
t.equal(colorized, '\u001B[37mUSERLVL\u001B[39m')
colorized = colorizer('err', opts)
t.equal(colorized, '\u001B[31mERR\u001B[39m')
colorized = colorizer('info', opts)
t.equal(colorized, '\u001B[37mINFO\u001B[39m')
colorized = colorizer('use-default')
t.equal(colorized, '\u001B[37mUSERLVL\u001B[39m')
colorized = colorizer(40, opts)
t.equal(colorized, '\u001B[33mWARN\u001B[39m')
colorized = colorizerWithCustomPropUse(50, opts)
t.equal(colorized, '\u001B[37mUSERLVL\u001B[39m')
}
test('returns default colorizer - private export', testDefaultColorizer(getColorizer))
test('returns colorizing colorizer - private export', testColoringColorizer(getColorizer))
test('returns custom colorizing colorizer - private export', testCustomColoringColorizer(getColorizer))
test('custom props defaults to standard levels', async t => {
const colorizer = getColorizer(true, [], true)
const colorized = colorizer('info')
t.equal(colorized, '\u001B[37mINFO\u001B[39m')
})
+55
View File
@@ -0,0 +1,55 @@
'use strict'
/**
* A set of property names that indicate the value represents an error object.
*
* @typedef {string[]} K_ERROR_LIKE_KEYS
*/
module.exports = {
DATE_FORMAT: 'yyyy-mm-dd HH:MM:ss.l o',
DATE_FORMAT_SIMPLE: 'HH:MM:ss.l',
/**
* @type {K_ERROR_LIKE_KEYS}
*/
ERROR_LIKE_KEYS: ['err', 'error'],
MESSAGE_KEY: 'msg',
LEVEL_KEY: 'level',
LEVEL_LABEL: 'levelLabel',
TIMESTAMP_KEY: 'time',
LEVELS: {
default: 'USERLVL',
60: 'FATAL',
50: 'ERROR',
40: 'WARN',
30: 'INFO',
20: 'DEBUG',
10: 'TRACE'
},
LEVEL_NAMES: {
fatal: 60,
error: 50,
warn: 40,
info: 30,
debug: 20,
trace: 10
},
// Object keys that probably came from a logger like Pino or Bunyan.
LOGGER_KEYS: [
'pid',
'hostname',
'name',
'level',
'time',
'timestamp',
'caller'
]
}
+169
View File
@@ -0,0 +1,169 @@
'use strict'
module.exports = pretty
const sjs = require('secure-json-parse')
const isObject = require('./utils/is-object')
const prettifyErrorLog = require('./utils/prettify-error-log')
const prettifyLevel = require('./utils/prettify-level')
const prettifyMessage = require('./utils/prettify-message')
const prettifyMetadata = require('./utils/prettify-metadata')
const prettifyObject = require('./utils/prettify-object')
const prettifyTime = require('./utils/prettify-time')
const filterLog = require('./utils/filter-log')
const {
LEVELS,
LEVEL_KEY,
LEVEL_NAMES
} = require('./constants')
const jsonParser = input => {
try {
return { value: sjs.parse(input, { protoAction: 'remove' }) }
} catch (err) {
return { err }
}
}
/**
* Orchestrates processing the received log data according to the provided
* configuration and returns a prettified log string.
*
* @typedef {function} LogPrettifierFunc
* @param {string|object} inputData A log string or a log-like object.
* @returns {string} A string that represents the prettified log data.
*/
function pretty (inputData) {
let log
if (!isObject(inputData)) {
const parsed = jsonParser(inputData)
if (parsed.err || !isObject(parsed.value)) {
// pass through
return inputData + this.EOL
}
log = parsed.value
} else {
log = inputData
}
if (this.minimumLevel) {
// We need to figure out if the custom levels has the desired minimum
// level & use that one if found. If not, determine if the level exists
// in the standard levels. In both cases, make sure we have the level
// number instead of the level name.
let condition
if (this.useOnlyCustomProps) {
condition = this.customLevels
} else {
condition = this.customLevelNames[this.minimumLevel] !== undefined
}
let minimum
if (condition) {
minimum = this.customLevelNames[this.minimumLevel]
} else {
minimum = LEVEL_NAMES[this.minimumLevel]
}
if (!minimum) {
minimum = typeof this.minimumLevel === 'string'
? LEVEL_NAMES[this.minimumLevel]
: LEVEL_NAMES[LEVELS[this.minimumLevel].toLowerCase()]
}
const level = log[this.levelKey === undefined ? LEVEL_KEY : this.levelKey]
if (level < minimum) return
}
const prettifiedMessage = prettifyMessage({ log, context: this.context })
if (this.ignoreKeys || this.includeKeys) {
log = filterLog({ log, context: this.context })
}
const prettifiedLevel = prettifyLevel({
log,
context: {
...this.context,
// This is odd. The colorizer ends up relying on the value of
// `customProperties` instead of the original `customLevels` and
// `customLevelNames`.
...this.context.customProperties
}
})
const prettifiedMetadata = prettifyMetadata({ log, context: this.context })
const prettifiedTime = prettifyTime({ log, context: this.context })
let line = ''
if (this.levelFirst && prettifiedLevel) {
line = `${prettifiedLevel}`
}
if (prettifiedTime && line === '') {
line = `${prettifiedTime}`
} else if (prettifiedTime) {
line = `${line} ${prettifiedTime}`
}
if (!this.levelFirst && prettifiedLevel) {
if (line.length > 0) {
line = `${line} ${prettifiedLevel}`
} else {
line = prettifiedLevel
}
}
if (prettifiedMetadata) {
if (line.length > 0) {
line = `${line} ${prettifiedMetadata}:`
} else {
line = prettifiedMetadata
}
}
if (line.endsWith(':') === false && line !== '') {
line += ':'
}
if (prettifiedMessage !== undefined) {
if (line.length > 0) {
line = `${line} ${prettifiedMessage}`
} else {
line = prettifiedMessage
}
}
if (line.length > 0 && !this.singleLine) {
line += this.EOL
}
// pino@7+ does not log this anymore
if (log.type === 'Error' && log.stack) {
const prettifiedErrorLog = prettifyErrorLog({ log, context: this.context })
if (this.singleLine) line += this.EOL
line += prettifiedErrorLog
} else if (this.hideObject === false) {
const skipKeys = [
this.messageKey,
this.levelKey,
this.timestampKey
].filter(key => {
return typeof log[key] === 'string' ||
typeof log[key] === 'number' ||
typeof log[key] === 'boolean'
})
const prettifiedObject = prettifyObject({
log,
skipKeys,
context: this.context
})
// In single line mode, include a space only if prettified version isn't empty
if (this.singleLine && !/^\s$/.test(prettifiedObject)) {
line += ' '
}
line += prettifiedObject
}
return line
}
@@ -0,0 +1,71 @@
'use strict'
module.exports = buildSafeSonicBoom
const { isMainThread } = require('worker_threads')
const SonicBoom = require('sonic-boom')
const noop = require('./noop')
/**
* Creates a safe SonicBoom instance
*
* @param {object} opts Options for SonicBoom
*
* @returns {object} A new SonicBoom stream
*/
function buildSafeSonicBoom (opts) {
const stream = new SonicBoom(opts)
stream.on('error', filterBrokenPipe)
// if we are sync: false, we must flush on exit
// NODE_V8_COVERAGE must breaks everything
// https://github.com/nodejs/node/issues/49344
if (!process.env.NODE_V8_COVERAGE && !opts.sync && isMainThread) {
setupOnExit(stream)
}
return stream
function filterBrokenPipe (err) {
if (err.code === 'EPIPE') {
stream.write = noop
stream.end = noop
stream.flushSync = noop
stream.destroy = noop
return
}
stream.removeListener('error', filterBrokenPipe)
}
}
function setupOnExit (stream) {
/* istanbul ignore next */
if (global.WeakRef && global.WeakMap && global.FinalizationRegistry) {
// This is leak free, it does not leave event handlers
const onExit = require('on-exit-leak-free')
onExit.register(stream, autoEnd)
stream.on('close', function () {
onExit.unregister(stream)
})
}
}
/* istanbul ignore next */
function autoEnd (stream, eventName) {
// This check is needed only on some platforms
if (stream.destroyed) {
return
}
if (eventName === 'beforeExit') {
// We still have an event loop, let's use it
stream.flush()
stream.on('drain', function () {
stream.end()
})
} else {
// We do not have an event loop, so flush synchronously
stream.flushSync()
}
}
@@ -0,0 +1,86 @@
'use strict'
const tap = require('tap')
const rimraf = require('rimraf')
const fs = require('fs')
const { join } = require('path')
const buildSafeSonicBoom = require('./build-safe-sonic-boom')
function noop () {}
const file = () => {
const dest = join(__dirname, `${process.pid}-${process.hrtime().toString()}`)
const fd = fs.openSync(dest, 'w')
return { dest, fd }
}
tap.test('should not write when error emitted and code is "EPIPE"', async t => {
t.plan(1)
const { fd, dest } = file()
const stream = buildSafeSonicBoom({ sync: true, fd, mkdir: true })
t.teardown(() => rimraf(dest, noop))
stream.emit('error', { code: 'EPIPE' })
stream.write('will not work')
const dataFile = fs.readFileSync(dest)
t.equal(dataFile.length, 0)
})
tap.test('should stream.write works when error code is not "EPIPE"', async t => {
t.plan(3)
const { fd, dest } = file()
const stream = buildSafeSonicBoom({ sync: true, fd, mkdir: true })
t.teardown(() => rimraf(dest, noop))
stream.on('error', () => t.pass('error emitted'))
stream.emit('error', 'fake error description')
t.ok(stream.write('will work'))
const dataFile = fs.readFileSync(dest)
t.equal(dataFile.toString(), 'will work')
})
tap.test('cover setupOnExit', async t => {
t.plan(3)
const { fd, dest } = file()
const stream = buildSafeSonicBoom({ sync: false, fd, mkdir: true })
t.teardown(() => rimraf(dest, noop))
stream.on('error', () => t.pass('error emitted'))
stream.emit('error', 'fake error description')
t.ok(stream.write('will work'))
await watchFileCreated(dest)
const dataFile = fs.readFileSync(dest)
t.equal(dataFile.toString(), 'will work')
})
function watchFileCreated (filename) {
return new Promise((resolve, reject) => {
const TIMEOUT = 2000
const INTERVAL = 100
const threshold = TIMEOUT / INTERVAL
let counter = 0
const interval = setInterval(() => {
// On some CI runs file is created but not filled
if (fs.existsSync(filename) && fs.statSync(filename).size !== 0) {
clearInterval(interval)
resolve()
} else if (counter <= threshold) {
counter++
} else {
clearInterval(interval)
reject(new Error(`${filename} was not created.`))
}
}, INTERVAL)
})
}
@@ -0,0 +1,26 @@
'use strict'
module.exports = createDate
const isValidDate = require('./is-valid-date')
/**
* Constructs a JS Date from a number or string. Accepts any single number
* or single string argument that is valid for the Date() constructor,
* or an epoch as a string.
*
* @param {string|number} epoch The representation of the Date.
*
* @returns {Date} The constructed Date.
*/
function createDate (epoch) {
// If epoch is already a valid argument, return the valid Date
let date = new Date(epoch)
if (isValidDate(date)) {
return date
}
// Convert to a number to permit epoch as a string
date = new Date(+epoch)
return date
}
@@ -0,0 +1,20 @@
'use strict'
const tap = require('tap')
const createDate = require('./create-date')
const wanted = 1624450038567
tap.test('accepts arguments the Date constructor would accept', async t => {
t.plan(2)
t.same(createDate(1624450038567).getTime(), wanted)
t.same(createDate('2021-06-23T12:07:18.567Z').getTime(), wanted)
})
tap.test('accepts epoch as a string', async t => {
// If Date() accepts this argument, the createDate function is not needed
// and can be replaced with Date()
t.plan(2)
t.notSame(new Date('16244500385-67').getTime(), wanted)
t.same(createDate('1624450038567').getTime(), wanted)
})
@@ -0,0 +1,28 @@
'use strict'
module.exports = deleteLogProperty
const getPropertyValue = require('./get-property-value')
const splitPropertyKey = require('./split-property-key')
/**
* Deletes a specified property from a log object if it exists.
* This function mutates the passed in `log` object.
*
* @param {object} log The log object to be modified.
* @param {string} property A string identifying the property to be deleted from
* the log object. Accepts nested properties delimited by a `.`
* Delimiter can be escaped to preserve property names that contain the delimiter.
* e.g. `'prop1.prop2'` or `'prop2\.domain\.corp.prop2'`
*/
function deleteLogProperty (log, property) {
const props = splitPropertyKey(property)
const propToDelete = props.pop()
log = getPropertyValue(log, props)
/* istanbul ignore else */
if (log !== null && typeof log === 'object' && Object.prototype.hasOwnProperty.call(log, propToDelete)) {
delete log[propToDelete]
}
}
@@ -0,0 +1,31 @@
'use strict'
const tap = require('tap')
const { createCopier } = require('fast-copy')
const fastCopy = createCopier({})
const deleteLogProperty = require('./delete-log-property')
const logData = {
level: 30,
data1: {
data2: { 'data-3': 'bar' }
}
}
tap.test('deleteLogProperty deletes property of depth 1', async t => {
const log = fastCopy(logData)
deleteLogProperty(log, 'data1')
t.same(log, { level: 30 })
})
tap.test('deleteLogProperty deletes property of depth 2', async t => {
const log = fastCopy(logData)
deleteLogProperty(log, 'data1.data2')
t.same(log, { level: 30, data1: { } })
})
tap.test('deleteLogProperty deletes property of depth 3', async t => {
const log = fastCopy(logData)
deleteLogProperty(log, 'data1.data2.data-3')
t.same(log, { level: 30, data1: { data2: { } } })
})
@@ -0,0 +1,45 @@
'use strict'
module.exports = filterLog
const { createCopier } = require('fast-copy')
const fastCopy = createCopier({})
const deleteLogProperty = require('./delete-log-property')
/**
* @typedef {object} FilterLogParams
* @property {object} log The log object to be modified.
* @property {PrettyContext} context The context object built from parsing
* the options.
*/
/**
* Filter a log object by removing or including keys accordingly.
* When `includeKeys` is passed, `ignoredKeys` will be ignored.
* One of ignoreKeys or includeKeys must be pass in.
*
* @param {FilterLogParams} input
*
* @returns {object} A new `log` object instance that
* either only includes the keys in ignoreKeys
* or does not include those in ignoredKeys.
*/
function filterLog ({ log, context }) {
const { ignoreKeys, includeKeys } = context
const logCopy = fastCopy(log)
if (includeKeys) {
const logIncluded = {}
includeKeys.forEach((key) => {
logIncluded[key] = logCopy[key]
})
return logIncluded
}
ignoreKeys.forEach((ignoreKey) => {
deleteLogProperty(logCopy, ignoreKey)
})
return logCopy
}
@@ -0,0 +1,190 @@
'use strict'
const tap = require('tap')
const filterLog = require('./filter-log')
const context = {
includeKeys: undefined,
ignoreKeys: undefined
}
const logData = {
level: 30,
time: 1522431328992,
data1: {
data2: { 'data-3': 'bar' },
error: new Error('test')
}
}
const logData2 = Object.assign({
'logging.domain.corp/operation': {
id: 'foo',
producer: 'bar'
}
}, logData)
tap.test('#filterLog with an ignoreKeys option', t => {
t.test('filterLog removes single entry', async t => {
const result = filterLog({
log: logData,
context: {
...context,
ignoreKeys: ['data1.data2.data-3']
}
})
t.same(result, { level: 30, time: 1522431328992, data1: { data2: { }, error: new Error('test') } })
})
t.test('filterLog removes multiple entries', async t => {
const result = filterLog({
log: logData,
context: {
...context,
ignoreKeys: ['time', 'data1']
}
})
t.same(result, { level: 30 })
})
t.test('filterLog keeps error instance', async t => {
const result = filterLog({
log: logData,
context: {
...context,
ignoreKeys: []
}
})
t.equal(logData.data1.error, result.data1.error)
})
t.test('filterLog removes entry with escape sequence', async t => {
const result = filterLog({
log: logData2,
context: {
...context,
ignoreKeys: ['data1', 'logging\\.domain\\.corp/operation']
}
})
t.same(result, { level: 30, time: 1522431328992 })
})
t.test('filterLog removes entry with escape sequence nested', async t => {
const result = filterLog({
log: logData2,
context: {
...context,
ignoreKeys: ['data1', 'logging\\.domain\\.corp/operation.producer']
}
})
t.same(result, { level: 30, time: 1522431328992, 'logging.domain.corp/operation': { id: 'foo' } })
})
t.end()
})
const ignoreKeysArray = [
undefined,
['level'],
['level', 'data1.data2.data-3']
]
ignoreKeysArray.forEach(ignoreKeys => {
tap.test(`#filterLog with an includeKeys option when the ignoreKeys being ${ignoreKeys}`, t => {
t.test('filterLog include nothing', async t => {
const result = filterLog({
log: logData,
context: {
...context,
ignoreKeys,
includeKeys: []
}
})
t.same(result, {})
})
t.test('filterLog include single entry', async t => {
const result = filterLog({
log: logData,
context: {
...context,
ignoreKeys,
includeKeys: ['time']
}
})
t.same(result, { time: 1522431328992 })
})
t.test('filterLog include multiple entries', async t => {
const result = filterLog({
log: logData,
context: {
...context,
ignoreKeys,
includeKeys: ['time', 'data1']
}
})
t.same(result, {
time: 1522431328992,
data1: {
data2: { 'data-3': 'bar' },
error: new Error('test')
}
})
})
t.end()
})
})
tap.test('#filterLog with circular references', t => {
const logData = {
level: 30,
time: 1522431328992,
data1: 'test'
}
logData.circular = logData
t.test('filterLog removes single entry', async t => {
const result = filterLog({
log: logData,
context: {
...context,
ignoreKeys: ['data1']
}
})
t.same(result.circular.level, result.level)
t.same(result.circular.time, result.time)
delete result.circular
t.same(result, { level: 30, time: 1522431328992 })
})
t.test('filterLog includes single entry', async t => {
const result = filterLog({
log: logData,
context: {
...context,
includeKeys: ['data1']
}
})
t.same(result, { data1: 'test' })
})
t.test('filterLog includes circular keys', async t => {
const result = filterLog({
log: logData,
context: {
...context,
includeKeys: ['level', 'circular']
}
})
t.same(result.circular.level, logData.level)
t.same(result.circular.time, logData.time)
delete result.circular
t.same(result, { level: 30 })
})
t.end()
})
@@ -0,0 +1,66 @@
'use strict'
module.exports = formatTime
const {
DATE_FORMAT,
DATE_FORMAT_SIMPLE
} = require('../constants')
const dateformat = require('dateformat')
const createDate = require('./create-date')
const isValidDate = require('./is-valid-date')
/**
* Converts a given `epoch` to a desired display format.
*
* @param {number|string} epoch The time to convert. May be any value that is
* valid for `new Date()`.
* @param {boolean|string} [translateTime=false] When `false`, the given `epoch`
* will simply be returned. When `true`, the given `epoch` will be converted
* to a string at UTC using the `DATE_FORMAT` constant. If `translateTime` is
* a string, the following rules are available:
*
* - `<format string>`: The string is a literal format string. This format
* string will be used to interpret the `epoch` and return a display string
* at UTC.
* - `SYS:STANDARD`: The returned display string will follow the `DATE_FORMAT`
* constant at the system's local timezone.
* - `SYS:<format string>`: The returned display string will follow the given
* `<format string>` at the system's local timezone.
* - `UTC:<format string>`: The returned display string will follow the given
* `<format string>` at UTC.
*
* @returns {number|string} The formatted time.
*/
function formatTime (epoch, translateTime = false) {
if (translateTime === false) {
return epoch
}
const instant = createDate(epoch)
// If the Date is invalid, do not attempt to format
if (!isValidDate(instant)) {
return epoch
}
if (translateTime === true) {
return dateformat(instant, DATE_FORMAT_SIMPLE)
}
const upperFormat = translateTime.toUpperCase()
if (upperFormat === 'SYS:STANDARD') {
return dateformat(instant, DATE_FORMAT)
}
const prefix = upperFormat.substr(0, 4)
if (prefix === 'SYS:' || prefix === 'UTC:') {
if (prefix === 'UTC:') {
return dateformat(instant, translateTime)
}
return dateformat(instant, translateTime.slice(4))
}
return dateformat(instant, `UTC:${translateTime}`)
}
@@ -0,0 +1,71 @@
'use strict'
process.env.TZ = 'UTC'
const tap = require('tap')
const formatTime = require('./format-time')
const dateStr = '2019-04-06T13:30:00.000-04:00'
const epoch = new Date(dateStr)
const epochMS = epoch.getTime()
tap.test('passes through epoch if `translateTime` is `false`', async t => {
const formattedTime = formatTime(epochMS)
t.equal(formattedTime, epochMS)
})
tap.test('passes through epoch if date is invalid', async t => {
const input = 'this is not a date'
const formattedTime = formatTime(input, true)
t.equal(formattedTime, input)
})
tap.test('translates epoch milliseconds if `translateTime` is `true`', async t => {
const formattedTime = formatTime(epochMS, true)
t.equal(formattedTime, '17:30:00.000')
})
tap.test('translates epoch milliseconds to UTC string given format', async t => {
const formattedTime = formatTime(epochMS, 'd mmm yyyy H:MM')
t.equal(formattedTime, '6 Apr 2019 17:30')
})
tap.test('translates epoch milliseconds to SYS:STANDARD', async t => {
const formattedTime = formatTime(epochMS, 'SYS:STANDARD')
t.match(formattedTime, /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [-+]?\d{4}/)
})
tap.test('translates epoch milliseconds to SYS:<FORMAT>', async t => {
const formattedTime = formatTime(epochMS, 'SYS:d mmm yyyy H:MM')
t.match(formattedTime, /\d{1} \w{3} \d{4} \d{1,2}:\d{2}/)
})
tap.test('passes through date string if `translateTime` is `false`', async t => {
const formattedTime = formatTime(dateStr)
t.equal(formattedTime, dateStr)
})
tap.test('translates date string if `translateTime` is `true`', async t => {
const formattedTime = formatTime(dateStr, true)
t.equal(formattedTime, '17:30:00.000')
})
tap.test('translates date string to UTC string given format', async t => {
const formattedTime = formatTime(dateStr, 'd mmm yyyy H:MM')
t.equal(formattedTime, '6 Apr 2019 17:30')
})
tap.test('translates date string to SYS:STANDARD', async t => {
const formattedTime = formatTime(dateStr, 'SYS:STANDARD')
t.match(formattedTime, /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [-+]?\d{4}/)
})
tap.test('translates date string to UTC:<FORMAT>', async t => {
const formattedTime = formatTime(dateStr, 'UTC:d mmm yyyy H:MM')
t.equal(formattedTime, '6 Apr 2019 17:30')
})
tap.test('translates date string to SYS:<FORMAT>', async t => {
const formattedTime = formatTime(dateStr, 'SYS:d mmm yyyy H:MM')
t.match(formattedTime, /\d{1} \w{3} \d{4} \d{1,2}:\d{2}/)
})
@@ -0,0 +1,30 @@
'use strict'
module.exports = getPropertyValue
const splitPropertyKey = require('./split-property-key')
/**
* Gets a specified property from an object if it exists.
*
* @param {object} obj The object to be searched.
* @param {string|string[]} property A string, or an array of strings, identifying
* the property to be retrieved from the object.
* Accepts nested properties delimited by a `.`.
* Delimiter can be escaped to preserve property names that contain the delimiter.
* e.g. `'prop1.prop2'` or `'prop2\.domain\.corp.prop2'`.
*
* @returns {*}
*/
function getPropertyValue (obj, property) {
const props = Array.isArray(property) ? property : splitPropertyKey(property)
for (const prop of props) {
if (!Object.prototype.hasOwnProperty.call(obj, prop)) {
return
}
obj = obj[prop]
}
return obj
}
@@ -0,0 +1,31 @@
'use strict'
const tap = require('tap')
const getPropertyValue = require('./get-property-value')
tap.test('getPropertyValue returns the value of the property', async t => {
const result = getPropertyValue({
foo: 'bar'
}, 'foo')
t.same(result, 'bar')
})
tap.test('getPropertyValue returns the value of the nested property', async t => {
const result = getPropertyValue({ extra: { foo: { value: 'bar' } } }, 'extra.foo.value')
t.same(result, 'bar')
})
tap.test('getPropertyValue returns the value of the nested property using the array of nested property keys', async t => {
const result = getPropertyValue({ extra: { foo: { value: 'bar' } } }, ['extra', 'foo', 'value'])
t.same(result, 'bar')
})
tap.test('getPropertyValue returns undefined for non-existing properties', async t => {
const result = getPropertyValue({ extra: { foo: { value: 'bar' } } }, 'extra.foo.value-2')
t.same(result, undefined)
})
tap.test('getPropertyValue returns undefined for non-existing properties using the array of nested property keys', async t => {
const result = getPropertyValue({ extra: { foo: { value: 'bar' } } }, ['extra', 'foo', 'value-2'])
t.same(result, undefined)
})
@@ -0,0 +1,38 @@
'use strict'
module.exports = handleCustomLevelsNamesOpts
/**
* Parse a CSV string or options object that maps level
* labels to level values.
*
* @param {string|object} cLevels An object mapping level
* names to level values, e.g. `{ info: 30, debug: 65 }`, or a
* CSV string in the format `level_name:level_value`, e.g.
* `info:30,debug:65`.
*
* @returns {object} An object mapping levels names to level values
* e.g. `{ info: 30, debug: 65 }`.
*/
function handleCustomLevelsNamesOpts (cLevels) {
if (!cLevels) return {}
if (typeof cLevels === 'string') {
return cLevels
.split(',')
.reduce((agg, value, idx) => {
const [levelName, levelNum = idx] = value.split(':')
agg[levelName.toLowerCase()] = levelNum
return agg
}, {})
} else if (Object.prototype.toString.call(cLevels) === '[object Object]') {
return Object
.keys(cLevels)
.reduce((agg, levelName) => {
agg[levelName.toLowerCase()] = cLevels[levelName]
return agg
}, {})
} else {
return {}
}
}
@@ -0,0 +1,44 @@
'use strict'
const tap = require('tap')
const handleCustomLevelsNamesOpts = require('./handle-custom-levels-names-opts')
tap.test('returns a empty object `{}` for undefined parameter', async t => {
const handledCustomLevelNames = handleCustomLevelsNamesOpts()
t.same(handledCustomLevelNames, {})
})
tap.test('returns a empty object `{}` for unknown parameter', async t => {
const handledCustomLevelNames = handleCustomLevelsNamesOpts(123)
t.same(handledCustomLevelNames, {})
})
tap.test('returns a filled object for string parameter', async t => {
const handledCustomLevelNames = handleCustomLevelsNamesOpts('ok:10,warn:20,error:35')
t.same(handledCustomLevelNames, {
ok: 10,
warn: 20,
error: 35
})
})
tap.test('returns a filled object for object parameter', async t => {
const handledCustomLevelNames = handleCustomLevelsNamesOpts({
ok: 10,
warn: 20,
error: 35
})
t.same(handledCustomLevelNames, {
ok: 10,
warn: 20,
error: 35
})
})
tap.test('defaults missing level num to first index', async t => {
const result = handleCustomLevelsNamesOpts('ok:10,info')
t.same(result, {
ok: 10,
info: 1
})
})
@@ -0,0 +1,39 @@
'use strict'
module.exports = handleCustomLevelsOpts
/**
* Parse a CSV string or options object that specifies
* configuration for custom levels.
*
* @param {string|object} cLevels An object mapping level
* names to values, e.g. `{ info: 30, debug: 65 }`, or a
* CSV string in the format `level_name:level_value`, e.g.
* `info:30,debug:65`.
*
* @returns {object} An object mapping levels to labels that
* appear in logs, e.g. `{ '30': 'INFO', '65': 'DEBUG' }`.
*/
function handleCustomLevelsOpts (cLevels) {
if (!cLevels) return {}
if (typeof cLevels === 'string') {
return cLevels
.split(',')
.reduce((agg, value, idx) => {
const [levelName, levelNum = idx] = value.split(':')
agg[levelNum] = levelName.toUpperCase()
return agg
},
{ default: 'USERLVL' })
} else if (Object.prototype.toString.call(cLevels) === '[object Object]') {
return Object
.keys(cLevels)
.reduce((agg, levelName) => {
agg[cLevels[levelName]] = levelName.toUpperCase()
return agg
}, { default: 'USERLVL' })
} else {
return {}
}
}
@@ -0,0 +1,47 @@
'use strict'
const tap = require('tap')
const handleCustomLevelsOpts = require('./handle-custom-levels-opts')
tap.test('returns a empty object `{}` for undefined parameter', async t => {
const handledCustomLevel = handleCustomLevelsOpts()
t.same(handledCustomLevel, {})
})
tap.test('returns a empty object `{}` for unknown parameter', async t => {
const handledCustomLevel = handleCustomLevelsOpts(123)
t.same(handledCustomLevel, {})
})
tap.test('returns a filled object for string parameter', async t => {
const handledCustomLevel = handleCustomLevelsOpts('ok:10,warn:20,error:35')
t.same(handledCustomLevel, {
10: 'OK',
20: 'WARN',
35: 'ERROR',
default: 'USERLVL'
})
})
tap.test('returns a filled object for object parameter', async t => {
const handledCustomLevel = handleCustomLevelsOpts({
ok: 10,
warn: 20,
error: 35
})
t.same(handledCustomLevel, {
10: 'OK',
20: 'WARN',
35: 'ERROR',
default: 'USERLVL'
})
})
tap.test('defaults missing level num to first index', async t => {
const result = handleCustomLevelsOpts('ok:10,info')
t.same(result, {
10: 'OK',
1: 'INFO',
default: 'USERLVL'
})
})
+99
View File
@@ -0,0 +1,99 @@
'use strict'
module.exports = {
buildSafeSonicBoom: require('./build-safe-sonic-boom.js'),
createDate: require('./create-date.js'),
deleteLogProperty: require('./delete-log-property.js'),
filterLog: require('./filter-log.js'),
formatTime: require('./format-time.js'),
getPropertyValue: require('./get-property-value.js'),
handleCustomLevelsNamesOpts: require('./handle-custom-levels-names-opts.js'),
handleCustomLevelsOpts: require('./handle-custom-levels-opts.js'),
interpretConditionals: require('./interpret-conditionals.js'),
isObject: require('./is-object.js'),
isValidDate: require('./is-valid-date.js'),
joinLinesWithIndentation: require('./join-lines-with-indentation.js'),
noop: require('./noop.js'),
parseFactoryOptions: require('./parse-factory-options.js'),
prettifyErrorLog: require('./prettify-error-log.js'),
prettifyError: require('./prettify-error.js'),
prettifyLevel: require('./prettify-level.js'),
prettifyMessage: require('./prettify-message.js'),
prettifyMetadata: require('./prettify-metadata.js'),
prettifyObject: require('./prettify-object.js'),
prettifyTime: require('./prettify-time.js'),
splitPropertyKey: require('./split-property-key.js')
}
// The remainder of this file consists of jsdoc blocks that are difficult to
// determine a more appropriate "home" for. As an example, the blocks associated
// with custom prettifiers could live in either the `prettify-level`,
// `prettify-metadata`, or `prettify-time` files since they are the primary
// files where such code is used. But we want a central place to define common
// doc blocks, so we are picking this file as the answer.
/**
* A hash of log property names mapped to prettifier functions. When the
* incoming log data is being processed for prettification, any key on the log
* that matches a key in a custom prettifiers hash will be prettified using
* that matching custom prettifier. The value passed to the custom prettifier
* will the value associated with the corresponding log key.
*
* The hash may contain any arbitrary keys for arbitrary log properties, but it
* may also contain a set of predefined key names that map to well-known log
* properties. These keys are:
*
* + `time` (for the timestamp field)
* + `level` (for the level label field; value may be a level number instead
* of a level label)
* + `hostname`
* + `pid`
* + `name`
* + `caller`
*
* @typedef {Object.<string, CustomPrettifierFunc>} CustomPrettifiers
*/
/**
* A synchronous function to be used for prettifying a log property. It must
* return a string.
*
* @typedef {function} CustomPrettifierFunc
* @param {any} value The value to be prettified for the key associated with
* the prettifier.
* @returns {string}
*/
/**
* A tokenized string that indicates how the prettified log line should be
* formatted. Tokens are either log properties enclosed in curly braces, e.g.
* `{levelLabel}`, `{pid}`, or `{req.url}`, or conditional directives in curly
* braces. The only conditional directives supported are `if` and `end`, e.g.
* `{if pid}{pid}{end}`; every `if` must have a matching `end`. Nested
* conditions are not supported.
*
* @typedef {string} MessageFormatString
*
* @example
* `{levelLabel} - {if pid}{pid} - {end}url:{req.url}`
*/
/**
* A function that accepts a log object, name of the message key, and name of
* the level label key and returns a formatted log line.
*
* Note: this function must be synchronous.
*
* @typedef {function} MessageFormatFunction
* @param {object} log The log object to be processed.
* @param {string} messageKey The name of the key in the `log` object that
* contains the log message.
* @param {string} levelLabel The name of the key in the `log` object that
* contains the log level name.
* @returns {string}
*
* @example
* function (log, messageKey, levelLabel) {
* return `${log[levelLabel]} - ${log[messageKey]}`
* }
*/
@@ -0,0 +1,37 @@
'use strict'
const tap = require('tap')
const index = require('./index.js')
const { readdirSync } = require('fs')
const { basename } = require('path')
tap.test(
'index exports exactly all non-test files excluding itself',
async t => {
// Read all files in the `util` directory
const files = readdirSync(__dirname)
for (const file of files) {
const kebabName = basename(file, '.js')
const snakeName = kebabName.split('-').map((part, idx) => {
if (idx === 0) return part
return part[0].toUpperCase() + part.slice(1)
}).join('')
if (file.endsWith('.test.js') === false && file !== 'index.js') {
// We expect all files to be exported except…
t.ok(index[snakeName], `exports ${snakeName}`)
} else {
// …test files and the index file itself those must not be exported
t.notOk(index[snakeName], `does not export ${snakeName}`)
}
// Remove the exported file from the index object
delete index[snakeName]
}
// Now the index is expected to be empty, as nothing else should be
// exported from it
t.same(index, {}, 'does not export anything else')
}
)
@@ -0,0 +1,37 @@
'use strict'
module.exports = interpretConditionals
const getPropertyValue = require('./get-property-value')
/**
* Translates all conditional blocks from within the messageFormat. Translates
* any matching {if key}{key}{end} statements and returns everything between
* if and else blocks if the key provided was found in log.
*
* @param {MessageFormatString|MessageFormatFunction} messageFormat A format
* string or function that defines how the logged message should be
* conditionally formatted.
* @param {object} log The log object to be modified.
*
* @returns {string} The parsed messageFormat.
*/
function interpretConditionals (messageFormat, log) {
messageFormat = messageFormat.replace(/{if (.*?)}(.*?){end}/g, replacer)
// Remove non-terminated if blocks
messageFormat = messageFormat.replace(/{if (.*?)}/g, '')
// Remove floating end blocks
messageFormat = messageFormat.replace(/{end}/g, '')
return messageFormat.replace(/\s+/g, ' ').trim()
function replacer (_, key, value) {
const propertyValue = getPropertyValue(log, key)
if (propertyValue && value.includes(key)) {
return value.replace(new RegExp('{' + key + '}', 'g'), propertyValue)
} else {
return ''
}
}
}
@@ -0,0 +1,69 @@
'use strict'
const tap = require('tap')
const { createCopier } = require('fast-copy')
const fastCopy = createCopier({})
const interpretConditionals = require('./interpret-conditionals')
const logData = {
level: 30,
data1: {
data2: 'bar'
},
msg: 'foo'
}
tap.test('interpretConditionals translates if / else statement to found property value', async t => {
const log = fastCopy(logData)
t.equal(interpretConditionals('{level} - {if data1.data2}{data1.data2}{end}', log), '{level} - bar')
})
tap.test('interpretConditionals translates if / else statement to found property value and leave unmatched property key untouched', async t => {
const log = fastCopy(logData)
t.equal(interpretConditionals('{level} - {if data1.data2}{data1.data2} ({msg}){end}', log), '{level} - bar ({msg})')
})
tap.test('interpretConditionals removes non-terminated if statements', async t => {
const log = fastCopy(logData)
t.equal(interpretConditionals('{level} - {if data1.data2}{data1.data2}', log), '{level} - {data1.data2}')
})
tap.test('interpretConditionals removes floating end statements', async t => {
const log = fastCopy(logData)
t.equal(interpretConditionals('{level} - {data1.data2}{end}', log), '{level} - {data1.data2}')
})
tap.test('interpretConditionals removes floating end statements within translated if / end statements', async t => {
const log = fastCopy(logData)
t.equal(interpretConditionals('{level} - {if msg}({msg}){end}{end}', log), '{level} - (foo)')
})
tap.test('interpretConditionals removes if / end blocks if existent condition key does not match existent property key', async t => {
const log = fastCopy(logData)
t.equal(interpretConditionals('{level}{if msg}{data1.data2}{end}', log), '{level}')
})
tap.test('interpretConditionals removes if / end blocks if non-existent condition key does not match existent property key', async t => {
const log = fastCopy(logData)
t.equal(interpretConditionals('{level}{if foo}{msg}{end}', log), '{level}')
})
tap.test('interpretConditionals removes if / end blocks if existent condition key does not match non-existent property key', async t => {
const log = fastCopy(logData)
t.equal(interpretConditionals('{level}{if msg}{foo}{end}', log), '{level}')
})
tap.test('interpretConditionals removes if / end blocks if non-existent condition key does not match non-existent property key', async t => {
const log = fastCopy(logData)
t.equal(interpretConditionals('{level}{if foo}{bar}{end}', log), '{level}')
})
tap.test('interpretConditionals removes if / end blocks if nested condition key does not match property key', async t => {
const log = fastCopy(logData)
t.equal(interpretConditionals('{level}{if data1.msg}{data1.data2}{end}', log), '{level}')
})
tap.test('interpretConditionals removes nested if / end statement blocks', async t => {
const log = fastCopy(logData)
t.equal(interpretConditionals('{if msg}{if data1.data2}{msg}{data1.data2}{end}{end}', log), 'foo{data1.data2}')
})
@@ -0,0 +1,7 @@
'use strict'
module.exports = isObject
function isObject (input) {
return Object.prototype.toString.apply(input) === '[object Object]'
}
@@ -0,0 +1,10 @@
'use strict'
const tap = require('tap')
const isObject = require('./is-object')
tap.test('returns correct answer', async t => {
t.equal(isObject({}), true)
t.equal(isObject([]), false)
t.equal(isObject(42), false)
})
@@ -0,0 +1,14 @@
'use strict'
module.exports = isValidDate
/**
* Checks if the argument is a JS Date and not 'Invalid Date'.
*
* @param {Date} date The date to check.
*
* @returns {boolean} true if the argument is a JS Date and not 'Invalid Date'.
*/
function isValidDate (date) {
return date instanceof Date && !Number.isNaN(date.getTime())
}
@@ -0,0 +1,16 @@
'use strict'
process.env.TZ = 'UTC'
const tap = require('tap')
const isValidDate = require('./is-valid-date')
tap.test('returns true for valid dates', async t => {
t.same(isValidDate(new Date()), true)
})
tap.test('returns false for non-dates and invalid dates', async t => {
t.plan(2)
t.same(isValidDate('20210621'), false)
t.same(isValidDate(new Date('2021-41-99')), false)
})
@@ -0,0 +1,29 @@
'use strict'
module.exports = joinLinesWithIndentation
/**
* @typedef {object} JoinLinesWithIndentationParams
* @property {string} input The string to split and reformat.
* @property {string} [ident] The indentation string. Default: ` ` (4 spaces).
* @property {string} [eol] The end of line sequence to use when rejoining
* the lines. Default: `'\n'`.
*/
/**
* Given a string with line separators, either `\r\n` or `\n`, add indentation
* to all lines subsequent to the first line and rejoin the lines using an
* end of line sequence.
*
* @param {JoinLinesWithIndentationParams} input
*
* @returns {string} A string with lines subsequent to the first indented
* with the given indentation sequence.
*/
function joinLinesWithIndentation ({ input, ident = ' ', eol = '\n' }) {
const lines = input.split(/\r?\n/)
for (let i = 1; i < lines.length; i += 1) {
lines[i] = ident + lines[i]
}
return lines.join(eol)
}
@@ -0,0 +1,16 @@
'use strict'
const tap = require('tap')
const joinLinesWithIndentation = require('./join-lines-with-indentation')
tap.test('joinLinesWithIndentation adds indentation to beginning of subsequent lines', async t => {
const input = 'foo\nbar\nbaz'
const result = joinLinesWithIndentation({ input })
t.equal(result, 'foo\n bar\n baz')
})
tap.test('joinLinesWithIndentation accepts custom indentation, line breaks, and eol', async t => {
const input = 'foo\nbar\r\nbaz'
const result = joinLinesWithIndentation({ input, ident: ' ', eol: '^' })
t.equal(result, 'foo^ bar^ baz')
})
+3
View File
@@ -0,0 +1,3 @@
'use strict'
module.exports = function noop () {}
@@ -0,0 +1,12 @@
'use strict'
const tap = require('tap')
const noop = require('./noop')
tap.test('is a function', async t => {
t.type(noop, Function)
})
tap.test('does nothing', async t => {
t.equal(noop('stuff'), undefined)
})
@@ -0,0 +1,153 @@
'use strict'
module.exports = parseFactoryOptions
const {
LEVEL_NAMES
} = require('../constants')
const colors = require('../colors')
const handleCustomLevelsOpts = require('./handle-custom-levels-opts')
const handleCustomLevelsNamesOpts = require('./handle-custom-levels-names-opts')
/**
* A `PrettyContext` is an object to be used by the various functions that
* process log data. It is derived from the provided {@link PinoPrettyOptions}.
* It may be used as a `this` context.
*
* @typedef {object} PrettyContext
* @property {string} EOL The escape sequence chosen as the line terminator.
* @property {string} IDENT The string to use as the indentation sequence.
* @property {ColorizerFunc} colorizer A configured colorizer function.
* @property {Array[Array<number, string>]} customColors A set of custom color
* names associated with level numbers.
* @property {object} customLevelNames A hash of level numbers to level names,
* e.g. `{ 30: "info" }`.
* @property {object} customLevels A hash of level names to level numbers,
* e.g. `{ info: 30 }`.
* @property {CustomPrettifiers} customPrettifiers A hash of custom prettifier
* functions.
* @property {object} customProperties Comprised of `customLevels` and
* `customLevelNames` if such options are provided.
* @property {string[]} errorLikeObjectKeys The key names in the log data that
* should be considered as holding error objects.
* @property {string[]} errorProps A list of error object keys that should be
* included in the output.
* @property {boolean} hideObject Indicates the prettifier should omit objects
* in the output.
* @property {string[]} ignoreKeys Set of log data keys to omit.
* @property {string[]} includeKeys Opposite of `ignoreKeys`.
* @property {boolean} levelFirst Indicates the level should be printed first.
* @property {string} levelKey Name of the key in the log data that contains
* the message.
* @property {string} levelLabel Format token to represent the position of the
* level name in the output string.
* @property {MessageFormatString|MessageFormatFunction} messageFormat
* @property {string} messageKey Name of the key in the log data that contains
* the message.
* @property {string|number} minimumLevel The minimum log level to process
* and output.
* @property {ColorizerFunc} objectColorizer
* @property {boolean} singleLine Indicates objects should be printed on a
* single output line.
* @property {string} timestampKey The name of the key in the log data that
* contains the log timestamp.
* @property {boolean} translateTime Indicates if timestamps should be
* translated to a human-readable string.
* @property {boolean} useOnlyCustomProps
*/
/**
* @param {PinoPrettyOptions} options The user supplied object of options.
*
* @returns {PrettyContext}
*/
function parseFactoryOptions (options) {
const EOL = options.crlf ? '\r\n' : '\n'
const IDENT = ' '
const {
customPrettifiers,
errorLikeObjectKeys,
hideObject,
levelFirst,
levelKey,
levelLabel,
messageFormat,
messageKey,
minimumLevel,
singleLine,
timestampKey,
translateTime
} = options
const errorProps = options.errorProps.split(',')
const useOnlyCustomProps = typeof options.useOnlyCustomProps === 'boolean'
? options.useOnlyCustomProps
: (options.useOnlyCustomProps === 'true')
const customLevels = handleCustomLevelsOpts(options.customLevels)
const customLevelNames = handleCustomLevelsNamesOpts(options.customLevels)
let customColors
if (options.customColors) {
customColors = options.customColors.split(',').reduce((agg, value) => {
const [level, color] = value.split(':')
const condition = useOnlyCustomProps
? options.customLevels
: customLevelNames[level] !== undefined
const levelNum = condition
? customLevelNames[level]
: LEVEL_NAMES[level]
const colorIdx = levelNum !== undefined
? levelNum
: level
agg.push([colorIdx, color])
return agg
}, [])
}
const customProperties = { customLevels, customLevelNames }
if (useOnlyCustomProps === true && !options.customLevels) {
customProperties.customLevels = undefined
customProperties.customLevelNames = undefined
}
const includeKeys = options.include !== undefined
? new Set(options.include.split(','))
: undefined
const ignoreKeys = (!includeKeys && options.ignore)
? new Set(options.ignore.split(','))
: undefined
const colorizer = colors(options.colorize, customColors, useOnlyCustomProps)
const objectColorizer = options.colorizeObjects
? colorizer
: colors(false, [], false)
return {
EOL,
IDENT,
colorizer,
customColors,
customLevelNames,
customLevels,
customPrettifiers,
customProperties,
errorLikeObjectKeys,
errorProps,
hideObject,
ignoreKeys,
includeKeys,
levelFirst,
levelKey,
levelLabel,
messageFormat,
messageKey,
minimumLevel,
objectColorizer,
singleLine,
timestampKey,
translateTime,
useOnlyCustomProps
}
}
@@ -0,0 +1,73 @@
'use strict'
module.exports = prettifyErrorLog
const {
LOGGER_KEYS
} = require('../constants')
const isObject = require('./is-object')
const joinLinesWithIndentation = require('./join-lines-with-indentation')
const prettifyObject = require('./prettify-object')
/**
* @typedef {object} PrettifyErrorLogParams
* @property {object} log The error log to prettify.
* @property {PrettyContext} context The context object built from parsing
* the options.
*/
/**
* Given a log object that has a `type: 'Error'` key, prettify the object and
* return the result. In other
*
* @param {PrettifyErrorLogParams} input
*
* @returns {string} A string that represents the prettified error log.
*/
function prettifyErrorLog ({ log, context }) {
const {
EOL: eol,
IDENT: ident,
errorProps: errorProperties,
messageKey
} = context
const stack = log.stack
const joinedLines = joinLinesWithIndentation({ input: stack, ident, eol })
let result = `${ident}${joinedLines}${eol}`
if (errorProperties.length > 0) {
const excludeProperties = LOGGER_KEYS.concat(messageKey, 'type', 'stack')
let propertiesToPrint
if (errorProperties[0] === '*') {
// Print all sibling properties except for the standard exclusions.
propertiesToPrint = Object.keys(log).filter(k => excludeProperties.includes(k) === false)
} else {
// Print only specified properties unless the property is a standard exclusion.
propertiesToPrint = errorProperties.filter(k => excludeProperties.includes(k) === false)
}
for (let i = 0; i < propertiesToPrint.length; i += 1) {
const key = propertiesToPrint[i]
if (key in log === false) continue
if (isObject(log[key])) {
// The nested object may have "logger" type keys but since they are not
// at the root level of the object being processed, we want to print them.
// Thus, we invoke with `excludeLoggerKeys: false`.
const prettifiedObject = prettifyObject({
log: log[key],
excludeLoggerKeys: false,
context: {
...context,
IDENT: ident + ident
}
})
result = `${result}${ident}${key}: {${eol}${prettifiedObject}${ident}}${eol}`
continue
}
result = `${result}${ident}${key}: ${log[key]}${eol}`
}
}
return result
}
@@ -0,0 +1,110 @@
'use strict'
const tap = require('tap')
const prettifyErrorLog = require('./prettify-error-log')
const {
ERROR_LIKE_KEYS,
MESSAGE_KEY
} = require('../constants')
const context = {
EOL: '\n',
IDENT: ' ',
customPrettifiers: {},
errorLikeObjectKeys: ERROR_LIKE_KEYS,
errorProps: [],
messageKey: MESSAGE_KEY
}
tap.test('returns string with default settings', async t => {
const err = Error('Something went wrong')
const str = prettifyErrorLog({ log: err, context })
t.ok(str.startsWith(' Error: Something went wrong'))
})
tap.test('returns string with custom ident', async t => {
const err = Error('Something went wrong')
const str = prettifyErrorLog({
log: err,
context: {
...context,
IDENT: ' '
}
})
t.ok(str.startsWith(' Error: Something went wrong'))
})
tap.test('returns string with custom eol', async t => {
const err = Error('Something went wrong')
const str = prettifyErrorLog({
log: err,
context: {
...context,
EOL: '\r\n'
}
})
t.ok(str.startsWith(' Error: Something went wrong\r\n'))
})
tap.test('errorProperties', t => {
t.test('excludes all for wildcard', async t => {
const err = Error('boom')
err.foo = 'foo'
const str = prettifyErrorLog({
log: err,
context: {
...context,
errorProps: ['*']
}
})
t.ok(str.startsWith(' Error: boom'))
t.equal(str.includes('foo: "foo"'), false)
})
t.test('excludes only selected properties', async t => {
const err = Error('boom')
err.foo = 'foo'
const str = prettifyErrorLog({
log: err,
context: {
...context,
errorProps: ['foo']
}
})
t.ok(str.startsWith(' Error: boom'))
t.equal(str.includes('foo: foo'), true)
})
t.test('ignores specified properties if not present', async t => {
const err = Error('boom')
err.foo = 'foo'
const str = prettifyErrorLog({
log: err,
context: {
...context,
errorProps: ['foo', 'bar']
}
})
t.ok(str.startsWith(' Error: boom'))
t.equal(str.includes('foo: foo'), true)
t.equal(str.includes('bar'), false)
})
t.test('processes nested objects', async t => {
const err = Error('boom')
err.foo = { bar: 'bar', message: 'included' }
const str = prettifyErrorLog({
log: err,
context: {
...context,
errorProps: ['foo']
}
})
t.ok(str.startsWith(' Error: boom'))
t.equal(str.includes('foo: {'), true)
t.equal(str.includes('bar: "bar"'), true)
t.equal(str.includes('message: "included"'), true)
})
t.end()
})
@@ -0,0 +1,49 @@
'use strict'
module.exports = prettifyError
const joinLinesWithIndentation = require('./join-lines-with-indentation')
/**
* @typedef {object} PrettifyErrorParams
* @property {string} keyName The key assigned to this error in the log object.
* @property {string} lines The STRINGIFIED error. If the error field has a
* custom prettifier, that should be pre-applied as well.
* @property {string} ident The indentation sequence to use.
* @property {string} eol The EOL sequence to use.
*/
/**
* Prettifies an error string into a multi-line format.
*
* @param {PrettifyErrorParams} input
*
* @returns {string}
*/
function prettifyError ({ keyName, lines, eol, ident }) {
let result = ''
const joinedLines = joinLinesWithIndentation({ input: lines, ident, eol })
const splitLines = `${ident}${keyName}: ${joinedLines}${eol}`.split(eol)
for (let j = 0; j < splitLines.length; j += 1) {
if (j !== 0) result += eol
const line = splitLines[j]
if (/^\s*"stack"/.test(line)) {
const matches = /^(\s*"stack":)\s*(".*"),?$/.exec(line)
/* istanbul ignore else */
if (matches && matches.length === 3) {
const indentSize = /^\s*/.exec(line)[0].length + 4
const indentation = ' '.repeat(indentSize)
const stackMessage = matches[2]
result += matches[1] + eol + indentation + JSON.parse(stackMessage).replace(/\n/g, eol + indentation)
} else {
result += line
}
} else {
result += line
}
}
return result
}
@@ -0,0 +1,14 @@
'use strict'
const tap = require('tap')
const stringifySafe = require('fast-safe-stringify')
const prettifyError = require('./prettify-error')
tap.test('prettifies error', t => {
const error = Error('Bad error!')
const lines = stringifySafe(error, Object.getOwnPropertyNames(error), 2)
const prettyError = prettifyError({ keyName: 'errorKey', lines, ident: ' ', eol: '\n' })
t.match(prettyError, /\s*errorKey: {\n\s*"stack":[\s\S]*"message": "Bad error!"/)
t.end()
})
@@ -0,0 +1,35 @@
'use strict'
module.exports = prettifyLevel
const getPropertyValue = require('./get-property-value')
/**
* @typedef {object} PrettifyLevelParams
* @property {object} log The log object.
* @property {PrettyContext} context The context object built from parsing
* the options.
*/
/**
* Checks if the passed in log has a `level` value and returns a prettified
* string for that level if so.
*
* @param {PrettifyLevelParams} input
*
* @returns {undefined|string} If `log` does not have a `level` property then
* `undefined` will be returned. Otherwise, a string from the specified
* `colorizer` is returned.
*/
function prettifyLevel ({ log, context }) {
const {
colorizer,
customLevels,
customLevelNames,
levelKey
} = context
const prettifier = context.customPrettifiers?.level
const output = getPropertyValue(log, levelKey)
if (output === undefined) return undefined
return prettifier ? prettifier(output) : colorizer(output, { customLevels, customLevelNames })
}
@@ -0,0 +1,68 @@
'use strict'
const tap = require('tap')
const prettifyLevel = require('./prettify-level')
const getColorizer = require('../colors')
const {
LEVEL_KEY
} = require('../constants')
const context = {
colorizer: getColorizer(),
customLevelNames: undefined,
customLevels: undefined,
levelKey: LEVEL_KEY,
customPrettifiers: undefined
}
tap.test('returns `undefined` for unknown level', async t => {
const colorized = prettifyLevel({
log: {},
context: {
...context
}
})
t.equal(colorized, undefined)
})
tap.test('returns non-colorized value for default colorizer', async t => {
const log = {
level: 30
}
const colorized = prettifyLevel({
log,
context: {
...context
}
})
t.equal(colorized, 'INFO')
})
tap.test('returns colorized value for color colorizer', async t => {
const log = {
level: 30
}
const colorizer = getColorizer(true)
const colorized = prettifyLevel({
log,
context: {
...context,
colorizer
}
})
t.equal(colorized, '\u001B[32mINFO\u001B[39m')
})
tap.test('passes output through provided prettifier', async t => {
const log = {
level: 30
}
const colorized = prettifyLevel({
log,
context: {
...context,
customPrettifiers: { level () { return 'modified' } }
}
})
t.equal(colorized, 'modified')
})
@@ -0,0 +1,63 @@
'use strict'
module.exports = prettifyMessage
const {
LEVELS
} = require('../constants')
const getPropertyValue = require('./get-property-value')
const interpretConditionals = require('./interpret-conditionals')
/**
* @typedef {object} PrettifyMessageParams
* @property {object} log The log object with the message to colorize.
* @property {PrettyContext} context The context object built from parsing
* the options.
*/
/**
* Prettifies a message string if the given `log` has a message property.
*
* @param {PrettifyMessageParams} input
*
* @returns {undefined|string} If the message key is not found, or the message
* key is not a string, then `undefined` will be returned. Otherwise, a string
* that is the prettified message.
*/
function prettifyMessage ({ log, context }) {
const {
colorizer,
customLevels,
levelKey,
levelLabel,
messageFormat,
messageKey,
useOnlyCustomProps
} = context
if (messageFormat && typeof messageFormat === 'string') {
const parsedMessageFormat = interpretConditionals(messageFormat, log)
const message = String(parsedMessageFormat).replace(
/{([^{}]+)}/g,
function (match, p1) {
// return log level as string instead of int
let level
if (p1 === levelLabel && (level = getPropertyValue(log, levelKey)) !== undefined) {
const condition = useOnlyCustomProps ? customLevels === undefined : customLevels[level] === undefined
return condition ? LEVELS[level] : customLevels[level]
}
// Parse nested key access, e.g. `{keyA.subKeyB}`.
return getPropertyValue(log, p1) || ''
})
return colorizer.message(message)
}
if (messageFormat && typeof messageFormat === 'function') {
const msg = messageFormat(log, messageKey, levelLabel)
return colorizer.message(msg)
}
if (messageKey in log === false) return undefined
if (typeof log[messageKey] !== 'string' && typeof log[messageKey] !== 'number' && typeof log[messageKey] !== 'boolean') return undefined
return colorizer.message(log[messageKey])
}
@@ -0,0 +1,187 @@
'use strict'
const tap = require('tap')
const prettifyMessage = require('./prettify-message')
const getColorizer = require('../colors')
const {
LEVEL_KEY,
LEVEL_LABEL
} = require('../constants')
const context = {
colorizer: getColorizer(),
levelKey: LEVEL_KEY,
levelLabel: LEVEL_LABEL,
messageKey: 'msg'
}
tap.test('returns `undefined` if `messageKey` not found', async t => {
const str = prettifyMessage({ log: {}, context })
t.equal(str, undefined)
})
tap.test('returns `undefined` if `messageKey` not string', async t => {
const str = prettifyMessage({ log: { msg: {} }, context })
t.equal(str, undefined)
})
tap.test('returns non-colorized value for default colorizer', async t => {
const colorizer = getColorizer()
const str = prettifyMessage({
log: { msg: 'foo' },
context: { ...context, colorizer }
})
t.equal(str, 'foo')
})
tap.test('returns non-colorized value for alternate `messageKey`', async t => {
const str = prettifyMessage({
log: { message: 'foo' },
context: { ...context, messageKey: 'message' }
})
t.equal(str, 'foo')
})
tap.test('returns colorized value for color colorizer', async t => {
const colorizer = getColorizer(true)
const str = prettifyMessage({
log: { msg: 'foo' },
context: { ...context, colorizer }
})
t.equal(str, '\u001B[36mfoo\u001B[39m')
})
tap.test('returns colorized value for color colorizer for alternate `messageKey`', async t => {
const colorizer = getColorizer(true)
const str = prettifyMessage({
log: { message: 'foo' },
context: { ...context, messageKey: 'message', colorizer }
})
t.equal(str, '\u001B[36mfoo\u001B[39m')
})
tap.test('returns message formatted by `messageFormat` option', async t => {
const str = prettifyMessage({
log: { msg: 'foo', context: 'appModule' },
context: { ...context, messageFormat: '{context} - {msg}' }
})
t.equal(str, 'appModule - foo')
})
tap.test('returns message formatted by `messageFormat` option - missing prop', async t => {
const str = prettifyMessage({
log: { context: 'appModule' },
context: { ...context, messageFormat: '{context} - {msg}' }
})
t.equal(str, 'appModule - ')
})
tap.test('returns message formatted by `messageFormat` option - levelLabel & useOnlyCustomProps false', async t => {
const str = prettifyMessage({
log: { msg: 'foo', context: 'appModule', level: 30 },
context: {
...context,
messageFormat: '[{level}] {levelLabel} {context} - {msg}',
customLevels: {}
}
})
t.equal(str, '[30] INFO appModule - foo')
})
tap.test('returns message formatted by `messageFormat` option - levelLabel & useOnlyCustomProps true', async t => {
const str = prettifyMessage({
log: { msg: 'foo', context: 'appModule', level: 30 },
context: {
...context,
messageFormat: '[{level}] {levelLabel} {context} - {msg}',
customLevels: { 30: 'CHECK' },
useOnlyCustomProps: true
}
})
t.equal(str, '[30] CHECK appModule - foo')
})
tap.test('returns message formatted by `messageFormat` option - levelLabel & customLevels', async t => {
const str = prettifyMessage({
log: { msg: 'foo', context: 'appModule', level: 123 },
context: {
...context,
messageFormat: '[{level}] {levelLabel} {context} - {msg}',
customLevels: { 123: 'CUSTOM' }
}
})
t.equal(str, '[123] CUSTOM appModule - foo')
})
tap.test('returns message formatted by `messageFormat` option - levelLabel, customLevels & useOnlyCustomProps', async t => {
const str = prettifyMessage({
log: { msg: 'foo', context: 'appModule', level: 123 },
context: {
...context,
messageFormat: '[{level}] {levelLabel} {context} - {msg}',
customLevels: { 123: 'CUSTOM' },
useOnlyCustomProps: true
}
})
t.equal(str, '[123] CUSTOM appModule - foo')
})
tap.test('returns message formatted by `messageFormat` option - levelLabel, customLevels & useOnlyCustomProps false', async t => {
const str = prettifyMessage({
log: { msg: 'foo', context: 'appModule', level: 40 },
context: {
...context,
messageFormat: '[{level}] {levelLabel} {context} - {msg}',
customLevels: { 123: 'CUSTOM' },
useOnlyCustomProps: false
}
})
t.equal(str, '[40] WARN appModule - foo')
})
tap.test('`messageFormat` supports nested curly brackets', async t => {
const str = prettifyMessage({
log: { level: 30 },
context: {
...context,
messageFormat: '{{level}}-{level}-{{level}-{level}}'
}
})
t.equal(str, '{30}-30-{30-30}')
})
tap.test('`messageFormat` supports nested object', async t => {
const str = prettifyMessage({
log: { level: 30, request: { url: 'localhost/test' }, msg: 'foo' },
context: {
...context,
messageFormat: '{request.url} - param: {request.params.process} - {msg}'
}
})
t.equal(str, 'localhost/test - param: - foo')
})
tap.test('`messageFormat` supports conditional blocks', async t => {
const str = prettifyMessage({
log: { level: 30, req: { id: 'foo' } },
context: {
...context,
messageFormat: '{level} | {if req.id}({req.id}){end}{if msg}{msg}{end}'
}
})
t.equal(str, '30 | (foo)')
})
tap.test('`messageFormat` supports function definition', async t => {
const str = prettifyMessage({
log: { level: 30, request: { url: 'localhost/test' }, msg: 'incoming request' },
context: {
...context,
messageFormat: (log, messageKey, levelLabel) => {
let msg = log[messageKey]
if (msg === 'incoming request') msg = `--> ${log.request.url}`
return msg
}
}
})
t.equal(str, '--> localhost/test')
})
@@ -0,0 +1,61 @@
'use strict'
module.exports = prettifyMetadata
/**
* @typedef {object} PrettifyMetadataParams
* @property {object} log The log that may or may not contain metadata to
* be prettified.
* @property {PrettyContext} context The context object built from parsing
* the options.
*/
/**
* Prettifies metadata that is usually present in a Pino log line. It looks for
* fields `name`, `pid`, `hostname`, and `caller` and returns a formatted string using
* the fields it finds.
*
* @param {PrettifyMetadataParams} input
*
* @returns {undefined|string} If no metadata is found then `undefined` is
* returned. Otherwise, a string of prettified metadata is returned.
*/
function prettifyMetadata ({ log, context }) {
const prettifiers = context.customPrettifiers
let line = ''
if (log.name || log.pid || log.hostname) {
line += '('
if (log.name) {
line += prettifiers.name ? prettifiers.name(log.name) : log.name
}
if (log.pid) {
const prettyPid = prettifiers.pid ? prettifiers.pid(log.pid) : log.pid
if (log.name && log.pid) {
line += '/' + prettyPid
} else {
line += prettyPid
}
}
if (log.hostname) {
// If `pid` and `name` were in the ignore keys list then we don't need
// the leading space.
line += `${line === '(' ? 'on' : ' on'} ${prettifiers.hostname ? prettifiers.hostname(log.hostname) : log.hostname}`
}
line += ')'
}
if (log.caller) {
line += `${line === '' ? '' : ' '}<${prettifiers.caller ? prettifiers.caller(log.caller) : log.caller}>`
}
if (line === '') {
return undefined
} else {
return line
}
}
@@ -0,0 +1,111 @@
'use strict'
const tap = require('tap')
const prettifyMetadata = require('./prettify-metadata')
const context = {
customPrettifiers: {}
}
tap.test('returns `undefined` if no metadata present', async t => {
const str = prettifyMetadata({ log: {}, context })
t.equal(str, undefined)
})
tap.test('works with only `name` present', async t => {
const str = prettifyMetadata({ log: { name: 'foo' }, context })
t.equal(str, '(foo)')
})
tap.test('works with only `pid` present', async t => {
const str = prettifyMetadata({ log: { pid: '1234' }, context })
t.equal(str, '(1234)')
})
tap.test('works with only `hostname` present', async t => {
const str = prettifyMetadata({ log: { hostname: 'bar' }, context })
t.equal(str, '(on bar)')
})
tap.test('works with only `name` & `pid` present', async t => {
const str = prettifyMetadata({ log: { name: 'foo', pid: '1234' }, context })
t.equal(str, '(foo/1234)')
})
tap.test('works with only `name` & `hostname` present', async t => {
const str = prettifyMetadata({ log: { name: 'foo', hostname: 'bar' }, context })
t.equal(str, '(foo on bar)')
})
tap.test('works with only `pid` & `hostname` present', async t => {
const str = prettifyMetadata({ log: { pid: '1234', hostname: 'bar' }, context })
t.equal(str, '(1234 on bar)')
})
tap.test('works with only `name`, `pid`, & `hostname` present', async t => {
const str = prettifyMetadata({ log: { name: 'foo', pid: '1234', hostname: 'bar' }, context })
t.equal(str, '(foo/1234 on bar)')
})
tap.test('works with only `name` & `caller` present', async t => {
const str = prettifyMetadata({ log: { name: 'foo', caller: 'baz' }, context })
t.equal(str, '(foo) <baz>')
})
tap.test('works with only `pid` & `caller` present', async t => {
const str = prettifyMetadata({ log: { pid: '1234', caller: 'baz' }, context })
t.equal(str, '(1234) <baz>')
})
tap.test('works with only `hostname` & `caller` present', async t => {
const str = prettifyMetadata({ log: { hostname: 'bar', caller: 'baz' }, context })
t.equal(str, '(on bar) <baz>')
})
tap.test('works with only `name`, `pid`, & `caller` present', async t => {
const str = prettifyMetadata({ log: { name: 'foo', pid: '1234', caller: 'baz' }, context })
t.equal(str, '(foo/1234) <baz>')
})
tap.test('works with only `name`, `hostname`, & `caller` present', async t => {
const str = prettifyMetadata({ log: { name: 'foo', hostname: 'bar', caller: 'baz' }, context })
t.equal(str, '(foo on bar) <baz>')
})
tap.test('works with only `caller` present', async t => {
const str = prettifyMetadata({ log: { caller: 'baz' }, context })
t.equal(str, '<baz>')
})
tap.test('works with only `pid`, `hostname`, & `caller` present', async t => {
const str = prettifyMetadata({ log: { pid: '1234', hostname: 'bar', caller: 'baz' }, context })
t.equal(str, '(1234 on bar) <baz>')
})
tap.test('works with all four present', async t => {
const str = prettifyMetadata({ log: { name: 'foo', pid: '1234', hostname: 'bar', caller: 'baz' }, context })
t.equal(str, '(foo/1234 on bar) <baz>')
})
tap.test('uses prettifiers from passed prettifiers object', async t => {
const prettifiers = {
name (input) {
return input.toUpperCase()
},
pid (input) {
return input + '__'
},
hostname (input) {
return input.toUpperCase()
},
caller (input) {
return input.toUpperCase()
}
}
const str = prettifyMetadata({
log: { pid: '1234', hostname: 'bar', caller: 'baz', name: 'joe' },
context: {
customPrettifiers: prettifiers
}
})
t.equal(str, '(JOE/1234__ on BAR) <BAZ>')
})
@@ -0,0 +1,111 @@
'use strict'
module.exports = prettifyObject
const {
LOGGER_KEYS
} = require('../constants')
const stringifySafe = require('fast-safe-stringify')
const joinLinesWithIndentation = require('./join-lines-with-indentation')
const prettifyError = require('./prettify-error')
/**
* @typedef {object} PrettifyObjectParams
* @property {object} log The object to prettify.
* @property {boolean} [excludeLoggerKeys] Indicates if known logger specific
* keys should be excluded from prettification. Default: `true`.
* @property {string[]} [skipKeys] A set of object keys to exclude from the
* * prettified result. Default: `[]`.
* @property {PrettyContext} context The context object built from parsing
* the options.
*/
/**
* Prettifies a standard object. Special care is taken when processing the object
* to handle child objects that are attached to keys known to contain error
* objects.
*
* @param {PrettifyObjectParams} input
*
* @returns {string} The prettified string. This can be as little as `''` if
* there was nothing to prettify.
*/
function prettifyObject ({
log,
excludeLoggerKeys = true,
skipKeys = [],
context
}) {
const {
EOL: eol,
IDENT: ident,
customPrettifiers,
errorLikeObjectKeys: errorLikeKeys,
objectColorizer,
singleLine
} = context
const keysToIgnore = [].concat(skipKeys)
/* istanbul ignore else */
if (excludeLoggerKeys === true) Array.prototype.push.apply(keysToIgnore, LOGGER_KEYS)
let result = ''
// Split object keys into two categories: error and non-error
const { plain, errors } = Object.entries(log).reduce(({ plain, errors }, [k, v]) => {
if (keysToIgnore.includes(k) === false) {
// Pre-apply custom prettifiers, because all 3 cases below will need this
const pretty = typeof customPrettifiers[k] === 'function'
? customPrettifiers[k](v, k, log)
: v
if (errorLikeKeys.includes(k)) {
errors[k] = pretty
} else {
plain[k] = pretty
}
}
return { plain, errors }
}, { plain: {}, errors: {} })
if (singleLine) {
// Stringify the entire object as a single JSON line
/* istanbul ignore else */
if (Object.keys(plain).length > 0) {
result += objectColorizer.greyMessage(stringifySafe(plain))
}
result += eol
// Avoid printing the escape character on escaped backslashes.
result = result.replace(/\\\\/gi, '\\')
} else {
// Put each object entry on its own line
Object.entries(plain).forEach(([keyName, keyValue]) => {
// custom prettifiers are already applied above, so we can skip it now
let lines = typeof customPrettifiers[keyName] === 'function'
? keyValue
: stringifySafe(keyValue, null, 2)
if (lines === undefined) return
// Avoid printing the escape character on escaped backslashes.
lines = lines.replace(/\\\\/gi, '\\')
const joinedLines = joinLinesWithIndentation({ input: lines, ident, eol })
result += `${ident}${keyName}:${joinedLines.startsWith(eol) ? '' : ' '}${joinedLines}${eol}`
})
}
// Errors
Object.entries(errors).forEach(([keyName, keyValue]) => {
// custom prettifiers are already applied above, so we can skip it now
const lines = typeof customPrettifiers[keyName] === 'function'
? keyValue
: stringifySafe(keyValue, null, 2)
if (lines === undefined) return
result += prettifyError({ keyName, lines, eol, ident })
})
return result
}
@@ -0,0 +1,152 @@
'use strict'
const tap = require('tap')
const colors = require('../colors')
const prettifyObject = require('./prettify-object')
const {
ERROR_LIKE_KEYS
} = require('../constants')
const context = {
EOL: '\n',
IDENT: ' ',
customPrettifiers: {},
errorLikeObjectKeys: ERROR_LIKE_KEYS,
objectColorizer: colors(),
singleLine: false
}
tap.test('returns empty string if no properties present', async t => {
const str = prettifyObject({ log: {}, context })
t.equal(str, '')
})
tap.test('works with single level properties', async t => {
const str = prettifyObject({ log: { foo: 'bar' }, context })
t.equal(str, ' foo: "bar"\n')
})
tap.test('works with multiple level properties', async t => {
const str = prettifyObject({ log: { foo: { bar: 'baz' } }, context })
t.equal(str, ' foo: {\n "bar": "baz"\n }\n')
})
tap.test('skips specified keys', async t => {
const str = prettifyObject({
log: { foo: 'bar', hello: 'world' },
skipKeys: ['foo'],
context
})
t.equal(str, ' hello: "world"\n')
})
tap.test('ignores predefined keys', async t => {
const str = prettifyObject({ log: { foo: 'bar', pid: 12345 }, context })
t.equal(str, ' foo: "bar"\n')
})
tap.test('ignores escaped backslashes in string values', async t => {
const str = prettifyObject({ log: { foo_regexp: '\\[^\\w\\s]\\' }, context })
t.equal(str, ' foo_regexp: "\\[^\\w\\s]\\"\n')
})
tap.test('ignores escaped backslashes in string values (singleLine option)', async t => {
const str = prettifyObject({
log: { foo_regexp: '\\[^\\w\\s]\\' },
context: {
...context,
singleLine: true
}
})
t.equal(str, '{"foo_regexp":"\\[^\\w\\s]\\"}\n')
})
tap.test('works with error props', async t => {
const err = Error('Something went wrong')
const serializedError = {
message: err.message,
stack: err.stack
}
const str = prettifyObject({ log: { error: serializedError }, context })
t.ok(str.startsWith(' error:'))
t.ok(str.includes(' "message": "Something went wrong",'))
t.ok(str.includes(' Error: Something went wrong'))
})
tap.test('customPrettifiers gets applied', async t => {
const customPrettifiers = {
foo: v => v.toUpperCase()
}
const str = prettifyObject({
log: { foo: 'foo' },
context: {
...context,
customPrettifiers
}
})
t.equal(str.startsWith(' foo: FOO'), true)
})
tap.test('skips lines omitted by customPrettifiers', async t => {
const customPrettifiers = {
foo: () => { return undefined }
}
const str = prettifyObject({
log: { foo: 'foo', bar: 'bar' },
context: {
...context,
customPrettifiers
}
})
t.equal(str.includes('bar: "bar"'), true)
t.equal(str.includes('foo: "foo"'), false)
})
tap.test('joined lines omits starting eol', async t => {
const str = prettifyObject({
log: { msg: 'doing work', calls: ['step 1', 'step 2', 'step 3'], level: 30 },
context: {
...context,
IDENT: '',
customPrettifiers: {
calls: val => '\n' + val.map(it => ' ' + it).join('\n')
}
}
})
t.equal(str, [
'msg: "doing work"',
'calls:',
' step 1',
' step 2',
' step 3',
''
].join('\n'))
})
tap.test('errors skips prettifiers', async t => {
const customPrettifiers = {
err: () => { return 'is_err' }
}
const str = prettifyObject({
log: { err: Error('boom') },
context: {
...context,
customPrettifiers
}
})
t.equal(str.includes('err: is_err'), true)
})
tap.test('errors skips prettifying if no lines are present', async t => {
const customPrettifiers = {
err: () => { return undefined }
}
const str = prettifyObject({
log: { err: Error('boom') },
context: {
...context,
customPrettifiers
}
})
t.equal(str, '')
})
@@ -0,0 +1,42 @@
'use strict'
module.exports = prettifyTime
const formatTime = require('./format-time')
/**
* @typedef {object} PrettifyTimeParams
* @property {object} log The log object with the timestamp to be prettified.
* @property {PrettyContext} context The context object built from parsing
* the options.
*/
/**
* Prettifies a timestamp if the given `log` has either `time`, `timestamp` or custom specified timestamp
* property.
*
* @param {PrettifyTimeParams} input
*
* @returns {undefined|string} If a timestamp property cannot be found then
* `undefined` is returned. Otherwise, the prettified time is returned as a
* string.
*/
function prettifyTime ({ log, context }) {
const {
timestampKey,
translateTime: translateFormat
} = context
const prettifier = context.customPrettifiers?.time
let time = null
if (timestampKey in log) {
time = log[timestampKey]
} else if ('timestamp' in log) {
time = log.timestamp
}
if (time === null) return undefined
const output = translateFormat ? formatTime(time, translateFormat) : time
return prettifier ? prettifier(output) : `[${output}]`
}
@@ -0,0 +1,227 @@
'use strict'
process.env.TZ = 'UTC'
const tap = require('tap')
const prettifyTime = require('./prettify-time')
const {
TIMESTAMP_KEY
} = require('../constants')
const context = {
timestampKey: TIMESTAMP_KEY,
translateTime: true,
customPrettifiers: {}
}
tap.test('returns `undefined` if `time` or `timestamp` not in log', async t => {
const str = prettifyTime({ log: {}, context })
t.equal(str, undefined)
})
tap.test('returns prettified formatted time from custom field', async t => {
const log = { customtime: 1554642900000 }
let str = prettifyTime({
log,
context: {
...context,
timestampKey: 'customtime'
}
})
t.equal(str, '[13:15:00.000]')
str = prettifyTime({
log,
context: {
...context,
translateTime: false,
timestampKey: 'customtime'
}
})
t.equal(str, '[1554642900000]')
})
tap.test('returns prettified formatted time', async t => {
let log = { time: 1554642900000 }
let str = prettifyTime({
log,
context: {
...context
}
})
t.equal(str, '[13:15:00.000]')
log = { timestamp: 1554642900000 }
str = prettifyTime({
log,
context: {
...context
}
})
t.equal(str, '[13:15:00.000]')
log = { time: '2019-04-07T09:15:00.000-04:00' }
str = prettifyTime({
log,
context: {
...context
}
})
t.equal(str, '[13:15:00.000]')
log = { timestamp: '2019-04-07T09:15:00.000-04:00' }
str = prettifyTime({
log,
context: {
...context
}
})
t.equal(str, '[13:15:00.000]')
log = { time: 1554642900000 }
str = prettifyTime({
log,
context: {
...context,
translateTime: 'd mmm yyyy H:MM'
}
})
t.equal(str, '[7 Apr 2019 13:15]')
log = { timestamp: 1554642900000 }
str = prettifyTime({
log,
context: {
...context,
translateTime: 'd mmm yyyy H:MM'
}
})
t.equal(str, '[7 Apr 2019 13:15]')
log = { time: '2019-04-07T09:15:00.000-04:00' }
str = prettifyTime({
log,
context: {
...context,
translateTime: 'd mmm yyyy H:MM'
}
})
t.equal(str, '[7 Apr 2019 13:15]')
log = { timestamp: '2019-04-07T09:15:00.000-04:00' }
str = prettifyTime({
log,
context: {
...context,
translateTime: 'd mmm yyyy H:MM'
}
})
t.equal(str, '[7 Apr 2019 13:15]')
})
tap.test('passes through value', async t => {
let log = { time: 1554642900000 }
let str = prettifyTime({
log,
context: {
...context,
translateTime: undefined
}
})
t.equal(str, '[1554642900000]')
log = { timestamp: 1554642900000 }
str = prettifyTime({
log,
context: {
...context,
translateTime: undefined
}
})
t.equal(str, '[1554642900000]')
log = { time: '2019-04-07T09:15:00.000-04:00' }
str = prettifyTime({
log,
context: {
...context,
translateTime: undefined
}
})
t.equal(str, '[2019-04-07T09:15:00.000-04:00]')
log = { timestamp: '2019-04-07T09:15:00.000-04:00' }
str = prettifyTime({
log,
context: {
...context,
translateTime: undefined
}
})
t.equal(str, '[2019-04-07T09:15:00.000-04:00]')
})
tap.test('handles the 0 timestamp', async t => {
let log = { time: 0 }
let str = prettifyTime({
log,
context: {
...context,
translateTime: undefined
}
})
t.equal(str, '[0]')
log = { timestamp: 0 }
str = prettifyTime({
log,
context: {
...context,
translateTime: undefined
}
})
t.equal(str, '[0]')
})
tap.test('works with epoch as a number or string', (t) => {
t.plan(3)
const epoch = 1522431328992
const asNumber = prettifyTime({
log: { time: epoch, msg: 'foo' },
context: {
...context,
translateTime: true
}
})
const asString = prettifyTime({
log: { time: `${epoch}`, msg: 'foo' },
context: {
...context,
translateTime: true
}
})
const invalid = prettifyTime({
log: { time: '2 days ago', msg: 'foo' },
context: {
...context,
translateTime: true
}
})
t.same(asString, '[17:35:28.992]')
t.same(asNumber, '[17:35:28.992]')
t.same(invalid, '[2 days ago]')
})
tap.test('uses custom prettifier', async t => {
const str = prettifyTime({
log: { time: 0 },
context: {
...context,
customPrettifiers: {
time () {
return 'done'
}
}
}
})
t.equal(str, 'done')
})
@@ -0,0 +1,49 @@
'use strict'
module.exports = splitPropertyKey
/**
* Splits the property key delimited by a dot character but not when it is preceded
* by a backslash.
*
* @param {string} key A string identifying the property.
*
* @returns {string[]} Returns a list of string containing each delimited property.
* e.g. `'prop2\.domain\.corp.prop2'` should return [ 'prop2.domain.com', 'prop2' ]
*/
function splitPropertyKey (key) {
const result = []
let backslash = false
let segment = ''
for (let i = 0; i < key.length; i++) {
const c = key.charAt(i)
if (c === '\\') {
backslash = true
continue
}
if (backslash) {
backslash = false
segment += c
continue
}
/* Non-escaped dot, push to result */
if (c === '.') {
result.push(segment)
segment = ''
continue
}
segment += c
}
/* Push last entry to result */
if (segment.length) {
result.push(segment)
}
return result
}
@@ -0,0 +1,29 @@
'use strict'
const tap = require('tap')
const splitPropertyKey = require('./split-property-key')
tap.test('splitPropertyKey does not change key', async t => {
const result = splitPropertyKey('data1')
t.same(result, ['data1'])
})
tap.test('splitPropertyKey splits nested key', async t => {
const result = splitPropertyKey('data1.data2.data-3')
t.same(result, ['data1', 'data2', 'data-3'])
})
tap.test('splitPropertyKey splits nested keys ending with a dot', async t => {
const result = splitPropertyKey('data1.data2.data-3.')
t.same(result, ['data1', 'data2', 'data-3'])
})
tap.test('splitPropertyKey splits nested escaped key', async t => {
const result = splitPropertyKey('logging\\.domain\\.corp/operation.foo.bar-2')
t.same(result, ['logging.domain.corp/operation', 'foo', 'bar-2'])
})
tap.test('splitPropertyKey splits nested escaped key with special characters', async t => {
const result = splitPropertyKey('logging\\.domain\\.corp/operation.!\t@#$%^&*()_+=-<>.bar\\.2')
t.same(result, ['logging.domain.corp/operation', '!\t@#$%^&*()_+=-<>', 'bar.2'])
})
+67
View File
@@ -0,0 +1,67 @@
{
"name": "pino-pretty",
"version": "10.3.1",
"description": "Prettifier for Pino log lines",
"type": "commonjs",
"main": "index.js",
"types": "index.d.ts",
"bin": {
"pino-pretty": "./bin.js"
},
"scripts": {
"ci": "standard && tap --coverage-report=lcovonly && npm run test-types",
"lint": "standard | snazzy",
"test": "tap",
"test-types": "tsc && tsd",
"test:watch": "tap --no-coverage-report -w",
"test:report": "tap --coverage-report=html"
},
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/pinojs/pino-pretty.git"
},
"keywords": [
"pino"
],
"author": "James Sumners <james.sumners@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/pinojs/pino-pretty/issues"
},
"homepage": "https://github.com/pinojs/pino-pretty#readme",
"precommit": [
"lint",
"test"
],
"dependencies": {
"colorette": "^2.0.7",
"dateformat": "^4.6.3",
"fast-copy": "^3.0.0",
"fast-safe-stringify": "^2.1.1",
"help-me": "^5.0.0",
"joycon": "^3.1.1",
"minimist": "^1.2.6",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^1.0.0",
"pump": "^3.0.0",
"readable-stream": "^4.0.0",
"secure-json-parse": "^2.4.0",
"sonic-boom": "^3.0.0",
"strip-json-comments": "^3.1.1"
},
"devDependencies": {
"@types/node": "^20.1.0",
"fastbench": "^1.0.1",
"pino": "^8.0.0",
"pre-commit": "^1.2.2",
"rimraf": "^3.0.2",
"snazzy": "^9.0.0",
"standard": "^17.0.0",
"tap": "^16.0.0",
"tsd": "^0.30.0",
"typescript": "^5.0.2"
},
"tsd": {
"directory": "./test/types"
}
}
+1088
View File
@@ -0,0 +1,1088 @@
'use strict'
process.env.TZ = 'UTC'
const { Writable } = require('readable-stream')
const os = require('os')
const test = require('tap').test
const pino = require('pino')
const dateformat = require('dateformat')
const path = require('path')
const rimraf = require('rimraf')
const { join } = require('path')
const fs = require('fs')
const pinoPretty = require('..')
const SonicBoom = require('sonic-boom')
const _prettyFactory = pinoPretty.prettyFactory
// Disable pino warnings
process.removeAllListeners('warning')
function prettyFactory (opts) {
if (!opts) {
opts = { colorize: false }
} else if (!Object.prototype.hasOwnProperty.call(opts, 'colorize')) {
opts.colorize = false
}
return _prettyFactory(opts)
}
// All dates are computed from 'Fri, 30 Mar 2018 17:35:28 GMT'
const epoch = 1522431328992
const formattedEpoch = '17:35:28.992'
const pid = process.pid
const hostname = os.hostname()
test('basic prettifier tests', (t) => {
t.beforeEach(() => {
Date.originalNow = Date.now
Date.now = () => epoch
})
t.afterEach(() => {
Date.now = Date.originalNow
delete Date.originalNow
})
t.test('preserves output if not valid JSON', (t) => {
t.plan(1)
const pretty = prettyFactory()
const formatted = pretty('this is not json\nit\'s just regular output\n')
t.equal(formatted, 'this is not json\nit\'s just regular output\n\n')
})
t.test('formats a line without any extra options', (t) => {
t.plan(1)
const pretty = prettyFactory()
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(
formatted,
`[${formattedEpoch}] INFO (${pid}): foo\n`
)
cb()
}
}))
log.info('foo')
})
t.test('will add color codes', (t) => {
t.plan(1)
const pretty = prettyFactory({ colorize: true })
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(
formatted,
`[${formattedEpoch}] \u001B[32mINFO\u001B[39m (${pid}): \u001B[36mfoo\u001B[39m\n`
)
cb()
}
}))
log.info('foo')
})
t.test('will omit color codes from objects when colorizeObjects = false', (t) => {
t.plan(1)
const pretty = prettyFactory({ colorize: true, singleLine: true, colorizeObjects: false })
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(
formatted,
`[${formattedEpoch}] \u001B[32mINFO\u001B[39m (${pid}): \u001B[36mfoo\u001B[39m {"foo":"bar"}\n`
)
cb()
}
}))
log.info({ foo: 'bar' }, 'foo')
})
t.test('can swap date and level position', (t) => {
t.plan(1)
const destination = new Writable({
write (formatted, enc, cb) {
t.equal(
formatted.toString(),
`INFO [${formattedEpoch}] (${pid}): foo\n`
)
cb()
}
})
const pretty = pinoPretty({
destination,
levelFirst: true,
colorize: false
})
const log = pino({}, pretty)
log.info('foo')
})
t.test('can print message key value when its a string', (t) => {
t.plan(1)
const pretty = prettyFactory()
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(
formatted,
`[${formattedEpoch}] INFO (${pid}): baz\n`
)
cb()
}
}))
log.info('baz')
})
t.test('can print message key value when its a number', (t) => {
t.plan(1)
const pretty = prettyFactory()
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(
formatted,
`[${formattedEpoch}] INFO (${pid}): 42\n`
)
cb()
}
}))
log.info(42)
})
t.test('can print message key value when its a Number(0)', (t) => {
t.plan(1)
const pretty = prettyFactory()
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(
formatted,
`[${formattedEpoch}] INFO (${pid}): 0\n`
)
cb()
}
}))
log.info(0)
})
t.test('can print message key value when its a boolean', (t) => {
t.plan(1)
const pretty = prettyFactory()
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(
formatted,
`[${formattedEpoch}] INFO (${pid}): true\n`
)
cb()
}
}))
log.info(true)
})
t.test('can use different message keys', (t) => {
t.plan(1)
const pretty = prettyFactory({ messageKey: 'bar' })
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(
formatted,
`[${formattedEpoch}] INFO (${pid}): baz\n`
)
cb()
}
}))
log.info({ bar: 'baz' })
})
t.test('can use different level keys', (t) => {
t.plan(1)
const pretty = prettyFactory({ levelKey: 'bar' })
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(
formatted,
`[${formattedEpoch}] WARN (${pid}): foo\n`
)
cb()
}
}))
log.info({ msg: 'foo', bar: 'warn' })
})
t.test('can use a customPrettifier on default level output', (t) => {
t.plan(1)
const veryCustomLevels = {
30: 'ok',
40: 'not great'
}
const customPrettifiers = {
level: (level) => `LEVEL: ${veryCustomLevels[level]}`
}
const pretty = prettyFactory({ customPrettifiers })
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(
formatted,
`[${formattedEpoch}] LEVEL: ok (${pid}): foo\n`
)
cb()
}
}))
log.info({ msg: 'foo' })
})
t.test('can use a customPrettifier on different-level-key output', (t) => {
t.plan(1)
const customPrettifiers = {
level: (level) => `LEVEL: ${level.toUpperCase()}`
}
const pretty = prettyFactory({ levelKey: 'bar', customPrettifiers })
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(
formatted,
`[${formattedEpoch}] LEVEL: WARN (${pid}): foo\n`
)
cb()
}
}))
log.info({ msg: 'foo', bar: 'warn' })
})
t.test('can use a customPrettifier on name output', (t) => {
t.plan(1)
const customPrettifiers = {
name: (hostname) => `NAME: ${hostname}`
}
const pretty = prettyFactory({ customPrettifiers })
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(
formatted,
`[${formattedEpoch}] INFO (NAME: logger/${pid}): foo\n`
)
cb()
}
}))
const child = log.child({ name: 'logger' })
child.info({ msg: 'foo' })
})
t.test('can use a customPrettifier on hostname and pid output', (t) => {
t.plan(1)
const customPrettifiers = {
hostname: (hostname) => `HOSTNAME: ${hostname}`,
pid: (pid) => `PID: ${pid}`
}
const pretty = prettyFactory({ customPrettifiers, ignore: '' })
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(
formatted,
`[${formattedEpoch}] INFO (PID: ${pid} on HOSTNAME: ${hostname}): foo\n`
)
cb()
}
}))
log.info({ msg: 'foo' })
})
t.test('can use a customPrettifier on default time output', (t) => {
t.plan(1)
const customPrettifiers = {
time: (timestamp) => `TIME: ${timestamp}`
}
const pretty = prettyFactory({ customPrettifiers })
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(
formatted,
`TIME: ${formattedEpoch} INFO (${pid}): foo\n`
)
cb()
}
}))
log.info('foo')
})
t.test('can use a customPrettifier on the caller', (t) => {
t.plan(1)
const customPrettifiers = {
caller: (caller) => `CALLER: ${caller}`
}
const pretty = prettyFactory({ customPrettifiers })
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(
formatted,
`[${formattedEpoch}] INFO (${pid}) <CALLER: test.js:10>: foo\n`
)
cb()
}
}))
log.info({ msg: 'foo', caller: 'test.js:10' })
})
t.test('can use a customPrettifier on translateTime-time output', (t) => {
t.plan(1)
const customPrettifiers = {
time: (timestamp) => `TIME: ${timestamp}`
}
const pretty = prettyFactory({ customPrettifiers, translateTime: true })
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(
formatted,
`TIME: ${formattedEpoch} INFO (${pid}): foo\n`
)
cb()
}
}))
log.info('foo')
})
t.test('will format time to UTC', (t) => {
t.plan(1)
const pretty = prettyFactory({ translateTime: true })
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(
formatted,
`[${formattedEpoch}] INFO (${pid}): foo\n`
)
cb()
}
}))
log.info('foo')
})
t.test('will format time to UTC in custom format', (t) => {
t.plan(1)
const pretty = prettyFactory({ translateTime: 'HH:MM:ss o' })
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
const utcHour = dateformat(epoch, 'UTC:' + 'HH')
const offset = dateformat(epoch, 'UTC:' + 'o')
t.equal(
formatted,
`[${utcHour}:35:28 ${offset}] INFO (${pid}): foo\n`
)
cb()
}
}))
log.info('foo')
})
t.test('will format time to local systemzone in ISO 8601 format', (t) => {
t.plan(1)
const pretty = prettyFactory({ translateTime: 'sys:standard' })
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
const localHour = dateformat(epoch, 'HH')
const localMinute = dateformat(epoch, 'MM')
const localDate = dateformat(epoch, 'yyyy-mm-dd')
const offset = dateformat(epoch, 'o')
t.equal(
formatted,
`[${localDate} ${localHour}:${localMinute}:28.992 ${offset}] INFO (${pid}): foo\n`
)
cb()
}
}))
log.info('foo')
})
t.test('will format time to local systemzone in custom format', (t) => {
t.plan(1)
const pretty = prettyFactory({
translateTime: 'SYS:yyyy/mm/dd HH:MM:ss o'
})
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
const localHour = dateformat(epoch, 'HH')
const localMinute = dateformat(epoch, 'MM')
const localDate = dateformat(epoch, 'yyyy/mm/dd')
const offset = dateformat(epoch, 'o')
t.equal(
formatted,
`[${localDate} ${localHour}:${localMinute}:28 ${offset}] INFO (${pid}): foo\n`
)
cb()
}
}))
log.info('foo')
})
// TODO: 2019-03-30 -- We don't really want the indentation in this case? Or at least some better formatting.
t.test('handles missing time', (t) => {
t.plan(1)
const pretty = prettyFactory()
const formatted = pretty('{"hello":"world"}')
t.equal(formatted, ' hello: "world"\n')
})
t.test('handles missing pid, hostname and name', (t) => {
t.plan(1)
const pretty = prettyFactory()
const log = pino({ base: null }, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.match(formatted, /\[.*\] INFO: hello world/)
cb()
}
}))
log.info('hello world')
})
t.test('handles missing pid', (t) => {
t.plan(1)
const pretty = prettyFactory()
const name = 'test'
const msg = 'hello world'
const regex = new RegExp('\\[.*\\] INFO \\(' + name + '\\): ' + msg)
const opts = {
base: {
name,
hostname
}
}
const log = pino(opts, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.match(formatted, regex)
cb()
}
}))
log.info(msg)
})
t.test('handles missing hostname', (t) => {
t.plan(1)
const pretty = prettyFactory()
const name = 'test'
const msg = 'hello world'
const regex = new RegExp('\\[.*\\] INFO \\(' + name + '/' + pid + '\\): ' + msg)
const opts = {
base: {
name,
pid: process.pid
}
}
const log = pino(opts, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.match(formatted, regex)
cb()
}
}))
log.info(msg)
})
t.test('handles missing name', (t) => {
t.plan(1)
const pretty = prettyFactory()
const msg = 'hello world'
const regex = new RegExp('\\[.*\\] INFO \\(' + process.pid + '\\): ' + msg)
const opts = {
base: {
hostname,
pid: process.pid
}
}
const log = pino(opts, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.match(formatted, regex)
cb()
}
}))
log.info(msg)
})
t.test('works without time', (t) => {
t.plan(1)
const pretty = prettyFactory()
const log = pino({ timestamp: null }, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(formatted, `INFO (${pid}): hello world\n`)
cb()
}
}))
log.info('hello world')
})
t.test('prettifies properties', (t) => {
t.plan(1)
const pretty = prettyFactory()
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.match(formatted, ' a: "b"')
cb()
}
}))
log.info({ a: 'b' }, 'hello world')
})
t.test('prettifies nested properties', (t) => {
t.plan(6)
const expectedLines = [
' a: {',
' "b": {',
' "c": "d"',
' }',
' }'
]
const pretty = prettyFactory()
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
const lines = formatted.split('\n')
t.equal(lines.length, expectedLines.length + 2)
lines.shift(); lines.pop()
for (let i = 0; i < lines.length; i += 1) {
t.equal(lines[i], expectedLines[i])
}
cb()
}
}))
log.info({ a: { b: { c: 'd' } } }, 'hello world')
})
t.test('treats the name with care', (t) => {
t.plan(1)
const pretty = prettyFactory()
const log = pino({ name: 'matteo' }, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(formatted, `[${formattedEpoch}] INFO (matteo/${pid}): hello world\n`)
cb()
}
}))
log.info('hello world')
})
t.test('handles spec allowed primitives', (t) => {
const pretty = prettyFactory()
let formatted = pretty(null)
t.equal(formatted, 'null\n')
formatted = pretty(true)
t.equal(formatted, 'true\n')
formatted = pretty(false)
t.equal(formatted, 'false\n')
t.end()
})
t.test('handles numbers', (t) => {
const pretty = prettyFactory()
let formatted = pretty(2)
t.equal(formatted, '2\n')
formatted = pretty(-2)
t.equal(formatted, '-2\n')
formatted = pretty(0.2)
t.equal(formatted, '0.2\n')
formatted = pretty(Infinity)
t.equal(formatted, 'Infinity\n')
formatted = pretty(NaN)
t.equal(formatted, 'NaN\n')
t.end()
})
t.test('handles `undefined` input', (t) => {
t.plan(1)
const pretty = prettyFactory()
const formatted = pretty(undefined)
t.equal(formatted, 'undefined\n')
})
t.test('handles customLogLevel', (t) => {
t.plan(1)
const pretty = prettyFactory()
const log = pino({ customLevels: { testCustom: 35 } }, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.match(formatted, /USERLVL/)
cb()
}
}))
log.testCustom('test message')
})
t.test('filter some lines based on minimumLevel', (t) => {
t.plan(3)
const pretty = prettyFactory({ minimumLevel: 'info' })
const expected = [
undefined,
undefined,
`[${formattedEpoch}] INFO (${pid}): baz\n`
]
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(
formatted,
expected.shift()
)
cb()
}
}))
log.info({ msg: 'foo', level: 10 })
log.info({ msg: 'bar', level: 20 })
// only this line will be formatted
log.info({ msg: 'baz', level: 30 })
})
t.test('filter lines based on minimumLevel using custom levels and level key', (t) => {
t.plan(3)
const pretty = prettyFactory({ minimumLevel: 20, levelKey: 'bar' })
const expected = [
undefined,
`[${formattedEpoch}] DEBUG (${pid}): bar\n`,
`[${formattedEpoch}] INFO (${pid}): baz\n`
]
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(
formatted,
expected.shift()
)
cb()
}
}))
log.info({ msg: 'foo', bar: 10 })
log.info({ msg: 'bar', bar: 20 })
log.info({ msg: 'baz', bar: 30 })
})
t.test('formats a line with an undefined field', (t) => {
t.plan(1)
const pretty = prettyFactory()
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const obj = JSON.parse(chunk.toString())
// weird hack, but we should not crash
obj.a = undefined
const formatted = pretty(obj)
t.equal(
formatted,
`[${formattedEpoch}] INFO (${pid}): foo\n`
)
cb()
}
}))
log.info('foo')
})
t.test('prettifies msg object', (t) => {
t.plan(6)
const expectedLines = [
' msg: {',
' "b": {',
' "c": "d"',
' }',
' }'
]
const pretty = prettyFactory()
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
const lines = formatted.split('\n')
t.equal(lines.length, expectedLines.length + 2)
lines.shift(); lines.pop()
for (let i = 0; i < lines.length; i += 1) {
t.equal(lines[i], expectedLines[i])
}
cb()
}
}))
log.info({ msg: { b: { c: 'd' } } })
})
t.test('prettifies msg object with circular references', (t) => {
t.plan(7)
const expectedLines = [
' msg: {',
' "a": "[Circular]",',
' "b": {',
' "c": "d"',
' }',
' }'
]
const pretty = prettyFactory()
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
const lines = formatted.split('\n')
t.equal(lines.length, expectedLines.length + 2)
lines.shift(); lines.pop()
for (let i = 0; i < lines.length; i += 1) {
t.equal(lines[i], expectedLines[i])
}
cb()
}
}))
const msg = { b: { c: 'd' } }
msg.a = msg
log.info({ msg })
})
t.test('prettifies custom key', (t) => {
t.plan(1)
const pretty = prettyFactory({
customPrettifiers: {
foo: val => `${val}_baz\nmultiline`,
cow: val => val.toUpperCase()
}
})
const arst = pretty('{"msg":"hello world", "foo": "bar", "cow": "moo", "level":30}')
t.equal(arst, 'INFO: hello world\n foo: bar_baz\n multiline\n cow: MOO\n')
})
t.test('does not add trailing space if prettified value begins with eol', (t) => {
t.plan(1)
const pretty = prettyFactory({
customPrettifiers: {
calls: val => '\n' + val.map(it => ' ' + it).join('\n')
}
})
const arst = pretty('{"msg":"doing work","calls":["step 1","step 2","step 3"],"level":30}')
t.equal(arst, 'INFO: doing work\n calls:\n step 1\n step 2\n step 3\n')
})
t.test('does not prettify custom key that does not exists', (t) => {
t.plan(1)
const pretty = prettyFactory({
customPrettifiers: {
foo: val => `${val}_baz`,
cow: val => val.toUpperCase()
}
})
const arst = pretty('{"msg":"hello world", "foo": "bar", "level":30}')
t.equal(arst, 'INFO: hello world\n foo: bar_baz\n')
})
t.test('prettifies object with some undefined values', (t) => {
t.plan(1)
const destination = new Writable({
write (chunk, _, cb) {
t.equal(
chunk + '',
`[${formattedEpoch}] INFO (${pid}):\n a: {\n "b": "c"\n }\n n: null\n`
)
cb()
}
})
const pretty = pinoPretty({
destination,
colorize: false
})
const log = pino({}, pretty)
log.info({
a: { b: 'c' },
s: Symbol.for('s'),
f: f => f,
c: class C {},
n: null,
err: { toJSON () {} }
})
})
t.test('ignores multiple keys', (t) => {
t.plan(1)
const pretty = prettyFactory({ ignore: 'pid,hostname' })
const arst = pretty(`{"msg":"hello world", "pid":"${pid}", "hostname":"${hostname}", "time":${epoch}, "level":30}`)
t.equal(arst, `[${formattedEpoch}] INFO: hello world\n`)
})
t.test('ignores a single key', (t) => {
t.plan(1)
const pretty = prettyFactory({ ignore: 'pid' })
const arst = pretty(`{"msg":"hello world", "pid":"${pid}", "hostname":"${hostname}", "time":${epoch}, "level":30}`)
t.equal(arst, `[${formattedEpoch}] INFO (on ${hostname}): hello world\n`)
})
t.test('ignores time', (t) => {
t.plan(1)
const pretty = prettyFactory({ ignore: 'time' })
const arst = pretty(`{"msg":"hello world", "pid":"${pid}", "hostname":"${hostname}", "time":${epoch}, "level":30}`)
t.equal(arst, `INFO (${pid} on ${hostname}): hello world\n`)
})
t.test('ignores time and level', (t) => {
t.plan(1)
const pretty = prettyFactory({ ignore: 'time,level' })
const arst = pretty(`{"msg":"hello world", "pid":"${pid}", "hostname":"${hostname}", "time":${epoch}, "level":30}`)
t.equal(arst, `(${pid} on ${hostname}): hello world\n`)
})
t.test('ignores all keys but message', (t) => {
t.plan(1)
const pretty = prettyFactory({ ignore: 'time,level,name,pid,hostname' })
const arst = pretty(`{"msg":"hello world", "pid":"${pid}", "hostname":"${hostname}", "time":${epoch}, "level":30}`)
t.equal(arst, 'hello world\n')
})
t.test('include nothing', (t) => {
t.plan(1)
const pretty = prettyFactory({ include: '' })
const arst = pretty(`{"msg":"hello world", "pid":"${pid}", "hostname":"${hostname}", "time":${epoch}, "level":30}`)
t.equal(arst, 'hello world\n')
})
t.test('include multiple keys', (t) => {
t.plan(1)
const pretty = prettyFactory({ include: 'time,level' })
const arst = pretty(`{"msg":"hello world", "pid":"${pid}", "hostname":"${hostname}", "time":${epoch}, "level":30}`)
t.equal(arst, `[${formattedEpoch}] INFO: hello world\n`)
})
t.test('include a single key', (t) => {
t.plan(1)
const pretty = prettyFactory({ include: 'level' })
const arst = pretty(`{"msg":"hello world", "pid":"${pid}", "hostname":"${hostname}", "time":${epoch}, "level":30}`)
t.equal(arst, 'INFO: hello world\n')
})
t.test('include should override ignore', (t) => {
t.plan(1)
const pretty = prettyFactory({ ignore: 'time,level', include: 'time,level' })
const arst = pretty(`{"msg":"hello world", "pid":"${pid}", "hostname":"${hostname}", "time":${epoch}, "level":30}`)
t.equal(arst, `[${formattedEpoch}] INFO: hello world\n`)
})
t.test('prettifies trace caller', (t) => {
t.plan(1)
const traceCaller = (instance) => {
const { symbols: { asJsonSym } } = pino
const get = (target, name) => name === asJsonSym ? asJson : target[name]
function asJson (...args) {
args[0] = args[0] || {}
args[0].caller = '/tmp/script.js'
return instance[asJsonSym].apply(this, args)
}
return new Proxy(instance, { get })
}
const pretty = prettyFactory()
const log = traceCaller(pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(
formatted,
`[${formattedEpoch}] INFO (${pid}) </tmp/script.js>: foo\n`
)
cb()
}
})))
log.info('foo')
})
t.test('handles specified timestampKey', (t) => {
t.plan(1)
const pretty = prettyFactory({ timestampKey: '@timestamp' })
const arst = pretty(`{"msg":"hello world", "@timestamp":${epoch}, "level":30}`)
t.equal(arst, `[${formattedEpoch}] INFO: hello world\n`)
})
t.test('keeps "v" key in log', (t) => {
t.plan(1)
const pretty = prettyFactory({ ignore: 'time' })
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(formatted, `INFO (${pid} on ${hostname}):\n v: 1\n`)
cb()
}
}))
log.info({ v: 1 })
})
t.test('Hide object `{ key: "value" }` from output when flag `hideObject` is set', (t) => {
t.plan(1)
const pretty = prettyFactory({ hideObject: true })
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(formatted, `[${formattedEpoch}] INFO (${pid}): hello world\n`)
cb()
}
}))
log.info({ key: 'value' }, 'hello world')
})
t.test('Prints extra objects on one line with singleLine=true', (t) => {
t.plan(1)
const pretty = prettyFactory({
singleLine: true,
colorize: false,
customPrettifiers: {
upper: val => val.toUpperCase(),
undef: () => undefined
}
})
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(formatted, `[${formattedEpoch}] INFO (${pid}): message {"extra":{"foo":"bar","number":42},"upper":"FOOBAR"}\n`)
cb()
}
}))
log.info({ msg: 'message', extra: { foo: 'bar', number: 42 }, upper: 'foobar', undef: 'this will not show up' })
})
t.test('Does not print empty object with singleLine=true', (t) => {
t.plan(1)
const pretty = prettyFactory({ singleLine: true, colorize: false })
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.equal(formatted, `[${formattedEpoch}] INFO (${pid}): message\n`)
cb()
}
}))
log.info({ msg: 'message' })
})
t.test('default options', (t) => {
t.plan(1)
t.doesNotThrow(pinoPretty)
})
t.test('does not call fs.close on stdout stream', (t) => {
t.plan(2)
const destination = pino.destination({ minLength: 4096, sync: true })
const prettyDestination = pinoPretty({ destination, colorize: false })
const log = pino(prettyDestination)
log.info('this message has been buffered')
const chunks = []
const { close, writeSync } = fs
let closeCalled = false
fs.close = new Proxy(close, {
apply: (target, self, args) => {
closeCalled = true
}
})
fs.writeSync = new Proxy(writeSync, {
apply: (target, self, args) => {
chunks.push(args[1])
return args[1].length
}
})
destination.end()
Object.assign(fs, { close, writeSync })
t.match(chunks.join(''), /INFO .+: this message has been buffered/)
t.equal(closeCalled, false)
})
t.test('stream usage', async (t) => {
t.plan(1)
const tmpDir = path.join(__dirname, '.tmp_' + Date.now())
t.teardown(() => rimraf(tmpDir, noop))
const destination = join(tmpDir, 'output')
const pretty = pinoPretty({
singleLine: true,
colorize: false,
mkdir: true,
append: false,
destination: new SonicBoom({ dest: destination, async: false, mkdir: true, append: true }),
customPrettifiers: {
upper: val => val.toUpperCase(),
undef: () => undefined
}
})
const log = pino(pretty)
log.info({ msg: 'message', extra: { foo: 'bar', number: 42 }, upper: 'foobar', undef: 'this will not show up' })
await watchFileCreated(destination)
const formatted = fs.readFileSync(destination, 'utf8')
t.equal(formatted, `[${formattedEpoch}] INFO (${pid}): message {"extra":{"foo":"bar","number":42},"upper":"FOOBAR"}\n`)
})
t.test('sync option', async (t) => {
t.plan(1)
const tmpDir = path.join(__dirname, '.tmp_' + Date.now())
t.teardown(() => rimraf(tmpDir, noop))
const destination = join(tmpDir, 'output')
const pretty = pinoPretty({
singleLine: true,
colorize: false,
mkdir: true,
append: false,
sync: true,
destination
})
const log = pino(pretty)
log.info({ msg: 'message', extra: { foo: 'bar', number: 42 }, upper: 'foobar' })
const formatted = fs.readFileSync(destination, 'utf8')
t.equal(formatted, `[${formattedEpoch}] INFO (${pid}): message {"extra":{"foo":"bar","number":42},"upper":"foobar"}\n`)
})
t.end()
})
function watchFileCreated (filename) {
return new Promise((resolve, reject) => {
const TIMEOUT = 2000
const INTERVAL = 100
const threshold = TIMEOUT / INTERVAL
let counter = 0
const interval = setInterval(() => {
// On some CI runs file is created but not filled
if (fs.existsSync(filename) && fs.statSync(filename).size !== 0) {
clearInterval(interval)
resolve()
} else if (counter <= threshold) {
counter++
} else {
clearInterval(interval)
reject(new Error(`${filename} was not created.`))
}
}, INTERVAL)
})
}
function noop () {}
+255
View File
@@ -0,0 +1,255 @@
'use strict'
process.env.TZ = 'UTC'
const path = require('path')
const spawn = require('child_process').spawn
const test = require('tap').test
const fs = require('fs')
const rimraf = require('rimraf')
const bin = require.resolve('../bin')
const logLine = '{"level":30,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n'
const noop = () => {}
test('cli', (t) => {
const tmpDir = path.join(__dirname, '.tmp_' + Date.now())
fs.mkdirSync(tmpDir)
t.teardown(() => rimraf(tmpDir, noop))
t.test('loads and applies default config file: pino-pretty.config.js', (t) => {
t.plan(1)
// Set translateTime: true on run configuration
const configFile = path.join(tmpDir, 'pino-pretty.config.js')
fs.writeFileSync(configFile, 'module.exports = { translateTime: true }')
const env = { TERM: 'dumb', TZ: 'UTC' }
const child = spawn(process.argv[0], [bin], { env, cwd: tmpDir })
// Validate that the time has been translated
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), '[17:35:28.992] INFO (42): hello world\n')
})
child.stdin.write(logLine)
t.teardown(() => {
fs.unlinkSync(configFile)
child.kill()
})
})
t.test('loads and applies default config file: pino-pretty.config.cjs', (t) => {
t.plan(1)
// Set translateTime: true on run configuration
const configFile = path.join(tmpDir, 'pino-pretty.config.cjs')
fs.writeFileSync(configFile, 'module.exports = { translateTime: true }')
// Tell the loader to expect ESM modules
const packageJsonFile = path.join(tmpDir, 'package.json')
fs.writeFileSync(packageJsonFile, JSON.stringify({ type: 'module' }, null, 4))
const env = { TERM: ' dumb', TZ: 'UTC' }
const child = spawn(process.argv[0], [bin], { env, cwd: tmpDir })
// Validate that the time has been translated
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), '[17:35:28.992] INFO (42): hello world\n')
})
child.stdin.write(logLine)
t.teardown(() => {
fs.unlinkSync(configFile)
fs.unlinkSync(packageJsonFile)
child.kill()
})
})
t.test('loads and applies default config file: .pino-prettyrc', (t) => {
t.plan(1)
// Set translateTime: true on run configuration
const configFile = path.join(tmpDir, '.pino-prettyrc')
fs.writeFileSync(configFile, JSON.stringify({ translateTime: true }, null, 4))
const env = { TERM: ' dumb', TZ: 'UTC' }
const child = spawn(process.argv[0], [bin], { env, cwd: tmpDir })
// Validate that the time has been translated
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), '[17:35:28.992] INFO (42): hello world\n')
})
child.stdin.write(logLine)
t.teardown(() => {
fs.unlinkSync(configFile)
child.kill()
})
})
t.test('loads and applies default config file: .pino-prettyrc.json', (t) => {
t.plan(1)
// Set translateTime: true on run configuration
const configFile = path.join(tmpDir, '.pino-prettyrc.json')
fs.writeFileSync(configFile, JSON.stringify({ translateTime: true }, null, 4))
const env = { TERM: ' dumb', TZ: 'UTC' }
const child = spawn(process.argv[0], [bin], { env, cwd: tmpDir })
// Validate that the time has been translated
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), '[17:35:28.992] INFO (42): hello world\n')
})
child.stdin.write(logLine)
t.teardown(() => {
fs.unlinkSync(configFile)
child.kill()
})
})
t.test('loads and applies custom config file: pino-pretty.config.test.json', (t) => {
t.plan(1)
// Set translateTime: true on run configuration
const configFile = path.join(tmpDir, 'pino-pretty.config.test.json')
fs.writeFileSync(configFile, JSON.stringify({ translateTime: true }, null, 4))
const env = { TERM: ' dumb', TZ: 'UTC' }
const child = spawn(process.argv[0], [bin, '--config', configFile], { env, cwd: tmpDir })
// Validate that the time has been translated
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), '[17:35:28.992] INFO (42): hello world\n')
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})
t.test('loads and applies custom config file: pino-pretty.config.test.js', (t) => {
t.plan(1)
// Set translateTime: true on run configuration
const configFile = path.join(tmpDir, 'pino-pretty.config.test.js')
fs.writeFileSync(configFile, 'module.exports = { translateTime: true }')
const env = { TERM: ' dumb', TZ: 'UTC' }
const child = spawn(process.argv[0], [bin, '--config', configFile], { env, cwd: tmpDir })
// Validate that the time has been translated
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), '[17:35:28.992] INFO (42): hello world\n')
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})
;['--messageKey', '-m'].forEach((optionName) => {
t.test(`cli options override config options via ${optionName}`, (t) => {
t.plan(1)
// Set translateTime: true on run configuration
const configFile = path.join(tmpDir, 'pino-pretty.config.js')
fs.writeFileSync(configFile, `
module.exports = {
translateTime: true,
messageKey: 'custom_msg'
}
`.trim())
// Set messageKey: 'new_msg' using command line option
const env = { TERM: ' dumb', TZ: 'UTC' }
const child = spawn(process.argv[0], [bin, optionName, 'new_msg'], { env, cwd: tmpDir })
// Validate that the time has been translated and correct message key has been used
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), '[17:35:28.992] INFO (42): hello world\n')
})
child.stdin.write(logLine.replace(/"msg"/, '"new_msg"'))
t.teardown(() => {
fs.unlinkSync(configFile)
child.kill()
})
})
})
t.test('cli options with defaults can be overridden by config', (t) => {
t.plan(1)
// Set errorProps: '*' on run configuration
const configFile = path.join(tmpDir, 'pino-pretty.config.js')
fs.writeFileSync(configFile, `
module.exports = {
errorProps: '*'
}
`.trim())
// Set messageKey: 'new_msg' using command line option
const env = { TERM: ' dumb', TZ: 'UTC' }
const child = spawn(process.argv[0], [bin], { env, cwd: tmpDir })
// Validate that the time has been translated and correct message key has been used
child.on('error', t.threw)
child.stdout.on('data', (data) => {
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')
})
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')
t.teardown(() => {
fs.unlinkSync(configFile)
child.kill()
})
})
t.test('throws on missing config file', (t) => {
t.plan(2)
const args = [bin, '--config', 'pino-pretty.config.missing.json']
const env = { TERM: ' dumb', TZ: 'UTC' }
const child = spawn(process.argv[0], args, { env, cwd: tmpDir })
child.on('close', (code) => t.equal(code, 1))
child.stdout.pipe(process.stdout)
child.stderr.setEncoding('utf8')
let data = ''
child.stderr.on('data', (chunk) => {
data += chunk
})
child.on('close', function () {
t.match(
data.toString(), 'Error: Failed to load runtime configuration file: pino-pretty.config.missing.json')
})
t.teardown(() => child.kill())
})
t.test('throws on invalid default config file', (t) => {
t.plan(2)
const configFile = path.join(tmpDir, 'pino-pretty.config.js')
fs.writeFileSync(configFile, 'module.exports = () => {}')
const env = { TERM: ' dumb', TZ: 'UTC' }
const child = spawn(process.argv[0], [bin], { env, cwd: tmpDir })
child.on('close', (code) => t.equal(code, 1))
child.stdout.pipe(process.stdout)
child.stderr.setEncoding('utf8')
let data = ''
child.stderr.on('data', (chunk) => {
data += chunk
})
child.on('close', function () {
t.match(data, 'Error: Invalid runtime configuration file: pino-pretty.config.js')
})
t.teardown(() => child.kill())
})
t.test('throws on invalid custom config file', (t) => {
t.plan(2)
const configFile = path.join(tmpDir, 'pino-pretty.config.invalid.js')
fs.writeFileSync(configFile, 'module.exports = () => {}')
const args = [bin, '--config', path.relative(tmpDir, configFile)]
const env = { TERM: ' dumb', TZ: 'UTC' }
const child = spawn(process.argv[0], args, { env, cwd: tmpDir })
child.on('close', (code) => t.equal(code, 1))
child.stdout.pipe(process.stdout)
child.stderr.setEncoding('utf8')
let data = ''
child.stderr.on('data', (chunk) => {
data += chunk
})
child.on('close', function () {
t.match(data, 'Error: Invalid runtime configuration file: pino-pretty.config.invalid.js')
})
t.teardown(() => child.kill())
})
t.test('test help', (t) => {
t.plan(1)
const env = { TERM: ' dumb', TZ: 'UTC' }
const child = spawn(process.argv[0], [bin, '--help'], { env })
const file = fs.readFileSync('help/help.txt').toString()
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), file)
})
t.teardown(() => child.kill())
})
t.end()
})
+335
View File
@@ -0,0 +1,335 @@
'use strict'
process.env.TZ = 'UTC'
const path = require('path')
const spawn = require('child_process').spawn
const test = require('tap').test
const bin = require.resolve(path.join(__dirname, '..', 'bin.js'))
const epoch = 1522431328992
const logLine = '{"level":30,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n'
const env = { TERM: 'dumb', TZ: 'UTC' }
const formattedEpoch = '17:35:28.992'
test('cli', (t) => {
t.test('does basic reformatting', (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})
;['--levelFirst', '-l'].forEach((optionName) => {
t.test(`flips epoch and level via ${optionName}`, (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, optionName], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `INFO [${formattedEpoch}] (42): hello world\n`)
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})
})
;['--translateTime', '-t'].forEach((optionName) => {
t.test(`translates time to default format via ${optionName}`, (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, optionName], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})
})
;['--ignore', '-i'].forEach((optionName) => {
t.test('does ignore multiple keys', (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, optionName, 'pid,hostname'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${formattedEpoch}] INFO: hello world\n`)
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})
})
;['--customLevels', '-x'].forEach((optionName) => {
t.test(`customize levels via ${optionName}`, (t) => {
t.plan(1)
const logLine = '{"level":1,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n'
const child = spawn(process.argv[0], [bin, optionName, 'err:99,info:1'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})
t.test(`customize levels via ${optionName} without index`, (t) => {
t.plan(1)
const logLine = '{"level":1,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n'
const child = spawn(process.argv[0], [bin, optionName, 'err:99,info'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})
t.test(`customize levels via ${optionName} with minimumLevel`, (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, '--minimumLevel', 'err', optionName, 'err:99,info:1'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${formattedEpoch}] ERR (42): hello world\n`)
})
child.stdin.write('{"level":1,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n')
child.stdin.write('{"level":99,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n')
t.teardown(() => child.kill())
})
t.test(`customize levels via ${optionName} with minimumLevel, customLevels and useOnlyCustomProps false`, (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, '--minimumLevel', 'custom', '--useOnlyCustomProps', 'false', optionName, 'custom:99,info:1'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${formattedEpoch}] CUSTOM (42): hello world\n`)
})
child.stdin.write('{"level":1,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n')
child.stdin.write('{"level":99,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n')
t.teardown(() => child.kill())
})
t.test(`customize levels via ${optionName} with minimumLevel, customLevels and useOnlyCustomProps true`, (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, '--minimumLevel', 'custom', '--useOnlyCustomProps', 'true', optionName, 'custom:99,info:1'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${formattedEpoch}] CUSTOM (42): hello world\n`)
})
child.stdin.write('{"level":1,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n')
child.stdin.write('{"level":99,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n')
t.teardown(() => child.kill())
})
})
;['--customColors', '-X'].forEach((optionName) => {
t.test(`customize levels via ${optionName}`, (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, optionName, 'info:blue,message:red'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})
t.test(`customize levels via ${optionName} with customLevels`, (t) => {
t.plan(1)
const logLine = '{"level":1,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n'
const child = spawn(process.argv[0], [bin, '--customLevels', 'err:99,info', optionName, 'info:blue,message:red'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})
})
;['--useOnlyCustomProps', '-U'].forEach((optionName) => {
t.test(`customize levels via ${optionName} false and customColors`, (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, '--customColors', 'err:blue,info:red', optionName, 'false'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})
t.test(`customize levels via ${optionName} true and customColors`, (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, '--customColors', 'err:blue,info:red', optionName, 'true'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})
t.test(`customize levels via ${optionName} true and customLevels`, (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, '--customLevels', 'err:99,custom:30', optionName, 'true'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${formattedEpoch}] CUSTOM (42): hello world\n`)
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})
t.test(`customize levels via ${optionName} true and no customLevels`, (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, optionName, 'true'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})
t.test(`customize levels via ${optionName} false and customLevels`, (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, '--customLevels', 'err:99,custom:25', optionName, 'false'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})
t.test(`customize levels via ${optionName} false and no customLevels`, (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, optionName, 'false'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})
})
t.test('does ignore escaped keys', (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, '-i', 'log\\.domain\\.corp/foo'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${formattedEpoch}] INFO: hello world\n`)
})
const logLine = '{"level":30,"time":1522431328992,"msg":"hello world","log.domain.corp/foo":"bar"}\n'
child.stdin.write(logLine)
t.teardown(() => child.kill())
})
t.test('passes through stringified date as string', (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin], { env })
child.on('error', t.threw)
const date = JSON.stringify(new Date(epoch))
child.stdout.on('data', (data) => {
t.equal(data.toString(), date + '\n')
})
child.stdin.write(date)
child.stdin.write('\n')
t.teardown(() => child.kill())
})
t.test('end stdin does not end the destination', (t) => {
t.plan(2)
const child = spawn(process.argv[0], [bin], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), 'aaa\n')
})
child.stdin.end('aaa\n')
child.on('exit', function (code) {
t.equal(code, 0)
})
t.teardown(() => child.kill())
})
;['--timestampKey', '-a'].forEach((optionName) => {
t.test(`uses specified timestamp key via ${optionName}`, (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin, optionName, '@timestamp'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${formattedEpoch}] INFO: hello world\n`)
})
const logLine = '{"level":30,"@timestamp":1522431328992,"msg":"hello world"}\n'
child.stdin.write(logLine)
t.teardown(() => child.kill())
})
})
;['--singleLine', '-S'].forEach((optionName) => {
t.test(`singleLine=true via ${optionName}`, (t) => {
t.plan(1)
const logLineWithExtra = JSON.stringify(Object.assign(JSON.parse(logLine), {
extra: {
foo: 'bar',
number: 42
}
})) + '\n'
const child = spawn(process.argv[0], [bin, optionName], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${formattedEpoch}] INFO (42): hello world {"extra":{"foo":"bar","number":42}}\n`)
})
child.stdin.write(logLineWithExtra)
t.teardown(() => child.kill())
})
})
t.test('does ignore nested keys', (t) => {
t.plan(1)
const logLineNested = JSON.stringify(Object.assign(JSON.parse(logLine), {
extra: {
foo: 'bar',
number: 42,
nested: {
foo2: 'bar2'
}
}
})) + '\n'
const child = spawn(process.argv[0], [bin, '-S', '-i', 'extra.foo,extra.nested,extra.nested.miss'], { env })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), `[${formattedEpoch}] INFO (42 on foo): hello world {"extra":{"number":42}}\n`)
})
child.stdin.write(logLineNested)
t.teardown(() => child.kill())
})
t.test('change TZ', (t) => {
t.plan(1)
const child = spawn(process.argv[0], [bin], { env: { ...env, TZ: 'Europe/Amsterdam' } })
child.on('error', t.threw)
child.stdout.on('data', (data) => {
t.equal(data.toString(), '[19:35:28.992] INFO (42): hello world\n')
})
child.stdin.write(logLine)
t.teardown(() => child.kill())
})
t.end()
})
+35
View File
@@ -0,0 +1,35 @@
'use strict'
process.env.TZ = 'UTC'
const test = require('tap').test
const _prettyFactory = require('../').prettyFactory
function prettyFactory (opts) {
if (!opts) {
opts = { colorize: false }
} else if (!Object.prototype.hasOwnProperty.call(opts, 'colorize')) {
opts.colorize = false
}
return _prettyFactory(opts)
}
const logLine = '{"level":30,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n'
test('crlf', (t) => {
t.test('uses LF by default', (t) => {
t.plan(1)
const pretty = prettyFactory()
const formatted = pretty(logLine)
t.equal(formatted.substr(-2), 'd\n')
})
t.test('can use CRLF', (t) => {
t.plan(1)
const pretty = prettyFactory({ crlf: true })
const formatted = pretty(logLine)
t.equal(formatted.substr(-3), 'd\r\n')
})
t.end()
})
@@ -0,0 +1,454 @@
'use strict'
process.env.TZ = 'UTC'
const Writable = require('stream').Writable
const test = require('tap').test
const pino = require('pino')
const serializers = pino.stdSerializers
const _prettyFactory = require('../').prettyFactory
function prettyFactory (opts) {
if (!opts) {
opts = { colorize: false }
} else if (!Object.prototype.hasOwnProperty.call(opts, 'colorize')) {
opts.colorize = false
}
return _prettyFactory(opts)
}
// All dates are computed from 'Fri, 30 Mar 2018 17:35:28 GMT'
const epoch = 1522431328992
const formattedEpoch = '17:35:28.992'
const pid = process.pid
test('error like objects tests', (t) => {
t.beforeEach(() => {
Date.originalNow = Date.now
Date.now = () => epoch
})
t.afterEach(() => {
Date.now = Date.originalNow
delete Date.originalNow
})
t.test('pino transform prettifies Error', (t) => {
t.plan(2)
const pretty = prettyFactory()
const err = Error('hello world')
const expected = err.stack.split('\n')
expected.unshift(err.message)
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
const lines = formatted.split('\n')
t.equal(lines.length, expected.length + 6)
t.equal(lines[0], `[${formattedEpoch}] INFO (${pid}): hello world`)
cb()
}
}))
log.info(err)
})
t.test('errorProps recognizes user specified properties', (t) => {
t.plan(3)
const pretty = prettyFactory({ errorProps: 'statusCode,originalStack' })
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.match(formatted, /\s{4}error stack/)
t.match(formatted, /"statusCode": 500/)
t.match(formatted, /"originalStack": "original stack"/)
cb()
}
}))
const error = Error('error message')
error.stack = 'error stack'
error.statusCode = 500
error.originalStack = 'original stack'
log.error(error)
})
t.test('prettifies ignores undefined errorLikeObject', (t) => {
const pretty = prettyFactory()
pretty({ err: undefined })
pretty({ error: undefined })
t.end()
})
t.test('prettifies Error in property within errorLikeObjectKeys', (t) => {
t.plan(8)
const pretty = prettyFactory({
errorLikeObjectKeys: ['err']
})
const err = Error('hello world')
const expected = err.stack.split('\n')
expected.unshift(err.message)
const log = pino({ serializers: { err: serializers.err } }, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
const lines = formatted.split('\n')
t.equal(lines.length, expected.length + 6)
t.equal(lines[0], `[${formattedEpoch}] INFO (${pid}): hello world`)
t.match(lines[1], /\s{4}err: {/)
t.match(lines[2], /\s{6}"type": "Error",/)
t.match(lines[3], /\s{6}"message": "hello world",/)
t.match(lines[4], /\s{6}"stack":/)
t.match(lines[5], /\s{6}Error: hello world/)
// Node 12 labels the test `<anonymous>`
t.match(lines[6], /\s{10}(at Test.t.test|at Test.<anonymous>)/)
cb()
}
}))
log.info({ err })
})
t.test('prettifies Error in property with singleLine=true', (t) => {
// singleLine=true doesn't apply to errors
t.plan(8)
const pretty = prettyFactory({
singleLine: true,
errorLikeObjectKeys: ['err']
})
const err = Error('hello world')
const expected = [
'{"extra":{"a":1,"b":2}}',
err.message,
...err.stack.split('\n')
]
const log = pino({ serializers: { err: serializers.err } }, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
const lines = formatted.split('\n')
t.equal(lines.length, expected.length + 5)
t.equal(lines[0], `[${formattedEpoch}] INFO (${pid}): hello world {"extra":{"a":1,"b":2}}`)
t.match(lines[1], /\s{4}err: {/)
t.match(lines[2], /\s{6}"type": "Error",/)
t.match(lines[3], /\s{6}"message": "hello world",/)
t.match(lines[4], /\s{6}"stack":/)
t.match(lines[5], /\s{6}Error: hello world/)
// Node 12 labels the test `<anonymous>`
t.match(lines[6], /\s{10}(at Test.t.test|at Test.<anonymous>)/)
cb()
}
}))
log.info({ err, extra: { a: 1, b: 2 } })
})
t.test('prettifies Error in property within errorLikeObjectKeys with custom function', (t) => {
t.plan(4)
const pretty = prettyFactory({
errorLikeObjectKeys: ['err'],
customPrettifiers: {
err: val => `error is ${val.message}`
}
})
const err = Error('hello world')
err.stack = 'Error: hello world\n at anonymous (C:\\project\\node_modules\\example\\index.js)'
const expected = err.stack.split('\n')
expected.unshift(err.message)
const log = pino({ serializers: { err: serializers.err } }, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
const lines = formatted.split('\n')
t.equal(lines.length, 3)
t.equal(lines[0], `[${formattedEpoch}] INFO (${pid}): hello world`)
t.equal(lines[1], ' err: error is hello world')
t.equal(lines[2], '')
cb()
}
}))
log.info({ err })
})
t.test('prettifies Error in property within errorLikeObjectKeys when stack has escaped characters', (t) => {
t.plan(8)
const pretty = prettyFactory({
errorLikeObjectKeys: ['err']
})
const err = Error('hello world')
err.stack = 'Error: hello world\n at anonymous (C:\\project\\node_modules\\example\\index.js)'
const expected = err.stack.split('\n')
expected.unshift(err.message)
const log = pino({ serializers: { err: serializers.err } }, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
const lines = formatted.split('\n')
t.equal(lines.length, expected.length + 6)
t.equal(lines[0], `[${formattedEpoch}] INFO (${pid}): hello world`)
t.match(lines[1], /\s{4}err: {$/)
t.match(lines[2], /\s{6}"type": "Error",$/)
t.match(lines[3], /\s{6}"message": "hello world",$/)
t.match(lines[4], /\s{6}"stack":$/)
t.match(lines[5], /\s{10}Error: hello world$/)
t.match(lines[6], /\s{10}at anonymous \(C:\\project\\node_modules\\example\\index.js\)$/)
cb()
}
}))
log.info({ err })
})
t.test('prettifies Error in property within errorLikeObjectKeys when stack is not the last property', (t) => {
t.plan(9)
const pretty = prettyFactory({
errorLikeObjectKeys: ['err']
})
const err = Error('hello world')
err.anotherField = 'dummy value'
const expected = err.stack.split('\n')
expected.unshift(err.message)
const log = pino({ serializers: { err: serializers.err } }, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
const lines = formatted.split('\n')
t.equal(lines.length, expected.length + 7)
t.equal(lines[0], `[${formattedEpoch}] INFO (${pid}): hello world`)
t.match(lines[1], /\s{4}err: {/)
t.match(lines[2], /\s{6}"type": "Error",/)
t.match(lines[3], /\s{6}"message": "hello world",/)
t.match(lines[4], /\s{6}"stack":/)
t.match(lines[5], /\s{6}Error: hello world/)
// Node 12 labels the test `<anonymous>`
t.match(lines[6], /\s{10}(at Test.t.test|at Test.<anonymous>)/)
t.match(lines[lines.length - 3], /\s{6}"anotherField": "dummy value"/)
cb()
}
}))
log.info({ err })
})
t.test('errorProps flag with "*" (print all nested props)', function (t) {
const pretty = prettyFactory({ errorProps: '*' })
const expectedLines = [
' err: {',
' "type": "Error",',
' "message": "error message",',
' "stack":',
' error stack',
' "statusCode": 500,',
' "originalStack": "original stack",',
' "dataBaseSpecificError": {',
' "erroMessage": "some database error message",',
' "evenMoreSpecificStuff": {',
' "someErrorRelatedObject": "error"',
' }',
' }',
' }'
]
t.plan(expectedLines.length)
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
const lines = formatted.split('\n')
lines.shift(); lines.pop()
for (let i = 0; i < lines.length; i += 1) {
t.equal(lines[i], expectedLines[i])
}
cb()
}
}))
const error = Error('error message')
error.stack = 'error stack'
error.statusCode = 500
error.originalStack = 'original stack'
error.dataBaseSpecificError = {
erroMessage: 'some database error message',
evenMoreSpecificStuff: {
someErrorRelatedObject: 'error'
}
}
log.error(error)
})
t.test('prettifies legacy error object at top level when singleLine=true', function (t) {
t.plan(4)
const pretty = prettyFactory({ singleLine: true })
const err = Error('hello world')
const expected = err.stack.split('\n')
expected.unshift(err.message)
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
const lines = formatted.split('\n')
t.equal(lines.length, expected.length + 1)
t.equal(lines[0], `[${formattedEpoch}] INFO (${pid}): ${expected[0]}`)
t.equal(lines[1], ` ${expected[1]}`)
t.equal(lines[2], ` ${expected[2]}`)
cb()
}
}))
log.info({ type: 'Error', stack: err.stack, msg: err.message })
})
t.test('errorProps: legacy error object at top level', function (t) {
const pretty = prettyFactory({ errorProps: '*' })
const expectedLines = [
'INFO:',
' error stack',
' message: hello message',
' statusCode: 500',
' originalStack: original stack',
' dataBaseSpecificError: {',
' errorMessage: "some database error message"',
' evenMoreSpecificStuff: {',
' "someErrorRelatedObject": "error"',
' }',
' }',
''
]
t.plan(expectedLines.length)
const error = {}
error.level = 30
error.message = 'hello message'
error.type = 'Error'
error.stack = 'error stack'
error.statusCode = 500
error.originalStack = 'original stack'
error.dataBaseSpecificError = {
errorMessage: 'some database error message',
evenMoreSpecificStuff: {
someErrorRelatedObject: 'error'
}
}
const formatted = pretty(JSON.stringify(error))
const lines = formatted.split('\n')
for (let i = 0; i < lines.length; i += 1) {
t.equal(lines[i], expectedLines[i])
}
})
t.test('errorProps flag with a single property', function (t) {
const pretty = prettyFactory({ errorProps: 'originalStack' })
const expectedLines = [
'INFO:',
' error stack',
' originalStack: original stack',
''
]
t.plan(expectedLines.length)
const error = {}
error.level = 30
error.message = 'hello message'
error.type = 'Error'
error.stack = 'error stack'
error.statusCode = 500
error.originalStack = 'original stack'
error.dataBaseSpecificError = {
erroMessage: 'some database error message',
evenMoreSpecificStuff: {
someErrorRelatedObject: 'error'
}
}
const formatted = pretty(JSON.stringify(error))
const lines = formatted.split('\n')
for (let i = 0; i < lines.length; i += 1) {
t.equal(lines[i], expectedLines[i])
}
})
t.test('errorProps flag with a single property non existent', function (t) {
const pretty = prettyFactory({ errorProps: 'originalStackABC' })
const expectedLines = [
'INFO:',
' error stack',
''
]
t.plan(expectedLines.length)
const error = {}
error.level = 30
error.message = 'hello message'
error.type = 'Error'
error.stack = 'error stack'
error.statusCode = 500
error.originalStack = 'original stack'
error.dataBaseSpecificError = {
erroMessage: 'some database error message',
evenMoreSpecificStuff: {
someErrorRelatedObject: 'error'
}
}
const formatted = pretty(JSON.stringify(error))
const lines = formatted.split('\n')
for (let i = 0; i < lines.length; i += 1) {
t.equal(lines[i], expectedLines[i])
}
})
t.test('handles errors with a null stack', (t) => {
t.plan(2)
const pretty = prettyFactory()
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
t.match(formatted, /\s{4}message: "foo"/)
t.match(formatted, /\s{4}stack: null/)
cb()
}
}))
const error = { message: 'foo', stack: null }
log.error(error)
})
t.test('handles errors with a null stack for Error object', (t) => {
const pretty = prettyFactory()
const expectedLines = [
' "type": "Error",',
' "message": "error message",',
' "stack":',
' ',
' "some": "property"'
]
t.plan(expectedLines.length)
const log = pino({}, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
const lines = formatted.split('\n')
lines.shift(); lines.shift(); lines.pop(); lines.pop()
for (let i = 0; i < lines.length; i += 1) {
t.ok(lines[i].includes(expectedLines[i]))
}
cb()
}
}))
const error = Error('error message')
error.stack = null
error.some = 'property'
log.error(error)
})
t.end()
})
@@ -0,0 +1,31 @@
// Run this to see how colouring works
const _prettyFactory = require('../../')
const pino = require('pino')
const { Writable } = require('readable-stream')
function prettyFactory () {
return _prettyFactory({
colorize: true
})
}
const pretty = prettyFactory()
const formatted = pretty('this is not json\nit\'s just regular output\n')
console.log(formatted)
const opts = {
base: {
hostname: 'localhost',
pid: process.pid
}
}
const log = pino(opts, new Writable({
write (chunk, enc, cb) {
const formatted = pretty(chunk.toString())
console.log(formatted)
cb()
}
}))
log.info('foobar')
@@ -0,0 +1,55 @@
import { expectType } from "tsd";
import pretty from "../../";
import PinoPretty, {
PinoPretty as PinoPrettyNamed,
PrettyOptions,
colorizerFactory,
prettyFactory
} from "../../";
import PinoPrettyDefault from "../../";
import * as PinoPrettyStar from "../../";
import PinoPrettyCjsImport = require("../../");
import PrettyStream = PinoPretty.PrettyStream;
const PinoPrettyCjs = require("../../");
const options: PinoPretty.PrettyOptions = {
colorize: true,
crlf: false,
errorLikeObjectKeys: ["err", "error"],
errorProps: "",
hideObject: true,
levelKey: "level",
levelLabel: "foo",
messageFormat: false,
ignore: "",
levelFirst: false,
messageKey: "msg",
timestampKey: "timestamp",
minimumLevel: "trace",
translateTime: "UTC:h:MM:ss TT Z",
singleLine: false,
customPrettifiers: {
key: (value) => {
return value.toString().toUpperCase();
}
},
customLevels: 'verbose:5',
customColors: 'default:white,verbose:gray',
sync: false,
destination: 2,
append: true,
mkdir: true,
};
expectType<PrettyStream>(pretty()); // #326
expectType<PrettyStream>(pretty(options));
expectType<PrettyStream>(PinoPrettyNamed(options));
expectType<PrettyStream>(PinoPrettyDefault(options));
expectType<PrettyStream>(PinoPrettyStar.PinoPretty(options));
expectType<PrettyStream>(PinoPrettyStar.default(options));
expectType<PrettyStream>(PinoPrettyCjsImport.PinoPretty(options));
expectType<PrettyStream>(PinoPrettyCjsImport.default(options));
expectType<any>(PinoPrettyCjs(options));
expectType<PinoPretty.ColorizerFactory>(colorizerFactory);
expectType<PinoPretty.PrettyFactory>(prettyFactory);
+13
View File
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "es6",
"lib": [ "es2015" ],
"module": "commonjs",
"noEmit": true,
"strict": true
},
"include": [
"./test/types/pino-pretty.test.d.ts",
"./index.d.ts"
]
}