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
+2
View File
@@ -0,0 +1,2 @@
pino.d.ts
test/types/pino.test-d.ts
+8
View File
@@ -0,0 +1,8 @@
{
"extends": [
"standard"
],
"rules": {
"no-var": "off"
}
}
+13
View File
@@ -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
+61
View File
@@ -0,0 +1,61 @@
name: Benchmarks
on:
push:
branches:
- main
paths-ignore:
- 'docs/**'
- '*.md'
pull_request:
paths-ignore:
- 'docs/**'
- '*.md'
permissions:
contents: read
jobs:
benchmark_current:
name: benchmark current
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
ref: ${{ github.base_ref }}
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install Modules
run: npm i --ignore-scripts
- name: Run Benchmark
run: npm run bench | tee current.txt
- name: Upload Current Results
uses: actions/upload-artifact@v4
with:
name: current
path: current.txt
benchmark_branch:
name: benchmark branch
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install Modules
run: npm i --ignore-scripts
- name: Run Benchmark
run: npm run bench | tee branch.txt
- name: Upload Branch Results
uses: actions/upload-artifact@v4
with:
name: branch
path: branch.txt
+86
View File
@@ -0,0 +1,86 @@
name: CI
on:
push:
branches:
- main
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: ${{ matrix.node-version }} ${{ matrix.os }}
runs-on: ${{ matrix.os }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
os: [macOS-latest, windows-latest, ubuntu-latest]
node-version: [14, 16, 18, 19, 20, 21]
exclude:
- os: windows-latest
node-version: 14
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: Install dependencies
run: npm i --ignore-scripts
- name: Run tests
run: npm run test-ci
- name: Run smoke test
if: >
matrix.os != 'windows-latest' &&
matrix.node-version > 14
run: npm run test:smoke
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 }}
exclude: 'sonic-boom,pino-std-serializers,quick-format-unescaped,fast-redact'
@@ -0,0 +1,30 @@
name: 'Lock Threads'
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
concurrency:
group: lock
jobs:
action:
runs-on: ubuntu-latest
steps:
- uses: jsumners/lock-threads@b27edac0ac998d42b2815e122b6c24b32b568321
with:
log-output: true
issue-inactive-days: '30'
issue-comment: >
This issue has been automatically locked since there
has not been any recent activity after it was closed.
Please open a new issue for related bugs.
pr-comment: >
This pull request has been automatically locked since there
has not been any recent activity after it was closed.
Please open a new issue for related bugs.
@@ -0,0 +1,99 @@
name: package-manager-ci
on:
push:
branches:
- main
- package-manager-load
permissions:
contents: read
jobs:
pnpm:
name: pnpm package manager on ${{ matrix.node-version }} ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest]
node-version: [16]
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Use pnpm
uses: pnpm/action-setup@v2.4.0
with:
version: ^6.0.0
- name: Install dependancies
run: pnpm install
- name: Tests
run: pnpm run test-ci-pnpm
yarn:
name: yarn package manager on ${{ matrix.node-version }} ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest]
node-version: [16]
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Use yarn
run: |
yarn set version berry && yarn set version 2
echo "nodeLinker: node-modules" >> .yarnrc.yml
# see https://github.com/yarnpkg/berry/issues/2935#issuecomment-911299992
yarn add --dev typescript@~4.3.2
yarn install
env:
# needed due the yarn.lock file in pino's .gitignore
YARN_ENABLE_IMMUTABLE_INSTALLS: false
- name: Tests
run: yarn run test-ci
yarn-pnp:
name: yarn-pnp package manager on ${{ matrix.node-version }} ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest]
node-version: [16]
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Use yarn
run: |
yarn set version berry
echo 'nodeLinker: pnp
packageExtensions:
debug@*:
dependencies:
supports-color: "*"
treport@*:
dependencies:
tap-yaml: "*"
' >> .yarnrc.yml
yarn add --dev typescript@~4.3.2
yarn install
yarn add --dev transport@link:./test/fixtures/transport
env:
# needed due the yarn.lock file in pino's .gitignore
YARN_ENABLE_IMMUTABLE_INSTALLS: false
- name: Tests
run: yarn run test-ci-yarn-pnp
@@ -0,0 +1,23 @@
name: PR Target Check
on:
pull_request_target:
types: [opened]
permissions:
pull-requests: write
jobs:
comment:
if: ${{ github.base_ref != "master" }}
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '⚠️ This pull request does not target the master branch.'
})
View File
+1
View File
@@ -0,0 +1 @@
*
+8
View File
@@ -0,0 +1,8 @@
coverage: true
timeout: 480
check-coverage: false
reporter: terse
files:
- 'test/**/*.test.js'
+1
View File
@@ -0,0 +1 @@
getpino.io
+26
View File
@@ -0,0 +1,26 @@
# Pino is an OPEN Open Source Project
## What?
Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project.
## Rules
There are a few basic ground-rules for contributors:
1. **No `--force` pushes** on `master` or modifying the Git history in any way after a PR has been merged.
1. **Non-master branches** ought to be used for ongoing work.
1. **Non-trivial changes** ought to be subject to an **internal pull-request** to solicit feedback from other contributors.
1. All pull-requests for new features **must** target the `master` branch. PRs to fix bugs in LTS releases are also allowed.
1. Contributors should attempt to adhere to the prevailing code-style.
1. 100% code coverage
## Releases
Declaring formal releases remains the prerogative of the project maintainer.
## Changes to this arrangement
This is an experiment and feedback is welcome! This document may also be subject to pull-requests or changes by contributors where you believe you have something valuable to add or change.
-----------------------------------------
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016-2024 Matteo Collina, David Mark Clements and the Pino contributors listed at https://github.com/pinojs/pino#the-team and in the README file.
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.
+160
View File
@@ -0,0 +1,160 @@
![banner](pino-banner.png)
# pino
[![npm version](https://img.shields.io/npm/v/pino)](https://www.npmjs.com/package/pino)
[![Build Status](https://img.shields.io/github/actions/workflow/status/pinojs/pino/ci.yml)](https://github.com/pinojs/pino/actions)
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/)
[Very low overhead](#low-overhead) Node.js logger.
## Documentation
* [Benchmarks ⇗](/docs/benchmarks.md)
* [API ⇗](/docs/api.md)
* [Browser API ⇗](/docs/browser.md)
* [Redaction ⇗](/docs/redaction.md)
* [Child Loggers ⇗](/docs/child-loggers.md)
* [Transports ⇗](/docs/transports.md)
* [Web Frameworks ⇗](/docs/web.md)
* [Pretty Printing ⇗](/docs/pretty.md)
* [Asynchronous Logging ⇗](/docs/asynchronous.md)
* [Ecosystem ⇗](/docs/ecosystem.md)
* [Help ⇗](/docs/help.md)
* [Long Term Support Policy ⇗](/docs/lts.md)
## Install
Using NPM:
```
$ npm install pino
```
Using YARN:
```
$ yarn add pino
```
If you would like to install pino v6, refer to https://github.com/pinojs/pino/tree/v6.x.
## Usage
```js
const logger = require('pino')()
logger.info('hello world')
const child = logger.child({ a: 'property' })
child.info('hello child!')
```
This produces:
```
{"level":30,"time":1531171074631,"msg":"hello world","pid":657,"hostname":"Davids-MBP-3.fritz.box"}
{"level":30,"time":1531171082399,"msg":"hello child!","pid":657,"hostname":"Davids-MBP-3.fritz.box","a":"property"}
```
For using Pino with a web framework see:
* [Pino with Fastify](docs/web.md#fastify)
* [Pino with Express](docs/web.md#express)
* [Pino with Hapi](docs/web.md#hapi)
* [Pino with Restify](docs/web.md#restify)
* [Pino with Koa](docs/web.md#koa)
* [Pino with Node core `http`](docs/web.md#http)
* [Pino with Nest](docs/web.md#nest)
<a name="essentials"></a>
## Essentials
### Development Formatting
The [`pino-pretty`](https://github.com/pinojs/pino-pretty) module can be used to
format logs during development:
![pretty demo](pretty-demo.png)
### Transports & Log Processing
Due to Node's single-threaded event-loop, it's highly recommended that sending,
alert triggering, reformatting, and all forms of log processing
are conducted in a separate process or thread.
In Pino terminology, we call all log processors "transports" and recommend that the
transports be run in a worker thread using our `pino.transport` API.
For more details see our [Transports⇗](docs/transports.md) document.
### Low overhead
Using minimum resources for logging is very important. Log messages
tend to get added over time and this can lead to a throttling effect
on applications  such as reduced requests per second.
In many cases, Pino is over 5x faster than alternatives.
See the [Benchmarks](docs/benchmarks.md) document for comparisons.
### Bundling support
Pino supports being bundled using tools like webpack or esbuild.
See [Bundling](docs/bundling.md) document for more information.
<a name="team"></a>
## The Team
### Matteo Collina
<https://github.com/mcollina>
<https://www.npmjs.com/~matteo.collina>
<https://twitter.com/matteocollina>
### David Mark Clements
<https://github.com/davidmarkclements>
<https://www.npmjs.com/~davidmarkclements>
<https://twitter.com/davidmarkclem>
### James Sumners
<https://github.com/jsumners>
<https://www.npmjs.com/~jsumners>
<https://twitter.com/jsumners79>
### Thomas Watson Steen
<https://github.com/watson>
<https://www.npmjs.com/~watson>
<https://twitter.com/wa7son>
## Contributing
Pino is an **OPEN Open Source Project**. This means that:
> Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project.
See the [CONTRIBUTING.md](https://github.com/pinojs/pino/blob/master/CONTRIBUTING.md) file for more details.
<a name="acknowledgments"></a>
## Acknowledgments
This project was kindly sponsored by [nearForm](https://nearform.com).
Logo and identity designed by Cosmic Fox Design: https://www.behance.net/cosmicfox.
## License
Licensed under [MIT](./LICENSE).
[elasticsearch]: https://www.elastic.co/products/elasticsearch
[kibana]: https://www.elastic.co/products/kibana
+68
View File
@@ -0,0 +1,68 @@
# Security Policy
This document describes the management of vulnerabilities for the
Pino project and all modules within the Pino organization.
## Reporting vulnerabilities
Individuals who find potential vulnerabilities in Pino are invited
to report them via email at matteo.collina@gmail.com.
### Strict measures when reporting vulnerabilities
Avoid creating new "informative" reports. Only create new
report a potential vulnerability if you are absolutely sure this
should be tagged as an actual vulnerability. Be careful on the maintainers time.
## Handling vulnerability reports
When a potential vulnerability is reported, the following actions are taken:
### Triage
**Delay:** 5 business days
Within 5 business days, a member of the security team provides a first answer to the
individual who submitted the potential vulnerability. The possible responses
can be:
* Acceptance: what was reported is considered as a new vulnerability
* Rejection: what was reported is not considered as a new vulnerability
* Need more information: the security team needs more information in order to evaluate what was reported.
Triaging should include updating issue fields:
* Asset - set/create the module affected by the report
* Severity - TBD, currently left empty
### Correction follow-up
**Delay:** 90 days
When a vulnerability is confirmed, a member of the security team volunteers to follow
up on this report.
With the help of the individual who reported the vulnerability, they contact
the maintainers of the vulnerable package to make them aware of the
vulnerability. The maintainers can be invited as participants to the reported issue.
With the package maintainer, they define a release date for the publication
of the vulnerability. Ideally, this release date should not happen before
the package has been patched.
The report's vulnerable versions upper limit should be set to:
* `*` if there is no fixed version available by the time of publishing the report.
* the last vulnerable version. For example: `<=1.2.3` if a fix exists in `1.2.4`
### Publication
**Delay:** 90 days
Within 90 days after the triage date, the vulnerability must be made public.
**Severity**: Vulnerability severity is assessed using [CVSS v.3](https://www.first.org/cvss/user-guide).
If the package maintainer is actively developing a patch, an additional delay
can be added with the approval of the security team and the individual who
reported the vulnerability.
At this point, a CVE will be requested by the team.
+95
View File
@@ -0,0 +1,95 @@
'use strict'
const bench = require('fastbench')
const pino = require('../')
const bunyan = require('bunyan')
const bole = require('bole')('bench')
const winston = require('winston')
const fs = require('fs')
const dest = fs.createWriteStream('/dev/null')
const loglevel = require('./utils/wrap-log-level')(dest)
const plogNodeStream = pino(dest)
delete require.cache[require.resolve('../')]
const plogMinLength = require('../')(pino.destination({ dest: '/dev/null', minLength: 4096 }))
delete require.cache[require.resolve('../')]
const plogDest = require('../')(pino.destination('/dev/null'))
process.env.DEBUG = 'dlog'
const debug = require('debug')
const dlog = debug('dlog')
dlog.log = function (s) { dest.write(s) }
const max = 10
const blog = bunyan.createLogger({
name: 'myapp',
streams: [{
level: 'trace',
stream: dest
}]
})
require('bole').output({
level: 'info',
stream: dest
}).setFastTime(true)
const chill = winston.createLogger({
transports: [
new winston.transports.Stream({
stream: fs.createWriteStream('/dev/null')
})
]
})
const run = bench([
function benchBunyan (cb) {
for (var i = 0; i < max; i++) {
blog.info('hello world')
}
setImmediate(cb)
},
function benchWinston (cb) {
for (var i = 0; i < max; i++) {
chill.log('info', 'hello world')
}
setImmediate(cb)
},
function benchBole (cb) {
for (var i = 0; i < max; i++) {
bole.info('hello world')
}
setImmediate(cb)
},
function benchDebug (cb) {
for (var i = 0; i < max; i++) {
dlog('hello world')
}
setImmediate(cb)
},
function benchLogLevel (cb) {
for (var i = 0; i < max; i++) {
loglevel.info('hello world')
}
setImmediate(cb)
},
function benchPino (cb) {
for (var i = 0; i < max; i++) {
plogDest.info('hello world')
}
setImmediate(cb)
},
function benchPinoMinLength (cb) {
for (var i = 0; i < max; i++) {
plogMinLength.info('hello world')
}
setImmediate(cb)
},
function benchPinoNodeStream (cb) {
for (var i = 0; i < max; i++) {
plogNodeStream.info('hello world')
}
setImmediate(cb)
}
], 10000)
run(run)
@@ -0,0 +1,52 @@
'use strict'
const bench = require('fastbench')
const pino = require('../')
const bunyan = require('bunyan')
const fs = require('fs')
const dest = fs.createWriteStream('/dev/null')
const plogNodeStream = pino(dest).child({ a: 'property' }).child({ sub: 'child' })
delete require.cache[require.resolve('../')]
const plogDest = require('../')(pino.destination('/dev/null')).child({ a: 'property' }).child({ sub: 'child' })
delete require.cache[require.resolve('../')]
const plogMinLength = require('../')(pino.destination({ dest: '/dev/null', sync: false, minLength: 4096 }))
.child({ a: 'property' })
.child({ sub: 'child' })
const max = 10
const blog = bunyan.createLogger({
name: 'myapp',
streams: [{
level: 'trace',
stream: dest
}]
}).child({ a: 'property' }).child({ sub: 'child' })
const run = bench([
function benchBunyanChildChild (cb) {
for (var i = 0; i < max; i++) {
blog.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoChildChild (cb) {
for (var i = 0; i < max; i++) {
plogDest.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoMinLengthChildChild (cb) {
for (var i = 0; i < max; i++) {
plogMinLength.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoNodeStreamChildChild (cb) {
for (var i = 0; i < max; i++) {
plogNodeStream.info({ hello: 'world' })
}
setImmediate(cb)
}
], 10000)
run(run)
@@ -0,0 +1,73 @@
'use strict'
const bench = require('fastbench')
const pino = require('../')
const bunyan = require('bunyan')
const bole = require('bole')('bench')
const fs = require('fs')
const dest = fs.createWriteStream('/dev/null')
const plogNodeStream = pino(dest)
const plogDest = pino(pino.destination(('/dev/null')))
delete require.cache[require.resolve('../')]
const plogMinLength = require('../')(pino.destination({ dest: '/dev/null', sync: false, minLength: 4096 }))
const max = 10
const blog = bunyan.createLogger({
name: 'myapp',
streams: [{
level: 'trace',
stream: dest
}]
})
require('bole').output({
level: 'info',
stream: dest
}).setFastTime(true)
const run = bench([
function benchBunyanCreation (cb) {
const child = blog.child({ a: 'property' })
for (var i = 0; i < max; i++) {
child.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchBoleCreation (cb) {
const child = bole('child')
for (var i = 0; i < max; i++) {
child.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoCreation (cb) {
const child = plogDest.child({ a: 'property' })
for (var i = 0; i < max; i++) {
child.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoMinLengthCreation (cb) {
const child = plogMinLength.child({ a: 'property' })
for (var i = 0; i < max; i++) {
child.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoNodeStreamCreation (cb) {
const child = plogNodeStream.child({ a: 'property' })
for (var i = 0; i < max; i++) {
child.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoCreationWithOption (cb) {
const child = plogDest.child({ a: 'property' }, { redact: [] })
for (var i = 0; i < max; i++) {
child.info({ hello: 'world' })
}
setImmediate(cb)
}
], 10000)
run(run)
+62
View File
@@ -0,0 +1,62 @@
'use strict'
const bench = require('fastbench')
const pino = require('../')
const bunyan = require('bunyan')
const bole = require('bole')('bench')('child')
const fs = require('fs')
const dest = fs.createWriteStream('/dev/null')
const plogNodeStream = pino(dest).child({ a: 'property' })
delete require.cache[require.resolve('../')]
const plogDest = require('../')(pino.destination('/dev/null')).child({ a: 'property' })
delete require.cache[require.resolve('../')]
const plogMinLength = require('../')(pino.destination({ dest: '/dev/null', sync: false, minLength: 4096 }))
const max = 10
const blog = bunyan.createLogger({
name: 'myapp',
streams: [{
level: 'trace',
stream: dest
}]
}).child({ a: 'property' })
require('bole').output({
level: 'info',
stream: dest
}).setFastTime(true)
const run = bench([
function benchBunyanChild (cb) {
for (var i = 0; i < max; i++) {
blog.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchBoleChild (cb) {
for (var i = 0; i < max; i++) {
bole.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoChild (cb) {
for (var i = 0; i < max; i++) {
plogDest.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoMinLengthChild (cb) {
for (var i = 0; i < max; i++) {
plogMinLength.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoNodeStreamChild (cb) {
for (var i = 0; i < max; i++) {
plogNodeStream.info({ hello: 'world' })
}
setImmediate(cb)
}
], 10000)
run(run)
@@ -0,0 +1,88 @@
'use strict'
const bench = require('fastbench')
const pino = require('../')
const bunyan = require('bunyan')
const bole = require('bole')('bench')
const winston = require('winston')
const fs = require('fs')
const dest = fs.createWriteStream('/dev/null')
const plogNodeStream = pino(dest)
delete require.cache[require.resolve('../')]
const plogDest = require('../')(pino.destination('/dev/null'))
delete require.cache[require.resolve('../')]
const plogMinLength = require('../')(pino.destination({ dest: '/dev/null', sync: false, minLength: 4096 }))
delete require.cache[require.resolve('../')]
const loglevel = require('./utils/wrap-log-level')(dest)
const deep = Object.assign({}, require('../package.json'), { level: 'info' })
const max = 10
const blog = bunyan.createLogger({
name: 'myapp',
streams: [{
level: 'trace',
stream: dest
}]
})
require('bole').output({
level: 'info',
stream: dest
}).setFastTime(true)
const chill = winston.createLogger({
transports: [
new winston.transports.Stream({
stream: fs.createWriteStream('/dev/null')
})
]
})
const run = bench([
function benchBunyanDeepObj (cb) {
for (var i = 0; i < max; i++) {
blog.info(deep)
}
setImmediate(cb)
},
function benchWinstonDeepObj (cb) {
for (var i = 0; i < max; i++) {
chill.log(deep)
}
setImmediate(cb)
},
function benchBoleDeepObj (cb) {
for (var i = 0; i < max; i++) {
bole.info(deep)
}
setImmediate(cb)
},
function benchLogLevelDeepObj (cb) {
for (var i = 0; i < max; i++) {
loglevel.info(deep)
}
setImmediate(cb)
},
function benchPinoDeepObj (cb) {
for (var i = 0; i < max; i++) {
plogDest.info(deep)
}
setImmediate(cb)
},
function benchPinoMinLengthDeepObj (cb) {
for (var i = 0; i < max; i++) {
plogMinLength.info(deep)
}
setImmediate(cb)
},
function benchPinoNodeStreamDeepObj (cb) {
for (var i = 0; i < max; i++) {
plogNodeStream.info(deep)
}
setImmediate(cb)
}
], 10000)
run(run)
@@ -0,0 +1,50 @@
'use strict'
const formatters = {
level (label, number) {
return {
log: {
level: label
}
}
},
bindings (bindings) {
return {
process: {
pid: bindings.pid
},
host: {
name: bindings.hostname
}
}
},
log (obj) {
return { foo: 'bar', ...obj }
}
}
const bench = require('fastbench')
const pino = require('../')
delete require.cache[require.resolve('../')]
const pinoNoFormatters = require('../')(pino.destination('/dev/null'))
delete require.cache[require.resolve('../')]
const pinoFormatters = require('../')({ formatters }, pino.destination('/dev/null'))
const max = 10
const run = bench([
function benchPinoNoFormatters (cb) {
for (var i = 0; i < max; i++) {
pinoNoFormatters.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoFormatters (cb) {
for (var i = 0; i < max; i++) {
pinoFormatters.info({ hello: 'world' })
}
setImmediate(cb)
}
], 10000)
run(run)
@@ -0,0 +1,67 @@
'use strict'
const bench = require('fastbench')
const pino = require('../../')
const base = pino(pino.destination('/dev/null'))
const baseCl = pino({
customLevels: { foo: 31 }
}, pino.destination('/dev/null'))
const child = base.child({})
const childCl = base.child({
customLevels: { foo: 31 }
})
const childOfBaseCl = baseCl.child({})
const max = 100
const run = bench([
function benchPinoNoCustomLevel (cb) {
for (var i = 0; i < max; i++) {
base.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoCustomLevel (cb) {
for (var i = 0; i < max; i++) {
baseCl.foo({ hello: 'world' })
}
setImmediate(cb)
},
function benchChildNoCustomLevel (cb) {
for (var i = 0; i < max; i++) {
child.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoChildCustomLevel (cb) {
for (var i = 0; i < max; i++) {
childCl.foo({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoChildInheritedCustomLevel (cb) {
for (var i = 0; i < max; i++) {
childOfBaseCl.foo({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoChildCreation (cb) {
const child = base.child({})
for (var i = 0; i < max; i++) {
child.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoChildCreationCustomLevel (cb) {
const child = base.child({
customLevels: { foo: 31 }
})
for (var i = 0; i < max; i++) {
child.foo({ hello: 'world' })
}
setImmediate(cb)
}
], 10000)
run(run)
@@ -0,0 +1,76 @@
'use strict'
const bench = require('fastbench')
const pino = require('../../')
const fs = require('fs')
const dest = fs.createWriteStream('/dev/null')
const plog = pino(dest)
delete require.cache[require.resolve('../../')]
const plogDest = require('../../')(pino.destination('/dev/null'))
delete require.cache[require.resolve('../../')]
const plogAsync = require('../../')(pino.destination({ dest: '/dev/null', sync: false }))
const deep = require('../../package.json')
deep.deep = JSON.parse(JSON.stringify(deep))
deep.deep.deep = JSON.parse(JSON.stringify(deep))
const longStr = JSON.stringify(deep)
const max = 10
const run = bench([
function benchPinoLongString (cb) {
for (var i = 0; i < max; i++) {
plog.info(longStr)
}
setImmediate(cb)
},
function benchPinoDestLongString (cb) {
for (var i = 0; i < max; i++) {
plogDest.info(longStr)
}
setImmediate(cb)
},
function benchPinoAsyncLongString (cb) {
for (var i = 0; i < max; i++) {
plogAsync.info(longStr)
}
setImmediate(cb)
},
function benchPinoDeepObj (cb) {
for (var i = 0; i < max; i++) {
plog.info(deep)
}
setImmediate(cb)
},
function benchPinoDestDeepObj (cb) {
for (var i = 0; i < max; i++) {
plogDest.info(deep)
}
setImmediate(cb)
},
function benchPinoAsyncDeepObj (cb) {
for (var i = 0; i < max; i++) {
plogAsync.info(deep)
}
setImmediate(cb)
},
function benchPinoInterpolateDeep (cb) {
for (var i = 0; i < max; i++) {
plog.info('hello %j', deep)
}
setImmediate(cb)
},
function benchPinoDestInterpolateDeep (cb) {
for (var i = 0; i < max; i++) {
plogDest.info('hello %j', deep)
}
setImmediate(cb)
},
function benchPinoAsyncInterpolateDeep (cb) {
for (var i = 0; i < max; i++) {
plogAsync.info('hello %j', deep)
}
setImmediate(cb)
}
], 1000)
run(run)
@@ -0,0 +1,182 @@
'use strict'
const bench = require('fastbench')
const pino = require('../../')
const fs = require('fs')
const dest = fs.createWriteStream('/dev/null')
const plog = pino(dest)
delete require.cache[require.resolve('../../')]
const plogDest = require('../../')(pino.destination('/dev/null'))
delete require.cache[require.resolve('../../')]
const plogAsync = require('../../')(pino.destination({ dest: '/dev/null', sync: false }))
const plogChild = plog.child({ a: 'property' })
const plogDestChild = plogDest.child({ a: 'property' })
const plogAsyncChild = plogAsync.child({ a: 'property' })
const plogChildChild = plog.child({ a: 'property' }).child({ sub: 'child' })
const plogDestChildChild = plogDest.child({ a: 'property' }).child({ sub: 'child' })
const plogAsyncChildChild = plogAsync.child({ a: 'property' }).child({ sub: 'child' })
const max = 10
const run = bench([
function benchPino (cb) {
for (var i = 0; i < max; i++) {
plog.info('hello world')
}
setImmediate(cb)
},
function benchPinoDest (cb) {
for (var i = 0; i < max; i++) {
plogDest.info('hello world')
}
setImmediate(cb)
},
function benchPinoExtreme (cb) {
for (var i = 0; i < max; i++) {
plogAsync.info('hello world')
}
setImmediate(cb)
},
function benchPinoObj (cb) {
for (var i = 0; i < max; i++) {
plog.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoDestObj (cb) {
for (var i = 0; i < max; i++) {
plogDest.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoAsyncObj (cb) {
for (var i = 0; i < max; i++) {
plogAsync.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoChild (cb) {
for (var i = 0; i < max; i++) {
plogChild.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoDestChild (cb) {
for (var i = 0; i < max; i++) {
plogDestChild.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoAsyncChild (cb) {
for (var i = 0; i < max; i++) {
plogAsyncChild.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoChildChild (cb) {
for (var i = 0; i < max; i++) {
plogChildChild.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoDestChildChild (cb) {
for (var i = 0; i < max; i++) {
plogDestChildChild.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoAsyncChildChild (cb) {
for (var i = 0; i < max; i++) {
plogAsyncChildChild.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoChildCreation (cb) {
const child = plog.child({ a: 'property' })
for (var i = 0; i < max; i++) {
child.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoDestChildCreation (cb) {
const child = plogDest.child({ a: 'property' })
for (var i = 0; i < max; i++) {
child.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoMulti (cb) {
for (var i = 0; i < max; i++) {
plog.info('hello', 'world')
}
setImmediate(cb)
},
function benchPinoDestMulti (cb) {
for (var i = 0; i < max; i++) {
plogDest.info('hello', 'world')
}
setImmediate(cb)
},
function benchPinoAsyncMulti (cb) {
for (var i = 0; i < max; i++) {
plogAsync.info('hello', 'world')
}
setImmediate(cb)
},
function benchPinoInterpolate (cb) {
for (var i = 0; i < max; i++) {
plog.info('hello %s', 'world')
}
setImmediate(cb)
},
function benchPinoDestInterpolate (cb) {
for (var i = 0; i < max; i++) {
plogDest.info('hello %s', 'world')
}
setImmediate(cb)
},
function benchPinoDestInterpolate (cb) {
for (var i = 0; i < max; i++) {
plogDest.info('hello %s', 'world')
}
setImmediate(cb)
},
function benchPinoInterpolateAll (cb) {
for (var i = 0; i < max; i++) {
plog.info('hello %s %j %d', 'world', { obj: true }, 4)
}
setImmediate(cb)
},
function benchPinoDestInterpolateAll (cb) {
for (var i = 0; i < max; i++) {
plogDest.info('hello %s %j %d', 'world', { obj: true }, 4)
}
setImmediate(cb)
},
function benchPinoAsyncInterpolateAll (cb) {
for (var i = 0; i < max; i++) {
plogAsync.info('hello %s %j %d', 'world', { obj: true }, 4)
}
setImmediate(cb)
},
function benchPinoInterpolateExtra (cb) {
for (var i = 0; i < max; i++) {
plog.info('hello %s %j %d', 'world', { obj: true }, 4, { another: 'obj' })
}
setImmediate(cb)
},
function benchPinoDestInterpolateExtra (cb) {
for (var i = 0; i < max; i++) {
plogDest.info('hello %s %j %d', 'world', { obj: true }, 4, { another: 'obj' })
}
setImmediate(cb)
},
function benchPinoAsyncInterpolateExtra (cb) {
for (var i = 0; i < max; i++) {
plogAsync.info('hello %s %j %d', 'world', { obj: true }, 4, { another: 'obj' })
}
setImmediate(cb)
}
], 10000)
run(run)
@@ -0,0 +1,75 @@
'use strict'
const bench = require('fastbench')
const pino = require('../../')
const base = pino(pino.destination('/dev/null'))
const child = base.child({})
const childChild = child.child({})
const childChildChild = childChild.child({})
const childChildChildChild = childChildChild.child({})
const child2 = base.child({})
const baseSerializers = pino(pino.destination('/dev/null'))
const baseSerializersChild = baseSerializers.child({})
const baseSerializersChildSerializers = baseSerializers.child({})
const max = 100
const run = bench([
function benchPinoBase (cb) {
for (var i = 0; i < max; i++) {
base.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoChild (cb) {
for (var i = 0; i < max; i++) {
child.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoChildChild (cb) {
for (var i = 0; i < max; i++) {
childChild.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoChildChildChild (cb) {
for (var i = 0; i < max; i++) {
childChildChild.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoChildChildChildChild (cb) {
for (var i = 0; i < max; i++) {
childChildChildChild.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoChild2 (cb) {
for (var i = 0; i < max; i++) {
child2.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoBaseSerializers (cb) {
for (var i = 0; i < max; i++) {
baseSerializers.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoBaseSerializersChild (cb) {
for (var i = 0; i < max; i++) {
baseSerializersChild.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoBaseSerializersChildSerializers (cb) {
for (var i = 0; i < max; i++) {
baseSerializersChildSerializers.info({ hello: 'world' })
}
setImmediate(cb)
}
], 10000)
run(run)
@@ -0,0 +1,86 @@
'use strict'
const bench = require('fastbench')
const pino = require('../../')
const fs = require('fs')
const dest = fs.createWriteStream('/dev/null')
const plog = pino(dest)
delete require.cache[require.resolve('../../')]
const plogAsync = require('../../')(pino.destination({ dest: '/dev/null', sync: false }))
delete require.cache[require.resolve('../../')]
const plogUnsafe = require('../../')({ safe: false }, dest)
delete require.cache[require.resolve('../../')]
const plogUnsafeAsync = require('../../')(
{ safe: false },
pino.destination({ dest: '/dev/null', sync: false })
)
const plogRedact = pino({ redact: ['a.b.c'] }, dest)
delete require.cache[require.resolve('../../')]
const plogAsyncRedact = require('../../')(
{ redact: ['a.b.c'] },
pino.destination({ dest: '/dev/null', sync: false })
)
delete require.cache[require.resolve('../../')]
const plogUnsafeRedact = require('../../')({ redact: ['a.b.c'], safe: false }, dest)
delete require.cache[require.resolve('../../')]
const plogUnsafeAsyncRedact = require('../../')(
{ redact: ['a.b.c'], safe: false },
pino.destination({ dest: '/dev/null', sync: false })
)
const max = 10
// note that "redact me." is the same amount of bytes as the censor: "[Redacted]"
const run = bench([
function benchPinoNoRedact (cb) {
for (var i = 0; i < max; i++) {
plog.info({ a: { b: { c: 'redact me.', d: 'leave me' } } })
}
setImmediate(cb)
},
function benchPinoRedact (cb) {
for (var i = 0; i < max; i++) {
plogRedact.info({ a: { b: { c: 'redact me.', d: 'leave me' } } })
}
setImmediate(cb)
},
function benchPinoUnsafeNoRedact (cb) {
for (var i = 0; i < max; i++) {
plogUnsafe.info({ a: { b: { c: 'redact me.', d: 'leave me' } } })
}
setImmediate(cb)
},
function benchPinoUnsafeRedact (cb) {
for (var i = 0; i < max; i++) {
plogUnsafeRedact.info({ a: { b: { c: 'redact me.', d: 'leave me' } } })
}
setImmediate(cb)
},
function benchPinoAsyncNoRedact (cb) {
for (var i = 0; i < max; i++) {
plogAsync.info({ a: { b: { c: 'redact me.', d: 'leave me' } } })
}
setImmediate(cb)
},
function benchPinoAsyncRedact (cb) {
for (var i = 0; i < max; i++) {
plogAsyncRedact.info({ a: { b: { c: 'redact me.', d: 'leave me' } } })
}
setImmediate(cb)
},
function benchPinoUnsafeAsyncNoRedact (cb) {
for (var i = 0; i < max; i++) {
plogUnsafeAsync.info({ a: { b: { c: 'redact me.', d: 'leave me' } } })
}
setImmediate(cb)
},
function benchPinoUnsafeAsyncRedact (cb) {
for (var i = 0; i < max; i++) {
plogUnsafeAsyncRedact.info({ a: { b: { c: 'redact me.', d: 'leave me' } } })
}
setImmediate(cb)
}
], 10000)
run(run)
@@ -0,0 +1,81 @@
'use strict'
const bench = require('fastbench')
const pino = require('../')
const bunyan = require('bunyan')
const bole = require('bole')('bench')
const winston = require('winston')
const fs = require('fs')
const dest = fs.createWriteStream('/dev/null')
const plogNodeStream = pino(dest)
delete require.cache[require.resolve('../')]
const plogDest = require('../')(pino.destination('/dev/null'))
delete require.cache[require.resolve('../')]
const plogMinLength = require('../')(pino.destination({ dest: '/dev/null', sync: false, minLength: 4096 }))
const crypto = require('crypto')
const longStr = crypto.randomBytes(2000).toString()
const max = 10
const blog = bunyan.createLogger({
name: 'myapp',
streams: [{
level: 'trace',
stream: dest
}]
})
require('bole').output({
level: 'info',
stream: dest
}).setFastTime(true)
const chill = winston.createLogger({
transports: [
new winston.transports.Stream({
stream: fs.createWriteStream('/dev/null')
})
]
})
const run = bench([
function benchBunyan (cb) {
for (var i = 0; i < max; i++) {
blog.info(longStr)
}
setImmediate(cb)
},
function benchWinston (cb) {
for (var i = 0; i < max; i++) {
chill.info(longStr)
}
setImmediate(cb)
},
function benchBole (cb) {
for (var i = 0; i < max; i++) {
bole.info(longStr)
}
setImmediate(cb)
},
function benchPino (cb) {
for (var i = 0; i < max; i++) {
plogDest.info(longStr)
}
setImmediate(cb)
},
function benchPinoMinLength (cb) {
for (var i = 0; i < max; i++) {
plogMinLength.info(longStr)
}
setImmediate(cb)
},
function benchPinoNodeStream (cb) {
for (var i = 0; i < max; i++) {
plogNodeStream.info(longStr)
}
setImmediate(cb)
}
], 1000)
run(run)
+193
View File
@@ -0,0 +1,193 @@
'use strict'
const bench = require('fastbench')
const pino = require('../')
const bunyan = require('bunyan')
const bole = require('bole')('bench')
const winston = require('winston')
const fs = require('fs')
const dest = fs.createWriteStream('/dev/null')
const plogNodeStream = pino(dest)
delete require.cache[require.resolve('../')]
const plogDest = require('../')(pino.destination('/dev/null'))
delete require.cache[require.resolve('../')]
const plogMinLength = require('../')(pino.destination({ dest: '/dev/null', sync: false, minLength: 4096 }))
delete require.cache[require.resolve('../')]
const deep = require('../package.json')
deep.deep = Object.assign({}, JSON.parse(JSON.stringify(deep)))
deep.deep.deep = Object.assign({}, JSON.parse(JSON.stringify(deep)))
deep.deep.deep.deep = Object.assign({}, JSON.parse(JSON.stringify(deep)))
const blog = bunyan.createLogger({
name: 'myapp',
streams: [{
level: 'trace',
stream: dest
}]
})
require('bole').output({
level: 'info',
stream: dest
}).setFastTime(true)
const chill = winston.createLogger({
transports: [
new winston.transports.Stream({
stream: fs.createWriteStream('/dev/null')
})
]
})
const max = 10
const run = bench([
function benchBunyanInterpolate (cb) {
for (var i = 0; i < max; i++) {
blog.info('hello %s', 'world')
}
setImmediate(cb)
},
function benchWinstonInterpolate (cb) {
for (var i = 0; i < max; i++) {
chill.log('info', 'hello %s', 'world')
}
setImmediate(cb)
},
function benchBoleInterpolate (cb) {
for (var i = 0; i < max; i++) {
bole.info('hello %s', 'world')
}
setImmediate(cb)
},
function benchPinoInterpolate (cb) {
for (var i = 0; i < max; i++) {
plogDest.info('hello %s', 'world')
}
setImmediate(cb)
},
function benchPinoMinLengthInterpolate (cb) {
for (var i = 0; i < max; i++) {
plogMinLength.info('hello %s', 'world')
}
setImmediate(cb)
},
function benchPinoNodeStreamInterpolate (cb) {
for (var i = 0; i < max; i++) {
plogNodeStream.info('hello %s', 'world')
}
setImmediate(cb)
},
function benchBunyanInterpolateAll (cb) {
for (var i = 0; i < max; i++) {
blog.info('hello %s %j %d', 'world', { obj: true }, 4)
}
setImmediate(cb)
},
function benchWinstonInterpolateAll (cb) {
for (var i = 0; i < max; i++) {
chill.log('info', 'hello %s %j %d', 'world', { obj: true }, 4)
}
setImmediate(cb)
},
function benchBoleInterpolateAll (cb) {
for (var i = 0; i < max; i++) {
bole.info('hello %s %j %d', 'world', { obj: true }, 4)
}
setImmediate(cb)
},
function benchPinoInterpolateAll (cb) {
for (var i = 0; i < max; i++) {
plogDest.info('hello %s %j %d', 'world', { obj: true }, 4)
}
setImmediate(cb)
},
function benchPinoMinLengthInterpolateAll (cb) {
for (var i = 0; i < max; i++) {
plogMinLength.info('hello %s %j %d', 'world', { obj: true }, 4)
}
setImmediate(cb)
},
function benchPinoNodeStreamInterpolateAll (cb) {
for (var i = 0; i < max; i++) {
plogNodeStream.info('hello %s %j %d', 'world', { obj: true }, 4)
}
setImmediate(cb)
},
function benchBunyanInterpolateExtra (cb) {
for (var i = 0; i < max; i++) {
blog.info('hello %s %j %d', 'world', { obj: true }, 4, { another: 'obj' })
}
setImmediate(cb)
},
function benchWinstonInterpolateExtra (cb) {
for (var i = 0; i < max; i++) {
chill.log('info', 'hello %s %j %d', 'world', { obj: true }, 4, { another: 'obj' })
}
setImmediate(cb)
},
function benchBoleInterpolateExtra (cb) {
for (var i = 0; i < max; i++) {
bole.info('hello %s %j %d', 'world', { obj: true }, 4, { another: 'obj' })
}
setImmediate(cb)
},
function benchPinoInterpolateExtra (cb) {
for (var i = 0; i < max; i++) {
plogDest.info('hello %s %j %d', 'world', { obj: true }, 4, { another: 'obj' })
}
setImmediate(cb)
},
function benchPinoMinLengthInterpolateExtra (cb) {
for (var i = 0; i < max; i++) {
plogMinLength.info('hello %s %j %d', 'world', { obj: true }, 4, { another: 'obj' })
}
setImmediate(cb)
},
function benchPinoNodeStreamInterpolateExtra (cb) {
for (var i = 0; i < max; i++) {
plogNodeStream.info('hello %s %j %d', 'world', { obj: true }, 4, { another: 'obj' })
}
setImmediate(cb)
},
function benchBunyanInterpolateDeep (cb) {
for (var i = 0; i < max; i++) {
blog.info('hello %j', deep)
}
setImmediate(cb)
},
function benchWinstonInterpolateDeep (cb) {
for (var i = 0; i < max; i++) {
chill.log('info', 'hello %j', deep)
}
setImmediate(cb)
},
function benchBoleInterpolateDeep (cb) {
for (var i = 0; i < max; i++) {
bole.info('hello %j', deep)
}
setImmediate(cb)
},
function benchPinoInterpolateDeep (cb) {
for (var i = 0; i < max; i++) {
plogDest.info('hello %j', deep)
}
setImmediate(cb)
},
function benchPinoMinLengthInterpolateDeep (cb) {
for (var i = 0; i < max; i++) {
plogMinLength.info('hello %j', deep)
}
setImmediate(cb)
},
function benchPinoNodeStreamInterpolateDeep (cb) {
for (var i = 0; i < max; i++) {
plogNodeStream.info('hello %j', deep)
}
setImmediate(cb)
}
], 10000)
run(run)
+98
View File
@@ -0,0 +1,98 @@
'use strict'
const bench = require('fastbench')
const bunyan = require('bunyan')
const pino = require('../')
const fs = require('fs')
const dest = fs.createWriteStream('/dev/null')
const tenStreams = [
{ stream: dest },
{ stream: dest },
{ stream: dest },
{ stream: dest },
{ stream: dest },
{ level: 'debug', stream: dest },
{ level: 'debug', stream: dest },
{ level: 'trace', stream: dest },
{ level: 'warn', stream: dest },
{ level: 'fatal', stream: dest }
]
const pinomsTen = pino({ level: 'debug' }, pino.multistream(tenStreams))
const fourStreams = [
{ stream: dest },
{ stream: dest },
{ level: 'debug', stream: dest },
{ level: 'trace', stream: dest }
]
const pinomsFour = pino({ level: 'debug' }, pino.multistream(fourStreams))
const pinomsOne = pino({ level: 'info' }, pino.multistream(dest))
const blogOne = bunyan.createLogger({
name: 'myapp',
streams: [{ stream: dest }]
})
const blogTen = bunyan.createLogger({
name: 'myapp',
streams: tenStreams
})
const blogFour = bunyan.createLogger({
name: 'myapp',
streams: fourStreams
})
const max = 10
const run = bench([
function benchBunyanTen (cb) {
for (let i = 0; i < max; i++) {
blogTen.info('hello world')
blogTen.debug('hello world')
blogTen.trace('hello world')
blogTen.warn('hello world')
blogTen.fatal('hello world')
}
setImmediate(cb)
},
function benchPinoMSTen (cb) {
for (let i = 0; i < max; i++) {
pinomsTen.info('hello world')
pinomsTen.debug('hello world')
pinomsTen.trace('hello world')
pinomsTen.warn('hello world')
pinomsTen.fatal('hello world')
}
setImmediate(cb)
},
function benchBunyanFour (cb) {
for (let i = 0; i < max; i++) {
blogFour.info('hello world')
blogFour.debug('hello world')
blogFour.trace('hello world')
}
setImmediate(cb)
},
function benchPinoMSFour (cb) {
for (let i = 0; i < max; i++) {
pinomsFour.info('hello world')
pinomsFour.debug('hello world')
pinomsFour.trace('hello world')
}
setImmediate(cb)
},
function benchBunyanOne (cb) {
for (let i = 0; i < max; i++) {
blogOne.info('hello world')
}
setImmediate(cb)
},
function benchPinoMSOne (cb) {
for (let i = 0; i < max; i++) {
pinomsOne.info('hello world')
}
setImmediate(cb)
}
], 10000)
run()
+82
View File
@@ -0,0 +1,82 @@
'use strict'
const bench = require('fastbench')
const pino = require('../')
const bunyan = require('bunyan')
const bole = require('bole')('bench')
const winston = require('winston')
const fs = require('fs')
const dest = fs.createWriteStream('/dev/null')
const loglevel = require('./utils/wrap-log-level')(dest)
const plogNodeStream = pino(dest)
delete require.cache[require.resolve('../')]
const plogDest = require('../')(pino.destination('/dev/null'))
delete require.cache[require.resolve('../')]
const plogMinLength = require('../')(pino.destination({ dest: '/dev/null', sync: false, minLength: 4096 }))
const blog = bunyan.createLogger({
name: 'myapp',
streams: [{
level: 'trace',
stream: dest
}]
})
require('bole').output({
level: 'info',
stream: dest
}).setFastTime(true)
const chill = winston.createLogger({
transports: [
new winston.transports.Stream({
stream: fs.createWriteStream('/dev/null')
})
]
})
const max = 10
const run = bench([
function benchBunyanObj (cb) {
for (var i = 0; i < max; i++) {
blog.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchWinstonObj (cb) {
for (var i = 0; i < max; i++) {
chill.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchBoleObj (cb) {
for (var i = 0; i < max; i++) {
bole.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchLogLevelObject (cb) {
for (var i = 0; i < max; i++) {
loglevel.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoObj (cb) {
for (var i = 0; i < max; i++) {
plogDest.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoMinLengthObj (cb) {
for (var i = 0; i < max; i++) {
plogMinLength.info({ hello: 'world' })
}
setImmediate(cb)
},
function benchPinoNodeStreamObj (cb) {
for (var i = 0; i < max; i++) {
plogNodeStream.info({ hello: 'world' })
}
setImmediate(cb)
}
], 10000)
run(run)
@@ -0,0 +1,36 @@
'use strict'
const { join } = require('path')
const { execSync } = require('child_process')
const run = (type) => {
process.stderr.write(`benchmarking ${type}\n`)
return execSync(`node ${join(__dirname, 'runbench')} ${type} -q`)
}
console.log(`
# Benchmarks
\`pino.info('hello world')\`:
\`\`\`
${run('basic')}
\`\`\`
\`pino.info({'hello': 'world'})\`:
\`\`\`
${run('object')}
\`\`\`
\`pino.info(aBigDeeplyNestedObject)\`:
\`\`\`
${run('deep-object')}
\`\`\`
\`pino.info('hello %s %j %d', 'world', {obj: true}, 4, {another: 'obj'})\`:
For a fair comparison, [LogLevel](http://npm.im/loglevel) was extended
to include a timestamp and [bole](http://npm.im/bole) had
\`fastTime\` mode switched on.
`)
+138
View File
@@ -0,0 +1,138 @@
'use strict'
const { type, platform, arch, release, cpus } = require('os')
const { resolve, join } = require('path')
const spawn = require('child_process').spawn
const pump = require('pump')
const split = require('split2')
const through = require('through2')
const steed = require('steed')
function usage () {
console.log(`
Pino Benchmarks
To run a benchmark, specify which to run:
・all run all benchmarks (takes a while)
・basic log a simple string
・object logging a basic object
・deep-object logging a large object
・multi-arg multiple log method arguments
・child child from a parent
・child-child child from a child
・child-creation child constructor
・formatters difference between with or without formatters
Example:
node runbench basic
`)
}
if (!process.argv[2]) {
usage()
process.exit()
}
const quiet = process.argv[3] === '-q'
const selectedBenchmark = process.argv[2].toLowerCase()
const benchmarkDir = resolve(__dirname, '..')
const benchmarks = {
basic: 'basic.bench.js',
object: 'object.bench.js',
'deep-object': 'deep-object.bench.js',
'multi-arg': 'multi-arg.bench.js',
'long-string': 'long-string.bench.js',
child: 'child.bench.js',
'child-child': 'child-child.bench.js',
'child-creation': 'child-creation.bench.js',
formatters: 'formatters.bench.js'
}
function runBenchmark (name, done) {
const benchmarkResults = {}
benchmarkResults[name] = {}
const processor = through(function (line, enc, cb) {
const [label, time] = ('' + line).split(': ')
const [target, iterations] = label.split('*')
const logger = target.replace('bench', '')
if (!benchmarkResults[name][logger]) benchmarkResults[name][logger] = []
benchmarkResults[name][logger].push({
time: time.replace('ms', ''),
iterations: iterations.replace(':', '')
})
cb()
})
if (quiet === false) console.log(`Running ${name.toUpperCase()} benchmark\n`)
const benchmark = spawn(
process.argv[0],
[join(benchmarkDir, benchmarks[name])]
)
if (quiet === false) {
benchmark.stdout.pipe(process.stdout)
}
pump(benchmark.stdout, split(), processor)
benchmark.on('exit', () => {
console.log()
if (done && typeof done === 'function') done(null, benchmarkResults)
})
}
function sum (arr) {
let result = 0
for (var i = 0; i < arr.length; i += 1) {
result += Number.parseFloat(arr[i].time)
}
return result
}
function displayResults (results) {
if (quiet === false) console.log('==========')
const benchNames = Object.keys(results)
for (var i = 0; i < benchNames.length; i += 1) {
console.log(`${benchNames[i].toUpperCase()} benchmark averages`)
const benchmark = results[benchNames[i]]
const loggers = Object.keys(benchmark)
for (var j = 0; j < loggers.length; j += 1) {
const logger = benchmark[loggers[j]]
const average = sum(logger) / logger.length
console.log(`${loggers[j]} average: ${average.toFixed(3)}ms`)
}
}
if (quiet === false) {
console.log('==========')
console.log(
`System: ${type()}/${platform()} ${arch()} ${release()}`,
`~ ${cpus()[0].model} (cores/threads: ${cpus().length})`
)
}
}
function toBench (done) {
runBenchmark(this.name, done)
}
const benchQueue = []
if (selectedBenchmark !== 'all') {
benchQueue.push(toBench.bind({ name: selectedBenchmark }))
} else {
const keys = Object.keys(benchmarks)
for (var i = 0; i < keys.length; i += 1) {
benchQueue.push(toBench.bind({ name: keys[i] }))
}
}
steed.series(benchQueue, function (err, results) {
if (err) return console.error(err.message)
results.forEach(displayResults)
})
@@ -0,0 +1,55 @@
'use strict'
const { readFileSync } = require('fs')
const vm = require('vm')
const { join } = require('path')
const code = readFileSync(
join(__dirname, '..', '..', 'node_modules', 'loglevel', 'lib', 'loglevel.js')
)
const { Console } = require('console')
function build (dest) {
const sandbox = {
module: {},
console: new Console(dest, dest)
}
const context = vm.createContext(sandbox)
const script = new vm.Script(code)
script.runInContext(context)
const loglevel = sandbox.log
const originalFactory = loglevel.methodFactory
loglevel.methodFactory = function (methodName, logLevel, loggerName) {
const rawMethod = originalFactory(methodName, logLevel, loggerName)
return function () {
const time = new Date()
let array
if (typeof arguments[0] === 'string') {
arguments[0] = '[' + time.toISOString() + '] ' + arguments[0]
rawMethod.apply(null, arguments)
} else {
array = new Array(arguments.length + 1)
array[0] = '[' + time.toISOString() + ']'
for (var i = 0; i < arguments.length; i++) {
array[i + 1] = arguments[i]
}
rawMethod.apply(null, array)
}
}
}
loglevel.setLevel(loglevel.levels.INFO)
return loglevel
}
module.exports = build
if (require.main === module) {
const loglevel = build(process.stdout)
loglevel.info('hello')
loglevel.info({ hello: 'world' })
loglevel.info('hello %j', { hello: 'world' })
}
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env node
console.error(
'`pino` cli has been removed. Use `pino-pretty` cli instead.\n' +
'\nSee: https://github.com/pinojs/pino-pretty'
)
process.exit(1)
+465
View File
@@ -0,0 +1,465 @@
'use strict'
const format = require('quick-format-unescaped')
module.exports = pino
const _console = pfGlobalThisOrFallback().console || {}
const stdSerializers = {
mapHttpRequest: mock,
mapHttpResponse: mock,
wrapRequestSerializer: passthrough,
wrapResponseSerializer: passthrough,
wrapErrorSerializer: passthrough,
req: mock,
res: mock,
err: asErrValue,
errWithCause: asErrValue
}
function levelToValue (level, logger) {
return level === 'silent'
? Infinity
: logger.levels.values[level]
}
const baseLogFunctionSymbol = Symbol('pino.logFuncs')
const hierarchySymbol = Symbol('pino.hierarchy')
const logFallbackMap = {
error: 'log',
fatal: 'error',
warn: 'error',
info: 'log',
debug: 'log',
trace: 'log'
}
function appendChildLogger (parentLogger, childLogger) {
const newEntry = {
logger: childLogger,
parent: parentLogger[hierarchySymbol]
}
childLogger[hierarchySymbol] = newEntry
}
function setupBaseLogFunctions (logger, levels, proto) {
const logFunctions = {}
levels.forEach(level => {
logFunctions[level] = proto[level] ? proto[level] : (_console[level] || _console[logFallbackMap[level] || 'log'] || noop)
})
logger[baseLogFunctionSymbol] = logFunctions
}
function shouldSerialize (serialize, serializers) {
if (Array.isArray(serialize)) {
const hasToFilter = serialize.filter(function (k) {
return k !== '!stdSerializers.err'
})
return hasToFilter
} else if (serialize === true) {
return Object.keys(serializers)
}
return false
}
function pino (opts) {
opts = opts || {}
opts.browser = opts.browser || {}
const transmit = opts.browser.transmit
if (transmit && typeof transmit.send !== 'function') { throw Error('pino: transmit option must have a send function') }
const proto = opts.browser.write || _console
if (opts.browser.write) opts.browser.asObject = true
const serializers = opts.serializers || {}
const serialize = shouldSerialize(opts.browser.serialize, serializers)
let stdErrSerialize = opts.browser.serialize
if (
Array.isArray(opts.browser.serialize) &&
opts.browser.serialize.indexOf('!stdSerializers.err') > -1
) stdErrSerialize = false
const customLevels = Object.keys(opts.customLevels || {})
const levels = ['error', 'fatal', 'warn', 'info', 'debug', 'trace'].concat(customLevels)
if (typeof proto === 'function') {
levels.forEach(function (level) {
proto[level] = proto
})
}
if (opts.enabled === false || opts.browser.disabled) opts.level = 'silent'
const level = opts.level || 'info'
const logger = Object.create(proto)
if (!logger.log) logger.log = noop
setupBaseLogFunctions(logger, levels, proto)
// setup root hierarchy entry
appendChildLogger({}, logger)
Object.defineProperty(logger, 'levelVal', {
get: getLevelVal
})
Object.defineProperty(logger, 'level', {
get: getLevel,
set: setLevel
})
const setOpts = {
transmit,
serialize,
asObject: opts.browser.asObject,
formatters: opts.browser.formatters,
levels,
timestamp: getTimeFunction(opts)
}
logger.levels = getLevels(opts)
logger.level = level
logger.setMaxListeners = logger.getMaxListeners =
logger.emit = logger.addListener = logger.on =
logger.prependListener = logger.once =
logger.prependOnceListener = logger.removeListener =
logger.removeAllListeners = logger.listeners =
logger.listenerCount = logger.eventNames =
logger.write = logger.flush = noop
logger.serializers = serializers
logger._serialize = serialize
logger._stdErrSerialize = stdErrSerialize
logger.child = child
if (transmit) logger._logEvent = createLogEventShape()
function getLevelVal () {
return levelToValue(this.level, this)
}
function getLevel () {
return this._level
}
function setLevel (level) {
if (level !== 'silent' && !this.levels.values[level]) {
throw Error('unknown level ' + level)
}
this._level = level
set(this, setOpts, logger, 'error') // <-- must stay first
set(this, setOpts, logger, 'fatal')
set(this, setOpts, logger, 'warn')
set(this, setOpts, logger, 'info')
set(this, setOpts, logger, 'debug')
set(this, setOpts, logger, 'trace')
customLevels.forEach((level) => {
set(this, setOpts, logger, level)
})
}
function child (bindings, childOptions) {
if (!bindings) {
throw new Error('missing bindings for child Pino')
}
childOptions = childOptions || {}
if (serialize && bindings.serializers) {
childOptions.serializers = bindings.serializers
}
const childOptionsSerializers = childOptions.serializers
if (serialize && childOptionsSerializers) {
var childSerializers = Object.assign({}, serializers, childOptionsSerializers)
var childSerialize = opts.browser.serialize === true
? Object.keys(childSerializers)
: serialize
delete bindings.serializers
applySerializers([bindings], childSerialize, childSerializers, this._stdErrSerialize)
}
function Child (parent) {
this._childLevel = (parent._childLevel | 0) + 1
// make sure bindings are available in the `set` function
this.bindings = bindings
if (childSerializers) {
this.serializers = childSerializers
this._serialize = childSerialize
}
if (transmit) {
this._logEvent = createLogEventShape(
[].concat(parent._logEvent.bindings, bindings)
)
}
}
Child.prototype = this
const newLogger = new Child(this)
// must happen before the level is assigned
appendChildLogger(this, newLogger)
// required to actually initialize the logger functions for any given child
newLogger.level = this.level
return newLogger
}
return logger
}
function getLevels (opts) {
const customLevels = opts.customLevels || {}
const values = Object.assign({}, pino.levels.values, customLevels)
const labels = Object.assign({}, pino.levels.labels, invertObject(customLevels))
return {
values,
labels
}
}
function invertObject (obj) {
const inverted = {}
Object.keys(obj).forEach(function (key) {
inverted[obj[key]] = key
})
return inverted
}
pino.levels = {
values: {
fatal: 60,
error: 50,
warn: 40,
info: 30,
debug: 20,
trace: 10
},
labels: {
10: 'trace',
20: 'debug',
30: 'info',
40: 'warn',
50: 'error',
60: 'fatal'
}
}
pino.stdSerializers = stdSerializers
pino.stdTimeFunctions = Object.assign({}, { nullTime, epochTime, unixTime, isoTime })
function getBindingChain (logger) {
const bindings = []
if (logger.bindings) {
bindings.push(logger.bindings)
}
// traverse up the tree to get all bindings
let hierarchy = logger[hierarchySymbol]
while (hierarchy.parent) {
hierarchy = hierarchy.parent
if (hierarchy.logger.bindings) {
bindings.push(hierarchy.logger.bindings)
}
}
return bindings.reverse()
}
function set (self, opts, rootLogger, level) {
// override the current log functions with either `noop` or the base log function
Object.defineProperty(self, level, {
value: (levelToValue(self.level, rootLogger) > levelToValue(level, rootLogger)
? noop
: rootLogger[baseLogFunctionSymbol][level]),
writable: true,
enumerable: true,
configurable: true
})
if (!opts.transmit && self[level] === noop) {
return
}
// make sure the log format is correct
self[level] = createWrap(self, opts, rootLogger, level)
// prepend bindings if it is not the root logger
const bindings = getBindingChain(self)
if (bindings.length === 0) {
// early exit in case for rootLogger
return
}
self[level] = prependBindingsInArguments(bindings, self[level])
}
function prependBindingsInArguments (bindings, logFunc) {
return function () {
return logFunc.apply(this, [...bindings, ...arguments])
}
}
function createWrap (self, opts, rootLogger, level) {
return (function (write) {
return function LOG () {
const ts = opts.timestamp()
const args = new Array(arguments.length)
const proto = (Object.getPrototypeOf && Object.getPrototypeOf(this) === _console) ? _console : this
for (var i = 0; i < args.length; i++) args[i] = arguments[i]
if (opts.serialize && !opts.asObject) {
applySerializers(args, this._serialize, this.serializers, this._stdErrSerialize)
}
if (opts.asObject || opts.formatters) {
write.call(proto, asObject(this, level, args, ts, opts.formatters))
} else write.apply(proto, args)
if (opts.transmit) {
const transmitLevel = opts.transmit.level || self._level
const transmitValue = rootLogger.levels.values[transmitLevel]
const methodValue = rootLogger.levels.values[level]
if (methodValue < transmitValue) return
transmit(this, {
ts,
methodLevel: level,
methodValue,
transmitLevel,
transmitValue: rootLogger.levels.values[opts.transmit.level || self._level],
send: opts.transmit.send,
val: levelToValue(self._level, rootLogger)
}, args)
}
}
})(self[baseLogFunctionSymbol][level])
}
function asObject (logger, level, args, ts, formatters = {}) {
const {
level: levelFormatter = () => logger.levels.values[level],
log: logObjectFormatter = (obj) => obj
} = formatters
if (logger._serialize) applySerializers(args, logger._serialize, logger.serializers, logger._stdErrSerialize)
const argsCloned = args.slice()
let msg = argsCloned[0]
const logObject = {}
if (ts) {
logObject.time = ts
}
logObject.level = levelFormatter(level, logger.levels.values[level])
let lvl = (logger._childLevel | 0) + 1
if (lvl < 1) lvl = 1
// deliberate, catching objects, arrays
if (msg !== null && typeof msg === 'object') {
while (lvl-- && typeof argsCloned[0] === 'object') {
Object.assign(logObject, argsCloned.shift())
}
msg = argsCloned.length ? format(argsCloned.shift(), argsCloned) : undefined
} else if (typeof msg === 'string') msg = format(argsCloned.shift(), argsCloned)
if (msg !== undefined) logObject.msg = msg
const formattedLogObject = logObjectFormatter(logObject)
return formattedLogObject
}
function applySerializers (args, serialize, serializers, stdErrSerialize) {
for (const i in args) {
if (stdErrSerialize && args[i] instanceof Error) {
args[i] = pino.stdSerializers.err(args[i])
} else if (typeof args[i] === 'object' && !Array.isArray(args[i])) {
for (const k in args[i]) {
if (serialize && serialize.indexOf(k) > -1 && k in serializers) {
args[i][k] = serializers[k](args[i][k])
}
}
}
}
}
function transmit (logger, opts, args) {
const send = opts.send
const ts = opts.ts
const methodLevel = opts.methodLevel
const methodValue = opts.methodValue
const val = opts.val
const bindings = logger._logEvent.bindings
applySerializers(
args,
logger._serialize || Object.keys(logger.serializers),
logger.serializers,
logger._stdErrSerialize === undefined ? true : logger._stdErrSerialize
)
logger._logEvent.ts = ts
logger._logEvent.messages = args.filter(function (arg) {
// bindings can only be objects, so reference equality check via indexOf is fine
return bindings.indexOf(arg) === -1
})
logger._logEvent.level.label = methodLevel
logger._logEvent.level.value = methodValue
send(methodLevel, logger._logEvent, val)
logger._logEvent = createLogEventShape(bindings)
}
function createLogEventShape (bindings) {
return {
ts: 0,
messages: [],
bindings: bindings || [],
level: { label: '', value: 0 }
}
}
function asErrValue (err) {
const obj = {
type: err.constructor.name,
msg: err.message,
stack: err.stack
}
for (const key in err) {
if (obj[key] === undefined) {
obj[key] = err[key]
}
}
return obj
}
function getTimeFunction (opts) {
if (typeof opts.timestamp === 'function') {
return opts.timestamp
}
if (opts.timestamp === false) {
return nullTime
}
return epochTime
}
function mock () { return {} }
function passthrough (a) { return a }
function noop () {}
function nullTime () { return false }
function epochTime () { return Date.now() }
function unixTime () { return Math.round(Date.now() / 1000.0) }
function isoTime () { return new Date(Date.now()).toISOString() } // using Date.now() for testability
/* eslint-disable */
/* istanbul ignore next */
function pfGlobalThisOrFallback () {
function defd (o) { return typeof o !== 'undefined' && o }
try {
if (typeof globalThis !== 'undefined') return globalThis
Object.defineProperty(Object.prototype, 'globalThis', {
get: function () {
delete Object.prototype.globalThis
return (this.globalThis = this)
},
configurable: true
})
return globalThis
} catch (e) {
return defd(self) || defd(window) || defd(this) || {}
}
}
/* eslint-enable */
module.exports.default = pino
module.exports.pino = pino
+10
View File
@@ -0,0 +1,10 @@
const fs = require('fs')
const path = require('path')
const { version } = require('../package.json')
const metaContent = `'use strict'
module.exports = { version: '${version}' }
`
fs.writeFileSync(path.resolve('./lib/meta.js'), metaContent, { encoding: 'utf-8' })
+1446
View File
@@ -0,0 +1,1446 @@
# API
* [pino() => logger](#export)
* [options](#options)
* [destination](#destination)
* [destination\[Symbol.for('pino.metadata')\]](#metadata)
* [Logger Instance](#logger)
* [logger.trace()](#trace)
* [logger.debug()](#debug)
* [logger.info()](#info)
* [logger.warn()](#warn)
* [logger.error()](#error)
* [logger.fatal()](#fatal)
* [logger.silent()](#silent)
* [logger.child()](#child)
* [logger.bindings()](#logger-bindings)
* [logger.setBindings()](#logger-set-bindings)
* [logger.flush()](#flush)
* [logger.level](#logger-level)
* [logger.isLevelEnabled()](#islevelenabled)
* [logger.levels](#levels)
* [logger\[Symbol.for('pino.serializers')\]](#serializers)
* [Event: 'level-change'](#level-change)
* [logger.version](#version)
* [Statics](#statics)
* [pino.destination()](#pino-destination)
* [pino.transport()](#pino-transport)
* [pino.multistream()](#pino-multistream)
* [pino.stdSerializers](#pino-stdserializers)
* [pino.stdTimeFunctions](#pino-stdtimefunctions)
* [pino.symbols](#pino-symbols)
* [pino.version](#pino-version)
* [Interfaces](#interfaces)
* [MultiStreamRes](#multistreamres)
* [StreamEntry](#streamentry)
* [DestinationStream](#destinationstream)
* [Types](#types)
* [Level](#level-1)
<a id="export"></a>
## `pino([options], [destination]) => logger`
The exported `pino` function takes two optional arguments,
[`options`](#options) and [`destination`](#destination), and
returns a [logger instance](#logger).
<a id=options></a>
### `options` (Object)
#### `name` (String)
Default: `undefined`
The name of the logger. When set adds a `name` field to every JSON line logged.
#### `level` (String)
Default: `'info'`
The minimum level to log: Pino will not log messages with a lower level. Setting this option reduces the load, as typically, debug and trace logs are only valid for development, and not needed in production.
One of `'fatal'`, `'error'`, `'warn'`, `'info'`, `'debug'`, `'trace'` or `'silent'`.
Additional levels can be added to the instance via the `customLevels` option.
* See [`customLevels` option](#opt-customlevels)
<a id=opt-customlevels></a>
#### `levelComparison` ("ASC", "DESC", Function)
Default: `ASC`
Use this option to customize levels order.
In order to be able to define custom levels ordering pass a function which will accept `current` and `expected` values and return `boolean` which shows should `current` level to be shown or not.
```js
const logger = pino({
levelComparison: 'DESC',
customLevels: {
foo: 20, // `foo` is more valuable than `bar`
bar: 10
},
})
// OR
const logger = pino({
levelComparison: function(current, expected) {
return current >= expected;
}
})
```
#### `customLevels` (Object)
Default: `undefined`
Use this option to define additional logging levels.
The keys of the object correspond to the namespace of the log level,
and the values should be the numerical value of the level.
```js
const logger = pino({
customLevels: {
foo: 35
}
})
logger.foo('hi')
```
<a id=opt-useOnlyCustomLevels></a>
#### `useOnlyCustomLevels` (Boolean)
Default: `false`
Use this option to only use defined `customLevels` and omit Pino's levels.
Logger's default `level` must be changed to a value in `customLevels` to use `useOnlyCustomLevels`
Warning: this option may not be supported by downstream transports.
```js
const logger = pino({
customLevels: {
foo: 35
},
useOnlyCustomLevels: true,
level: 'foo'
})
logger.foo('hi')
logger.info('hello') // Will throw an error saying info is not found in logger object
```
#### `depthLimit` (Number)
Default: `5`
Option to limit stringification at a specific nesting depth when logging circular objects.
#### `edgeLimit` (Number)
Default: `100`
Option to limit stringification of properties/elements when logging a specific object/array with circular references.
<a id="opt-mixin"></a>
#### `mixin` (Function):
Default: `undefined`
If provided, the `mixin` function is called each time one of the active
logging methods is called. The first parameter is the value `mergeObject` or an empty object. The second parameter is the log level number.
The third parameter is the logger or child logger itself, which can be used to
retrieve logger-specific context from within the `mixin` function.
The function must synchronously return an object. The properties of the returned object will be added to the
logged JSON.
```js
let n = 0
const logger = pino({
mixin () {
return { line: ++n }
}
})
logger.info('hello')
// {"level":30,"time":1573664685466,"pid":78742,"hostname":"x","line":1,"msg":"hello"}
logger.info('world')
// {"level":30,"time":1573664685469,"pid":78742,"hostname":"x","line":2,"msg":"world"}
```
The result of `mixin()` is supposed to be a _new_ object. For performance reason, the object returned by `mixin()` will be mutated by pino.
In the following example, passing `mergingObject` argument to the first `info` call will mutate the global `mixin` object by default:
(* See [`mixinMergeStrategy` option](#opt-mixin-merge-strategy)):
```js
const mixin = {
appName: 'My app'
}
const logger = pino({
mixin() {
return mixin;
}
})
logger.info({
description: 'Ok'
}, 'Message 1')
// {"level":30,"time":1591195061437,"pid":16012,"hostname":"x","appName":"My app","description":"Ok","msg":"Message 1"}
logger.info('Message 2')
// {"level":30,"time":1591195061437,"pid":16012,"hostname":"x","appName":"My app","description":"Ok","msg":"Message 2"}
// Note: the second log contains "description":"Ok" text, even if it was not provided.
```
The `mixin` method can be used to add the level label to each log message such as in the following example:
```js
const logger = pino({
mixin(_context, level) {
return { 'level-label': logger.levels.labels[level] }
}
})
logger.info({
description: 'Ok'
}, 'Message 1')
// {"level":30,"time":1591195061437,"pid":16012,"hostname":"x","description":"Ok","level-label":"info","msg":"Message 1"}
logger.error('Message 2')
// {"level":30,"time":1591195061437,"pid":16012,"hostname":"x","level-label":"error","msg":"Message 2"}
```
If the `mixin` feature is being used merely to add static metadata to each log message,
then a [child logger ⇗](/docs/child-loggers.md) should be used instead. Unless your application
needs to concatenate values for a specific key multiple times, in which case `mixin` can be
used to avoid the [duplicate keys caveat](/docs/child-loggers.md#duplicate-keys-caveat):
```js
const logger = pino({
mixin (obj, num, logger) {
return {
tags: logger.tags
}
}
})
logger.tags = {}
logger.addTag = function (key, value) {
logger.tags[key] = value
}
function createChild (parent, ...context) {
const newChild = logger.child(...context)
newChild.tags = { ...logger.tags }
newChild.addTag = function (key, value) {
newChild.tags[key] = value
}
return newChild
}
logger.addTag('foo', 1)
const child = createChild(logger, {})
child.addTag('bar', 2)
logger.info('this will only have `foo: 1`')
child.info('this will have both `foo: 1` and `bar: 2`')
logger.info('this will still only have `foo: 1`')
```
As of pino 7.x, when the `mixin` is used with the [`nestedKey` option](#opt-nestedkey),
the object returned from the `mixin` method will also be nested. Prior versions would mix
this object into the root.
```js
const logger = pino({
nestedKey: 'payload',
mixin() {
return { requestId: requestId.currentId() }
}
})
logger.info({
description: 'Ok'
}, 'Message 1')
// {"level":30,"time":1591195061437,"pid":16012,"hostname":"x","payload":{"requestId":"dfe9a9014b","description":"Ok"},"msg":"Message 1"}
```
<a id="opt-mixin-merge-strategy"></a>
#### `mixinMergeStrategy` (Function):
Default: `undefined`
If provided, the `mixinMergeStrategy` function is called each time one of the active
logging methods is called. The first parameter is the value `mergeObject` or an empty object,
the second parameter is the value resulting from `mixin()` (* See [`mixin` option](#opt-mixin) or an empty object.
The function must synchronously return an object.
```js
// Default strategy, `mergeObject` has priority
const logger = pino({
mixin() {
return { tag: 'docker' }
},
// mixinMergeStrategy(mergeObject, mixinObject) {
// return Object.assign(mixinMeta, mergeObject)
// }
})
logger.info({
tag: 'local'
}, 'Message')
// {"level":30,"time":1591195061437,"pid":16012,"hostname":"x","tag":"local","msg":"Message"}
```
```js
// Custom mutable strategy, `mixin` has priority
const logger = pino({
mixin() {
return { tag: 'k8s' }
},
mixinMergeStrategy(mergeObject, mixinObject) {
return Object.assign(mergeObject, mixinObject)
}
})
logger.info({
tag: 'local'
}, 'Message')
// {"level":30,"time":1591195061437,"pid":16012,"hostname":"x","tag":"k8s","msg":"Message"}
```
```js
// Custom immutable strategy, `mixin` has priority
const logger = pino({
mixin() {
return { tag: 'k8s' }
},
mixinMergeStrategy(mergeObject, mixinObject) {
return Object.assign({}, mergeObject, mixinObject)
}
})
logger.info({
tag: 'local'
}, 'Message')
// {"level":30,"time":1591195061437,"pid":16012,"hostname":"x","tag":"k8s","msg":"Message"}
```
<a id="opt-redact"></a>
#### `redact` (Array | Object):
Default: `undefined`
As an array, the `redact` option specifies paths that should
have their values redacted from any log output.
Each path must be a string using a syntax that corresponds to JavaScript dot and bracket notation.
If an object is supplied, three options can be specified:
* `paths` (array): Required. An array of paths. See [redaction - Path Syntax ⇗](/docs/redaction.md#paths) for specifics.
* `censor` (String|Function|Undefined): Optional. When supplied as a String the `censor` option will overwrite keys that are to be redacted. When set to `undefined` the key will be removed entirely from the object.
The `censor` option may also be a mapping function. The (synchronous) mapping function has the signature `(value, path) => redactedValue` and is called with the unredacted `value` and `path` to the key being redacted, as an array. For example given a redaction path of `a.b.c` the `path` argument would be `['a', 'b', 'c']`. The value returned from the mapping function becomes the applied censor value. Default: `'[Redacted]'`
value synchronously.
Default: `'[Redacted]'`
* `remove` (Boolean): Optional. Instead of censoring the value, remove both the key and the value. Default: `false`
**WARNING**: Never allow user input to define redacted paths.
* See the [redaction ⇗](/docs/redaction.md) documentation.
* See [fast-redact#caveat ⇗](https://github.com/davidmarkclements/fast-redact#caveat)
<a id=opt-hooks></a>
#### `hooks` (Object)
An object mapping to hook functions. Hook functions allow for customizing
internal logger operations. Hook functions ***must*** be synchronous functions.
<a id="logmethod"></a>
##### `logMethod`
Allows for manipulating the parameters passed to logger methods. The signature
for this hook is `logMethod (args, method, level) {}`, where `args` is an array
of the arguments that were passed to the log method and `method` is the log
method itself, `level` is the log level itself. This hook ***must*** invoke the
`method` function by using apply, like so: `method.apply(this, newArgumentsArray)`.
For example, Pino expects a binding object to be the first parameter with an
optional string message as the second parameter. Using this hook the parameters
can be flipped:
```js
const hooks = {
logMethod (inputArgs, method, level) {
if (inputArgs.length >= 2) {
const arg1 = inputArgs.shift()
const arg2 = inputArgs.shift()
return method.apply(this, [arg2, arg1, ...inputArgs])
}
return method.apply(this, inputArgs)
}
}
```
<a id=opt-formatters></a>
#### `formatters` (Object)
An object containing functions for formatting the shape of the log lines.
These functions should return a JSONifiable object and
should never throw. These functions allow for full customization of
the resulting log lines. For example, they can be used to change
the level key name or to enrich the default metadata.
##### `level`
Changes the shape of the log level. The default shape is `{ level: number }`.
The function takes two arguments, the label of the level (e.g. `'info'`)
and the numeric value (e.g. `30`).
ps: The log level cannot be customized when using multiple transports
```js
const formatters = {
level (label, number) {
return { level: number }
}
}
```
##### `bindings`
Changes the shape of the bindings. The default shape is `{ pid, hostname }`.
The function takes a single argument, the bindings object, which can be configured
using the [`base` option](#opt-base). Called once when creating logger.
```js
const formatters = {
bindings (bindings) {
return { pid: bindings.pid, hostname: bindings.hostname }
}
}
```
##### `log`
Changes the shape of the log object. This function will be called every time
one of the log methods (such as `.info`) is called. All arguments passed to the
log method, except the message, will be passed to this function. By default, it does
not change the shape of the log object.
```js
const formatters = {
log (object) {
return object
}
}
```
<a id=opt-serializers></a>
#### `serializers` (Object)
Default: `{err: pino.stdSerializers.err}`
An object containing functions for custom serialization of objects.
These functions should return an JSONifiable object and they
should never throw. When logging an object, each top-level property
matching the exact key of a serializer will be serialized using the defined serializer.
The serializers are applied when a property in the logged object matches a property
in the serializers. The only exception is the `err` serializer as it is also applied in case
the object is an instance of `Error`, e.g. `logger.info(new Error('kaboom'))`.
See `errorKey` option to change `err` namespace.
* See [pino.stdSerializers](#pino-stdserializers)
#### `msgPrefix` (String)
Default: `undefined`
The `msgPrefix` property allows you to specify a prefix for every message of the logger and its children.
```js
const logger = pino({
msgPrefix: '[HTTP] '
})
logger.info('got new request!')
// > [HTTP] got new request!
const child = logger.child({})
child.info('User authenticated!')
// > [HTTP] User authenticated!
```
<a id=opt-base></a>
#### `base` (Object)
Default: `{pid: process.pid, hostname: os.hostname}`
Key-value object added as child logger to each log line.
Set to `undefined` to avoid adding `pid`, `hostname` properties to each log.
#### `enabled` (Boolean)
Default: `true`
Set to `false` to disable logging.
#### `crlf` (Boolean)
Default: `false`
Set to `true` to logs newline delimited JSON with `\r\n` instead of `\n`.
<a id=opt-timestamp></a>
#### `timestamp` (Boolean | Function)
Default: `true`
Enables or disables the inclusion of a timestamp in the
log message. If a function is supplied, it must synchronously return a partial JSON string
representation of the time, e.g. `,"time":1493426328206` (which is the default).
If set to `false`, no timestamp will be included in the output.
See [stdTimeFunctions](#pino-stdtimefunctions) for a set of available functions
for passing in as a value for this option.
Example:
```js
timestamp: () => `,"time":"${new Date(Date.now()).toISOString()}"`
// which is equivalent to:
// timestamp: stdTimeFunctions.isoTime
```
**Caution**: attempting to format time in-process will significantly impact logging performance.
<a id=opt-messagekey></a>
#### `messageKey` (String)
Default: `'msg'`
The string key for the 'message' in the JSON object.
<a id=opt-messagekey></a>
#### `errorKey` (String)
Default: `'err'`
The string key for the 'error' in the JSON object.
<a id=opt-nestedkey></a>
#### `nestedKey` (String)
Default: `null`
If there's a chance that objects being logged have properties that conflict with those from pino itself (`level`, `timestamp`, `pid`, etc)
and duplicate keys in your log records are undesirable, pino can be configured with a `nestedKey` option that causes any `object`s that are logged
to be placed under a key whose name is the value of `nestedKey`.
This way, when searching something like Kibana for values, one can consistently search under the configured `nestedKey` value instead of the root log record keys.
For example,
```js
const logger = require('pino')({
nestedKey: 'payload'
})
const thing = { level: 'hi', time: 'never', foo: 'bar'} // has pino-conflicting properties!
logger.info(thing)
// logs the following:
// {"level":30,"time":1578357790020,"pid":91736,"hostname":"x","payload":{"level":"hi","time":"never","foo":"bar"}}
```
In this way, logged objects' properties don't conflict with pino's standard logging properties,
and searching for logged objects can start from a consistent path.
#### `browser` (Object)
Browser only, may have `asObject` and `write` keys. This option is separately
documented in the [Browser API ⇗](/docs/browser.md) documentation.
* See [Browser API ⇗](/docs/browser.md)
#### `transport` (Object)
The `transport` option is a shorthand for the [pino.transport()](#pino-transport) function.
It supports the same input options:
```js
require('pino')({
transport: {
target: '/absolute/path/to/my-transport.mjs'
}
})
// or multiple transports
require('pino')({
transport: {
targets: [
{ target: '/absolute/path/to/my-transport.mjs', level: 'error' },
{ target: 'some-file-transport', options: { destination: '/dev/null' }
]
}
})
```
If the transport option is supplied to `pino`, a [`destination`](#destination) parameter may not also be passed as a separate argument to `pino`:
```js
pino({ transport: {}}, '/path/to/somewhere') // THIS WILL NOT WORK, DO NOT DO THIS
pino({ transport: {}}, process.stderr) // THIS WILL NOT WORK, DO NOT DO THIS
```
when using the `transport` option. In this case, an `Error` will be thrown.
* See [pino.transport()](#pino-transport)
#### `onChild` (Function)
The `onChild` function is a synchronous callback that will be called on each creation of a new child, passing the child instance as its first argument.
Any error thrown inside the callback will be uncaught and should be handled inside the callback.
```js
const parent = require('pino')({ onChild: (instance) => {
// Execute call back code for each newly created child.
}})
// `onChild` will now be executed with the new child.
parent.child(bindings)
```
<a id="destination"></a>
### `destination` (Number | String | Object | DestinationStream | SonicBoomOpts | WritableStream)
Default: `pino.destination(1)` (STDOUT)
The `destination` parameter can be a file descriptor, a file path, or an
object with `dest` property pointing to a fd or path.
An ordinary Node.js `stream` file descriptor can be passed as the
destination (such as the result
of `fs.createWriteStream`) but for peak log writing performance, it is strongly
recommended to use `pino.destination` to create the destination stream.
Note that the `destination` parameter can be the result of `pino.transport()`.
```js
// pino.destination(1) by default
const stdoutLogger = require('pino')()
// destination param may be in first position when no options:
const fileLogger = require('pino')( pino.destination('/log/path'))
// use the stderr file handle to log to stderr:
const opts = {name: 'my-logger'}
const stderrLogger = require('pino')(opts, pino.destination(2))
// automatic wrapping in pino.destination
const fileLogger = require('pino')('/log/path')
// Asynchronous logging
const fileLogger = pino(pino.destination({ dest: '/log/path', sync: false }))
```
However, there are some special instances where `pino.destination` is not used as the default:
+ When something, e.g a process manager, has monkey-patched `process.stdout.write`.
In these cases `process.stdout` is used instead.
Note: If the parameter is a string integer, e.g. `'1'`, it will be coerced to
a number and used as a file descriptor. If this is not desired, provide a full
path, e.g. `/tmp/1`.
* See [`pino.destination`](#pino-destination)
<a id="metadata"></a>
#### `destination[Symbol.for('pino.metadata')]`
Default: `false`
Using the global symbol `Symbol.for('pino.metadata')` as a key on the `destination` parameter and
setting the key to `true`, indicates that the following properties should be
set on the `destination` object after each log line is written:
* the last logging level as `destination.lastLevel`
* the last logging message as `destination.lastMsg`
* the last logging object as `destination.lastObj`
* the last time as `destination.lastTime`, which will be the partial string returned
by the time function.
* the last logger instance as `destination.lastLogger` (to support child
loggers)
The following is a succinct usage example:
```js
const dest = pino.destination('/dev/null')
dest[Symbol.for('pino.metadata')] = true
const logger = pino(dest)
logger.info({a: 1}, 'hi')
const { lastMsg, lastLevel, lastObj, lastTime} = dest
console.log(
'Logged message "%s" at level %d with object %o at time %s',
lastMsg, lastLevel, lastObj, lastTime
) // Logged message "hi" at level 30 with object { a: 1 } at time 1531590545089
```
<a id="logger"></a>
## Logger Instance
The logger instance is the object returned by the main exported
[`pino`](#export) function.
The primary purpose of the logger instance is to provide logging methods.
The default logging methods are `trace`, `debug`, `info`, `warn`, `error`, and `fatal`.
Each logging method has the following signature:
`([mergingObject], [message], [...interpolationValues])`.
The parameters are explained below using the `logger.info` method but the same applies to all logging methods.
### Logging Method Parameters
<a id=mergingobject></a>
#### `mergingObject` (Object)
An object can optionally be supplied as the first parameter. Each enumerable key and value
of the `mergingObject` is copied into the JSON log line.
```js
logger.info({MIX: {IN: true}})
// {"level":30,"time":1531254555820,"pid":55956,"hostname":"x","MIX":{"IN":true}}
```
If the object is of type Error, it is wrapped in an object containing a property err (`{ err: mergingObject }`).
This allows for a unified error handling flow.
Options `serializers` and `errorKey` could be used at instantiation time to change the namespace
from `err` to another string as preferred.
<a id="message"></a>
#### `message` (String)
A `message` string can optionally be supplied as the first parameter, or
as the second parameter after supplying a `mergingObject`.
By default, the contents of the `message` parameter will be merged into the
JSON log line under the `msg` key:
```js
logger.info('hello world')
// {"level":30,"time":1531257112193,"msg":"hello world","pid":55956,"hostname":"x"}
```
The `message` parameter takes precedence over the `mergingObject`.
That is, if a `mergingObject` contains a `msg` property, and a `message` parameter
is supplied in addition, the `msg` property in the output log will be the value of
the `message` parameter not the value of the `msg` property on the `mergingObject`.
See [Avoid Message Conflict](/docs/help.md#avoid-message-conflict) for information
on how to overcome this limitation.
If no `message` parameter is provided, and the `mergingObject` is of type `Error` or it has a property named `err`, the
`message` parameter is set to the `message` value of the error. See option `errorKey` if you want to change the namespace.
The `messageKey` option can be used at instantiation time to change the namespace
from `msg` to another string as preferred.
The `message` string may contain a printf style string with support for
the following placeholders:
* `%s` string placeholder
* `%d` digit placeholder
* `%O`, `%o`, and `%j` object placeholder
Values supplied as additional arguments to the logger method will
then be interpolated accordingly.
* See [`messageKey` pino option](#opt-messagekey)
* See [`...interpolationValues` log method parameter](#interpolationvalues)
<a id="interpolationvalues"></a>
#### `...interpolationValues` (Any)
All arguments supplied after `message` are serialized and interpolated according
to any supplied printf-style placeholders (`%s`, `%d`, `%o`|`%O`|`%j`) to form
the final output `msg` value for the JSON log line.
```js
logger.info('%o hello %s', {worldly: 1}, 'world')
// {"level":30,"time":1531257826880,"msg":"{\"worldly\":1} hello world","pid":55956,"hostname":"x"}
```
Since pino v6, we do not automatically concatenate and cast to string
consecutive parameters:
```js
logger.info('hello', 'world')
// {"level":30,"time":1531257618044,"msg":"hello","pid":55956,"hostname":"x"}
// world is missing
```
However, it's possible to inject a hook to modify this behavior:
```js
const pinoOptions = {
hooks: { logMethod }
}
function logMethod (args, method) {
if (args.length === 2) {
args[0] = `${args[0]} %j`
}
method.apply(this, args)
}
const logger = pino(pinoOptions)
```
* See [`message` log method parameter](#message)
* See [`logMethod` hook](#logmethod)
<a id="error-serialization"></a>
#### Errors
Errors can be supplied as either the first parameter or if already using `mergingObject` then as the `err` property on the `mergingObject`.
Options `serializers` and `errorKey` could be used at instantiation time to change the namespace
from `err` to another string as preferred.
> ## Note
> This section describes the default configuration. The error serializer can be
> mapped to a different key using the [`serializers`](#opt-serializers) option.
```js
logger.info(new Error("test"))
// {"level":30,"time":1531257618044,"msg":"test","stack":"...","type":"Error","pid":55956,"hostname":"x"}
logger.info({ err: new Error("test"), otherkey: 123 }, "some text")
// {"level":30,"time":1531257618044,"err":{"msg": "test", "stack":"...","type":"Error"},"msg":"some text","pid":55956,"hostname":"x","otherkey":123}
```
<a id="trace"></a>
### `logger.trace([mergingObject], [message], [...interpolationValues])`
Write a `'trace'` level log, if the configured [`level`](#level) allows for it.
* See [`mergingObject` log method parameter](#mergingobject)
* See [`message` log method parameter](#message)
* See [`...interpolationValues` log method parameter](#interpolationvalues)
<a id="debug"></a>
### `logger.debug([mergingObject], [message], [...interpolationValues])`
Write a `'debug'` level log, if the configured `level` allows for it.
* See [`mergingObject` log method parameter](#mergingobject)
* See [`message` log method parameter](#message)
* See [`...interpolationValues` log method parameter](#interpolationvalues)
<a id="info"></a>
### `logger.info([mergingObject], [message], [...interpolationValues])`
Write an `'info'` level log, if the configured `level` allows for it.
* See [`mergingObject` log method parameter](#mergingobject)
* See [`message` log method parameter](#message)
* See [`...interpolationValues` log method parameter](#interpolationvalues)
<a id="warn"></a>
### `logger.warn([mergingObject], [message], [...interpolationValues])`
Write a `'warn'` level log, if the configured `level` allows for it.
* See [`mergingObject` log method parameter](#mergingobject)
* See [`message` log method parameter](#message)
* See [`...interpolationValues` log method parameter](#interpolationvalues)
<a id="error"></a>
### `logger.error([mergingObject], [message], [...interpolationValues])`
Write a `'error'` level log, if the configured `level` allows for it.
* See [`mergingObject` log method parameter](#mergingobject)
* See [`message` log method parameter](#message)
* See [`...interpolationValues` log method parameter](#interpolationvalues)
<a id="fatal"></a>
### `logger.fatal([mergingObject], [message], [...interpolationValues])`
Write a `'fatal'` level log, if the configured `level` allows for it.
Since `'fatal'` level messages are intended to be logged just before the process exiting the `fatal`
method will always sync flush the destination.
Therefore it's important not to misuse `fatal` since
it will cause performance overhead if used for any
other purpose than writing final log messages before
the process crashes or exits.
* See [`mergingObject` log method parameter](#mergingobject)
* See [`message` log method parameter](#message)
* See [`...interpolationValues` log method parameter](#interpolationvalues)
<a id="silent"><a>
### `logger.silent()`
Noop function.
<a id="child"></a>
### `logger.child(bindings, [options]) => logger`
The `logger.child` method allows for the creation of stateful loggers,
where key-value pairs can be pinned to a logger causing them to be output
on every log line.
Child loggers use the same output stream as the parent and inherit
the current log level of the parent at the time they are spawned.
The log level of a child is mutable. It can be set independently
of the parent either by setting the [`level`](#level) accessor after creating
the child logger or using the [`options.level`](#optionslevel-string) key.
<a id="logger-child-bindings"></a>
#### `bindings` (Object)
An object of key-value pairs to include in every log line output
via the returned child logger.
```js
const child = logger.child({ MIX: {IN: 'always'} })
child.info('hello')
// {"level":30,"time":1531258616689,"msg":"hello","pid":64849,"hostname":"x","MIX":{"IN":"always"}}
child.info('child!')
// {"level":30,"time":1531258617401,"msg":"child!","pid":64849,"hostname":"x","MIX":{"IN":"always"}}
```
The `bindings` object may contain any key except for reserved configuration keys `level` and `serializers`.
##### `bindings.serializers` (Object) - DEPRECATED
Use `options.serializers` instead.
#### `options` (Object)
Options for child logger. These options will override the parent logger options.
##### `options.level` (String)
The `level` property overrides the log level of the child logger.
By default, the parent log level is inherited.
After the creation of the child logger, it is also accessible using the [`logger.level`](#logger-level) key.
```js
const logger = pino()
logger.debug('nope') // will not log, since default level is info
const child = logger.child({foo: 'bar'}, {level: 'debug'})
child.debug('debug!') // will log as the `level` property set the level to debug
```
##### `options.msgPrefix` (String)
Default: `undefined`
The `msgPrefix` property allows you to specify a prefix for every message of the child logger.
By default, the parent prefix is inherited.
If the parent already has a prefix, the prefix of the parent and then the child will be displayed.
```js
const logger = pino({
msgPrefix: '[HTTP] '
})
logger.info('got new request!')
// > [HTTP] got new request!
const child = logger.child({avengers: 'assemble'}, {msgPrefix: '[Proxy] '})
child.info('message proxied!')
// > [HTTP] [Proxy] message proxied!
```
##### `options.redact` (Array | Object)
Setting `options.redact` to an array or object will override the parent `redact` options. To remove `redact` options inherited from the parent logger set this value as an empty array (`[]`).
```js
const logger = require('pino')({ redact: ['hello'] })
logger.info({ hello: 'world' })
// {"level":30,"time":1625794363403,"pid":67930,"hostname":"x","hello":"[Redacted]"}
const child = logger.child({ foo: 'bar' }, { redact: ['foo'] })
logger.info({ hello: 'world' })
// {"level":30,"time":1625794553558,"pid":67930,"hostname":"x","hello":"world", "foo": "[Redacted]" }
```
* See [`redact` option](#opt-redact)
##### `options.serializers` (Object)
Child loggers inherit the [serializers](#opt-serializers) from the parent logger.
Setting the `serializers` key of the `options` object will override
any configured parent serializers.
```js
const logger = require('pino')()
logger.info({test: 'will appear'})
// {"level":30,"time":1531259759482,"pid":67930,"hostname":"x","test":"will appear"}
const child = logger.child({}, {serializers: {test: () => `child-only serializer`}})
child.info({test: 'will be overwritten'})
// {"level":30,"time":1531259784008,"pid":67930,"hostname":"x","test":"child-only serializer"}
```
* See [`serializers` option](#opt-serializers)
* See [pino.stdSerializers](#pino-stdSerializers)
<a id="logger-bindings"></a>
### `logger.bindings()`
Returns an object containing all the current bindings, cloned from the ones passed in via `logger.child()`.
```js
const child = logger.child({ foo: 'bar' })
console.log(child.bindings())
// { foo: 'bar' }
const anotherChild = child.child({ MIX: { IN: 'always' } })
console.log(anotherChild.bindings())
// { foo: 'bar', MIX: { IN: 'always' } }
```
<a id="logger-set-bindings"></a>
### `logger.setBindings(bindings)`
Adds to the bindings of this logger instance.
**Note:** Does not overwrite bindings. Can potentially result in duplicate keys in
log lines.
* See [`bindings` parameter in `logger.child`](#logger-child-bindings)
<a id="flush"></a>
### `logger.flush([cb])`
Flushes the content of the buffer when using `pino.destination({
sync: false })`.
This is an asynchronous, best used as fire and forget, operation.
The use case is primarily for asynchronous logging, which may buffer
log lines while others are being written. The `logger.flush` method can be
used to flush the logs
on a long interval, say ten seconds. Such a strategy can provide an
optimum balance between extremely efficient logging at high demand periods
and safer logging at low demand periods.
If there is a need to wait for the logs to be flushed, a callback should be used.
* See [`destination` parameter](#destination)
* See [Asynchronous Logging ⇗](/docs/asynchronous.md)
<a id="logger-level"></a>
### `logger.level` (String) [Getter/Setter]
Set this property to the desired logging level.
The core levels and their values are as follows:
| | | | | | | | |
|:-----------|-------|-------|------|------|-------|-------|---------:|
| **Level:** | trace | debug | info | warn | error | fatal | silent |
| **Value:** | 10 | 20 | 30 | 40 | 50 | 60 | Infinity |
The logging level is a *minimum* level based on the associated value of that level.
For instance if `logger.level` is `info` *(30)* then `info` *(30)*, `warn` *(40)*, `error` *(50)*, and `fatal` *(60)* log methods will be enabled but the `trace` *(10)* and `debug` *(20)* methods, being less than 30, will not.
The `silent` logging level is a specialized level that will disable all logging,
the `silent` log method is a noop function.
<a id="islevelenabled"></a>
### `logger.isLevelEnabled(level)`
A utility method for determining if a given log level will write to the destination.
#### `level` (String)
The given level to check against:
```js
if (logger.isLevelEnabled('debug')) logger.debug('conditional log')
```
#### `levelLabel` (String)
Defines the method name of the new level.
* See [`logger.level`](#level)
#### `levelValue` (Number)
Defines the associated minimum threshold value for the level, and
therefore where it sits in order of priority among other levels.
* See [`logger.level`](#level)
<a id="levelVal"></a>
### `logger.levelVal` (Number)
Supplies the integer value for the current logging level.
```js
if (logger.levelVal === 30) {
console.log('logger level is `info`')
}
```
<a id="levels"></a>
### `logger.levels` (Object)
Levels are mapped to values to determine the minimum threshold that a
logging method should be enabled at (see [`logger.level`](#level)).
The `logger.levels` property holds the mappings between levels and values,
and vice versa.
```sh
$ node -p "require('pino')().levels"
```
```js
{ labels:
{ '10': 'trace',
'20': 'debug',
'30': 'info',
'40': 'warn',
'50': 'error',
'60': 'fatal' },
values:
{ fatal: 60, error: 50, warn: 40, info: 30, debug: 20, trace: 10 } }
```
* See [`logger.level`](#level)
<a id="serializers"></a>
### logger\[Symbol.for('pino.serializers')\]
Returns the serializers as applied to the current logger instance. If a child logger did not
register its own serializer upon instantiation the serializers of the parent will be returned.
<a id="level-change"></a>
### Event: 'level-change'
The logger instance is also an [`EventEmitter ⇗`](https://nodejs.org/dist/latest/docs/api/events.html#events_class_eventemitter)
A listener function can be attached to a logger via the `level-change` event
The listener is passed five arguments:
* `levelLabel` the new level string, e.g `trace`
* `levelValue`  the new level number, e.g `10`
* `previousLevelLabel` the prior level string, e.g `info`
* `previousLevelValue` the prior level number, e.g `30`
* `logger` the logger instance from which the event originated
```js
const logger = require('pino')()
logger.on('level-change', (lvl, val, prevLvl, prevVal) => {
console.log('%s (%d) was changed to %s (%d)', prevLvl, prevVal, lvl, val)
})
logger.level = 'trace' // trigger event
```
Please note that due to a [known bug](https://github.com/pinojs/pino/issues/1006), every `logger.child()` call will
fire a `level-change` event. These events can be ignored by writing an event handler like:
```js
const logger = require('pino')()
logger.on('level-change', function (lvl, val, prevLvl, prevVal, instance) {
if (logger !== instance) {
return
}
console.log('%s (%d) was changed to %s (%d)', prevLvl, prevVal, lvl, val)
})
logger.child({}); // trigger an event by creating a child instance, notice no console.log
logger.level = 'trace' // trigger event using actual value change, notice console.log
```
<a id="version"></a>
### `logger.version` (String)
Exposes the Pino package version. Also available on the exported `pino` function.
* See [`pino.version`](#pino-version)
## Statics
<a id="pino-destination"></a>
### `pino.destination([opts]) => SonicBoom`
Create a Pino Destination instance: a stream-like object with
significantly more throughput than a standard Node.js stream.
```js
const pino = require('pino')
const logger = pino(pino.destination('./my-file'))
const logger2 = pino(pino.destination())
const logger3 = pino(pino.destination({
dest: './my-file',
minLength: 4096, // Buffer before writing
sync: false // Asynchronous logging, the default
}))
const logger4 = pino(pino.destination({
dest: './my-file2',
sync: true // Synchronous logging
}))
```
The `pino.destination` method may be passed a file path or a numerical file descriptor.
By default, `pino.destination` will use `process.stdout.fd` (1) as the file descriptor.
`pino.destination` is implemented on [`sonic-boom` ⇗](https://github.com/mcollina/sonic-boom).
A `pino.destination` instance can also be used to reopen closed files
(for example, for some log rotation scenarios), see [Reopening log files](/docs/help.md#reopening).
* See [`destination` parameter](#destination)
* See [`sonic-boom` ⇗](https://github.com/mcollina/sonic-boom)
* See [Reopening log files](/docs/help.md#reopening)
* See [Asynchronous Logging ⇗](/docs/asynchronous.md)
<a id="pino-transport"></a>
### `pino.transport(options) => ThreadStream`
Create a stream that routes logs to a worker thread that
wraps around a [Pino Transport](/docs/transports.md).
```js
const pino = require('pino')
const transport = pino.transport({
target: 'some-transport',
options: { some: 'options for', the: 'transport' }
})
pino(transport)
```
Multiple transports may also be defined, and specific levels can be logged to each transport:
```js
const pino = require('pino')
const transport = pino.transport({
targets: [{
level: 'info',
target: 'pino-pretty' // must be installed separately
}, {
level: 'trace',
target: 'pino/file',
options: { destination: '/path/to/store/logs' }
}]
})
pino(transport)
```
A pipeline could also be created to transform log lines _before_ sending them:
```js
const pino = require('pino')
const transport = pino.transport({
pipeline: [{
target: 'pino-syslog' // must be installed separately
}, {
target: 'pino-socket' // must be installed separately
}]
})
pino(transport)
```
If `WeakRef`, `WeakMap`, and `FinalizationRegistry` are available in the current runtime (v14.5.0+), then the thread
will be automatically terminated in case the stream or logger goes out of scope.
The `transport()` function adds a listener to `process.on('beforeExit')` and `process.on('exit')` to ensure the worker
is flushed and all data synced before the process exits.
Note that calling `process.exit()` on the main thread will stop the event loop on the main thread from turning. As a result,
using `console.log` and `process.stdout` after the main thread called `process.exit()` will not produce any output.
If you are embedding/integrating pino within your framework, you will need to make pino aware of the script that is calling it,
like so:
```js
const pino = require('pino')
const getCaller = require('get-caller-file')
module.exports = function build () {
const logger = pino({
transport: {
caller: getCaller(),
target: 'transport',
options: { destination: './destination' }
}
})
return logger
}
```
For more on transports, how they work, and how to create them see the [`Transports documentation`](/docs/transports.md).
* See [`Transports`](/docs/transports.md)
* See [`thread-stream` ⇗](https://github.com/mcollina/thread-stream)
#### Options
* `target`: The transport to pass logs through. This may be an installed module name or an absolute path.
* `options`: An options object which is serialized (see [Structured Clone Algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm)), passed to the worker thread, parsed and then passed to the exported transport function.
* `worker`: [Worker thread](https://nodejs.org/api/worker_threads.html#worker_threads_new_worker_filename_options) configuration options. Additionally, the `worker` option supports `worker.autoEnd`. If this is set to `false` logs will not be flushed on process exit. It is then up to the developer to call `transport.end()` to flush logs.
* `targets`: May be specified instead of `target`. Must be an array of transport configurations. Transport configurations include the aforementioned `options` and `target` options plus a `level` option which will send only logs above a specified level to a transport.
* `pipeline`: May be specified instead of `target`. Must be an array of transport configurations. Transport configurations include the aforementioned `options` and `target` options. All intermediate steps in the pipeline _must_ be `Transform` streams and not `Writable`.
* `dedupe`: See [pino.multistream options](#pino-multistream)
<a id="pino-multistream"></a>
### `pino.multistream(streamsArray, opts) => MultiStreamRes`
Create a stream composed by multiple destination streams and returns an
object implementing the [MultiStreamRes](#multistreamres) interface.
```js
var fs = require('fs')
var pino = require('pino')
var pretty = require('pino-pretty')
var streams = [
{stream: fs.createWriteStream('/tmp/info.stream.out')},
{stream: pretty() },
{level: 'debug', stream: fs.createWriteStream('/tmp/debug.stream.out')},
{level: 'fatal', stream: fs.createWriteStream('/tmp/fatal.stream.out')}
]
var log = pino({
level: 'debug' // this MUST be set at the lowest level of the
// destinations
}, pino.multistream(streams))
log.debug('this will be written to /tmp/debug.stream.out')
log.info('this will be written to /tmp/debug.stream.out and /tmp/info.stream.out')
log.fatal('this will be written to /tmp/debug.stream.out, /tmp/info.stream.out and /tmp/fatal.stream.out')
```
In order for `multistream` to work, the log level __must__ be set to the lowest level used in the streams array. Default is `info`.
#### Options
* `levels`: Pass custom log level definitions to the instance as an object.
+ `dedupe`: Set this to `true` to send logs only to the stream with the higher level. Default: `false`
`dedupe` flag can be useful for example when using `pino.multistream` to redirect `error` logs to `process.stderr` and others to `process.stdout`:
```js
var pino = require('pino')
var multistream = pino.multistream
var streams = [
{level: 'debug', stream: process.stdout},
{level: 'error', stream: process.stderr},
]
var opts = {
levels: {
silent: Infinity,
fatal: 60,
error: 50,
warn: 50,
info: 30,
debug: 20,
trace: 10
},
dedupe: true,
}
var log = pino({
level: 'debug' // this MUST be set at the lowest level of the
// destinations
}, multistream(streams, opts))
log.debug('this will be written ONLY to process.stdout')
log.info('this will be written ONLY to process.stdout')
log.error('this will be written ONLY to process.stderr')
log.fatal('this will be written ONLY to process.stderr')
```
<a id="pino-stdserializers"></a>
### `pino.stdSerializers` (Object)
The `pino.stdSerializers` object provides functions for serializing objects common to many projects. The standard serializers are directly imported from [pino-std-serializers](https://github.com/pinojs/pino-std-serializers).
* See [pino-std-serializers ⇗](https://github.com/pinojs/pino-std-serializers)
<a id="pino-stdtimefunctions"></a>
### `pino.stdTimeFunctions` (Object)
The [`timestamp`](#opt-timestamp) option can accept a function that determines the
`timestamp` value in a log line.
The `pino.stdTimeFunctions` object provides a very small set of common functions for generating the
`timestamp` property. These consist of the following
* `pino.stdTimeFunctions.epochTime`: Milliseconds since Unix epoch (Default)
* `pino.stdTimeFunctions.unixTime`: Seconds since Unix epoch
* `pino.stdTimeFunctions.nullTime`: Clears timestamp property (Used when `timestamp: false`)
* `pino.stdTimeFunctions.isoTime`: ISO 8601-formatted time in UTC
* See [`timestamp` option](#opt-timestamp)
<a id="pino-symbols"></a>
### `pino.symbols` (Object)
For integration purposes with ecosystem and third-party libraries `pino.symbols`
exposes the symbols used to hold non-public state and methods on the logger instance.
Access to the symbols allows logger state to be adjusted, and methods to be overridden or
proxied for performant integration where necessary.
The `pino.symbols` object is intended for library implementers and shouldn't be utilized
for general use.
<a id="pino-version"></a>
### `pino.version` (String)
Exposes the Pino package version. Also available on the logger instance.
* See [`logger.version`](#version)
## Interfaces
<a id="pino-multistreamres"></a>
### `MultiStreamRes`
Properties:
* `write(data)`
- `data` Object | string
- Returns: void
Write `data` onto the streams held by the current instance.
* `add(dest)`
- `dest` [StreamEntry](#streamentry) | [DestinationStream](#destinationstream)
- Returns: [MultiStreamRes](#multistreamres)
Add `dest` stream to the array of streams of the current instance.
* `flushSync()`
- Returns: `undefined`
Call `flushSync` on each stream held by the current instance.
* `minLevel`
- number
The minimum level amongst all the streams held by the current instance.
* `streams`
- Returns: [StreamEntry[]](#streamentry)
The array of streams currently held by the current instance.
* `clone(level)`
- `level` [Level](#level-1)
- Returns: [MultiStreamRes](#multistreamres)
Returns a cloned object of the current instance but with the provided `level`.
### `StreamEntry`
Properties:
* `stream`
- DestinationStream
* `level`
- Optional: [Level](#level-1)
### `DestinationStream`
Properties:
* `write(msg)`
- `msg` string
## Types
### `Level`
* Values: `"fatal"` | `"error"` | `"warn"` | `"info"` | `"debug"` | `"trace"`
+40
View File
@@ -0,0 +1,40 @@
# Asynchronous Logging
Asynchronous logging enables the minimum overhead of Pino.
Asynchronous logging works by buffering log messages and writing them in larger chunks.
```js
const pino = require('pino')
const logger = pino(pino.destination({
dest: './my-file', // omit for stdout
minLength: 4096, // Buffer before writing
sync: false // Asynchronous logging
}))
```
It's always possible to turn on synchronous logging by passing `sync: true`.
In this mode of operation, log messages are directly written to the
output stream as the messages are generated with a _blocking_ operation.
* See [`pino.destination`](/docs/api.md#pino-destination)
* `pino.destination` is implemented on [`sonic-boom` ⇗](https://github.com/mcollina/sonic-boom).
### AWS Lambda
Asynchronous logging is disabled by default on AWS Lambda or any other environment
that modifies `process.stdout`. If forcefully turned on, we recommend calling `dest.flushSync()` at the end
of each function execution to avoid losing data.
## Caveats
Asynchronous logging has a couple of important caveats:
* As opposed to the synchronous mode, there is not a one-to-one relationship between
calls to logging methods (e.g. `logger.info`) and writes to a log file
* There is a possibility of the most recently buffered log messages being lost
in case of a system failure, e.g. a power cut.
See also:
* [`pino.destination` API](/docs/api.md#pino-destination)
* [`destination` parameter](/docs/api.md#destination)
+55
View File
@@ -0,0 +1,55 @@
# Benchmarks
`pino.info('hello world')`:
```
BASIC benchmark averages
Bunyan average: 377.434ms
Winston average: 270.249ms
Bole average: 172.690ms
Debug average: 220.527ms
LogLevel average: 222.802ms
Pino average: 114.801ms
PinoMinLength average: 70.968ms
PinoNodeStream average: 159.192ms
```
`pino.info({'hello': 'world'})`:
```
OBJECT benchmark averages
BunyanObj average: 410.379ms
WinstonObj average: 273.120ms
BoleObj average: 185.069ms
LogLevelObject average: 433.425ms
PinoObj average: 119.315ms
PinoMinLengthObj average: 76.968ms
PinoNodeStreamObj average: 164.268ms
```
`pino.info(aBigDeeplyNestedObject)`:
```
DEEP-OBJECT benchmark averages
BunyanDeepObj average: 1.839ms
WinstonDeepObj average: 5.604ms
BoleDeepObj average: 3.422ms
LogLevelDeepObj average: 11.716ms
PinoDeepObj average: 2.256ms
PinoMinLengthDeepObj average: 2.240ms
PinoNodeStreamDeepObj average: 2.595ms
```
`pino.info('hello %s %j %d', 'world', {obj: true}, 4, {another: 'obj'})`:
For a fair comparison, [LogLevel](http://npm.im/loglevel) was extended
to include a timestamp and [bole](http://npm.im/bole) had
`fastTime` mode switched on.
+227
View File
@@ -0,0 +1,227 @@
# Browser API
Pino is compatible with [`browserify`](https://npm.im/browserify) for browser-side usage:
This can be useful with isomorphic/universal JavaScript code.
By default, in the browser,
`pino` uses corresponding [Log4j](https://en.wikipedia.org/wiki/Log4j) `console` methods (`console.error`, `console.warn`, `console.info`, `console.debug`, `console.trace`) and uses `console.error` for any `fatal` level logs.
## Options
Pino can be passed a `browser` object in the options object,
which can have the following properties:
### `asObject` (Boolean)
```js
const pino = require('pino')({browser: {asObject: true}})
```
The `asObject` option will create a pino-like log object instead of
passing all arguments to a console method, for instance:
```js
pino.info('hi') // creates and logs {msg: 'hi', level: 30, time: <ts>}
```
When `write` is set, `asObject` will always be `true`.
### `formatters` (Object)
An object containing functions for formatting the shape of the log lines. When provided, it enables the logger to produce a pino-like log object with customized formatting. Currently, it supports formatting for the `level` object only.
##### `level`
Changes the shape of the log level. The default shape is `{ level: number }`.
The function takes two arguments, the label of the level (e.g. `'info'`)
and the numeric value (e.g. `30`).
```js
const formatters = {
level (label, number) {
return { level: number }
}
}
```
### `write` (Function | Object)
Instead of passing log messages to `console.log` they can be passed to
a supplied function.
If `write` is set to a single function, all logging objects are passed
to this function.
```js
const pino = require('pino')({
browser: {
write: (o) => {
// do something with o
}
}
})
```
If `write` is an object, it can have methods that correspond to the
levels. When a message is logged at a given level, the corresponding
method is called. If a method isn't present, the logging falls back
to using the `console`.
```js
const pino = require('pino')({
browser: {
write: {
info: function (o) {
//process info log object
},
error: function (o) {
//process error log object
}
}
}
})
```
### `serialize`: (Boolean | Array)
The serializers provided to `pino` are ignored by default in the browser, including
the standard serializers provided with Pino. Since the default destination for log
messages is the console, values such as `Error` objects are enhanced for inspection,
which they otherwise wouldn't be if the Error serializer was enabled.
We can turn all serializers on,
```js
const pino = require('pino')({
browser: {
serialize: true
}
})
```
Or we can selectively enable them via an array:
```js
const pino = require('pino')({
serializers: {
custom: myCustomSerializer,
another: anotherSerializer
},
browser: {
serialize: ['custom']
}
})
// following will apply myCustomSerializer to the custom property,
// but will not apply anotherSerializer to another key
pino.info({custom: 'a', another: 'b'})
```
When `serialize` is `true` the standard error serializer is also enabled (see https://github.com/pinojs/pino/blob/master/docs/api.md#stdSerializers).
This is a global serializer, which will apply to any `Error` objects passed to the logger methods.
If `serialize` is an array the standard error serializer is also automatically enabled, it can
be explicitly disabled by including a string in the serialize array: `!stdSerializers.err`, like so:
```js
const pino = require('pino')({
serializers: {
custom: myCustomSerializer,
another: anotherSerializer
},
browser: {
serialize: ['!stdSerializers.err', 'custom'] //will not serialize Errors, will serialize `custom` keys
}
})
```
The `serialize` array also applies to any child logger serializers (see https://github.com/pinojs/pino/blob/master/docs/api.md#discussion-2
for how to set child-bound serializers).
Unlike server pino the serializers apply to every object passed to the logger method,
if the `asObject` option is `true`, this results in the serializers applying to the
first object (as in server pino).
For more info on serializers see https://github.com/pinojs/pino/blob/master/docs/api.md#parameters.
### `transmit` (Object)
An object with `send` and `level` properties.
The `transmit.level` property specifies the minimum level (inclusive) of when the `send` function
should be called, if not supplied the `send` function be called based on the main logging `level`
(set via `options.level`, defaulting to `info`).
The `transmit` object must have a `send` function which will be called after
writing the log message. The `send` function is passed the level of the log
message and a `logEvent` object.
The `logEvent` object is a data structure representing a log message, it represents
the arguments passed to a logger statement, the level
at which they were logged, and the hierarchy of child bindings.
The `logEvent` format is structured like so:
```js
{
ts = Number,
messages = Array,
bindings = Array,
level: { label = String, value = Number}
}
```
The `ts` property is a Unix epoch timestamp in milliseconds, the time is taken from the moment the
logger method is called.
The `messages` array is all arguments passed to logger method, (for instance `logger.info('a', 'b', 'c')`
would result in `messages` array `['a', 'b', 'c']`).
The `bindings` array represents each child logger (if any), and the relevant bindings.
For instance, given `logger.child({a: 1}).child({b: 2}).info({c: 3})`, the bindings array
would hold `[{a: 1}, {b: 2}]` and the `messages` array would be `[{c: 3}]`. The `bindings`
are ordered according to their position in the child logger hierarchy, with the lowest index
being the top of the hierarchy.
By default, serializers are not applied to log output in the browser, but they will *always* be
applied to `messages` and `bindings` in the `logEvent` object. This allows us to ensure a consistent
format for all values between server and client.
The `level` holds the label (for instance `info`), and the corresponding numerical value
(for instance `30`). This could be important in cases where client-side level values and
labels differ from server-side.
The point of the `send` function is to remotely record log messages:
```js
const pino = require('pino')({
browser: {
transmit: {
level: 'warn',
send: function (level, logEvent) {
if (level === 'warn') {
// maybe send the logEvent to a separate endpoint
// or maybe analyze the messages further before sending
}
// we could also use the `logEvent.level.value` property to determine
// numerical value
if (logEvent.level.value >= 50) { // covers error and fatal
// send the logEvent somewhere
}
}
}
}
})
```
### `disabled` (Boolean)
```js
const pino = require('pino')({browser: {disabled: true}})
```
The `disabled` option will disable logging in browser if set
to `true`, by default it is set to `false`.
+38
View File
@@ -0,0 +1,38 @@
# Bundling
Due to its internal architecture based on Worker Threads, it is not possible to bundle Pino *without* generating additional files.
In particular, a bundler must ensure that the following files are also bundled separately:
* `lib/worker.js` from the `thread-stream` dependency
* `file.js`
* `lib/worker.js`
* `lib/worker-pipeline.js`
* Any transport used by the user (like `pino-pretty`)
Once the files above have been generated, the bundler must also add information about the files above by injecting a code that sets `__bundlerPathsOverrides` in the `globalThis` object.
The variable is an object whose keys are an identifier for the files and the values are the paths of files relative to the currently bundle files.
Example:
```javascript
// Inject this using your bundle plugin
globalThis.__bundlerPathsOverrides = {
'thread-stream-worker': pinoWebpackAbsolutePath('./thread-stream-worker.js')
'pino/file': pinoWebpackAbsolutePath('./pino-file.js'),
'pino-worker': pinoWebpackAbsolutePath('./pino-worker.js'),
'pino-pipeline-worker': pinoWebpackAbsolutePath('./pino-pipeline-worker.js'),
'pino-pretty': pinoWebpackAbsolutePath('./pino-pretty.js'),
};
```
Note that `pino/file`, `pino-worker`, `pino-pipeline-worker`, and `thread-stream-worker` are required identifiers. Other identifiers are possible based on the user configuration.
## Webpack Plugin
If you are a Webpack user, you can achieve this with [pino-webpack-plugin](https://github.com/pinojs/pino-webpack-plugin) without manual configuration of `__bundlerPathsOverrides`; however, you still need to configure it manually if you are using other bundlers.
## Esbuild Plugin
[esbuild-plugin-pino](https://github.com/davipon/esbuild-plugin-pino) is the esbuild plugin to generate extra pino files for bundling.
+95
View File
@@ -0,0 +1,95 @@
# Child loggers
Let's assume we want to have `"module":"foo"` added to every log within a
module `foo.js`.
To accomplish this, simply use a child logger:
```js
'use strict'
// imports a pino logger instance of `require('pino')()`
const parentLogger = require('./lib/logger')
const log = parentLogger.child({module: 'foo'})
function doSomething () {
log.info('doSomething invoked')
}
module.exports = {
doSomething
}
```
## Cost of child logging
Child logger creation is fast:
```
benchBunyanCreation*10000: 564.514ms
benchBoleCreation*10000: 283.276ms
benchPinoCreation*10000: 258.745ms
benchPinoExtremeCreation*10000: 150.506ms
```
Logging through a child logger has little performance penalty:
```
benchBunyanChild*10000: 556.275ms
benchBoleChild*10000: 288.124ms
benchPinoChild*10000: 231.695ms
benchPinoExtremeChild*10000: 122.117ms
```
Logging via the child logger of a child logger also has negligible overhead:
```
benchBunyanChildChild*10000: 559.082ms
benchPinoChildChild*10000: 229.264ms
benchPinoExtremeChildChild*10000: 127.753ms
```
## Duplicate keys caveat
Naming conflicts can arise between child loggers and
children of child loggers.
This isn't as bad as it sounds, even if the same keys between
parent and child loggers are used, Pino resolves the conflict in the sanest way.
For example, consider the following:
```js
const pino = require('pino')
pino(pino.destination('./my-log'))
.child({a: 'property'})
.child({a: 'prop'})
.info('howdy')
```
```sh
$ cat my-log
{"pid":95469,"hostname":"MacBook-Pro-3.home","level":30,"msg":"howdy","time":1459534114473,"a":"property","a":"prop"}
```
Notice how there are two keys named `a` in the JSON output. The sub-child's properties
appear after the parent child properties.
At some point, the logs will most likely be processed (for instance with a [transport](transports.md)),
and this generally involves parsing. `JSON.parse` will return an object where the conflicting
namespace holds the final value assigned to it:
```sh
$ cat my-log | node -e "process.stdin.once('data', (line) => console.log(JSON.stringify(JSON.parse(line))))"
{"pid":95469,"hostname":"MacBook-Pro-3.home","level":30,"msg":"howdy","time":"2016-04-01T18:08:34.473Z","a":"prop"}
```
Ultimately the conflict is resolved by taking the last value, which aligns with Bunyan's child logging
behavior.
There may be cases where this edge case becomes problematic if a JSON parser with alternative behavior
is used to process the logs. It's recommended to be conscious of namespace conflicts with child loggers,
in light of an expected log processing approach.
One of Pino's performance tricks is to avoid building objects and stringifying
them, so we're building strings instead. This is why duplicate keys between
parents and children will end up in the log output.
+83
View File
@@ -0,0 +1,83 @@
# Pino Ecosystem
This is a list of ecosystem modules that integrate with `pino`.
Modules listed under [Core](#core) are maintained by the Pino team. Modules
listed under [Community](#community) are maintained by independent community
members.
Please send a PR to add new modules!
<a id="core"></a>
## Core
### Frameworks
+ [`express-pino-logger`](https://github.com/pinojs/express-pino-logger): use
Pino to log requests within [express](https://expressjs.com/).
+ [`koa-pino-logger`](https://github.com/pinojs/koa-pino-logger): use Pino to
log requests within [Koa](https://koajs.com/).
+ [`rill-pino-logger`](https://github.com/pinojs/rill-pino-logger): use Pino as
the logger for the [Rill framework](https://rill.site/).
+ [`restify-pino-logger`](https://github.com/pinojs/restify-pino-logger): use
Pino to log requests within [restify](http://restify.com/).
### Utilities
+ [`pino-arborsculpture`](https://github.com/pinojs/pino-arborsculpture): change
log levels at runtime.
+ [`pino-caller`](https://github.com/pinojs/pino-caller): add callsite to the log line.
+ [`pino-clf`](https://github.com/pinojs/pino-clf): reformat Pino logs into
Common Log Format.
+ [`pino-debug`](https://github.com/pinojs/pino-debug): use Pino to interpret
[`debug`](https://npm.im/debug) logs.
+ [`pino-elasticsearch`](https://github.com/pinojs/pino-elasticsearch): send
Pino logs to an Elasticsearch instance.
+ [`pino-eventhub`](https://github.com/pinojs/pino-eventhub): send Pino logs
to an [Event Hub](https://docs.microsoft.com/en-us/azure/event-hubs/event-hubs-what-is-event-hubs).
+ [`pino-filter`](https://github.com/pinojs/pino-filter): filter Pino logs in
the same fashion as the [`debug`](https://npm.im/debug) module.
+ [`pino-gelf`](https://github.com/pinojs/pino-gelf): reformat Pino logs into
GELF format for Graylog.
+ [`pino-hapi`](https://github.com/pinojs/hapi-pino): use Pino as the logger
for [Hapi](https://hapijs.com/).
+ [`pino-http`](https://github.com/pinojs/pino-http): easily use Pino to log
requests with the core `http` module.
+ [`pino-http-print`](https://github.com/pinojs/pino-http-print): reformat Pino
logs into traditional [HTTPD](https://httpd.apache.org/) style request logs.
+ [`pino-multi-stream`](https://github.com/pinojs/pino-multi-stream): send
logs to multiple destination streams (slow!).
+ [`pino-mongodb`](https://github.com/pinojs/pino-mongodb): store Pino logs
in a MongoDB database.
+ [`pino-noir`](https://github.com/pinojs/pino-noir): redact sensitive information
in logs.
+ [`pino-pretty`](https://github.com/pinojs/pino-pretty): basic prettifier to
make log lines human-readable.
+ [`pino-socket`](https://github.com/pinojs/pino-socket): send logs to TCP or UDP
destinations.
+ [`pino-std-serializers`](https://github.com/pinojs/pino-std-serializers): the
core object serializers used within Pino.
+ [`pino-syslog`](https://github.com/pinojs/pino-syslog): reformat Pino logs
to standard syslog format.
+ [`pino-tee`](https://github.com/pinojs/pino-tee): pipe Pino logs into files
based upon log levels.
+ [`pino-toke`](https://github.com/pinojs/pino-toke): reformat Pino logs
according to a given format string.
+ [`pino-test`](https://github.com/pinojs/pino-test): a set of utilities for
verifying logs generated by the Pino logger.
<a id="community"></a>
## Community
+ [`pino-colada`](https://github.com/lrlna/pino-colada): cute ndjson formatter for pino.
+ [`pino-fluentd`](https://github.com/davidedantonio/pino-fluentd): send Pino logs to Elasticsearch,
MongoDB, and many [others](https://www.fluentd.org/dataoutputs) via Fluentd.
+ [`pino-pretty-min`](https://github.com/unjello/pino-pretty-min): a minimal
prettifier inspired by the [logrus](https://github.com/sirupsen/logrus) logger.
+ [`pino-rotating-file`](https://github.com/homeaway/pino-rotating-file): a hapi-pino log transport for splitting logs into separate, automatically rotating files.
+ [`cls-proxify`](https://github.com/keenondrums/cls-proxify): integration of pino and [CLS](https://github.com/jeff-lewis/cls-hooked). Useful for creating dynamically configured child loggers (e.g. with added trace ID) for each request.
+ [`pino-tiny`](https://github.com/holmok/pino-tiny): a tiny (and extensible?) little log formatter for pino.
+ [`pino-dev`](https://github.com/dnjstrom/pino-dev): simple prettifier for pino with built-in support for common ecosystem packages.
+ [`@newrelic/pino-enricher`](https://github.com/newrelic/newrelic-node-log-extensions/blob/main/packages/pino-log-enricher): a log customization to add New Relic context to use [Logs In Context](https://docs.newrelic.com/docs/logs/logs-context/logs-in-context/)
+ [`pino-lambda`](https://github.com/FormidableLabs/pino-lambda): log transport for cloudwatch support inside aws-lambda
+ [`cloud-pine`](https://github.com/metcoder95/cloud-pine): transport that provides abstraction and compatibility with [`@google-cloud/logging`](https://www.npmjs.com/package/@google-cloud/logging).
+ [`crawlee-pino`](https://github.com/imyelo/crawlee-pino): use Pino to log within Crawlee
+319
View File
@@ -0,0 +1,319 @@
# Help
* [Log rotation](#rotate)
* [Reopening log files](#reopening)
* [Saving to multiple files](#multiple)
* [Log filtering](#filter-logs)
* [Transports and systemd](#transport-systemd)
* [Log to different streams](#multi-stream)
* [Duplicate keys](#dupe-keys)
* [Log levels as labels instead of numbers](#level-string)
* [Pino with `debug`](#debug)
* [Unicode and Windows terminal](#windows)
* [Mapping Pino Log Levels to Google Cloud Logging (Stackdriver) Severity Levels](#stackdriver)
* [Using Grafana Loki to evaluate pino logs in a kubernetes cluster](#grafana-loki)
* [Avoid Message Conflict](#avoid-message-conflict)
* [Best performance for logging to `stdout`](#best-performance-for-stdout)
* [Testing](#testing)
<a id="rotate"></a>
## Log rotation
Use a separate tool for log rotation:
We recommend [logrotate](https://github.com/logrotate/logrotate).
Consider we output our logs to `/var/log/myapp.log` like so:
```
$ node server.js > /var/log/myapp.log
```
We would rotate our log files with logrotate, by adding the following to `/etc/logrotate.d/myapp`:
```
/var/log/myapp.log {
su root
daily
rotate 7
delaycompress
compress
notifempty
missingok
copytruncate
}
```
The `copytruncate` configuration has a very slight possibility of lost log lines due
to a gap between copying and truncating - the truncate may occur after additional lines
have been written. To perform log rotation without `copytruncate`, see the [Reopening log files](#reopening)
help.
<a id="reopening"></a>
## Reopening log files
In cases where a log rotation tool doesn't offer copy-truncate capabilities,
or where using them is deemed inappropriate, `pino.destination`
can reopen file paths after a file has been moved away.
One way to use this is to set up a `SIGUSR2` or `SIGHUP` signal handler that
reopens the log file destination, making sure to write the process PID out
somewhere so the log rotation tool knows where to send the signal.
```js
// write the process pid to a well known location for later
const fs = require('fs')
fs.writeFileSync('/var/run/myapp.pid', process.pid)
const dest = pino.destination('/log/file')
const logger = require('pino')(dest)
process.on('SIGHUP', () => dest.reopen())
```
The log rotation tool can then be configured to send this signal to the process
after a log rotation event has occurred.
Given a similar scenario as in the [Log rotation](#rotate) section a basic
`logrotate` config that aligns with this strategy would look similar to the following:
```
/var/log/myapp.log {
su root
daily
rotate 7
delaycompress
compress
notifempty
missingok
postrotate
kill -HUP `cat /var/run/myapp.pid`
endscript
}
```
<a id="multiple"></a>
## Saving to multiple files
See [`pino.multistream`](/docs/api.md#pino-multistream).
<a id="filter-logs"></a>
## Log Filtering
The Pino philosophy advocates common, preexisting, system utilities.
Some recommendations in line with this philosophy are:
1. Use [`grep`](https://linux.die.net/man/1/grep):
```sh
$ # View all "INFO" level logs
$ node app.js | grep '"level":30'
```
1. Use [`jq`](https://stedolan.github.io/jq/):
```sh
$ # View all "ERROR" level logs
$ node app.js | jq 'select(.level == 50)'
```
<a id="transport-systemd"></a>
## Transports and systemd
`systemd` makes it complicated to use pipes in services. One method for overcoming
this challenge is to use a subshell:
```
ExecStart=/bin/sh -c '/path/to/node app.js | pino-transport'
```
<a id="multi-stream"></a>
## Log to different streams
Pino's default log destination is the singular destination of `stdout`. While
not recommended for performance reasons, multiple destinations can be targeted
by using [`pino.multistream`](/doc/api.md#pino-multistream).
In this example, we use `stderr` for `error` level logs and `stdout` as default
for all other levels (e.g. `debug`, `info`, and `warn`).
```js
const pino = require('pino')
var streams = [
{level: 'debug', stream: process.stdout},
{level: 'error', stream: process.stderr},
{level: 'fatal', stream: process.stderr}
]
const logger = pino({
name: 'my-app',
level: 'debug', // must be the lowest level of all streams
}, pino.multistream(streams))
```
<a id="dupe-keys"></a>
## How Pino handles duplicate keys
Duplicate keys are possibly when a child logger logs an object with a key that
collides with a key in the child loggers bindings.
See the [child logger duplicate keys caveat](/docs/child-loggers.md#duplicate-keys-caveat)
for information on this is handled.
<a id="level-string"></a>
## Log levels as labels instead of numbers
Pino log lines are meant to be parsable. Thus, Pino's default mode of operation
is to print the level value instead of the string name.
However, you can use the [`formatters`](/docs/api.md#formatters-object) option
with a [`level`](/docs/api.md#level) function to print the string name instead of the level value :
```js
const pino = require('pino')
const log = pino({
formatters: {
level: (label) => {
return {
level: label
}
}
}
})
log.info('message')
// {"level":"info","time":1661632832200,"pid":18188,"hostname":"foo","msg":"message"}
```
Although it works, we recommend using one of these options instead if you are able:
1. If the only change desired is the name then a transport can be used. One such
transport is [`pino-text-level-transport`](https://npm.im/pino-text-level-transport).
1. Use a prettifier like [`pino-pretty`](https://npm.im/pino-pretty) to make
the logs human friendly.
<a id="debug"></a>
## Pino with `debug`
The popular [`debug`](https://npm.im/debug) is used in many modules across the ecosystem.
The [`pino-debug`](https://github.com/pinojs/pino-debug) module
can capture calls to `debug` loggers and run them
through `pino` instead. This results in a 10x (20x in asynchronous mode)
performance improvement - even though `pino-debug` is logging additional
data and wrapping it in JSON.
To quickly enable this install [`pino-debug`](https://github.com/pinojs/pino-debug)
and preload it with the `-r` flag, enabling any `debug` logs with the
`DEBUG` environment variable:
```sh
$ npm i pino-debug
$ DEBUG=* node -r pino-debug app.js
```
[`pino-debug`](https://github.com/pinojs/pino-debug) also offers fine-grain control to map specific `debug`
namespaces to `pino` log levels. See [`pino-debug`](https://github.com/pinojs/pino-debug)
for more.
<a id="windows"></a>
## Unicode and Windows terminal
Pino uses [sonic-boom](https://github.com/mcollina/sonic-boom) to speed
up logging. Internally, it uses [`fs.write`](https://nodejs.org/dist/latest-v10.x/docs/api/fs.html#fs_fs_write_fd_string_position_encoding_callback) to write log lines directly to a file
descriptor. On Windows, Unicode output is not handled properly in the
terminal (both `cmd.exe` and PowerShell), and as such the output could
be visualized incorrectly if the log lines include utf8 characters. It
is possible to configure the terminal to visualize those characters
correctly with the use of [`chcp`](https://ss64.com/nt/chcp.html) by
executing in the terminal `chcp 65001`. This is a known limitation of
Node.js.
<a id="stackdriver"></a>
## Mapping Pino Log Levels to Google Cloud Logging (Stackdriver) Severity Levels
Google Cloud Logging uses `severity` levels instead of log levels. As a result, all logs may show as INFO
level logs while completely ignoring the level set in the pino log. Google Cloud Logging also prefers that
log data is present inside a `message` key instead of the default `msg` key that Pino uses. Use a technique
similar to the one below to retain log levels in Google Cloud Logging
```js
const pino = require('pino')
// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity
const PinoLevelToSeverityLookup = {
trace: 'DEBUG',
debug: 'DEBUG',
info: 'INFO',
warn: 'WARNING',
error: 'ERROR',
fatal: 'CRITICAL',
};
const defaultPinoConf = {
messageKey: 'message',
formatters: {
level(label, number) {
return {
severity: PinoLevelToSeverityLookup[label] || PinoLevelToSeverityLookup['info'],
level: number,
}
}
},
}
module.exports = function createLogger(options) {
return pino(Object.assign({}, options, defaultPinoConf))
}
```
<a id="grafana-loki"></a>
## Using Grafana Loki to evaluate pino logs in a kubernetes cluster
To get pino logs into Grafana Loki there are two options:
1. **Push:** Use [pino-loki](https://github.com/Julien-R44/pino-loki) to send logs directly to Loki.
1. **Pull:** Configure Grafana Promtail to read and properly parse the logs before sending them to Loki.
Similar to Google Cloud logging, this involves remapping the log levels. See this [article](https://medium.com/@janpaepke/structured-logging-in-the-grafana-monitoring-stack-8aff0a5af2f5) for details.
<a id="avoid-message-conflict"></a>
## Avoid Message Conflict
As described in the [`message` documentation](/docs/api.md#message), when a log
is written like `log.info({ msg: 'a message' }, 'another message')` then the
final output JSON will have `"msg":"another message"` and the `'a message'`
string will be lost. To overcome this, the [`logMethod` hook](/docs/api.md#logmethod)
can be used:
```js
'use strict'
const log = require('pino')({
level: 'debug',
hooks: {
logMethod (inputArgs, method) {
if (inputArgs.length === 2 && inputArgs[0].msg) {
inputArgs[0].originalMsg = inputArgs[0].msg
}
return method.apply(this, inputArgs)
}
}
})
log.info('no original message')
log.info({ msg: 'mapped to originalMsg' }, 'a message')
// {"level":30,"time":1596313323106,"pid":63739,"hostname":"foo","msg":"no original message"}
// {"level":30,"time":1596313323107,"pid":63739,"hostname":"foo","msg":"a message","originalMsg":"mapped to originalMsg"}
```
<a id="best-performance-for-stdout"></a>
## Best performance for logging to `stdout`
The best performance for logging directly to stdout is _usually_ achieved by using the
default configuration:
```js
const log = require('pino')();
```
You should only have to configure custom transports or other settings
if you have broader logging requirements.
<a id="testing"></a>
## Testing
See [`pino-test`](https://github.com/pinojs/pino-test).
+63
View File
@@ -0,0 +1,63 @@
## Long Term Support
Pino's Long Term Support (LTS) is provided according to the schedule laid
out in this document:
1. Major releases, "X" release of [semantic versioning][semver] X.Y.Z release
versions, are supported for a minimum period of six months from their release
date. The release date of any specific version can be found at
[https://github.com/pinojs/pino/releases](https://github.com/pinojs/pino/releases).
1. Major releases will receive security updates for an additional six months
from the release of the next major release. After this period
we will still review and release security fixes as long as they are
provided by the community and they do not violate other constraints,
e.g. minimum supported Node.js version.
1. Major releases will be tested and verified against all Node.js
release lines that are supported by the
[Node.js LTS policy](https://github.com/nodejs/Release) within the
LTS period of that given Pino release line. This implies that only
the latest Node.js release of a given line is supported.
A "month" is defined as 30 consecutive days.
> ## Security Releases and Semver
>
> As a consequence of providing long-term support for major releases, there
> are occasions where we need to release breaking changes as a _minor_
> version release. Such changes will _always_ be noted in the
> [release notes](https://github.com/pinojs/pino/releases).
>
> To avoid automatically receiving breaking security updates it is possible to use
> the tilde (`~`) range qualifier. For example, to get patches for the 6.1
> release, and avoid automatically updating to the 6.1 release, specify
> the dependency as `"pino": "~6.1.x"`. This will leave your application vulnerable,
> so please use with caution.
[semver]: https://semver.org/
<a name="lts-schedule"></a>
### Schedule
| Version | Release Date | End Of LTS Date | Node.js |
| :------ | :----------- | :-------------- | :------------------- |
| 8.x | 2022-06-01 | TBD | 14, 16, 18 |
| 7.x | 2021-10-14 | 2023-06-01 | 12, 14, 16 |
| 6.x | 2020-03-07 | 2022-04-14 | 10, 12, 14, 16 |
<a name="supported-os"></a>
### CI tested operating systems
Pino uses GitHub Actions for CI testing, please refer to
[GitHub's documentation regarding workflow runners](https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources)
for further details on what the latest virtual environment is in relation to
the YAML workflow labels below:
| OS | YAML Workflow Label | Node.js |
|---------|------------------------|--------------|
| Linux | `ubuntu-latest` | 14,16,18 |
| Windows | `windows-latest` | 14,16,18 |
| MacOS | `macos-latest` | 14,16,18 |
+35
View File
@@ -0,0 +1,35 @@
# Pretty Printing
By default, Pino log lines are newline delimited JSON (NDJSON). This is perfect
for production usage and long-term storage. It's not so great for development
environments. Thus, Pino logs can be prettified by using a Pino prettifier
module like [`pino-pretty`][pp]:
1. Install a prettifier module as a separate dependency, e.g. `npm install pino-pretty`.
2. Instantiate the logger with the `transport.target` option set to `'pino-pretty'`:
```js
const pino = require('pino')
const logger = pino({
transport: {
target: 'pino-pretty'
},
})
logger.info('hi')
```
3. 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')
```
[pp]: https://github.com/pinojs/pino-pretty
+135
View File
@@ -0,0 +1,135 @@
# Redaction
> Redaction is not supported in the browser [#670](https://github.com/pinojs/pino/issues/670)
To redact sensitive information, supply paths to keys that hold sensitive data
using the `redact` option. Note that paths that contain hyphens need to use
brackets to access the hyphenated property:
```js
const logger = require('.')({
redact: ['key', 'path.to.key', 'stuff.thats[*].secret', 'path["with-hyphen"]']
})
logger.info({
key: 'will be redacted',
path: {
to: {key: 'sensitive', another: 'thing'}
},
stuff: {
thats: [
{secret: 'will be redacted', logme: 'will be logged'},
{secret: 'as will this', logme: 'as will this'}
]
}
})
```
This will output:
```JSON
{"level":30,"time":1527777350011,"pid":3186,"hostname":"Davids-MacBook-Pro-3.local","key":"[Redacted]","path":{"to":{"key":"[Redacted]","another":"thing"}},"stuff":{"thats":[{"secret":"[Redacted]","logme":"will be logged"},{"secret":"[Redacted]","logme":"as will this"}]}}
```
The `redact` option can take an array (as shown in the above example) or
an object. This allows control over *how* information is redacted.
For instance, setting the censor:
```js
const logger = require('.')({
redact: {
paths: ['key', 'path.to.key', 'stuff.thats[*].secret'],
censor: '**GDPR COMPLIANT**'
}
})
logger.info({
key: 'will be redacted',
path: {
to: {key: 'sensitive', another: 'thing'}
},
stuff: {
thats: [
{secret: 'will be redacted', logme: 'will be logged'},
{secret: 'as will this', logme: 'as will this'}
]
}
})
```
This will output:
```JSON
{"level":30,"time":1527778563934,"pid":3847,"hostname":"Davids-MacBook-Pro-3.local","key":"**GDPR COMPLIANT**","path":{"to":{"key":"**GDPR COMPLIANT**","another":"thing"}},"stuff":{"thats":[{"secret":"**GDPR COMPLIANT**","logme":"will be logged"},{"secret":"**GDPR COMPLIANT**","logme":"as will this"}]}}
```
The `redact.remove` option also allows for the key and value to be removed from output:
```js
const logger = require('.')({
redact: {
paths: ['key', 'path.to.key', 'stuff.thats[*].secret'],
remove: true
}
})
logger.info({
key: 'will be redacted',
path: {
to: {key: 'sensitive', another: 'thing'}
},
stuff: {
thats: [
{secret: 'will be redacted', logme: 'will be logged'},
{secret: 'as will this', logme: 'as will this'}
]
}
})
```
This will output
```JSON
{"level":30,"time":1527782356751,"pid":5758,"hostname":"Davids-MacBook-Pro-3.local","path":{"to":{"another":"thing"}},"stuff":{"thats":[{"logme":"will be logged"},{"logme":"as will this"}]}}
```
See [pino options in API](/docs/api.md#redact-array-object) for `redact` API details.
<a name="paths"></a>
## Path Syntax
The syntax for paths supplied to the `redact` option conform to the syntax in path lookups
in standard ECMAScript, with two additions:
* paths may start with bracket notation
* paths may contain the asterisk `*` to denote a wildcard
* paths are **case sensitive**
By way of example, the following are all valid paths:
* `a.b.c`
* `a["b-c"].d`
* `["a-b"].c`
* `a.b.*`
* `a[*].b`
## Overhead
Pino's redaction functionality is built on top of [`fast-redact`](https://github.com/davidmarkclements/fast-redact)
which adds about 2% overhead to `JSON.stringify` when using paths without wildcards.
When used with pino logger with a single redacted path, any overhead is within noise -
a way to deterministically measure its effect has not been found. This is because it is not a bottleneck.
However, wildcard redaction does carry a non-trivial cost relative to explicitly declaring the keys
(50% in a case where four keys are redacted across two objects). See
the [`fast-redact` benchmarks](https://github.com/davidmarkclements/fast-redact#benchmarks) for details.
## Safety
The `redact` option is intended as an initialization time configuration option.
Path strings must not originate from user input.
The `fast-redact` module uses a VM context to syntax check the paths, user input
should never be combined with such an approach. See the [`fast-redact` Caveat](https://github.com/davidmarkclements/fast-redact#caveat)
and the [`fast-redact` Approach](https://github.com/davidmarkclements/fast-redact#approach) for in-depth information.
+1092
View File
@@ -0,0 +1,1092 @@
# Transports
Pino transports can be used for both transmitting and transforming log output.
The way Pino generates logs:
1. Reduces the impact of logging on an application to the absolute minimum.
2. Gives greater flexibility in how logs are processed and stored.
It is recommended that any log transformation or transmission is performed either
in a separate thread or a separate process.
Before Pino v7 transports would ideally operate in a separate process - these are
now referred to as [Legacy Transports](#legacy-transports).
From Pino v7 and upwards transports can also operate inside a [Worker Thread][worker-thread]
and can be used or configured via the options object passed to `pino` on initialization.
In this case the transports would always operate asynchronously, and logs would be
flushed as quickly as possible (there is nothing to do).
[worker-thread]: https://nodejs.org/dist/latest-v14.x/docs/api/worker_threads.html
## v7+ Transports
A transport is a module that exports a default function that returns a writable stream:
```js
import { createWriteStream } from 'fs'
export default (options) => {
return createWriteStream(options.destination)
}
```
Let's imagine the above defines our "transport" as the file `my-transport.mjs`
(ESM files are supported even if the project is written in CJS).
We would set up our transport by creating a transport stream with `pino.transport`
and passing it to the `pino` function:
```js
const pino = require('pino')
const transport = pino.transport({
target: '/absolute/path/to/my-transport.mjs'
})
pino(transport)
```
The transport code will be executed in a separate worker thread. The main thread
will write logs to the worker thread, which will write them to the stream returned
from the function exported from the transport file/module.
The exported function can also be async. If we use an async function we can throw early
if the transform could not be opened. As an example:
```js
import fs from 'fs'
import { once } from 'events'
export default async (options) => {
const stream = fs.createWriteStream(options.destination)
await once(stream, 'open')
return stream
}
```
While initializing the stream we're able to use `await` to perform asynchronous operations. In this
case, waiting for the write streams `open` event.
Let's imagine the above was published to npm with the module name `some-file-transport`.
The `options.destination` value can be set when creating the transport stream with `pino.transport` like so:
```js
const pino = require('pino')
const transport = pino.transport({
target: 'some-file-transport',
options: { destination: '/dev/null' }
})
pino(transport)
```
Note here we've specified a module by package rather than by relative path. The options object we provide
is serialized and injected into the transport worker thread, then passed to the module's exported function.
This means that the options object can only contain types that are supported by the
[Structured Clone Algorithm][sca] which is used to (de)serialize objects between threads.
What if we wanted to use both transports, but send only error logs to `some-file-transport` while
sending all logs to `my-transport.mjs`? We can use the `pino.transport` function's `destinations` option:
```js
const pino = require('pino')
const transport = pino.transport({
targets: [
{ target: '/absolute/path/to/my-transport.mjs', level: 'error' },
{ target: 'some-file-transport', options: { destination: '/dev/null' }}
]
})
pino(transport)
```
If we're using custom levels, they should be passed in when using more than one transport.
```js
const pino = require('pino')
const transport = pino.transport({
targets: [
{ target: '/absolute/path/to/my-transport.mjs', level: 'error' },
{ target: 'some-file-transport', options: { destination: '/dev/null' }
],
levels: { foo: 35 }
})
pino(transport)
```
It is also possible to use the `dedupe` option to send logs only to the stream with the higher level.
```js
const pino = require('pino')
const transport = pino.transport({
targets: [
{ target: '/absolute/path/to/my-transport.mjs', level: 'error' },
{ target: 'some-file-transport', options: { destination: '/dev/null' }
],
dedupe: true
})
pino(transport)
```
For more details on `pino.transport` see the [API docs for `pino.transport`][pino-transport].
[pino-transport]: /docs/api.md#pino-transport
[sca]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
<a id="writing"></a>
### Writing a Transport
The module [pino-abstract-transport](https://github.com/pinojs/pino-abstract-transport) provides
a simple utility to parse each line. Its usage is highly recommended.
You can see an example using an async iterator with ESM:
```js
import build from 'pino-abstract-transport'
import SonicBoom from 'sonic-boom'
import { once } from 'events'
export default async function (opts) {
// SonicBoom is necessary to avoid loops with the main thread.
// It is the same of pino.destination().
const destination = new SonicBoom({ dest: opts.destination || 1, sync: false })
await once(destination, 'ready')
return build(async function (source) {
for await (let obj of source) {
const toDrain = !destination.write(obj.msg.toUpperCase() + '\n')
// This block will handle backpressure
if (toDrain) {
await once(destination, 'drain')
}
}
}, {
async close (err) {
destination.end()
await once(destination, 'close')
}
})
}
```
or using Node.js streams and CommonJS:
```js
'use strict'
const build = require('pino-abstract-transport')
const SonicBoom = require('sonic-boom')
module.exports = function (opts) {
const destination = new SonicBoom({ dest: opts.destination || 1, sync: false })
return build(function (source) {
source.pipe(destination)
}, {
close (err, cb) {
destination.end()
destination.on('close', cb.bind(null, err))
}
})
}
```
(It is possible to use the async iterators with CommonJS and streams with ESM.)
To consume async iterators in batches, consider using the [hwp](https://github.com/mcollina/hwp) library.
The `close()` function is needed to make sure that the stream is closed and flushed when its
callback is called or the returned promise resolves. Otherwise, log lines will be lost.
### Writing to a custom transport & stdout
In case you want to both use a custom transport, and output the log entries with default processing to STDOUT, you can use 'pino/file' transport configured with `destination: 1`:
```js
const transports = [
{
target: 'pino/file',
options: { destination: 1 } // this writes to STDOUT
},
{
target: 'my-custom-transport',
options: { someParameter: true }
}
]
const logger = pino(pino.transport({ targets: transports })
```
### Creating a transport pipeline
As an example, the following transport returns a `Transform` stream:
```js
import build from 'pino-abstract-transport'
import { pipeline, Transform } from 'stream'
export default async function (options) {
return build(function (source) {
const myTransportStream = new Transform({
// Make sure autoDestroy is set,
// this is needed in Node v12 or when using the
// readable-stream module.
autoDestroy: true,
objectMode: true,
transform (chunk, enc, cb) {
// modifies the payload somehow
chunk.service = 'pino'
// stringify the payload again
this.push(`${JSON.stringify(chunk)}\n`)
cb()
}
})
pipeline(source, myTransportStream, () => {})
return myTransportStream
}, {
// This is needed to be able to pipeline transports.
enablePipelining: true
})
}
```
Then you can pipeline them with:
```js
import pino from 'pino'
const logger = pino({
transport: {
pipeline: [{
target: './my-transform.js'
}, {
// Use target: 'pino/file' with STDOUT descriptor 1 to write
// logs without any change.
target: 'pino/file',
options: { destination: 1 }
}]
}
})
logger.info('hello world')
```
__NOTE: there is no "default" destination for a pipeline but
a terminating target, i.e. a `Writable` stream.__
### TypeScript compatibility
Pino provides basic support for transports written in TypeScript.
Ideally, they should be transpiled to ensure maximum compatibility, but sometimes
you might want to use tools such as TS-Node, to execute your TypeScript
code without having to go through an explicit transpilation step.
You can use your TypeScript code without explicit transpilation, but there are
some known caveats:
- For "pure" TypeScript code, ES imports are still not supported (ES imports are
supported once the code is transpiled).
- Only TS-Node is supported for now, there's no TSM support.
- Running transports TypeScript code on TS-Node seems to be problematic on
Windows systems, there's no official support for that yet.
### Notable transports
#### `pino/file`
The `pino/file` transport routes logs to a file (or file descriptor).
The `options.destination` property may be set to specify the desired file destination.
```js
const pino = require('pino')
const transport = pino.transport({
target: 'pino/file',
options: { destination: '/path/to/file' }
})
pino(transport)
```
By default, the `pino/file` transport assumes the directory of the destination file exists. If it does not exist, the transport will throw an error when it attempts to open the file for writing. The `mkdir` option may be set to `true` to configure the transport to create the directory, if it does not exist, before opening the file for writing.
```js
const pino = require('pino')
const transport = pino.transport({
target: 'pino/file',
options: { destination: '/path/to/file', mkdir: true }
})
pino(transport)
```
By default, the `pino/file` transport appends to the destination file if it exists. The `append` option may be set to `false` to configure the transport to truncate the file upon opening it for writing.
```js
const pino = require('pino')
const transport = pino.transport({
target: 'pino/file',
options: { destination: '/path/to/file', append: false }
})
pino(transport)
```
The `options.destination` property may also be a number to represent a file descriptor. Typically this would be `1` to write to STDOUT or `2` to write to STDERR. If `options.destination` is not set, it defaults to `1` which means logs will be written to STDOUT. If `options.destination` is a string integer, e.g. `'1'`, it will be coerced to a number and used as a file descriptor. If this is not desired, provide a full path, e.g. `/tmp/1`.
The difference between using the `pino/file` transport builtin and using `pino.destination` is that `pino.destination` runs in the main thread, whereas `pino/file` sets up `pino.destination` in a worker thread.
#### `pino-pretty`
The [`pino-pretty`][pino-pretty] transport prettifies logs.
By default the `pino-pretty` builtin logs to STDOUT.
The `options.destination` property may be set to log pretty logs to a file descriptor or file. The following would send the prettified logs to STDERR:
```js
const pino = require('pino')
const transport = pino.transport({
target: 'pino-pretty',
options: { destination: 1 } // use 2 for stderr
})
pino(transport)
```
### Asynchronous startup
The new transports boot asynchronously and calling `process.exit()` before the transport
starts will cause logs to not be delivered.
```js
const pino = require('pino')
const transport = pino.transport({
targets: [
{ target: '/absolute/path/to/my-transport.mjs', level: 'error' },
{ target: 'some-file-transport', options: { destination: '/dev/null' } }
]
})
const logger = pino(transport)
logger.info('hello')
// If logs are printed before the transport is ready when process.exit(0) is called,
// they will be lost.
transport.on('ready', function () {
process.exit(0)
})
```
## Legacy Transports
A legacy Pino "transport" is a supplementary tool that consumes Pino logs.
Consider the following example for creating a transport:
```js
const { pipeline, Writable } = require('stream')
const split = require('split2')
const myTransportStream = new Writable({
write (chunk, enc, cb) {
// apply a transform and send to STDOUT
console.log(chunk.toString().toUpperCase())
cb()
}
})
pipeline(process.stdin, split(JSON.parse), myTransportStream)
```
The above defines our "transport" as the file `my-transport-process.js`.
Logs can now be consumed using shell piping:
```sh
node my-app-which-logs-stuff-to-stdout.js | node my-transport-process.js
```
Ideally, a transport should consume logs in a separate process to the application,
Using transports in the same process causes unnecessary load and slows down
Node's single-threaded event loop.
## Known Transports
PRs to this document are welcome for any new transports!
### Pino v7+ Compatible
+ [@logtail/pino](#@logtail/pino)
+ [pino-elasticsearch](#pino-elasticsearch)
+ [pino-pretty](#pino-pretty)
+ [pino-loki](#pino-loki)
+ [pino-seq-transport](#pino-seq-transport)
+ [pino-sentry-transport](#pino-sentry-transport)
+ [pino-airbrake-transport](#pino-airbrake-transport)
+ [pino-datadog-transport](#pino-datadog-transport)
+ [pino-slack-webhook](#pino-slack-webhook)
+ [pino-axiom](#pino-axiom)
+ [pino-opentelemetry-transport](#pino-opentelemetry-transport)
+ [@axiomhq/pino](#@axiomhq/pino)
+ [pino-discord-webhook](#pino-discord-webhook)
+ [pino-logfmt](#pino-logfmt)
### Legacy
+ [pino-applicationinsights](#pino-applicationinsights)
+ [pino-azuretable](#pino-azuretable)
+ [pino-cloudwatch](#pino-cloudwatch)
+ [pino-couch](#pino-couch)
+ [pino-datadog](#pino-datadog)
+ [pino-gelf](#pino-gelf)
+ [pino-http-send](#pino-http-send)
+ [pino-kafka](#pino-kafka)
+ [pino-logdna](#pino-logdna)
+ [pino-logflare](#pino-logflare)
+ [pino-loki](#pino-loki)
+ [pino-mq](#pino-mq)
+ [pino-mysql](#pino-mysql)
+ [pino-papertrail](#pino-papertrail)
+ [pino-pg](#pino-pg)
+ [pino-redis](#pino-redis)
+ [pino-sentry](#pino-sentry)
+ [pino-seq](#pino-seq)
+ [pino-socket](#pino-socket)
+ [pino-stackdriver](#pino-stackdriver)
+ [pino-syslog](#pino-syslog)
+ [pino-websocket](#pino-websocket)
<a id="@logtail/pino"></a>
### @logtail/pino
The [@logtail/pino](https://www.npmjs.com/package/@logtail/pino) NPM package is a transport that forwards logs to [Logtail](https://logtail.com) by [Better Stack](https://betterstack.com).
[Quick start guide ⇗](https://betterstack.com/docs/logs/javascript/pino)
<a id="pino-applicationinsights"></a>
### pino-applicationinsights
The [pino-applicationinsights](https://www.npmjs.com/package/pino-applicationinsights) module is a transport that will forward logs to [Azure Application Insights](https://docs.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview).
Given an application `foo` that logs via pino, you would use `pino-applicationinsights` like so:
``` sh
$ node foo | pino-applicationinsights --key blablabla
```
For full documentation of command line switches read [README](https://github.com/ovhemert/pino-applicationinsights#readme)
<a id="pino-azuretable"></a>
### pino-azuretable
The [pino-azuretable](https://www.npmjs.com/package/pino-azuretable) module is a transport that will forward logs to the [Azure Table Storage](https://azure.microsoft.com/en-us/services/storage/tables/).
Given an application `foo` that logs via pino, you would use `pino-azuretable` like so:
``` sh
$ node foo | pino-azuretable --account storageaccount --key blablabla
```
For full documentation of command line switches read [README](https://github.com/ovhemert/pino-azuretable#readme)
<a id="pino-cloudwatch"></a>
### pino-cloudwatch
[pino-cloudwatch][pino-cloudwatch] is a transport that buffers and forwards logs to [Amazon CloudWatch][].
```sh
$ node app.js | pino-cloudwatch --group my-log-group
```
[pino-cloudwatch]: https://github.com/dbhowell/pino-cloudwatch
[Amazon CloudWatch]: https://aws.amazon.com/cloudwatch/
<a id="pino-couch"></a>
### pino-couch
[pino-couch][pino-couch] uploads each log line as a [CouchDB][CouchDB] document.
```sh
$ node app.js | pino-couch -U https://couch-server -d mylogs
```
[pino-couch]: https://github.com/IBM/pino-couch
[CouchDB]: https://couchdb.apache.org
<a id="pino-datadog"></a>
### pino-datadog
The [pino-datadog](https://www.npmjs.com/package/pino-datadog) module is a transport that will forward logs to [DataDog](https://www.datadoghq.com/) through its API.
Given an application `foo` that logs via pino, you would use `pino-datadog` like so:
``` sh
$ node foo | pino-datadog --key blablabla
```
For full documentation of command line switches read [README](https://github.com/ovhemert/pino-datadog#readme)
<a id="pino-elasticsearch"></a>
### pino-elasticsearch
[pino-elasticsearch][pino-elasticsearch] uploads the log lines in bulk
to [Elasticsearch][elasticsearch], to be displayed in [Kibana][kibana].
It is extremely simple to use and setup
```sh
$ node app.js | pino-elasticsearch
```
Assuming Elasticsearch is running on localhost.
To connect to an external Elasticsearch instance (recommended for production):
* Check that `network.host` is defined in the `elasticsearch.yml` configuration file. See [Elasticsearch Network Settings documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-network.html#common-network-settings) for more details.
* Launch:
```sh
$ node app.js | pino-elasticsearch --node http://192.168.1.42:9200
```
Assuming Elasticsearch is running on `192.168.1.42`.
To connect to AWS Elasticsearch:
```sh
$ node app.js | pino-elasticsearch --node https://es-url.us-east-1.es.amazonaws.com --es-version 6
```
Then [create an index pattern](https://www.elastic.co/guide/en/kibana/current/setup.html) on `'pino'` (the default index key for `pino-elasticsearch`) on the Kibana instance.
[pino-elasticsearch]: https://github.com/pinojs/pino-elasticsearch
[elasticsearch]: https://www.elastic.co/products/elasticsearch
[kibana]: https://www.elastic.co/products/kibana
<a id="pino-gelf"></a>
### pino-gelf
Pino GELF ([pino-gelf]) is a transport for the Pino logger. Pino GELF receives Pino logs from stdin and transforms them into [GELF format][gelf] before sending them to a remote [Graylog server][graylog] via UDP.
```sh
$ node your-app.js | pino-gelf log
```
[pino-gelf]: https://github.com/pinojs/pino-gelf
[gelf]: https://docs.graylog.org/en/2.1/pages/gelf.html
[graylog]: https://www.graylog.org/
<a id="pino-http-send"></a>
### pino-http-send
[pino-http-send](https://npmjs.com/package/pino-http-send) is a configurable and low overhead
transport that will batch logs and send to a specified URL.
```console
$ node app.js | pino-http-send -u http://localhost:8080/logs
```
<a id="pino-kafka"></a>
### pino-kafka
[pino-kafka](https://github.com/ayZagen/pino-kafka) transport to send logs to [Apache Kafka](https://kafka.apache.org/).
```sh
$ node index.js | pino-kafka -b 10.10.10.5:9200 -d mytopic
```
<a id="pino-logdna"></a>
### pino-logdna
[pino-logdna](https://github.com/logdna/pino-logdna) transport to send logs to [LogDNA](https://logdna.com).
```sh
$ node index.js | pino-logdna --key YOUR_INGESTION_KEY
```
Tags and other metadata can be included using the available command line options. See the [pino-logdna README](https://github.com/logdna/pino-logdna#options) for a full list.
<a id="pino-logflare"></a>
### pino-logflare
[pino-logflare](https://github.com/Logflare/pino-logflare) transport to send logs to a [Logflare](https://logflare.app) `source`.
```sh
$ node index.js | pino-logflare --key YOUR_KEY --source YOUR_SOURCE
```
<a id="pino-mq"></a>
### pino-mq
The `pino-mq` transport will take all messages received on `process.stdin` and send them over a message bus using JSON serialization.
This is useful for:
* moving backpressure from application to broker
* transforming messages pressure to another component
```
node app.js | pino-mq -u "amqp://guest:guest@localhost/" -q "pino-logs"
```
Alternatively, a configuration file can be used:
```
node app.js | pino-mq -c pino-mq.json
```
A base configuration file can be initialized with:
```
pino-mq -g
```
For full documentation of command line switches and configuration see [the `pino-mq` README](https://github.com/itavy/pino-mq#readme)
<a id="pino-loki"></a>
### pino-loki
pino-loki is a transport that will forwards logs into [Grafana Loki](https://grafana.com/oss/loki/).
Can be used in CLI version in a separate process or in a dedicated worker:
CLI :
```console
node app.js | pino-loki --hostname localhost:3100 --labels='{ "application": "my-application"}' --user my-username --password my-password
```
Worker :
```js
const pino = require('pino')
const transport = pino.transport({
target: 'pino-loki',
options: { host: 'localhost:3100' }
})
pino(transport)
```
For full documentation and configuration, see the [README](https://github.com/Julien-R44/pino-loki).
<a id="pino-papertrail"></a>
### pino-papertrail
pino-papertrail is a transport that will forward logs to the [papertrail](https://papertrailapp.com) log service through an UDPv4 socket.
Given an application `foo` that logs via pino, and a papertrail destination that collects logs on port UDP `12345` on address `bar.papertrailapp.com`, you would use `pino-papertrail`
like so:
```
node yourapp.js | pino-papertrail --host bar.papertrailapp.com --port 12345 --appname foo
```
for full documentation of command line switches read [README](https://github.com/ovhemert/pino-papertrail#readme)
<a id="pino-pg"></a>
### pino-pg
[pino-pg](https://www.npmjs.com/package/pino-pg) stores logs into PostgreSQL.
Full documentation in the [README](https://github.com/Xstoudi/pino-pg).
<a id="pino-mysql"></a>
### pino-mysql
[pino-mysql][pino-mysql] loads pino logs into [MySQL][MySQL] and [MariaDB][MariaDB].
```sh
$ node app.js | pino-mysql -c db-configuration.json
```
`pino-mysql` can extract and save log fields into corresponding database fields
and/or save the entire log stream as a [JSON Data Type][JSONDT].
For full documentation and command line switches read the [README][pino-mysql].
[pino-mysql]: https://www.npmjs.com/package/pino-mysql
[MySQL]: https://www.mysql.com/
[MariaDB]: https://mariadb.org/
[JSONDT]: https://dev.mysql.com/doc/refman/8.0/en/json.html
<a id="pino-redis"></a>
### pino-redis
[pino-redis][pino-redis] loads pino logs into [Redis][Redis].
```sh
$ node app.js | pino-redis -U redis://username:password@localhost:6379
```
[pino-redis]: https://github.com/buianhthang/pino-redis
[Redis]: https://redis.io/
<a id="pino-sentry"></a>
### pino-sentry
[pino-sentry][pino-sentry] loads pino logs into [Sentry][Sentry].
```sh
$ node app.js | pino-sentry --dsn=https://******@sentry.io/12345
```
For full documentation of command line switches see the [pino-sentry README](https://github.com/aandrewww/pino-sentry/blob/master/README.md).
[pino-sentry]: https://www.npmjs.com/package/pino-sentry
[Sentry]: https://sentry.io/
<a id="pino-seq"></a>
### pino-seq
[pino-seq][pino-seq] supports both out-of-process and in-process log forwarding to [Seq][Seq].
```sh
$ node app.js | pino-seq --serverUrl http://localhost:5341 --apiKey 1234567890 --property applicationName=MyNodeApp
```
[pino-seq]: https://www.npmjs.com/package/pino-seq
[Seq]: https://datalust.co/seq
<a id="pino-seq-transport"></a>
### pino-seq-transport
[pino-seq-transport][pino-seq-transport] is a Pino v7+ compatible transport to forward log events to [Seq][Seq]
from a dedicated worker:
```js
const pino = require('pino')
const transport = pino.transport({
target: '@autotelic/pino-seq-transport',
options: { serverUrl: 'http://localhost:5341' }
})
pino(transport)
```
[pino-seq-transport]: https://github.com/autotelic/pino-seq-transport
[Seq]: https://datalust.co/seq
<a id="pino-sentry-transport"></a>
### pino-sentry-transport
[pino-sentry-transport][pino-sentry-transport] is a Pino v7+ compatible transport to forward log events to [Sentry][Sentry]
from a dedicated worker:
```js
const pino = require('pino')
const transport = pino.transport({
target: 'pino-sentry-transport',
options: {
sentry: {
dsn: 'https://******@sentry.io/12345',
}
}
})
pino(transport)
```
[pino-sentry-transport]: https://github.com/tomer-yechiel/pino-sentry-transport
[Sentry]: https://sentry.io/
<a id="pino-airbrake-transport"></a>
### pino-airbrake-transport
[pino-airbrake-transport][pino-airbrake-transport] is a Pino v7+ compatible transport to forward log events to [Airbrake][Airbrake]
from a dedicated worker:
```js
const pino = require('pino')
const transport = pino.transport({
target: 'pino-airbrake-transport',
options: {
airbrake: {
projectId: 1,
projectKey: "REPLACE_ME",
environment: "production",
// additional options for airbrake
performanceStats: false,
},
},
level: "error", // minimum log level that should be sent to airbrake
})
pino(transport)
```
[pino-airbrake-transport]: https://github.com/enricodeleo/pino-airbrake-transport
[Airbrake]: https://airbrake.io/
<a id="pino-socket"></a>
### pino-socket
[pino-socket][pino-socket] is a transport that will forward logs to an IPv4
UDP or TCP socket.
As an example, use `socat` to fake a listener:
```sh
$ socat -v udp4-recvfrom:6000,fork exec:'/bin/cat'
```
Then run an application that uses `pino` for logging:
```sh
$ node app.js | pino-socket -p 6000
```
Logs from the application should be observed on both consoles.
[pino-socket]: https://www.npmjs.com/package/pino-socket
<a id="pino-datadog-transport"></a>
### pino-datadog-transport
[pino-datadog-transport][pino-datadog-transport] is a Pino v7+ compatible transport to forward log events to [Datadog][Datadog]
from a dedicated worker:
```js
const pino = require('pino')
const transport = pino.transport({
target: 'pino-datadog-transport',
options: {
ddClientConf: {
authMethods: {
apiKeyAuth: <your datadog API key>
}
},
},
level: "error", // minimum log level that should be sent to datadog
})
pino(transport)
```
[pino-datadog-transport]: https://github.com/theogravity/pino-datadog-transport
[Datadog]: https://www.datadoghq.com/
#### Logstash
The [pino-socket][pino-socket] module can also be used to upload logs to
[Logstash][logstash] via:
```
$ node app.js | pino-socket -a 127.0.0.1 -p 5000 -m tcp
```
Assuming logstash is running on the same host and configured as
follows:
```
input {
tcp {
port => 5000
}
}
filter {
json {
source => "message"
}
}
output {
elasticsearch {
hosts => "127.0.0.1:9200"
}
}
```
See <https://www.elastic.co/guide/en/kibana/current/setup.html> to learn
how to setup [Kibana][kibana].
For Docker users, see
https://github.com/deviantony/docker-elk to setup an ELK stack.
<a id="pino-stackdriver"></a>
### pino-stackdriver
The [pino-stackdriver](https://www.npmjs.com/package/pino-stackdriver) module is a transport that will forward logs to the [Google Stackdriver](https://cloud.google.com/logging/) log service through its API.
Given an application `foo` that logs via pino, a stackdriver log project `bar`, and credentials in the file `/credentials.json`, you would use `pino-stackdriver`
like so:
``` sh
$ node foo | pino-stackdriver --project bar --credentials /credentials.json
```
For full documentation of command line switches read [README](https://github.com/ovhemert/pino-stackdriver#readme)
<a id="pino-syslog"></a>
### pino-syslog
[pino-syslog][pino-syslog] is a transforming transport that converts
`pino` NDJSON logs to [RFC3164][rfc3164] compatible log messages. The `pino-syslog` module does not
forward the logs anywhere, it merely re-writes the messages to `stdout`. But
when used in combination with `pino-socket` the log messages can be relayed to a syslog server:
```sh
$ node app.js | pino-syslog | pino-socket -a syslog.example.com
```
Example output for the "hello world" log:
```
<134>Apr 1 16:44:58 MacBook-Pro-3 none[94473]: {"pid":94473,"hostname":"MacBook-Pro-3","level":30,"msg":"hello world","time":1459529098958}
```
[pino-syslog]: https://www.npmjs.com/package/pino-syslog
[rfc3164]: https://tools.ietf.org/html/rfc3164
[logstash]: https://www.elastic.co/products/logstash
<a id="pino-websocket"></a>
### pino-websocket
[pino-websocket](https://www.npmjs.com/package/@abeai/pino-websocket) is a transport that will forward each log line to a websocket server.
```sh
$ node app.js | pino-websocket -a my-websocket-server.example.com -p 3004
```
For full documentation of command line switches read the [README](https://github.com/abeai/pino-websocket#readme).
<a id="pino-slack-webhook"></a>
### pino-slack-webhook
[pino-slack-webhook][pino-slack-webhook] is a Pino v7+ compatible transport to forward log events to [Slack][Slack]
from a dedicated worker:
```js
const pino = require('pino')
const transport = pino.transport({
target: '@youngkiu/pino-slack-webhook',
options: {
webhookUrl: 'https://hooks.slack.com/services/xxx/xxx/xxx',
channel: '#pino-log',
username: 'webhookbot',
icon_emoji: ':ghost:'
}
})
pino(transport)
```
[pino-slack-webhook]: https://github.com/youngkiu/pino-slack-webhook
[Slack]: https://slack.com/
[pino-pretty]: https://github.com/pinojs/pino-pretty
For full documentation of command line switches read the [README](https://github.com/abeai/pino-websocket#readme).
<a id="pino-axiom"></a>
### pino-axiom
[pino-axiom](https://www.npmjs.com/package/pino-axiom) is a transport that will forward logs to [Axiom](https://axiom.co).
```javascript
const pino = require('pino')
const transport = pino.transport({
target: 'pino-axiom',
options: {
orgId: 'YOUR-ORG-ID',
token: 'YOUR-TOKEN',
dataset: 'YOUR-DATASET',
},
})
pino(transport)
```
<a id="pino-opentelemetry-transport"></a>
### pino-opentelemetry-transport
[pino-opentelemetry-transport](https://www.npmjs.com/package/pino-opentelemetry-transport) is a transport that will forward logs to an [OpenTelemetry log collector](https://opentelemetry.io/docs/collector/) using [OpenTelemetry JS instrumentation](https://opentelemetry.io/docs/instrumentation/js/).
```javascript
const pino = require('pino')
const transport = pino.transport({
target: 'pino-opentelemetry-transport',
options: {
resourceAttributes: {
'service.name': 'test-service',
'service.version': '1.0.0'
}
}
})
pino(transport)
```
Documentation on running a minimal example is available in the [README](https://github.com/Vunovati/pino-opentelemetry-transport#minimalistic-example).
<a id="@axiomhq/pino"></a>
### @axiomhq/pino
[@axiomhq/pino](https://www.npmjs.com/package/@axiomhq/pino) is the official [Axiom](https://axiom.co/) transport for Pino, using [axiom-js](https://github.com/axiomhq/axiom-js).
```javascript
import pino from 'pino';
const logger = pino(
{ level: 'info' },
pino.transport({
target: '@axiomhq/pino',
options: {
dataset: process.env.AXIOM_DATASET,
token: process.env.AXIOM_TOKEN,
},
}),
);
```
then you can use the logger as usual:
```js
logger.info('Hello from pino!');
```
For further examples, head over to the [examples](https://github.com/axiomhq/axiom-js/tree/main/examples/pino) directory.
<a id="pino-discord-webhook"></a>
### pino-discord-webhook
[pino-discord-webhook](https://github.com/fabulousgk/pino-discord-webhook) is a Pino v7+ compatible transport to forward log events to a [Discord](http://discord.com) webhook from a dedicated worker.
```js
import pino from 'pino'
const logger = pino({
transport: {
target: 'pino-discord-webhook',
options: {
webhookUrl: 'https://discord.com/api/webhooks/xxxx/xxxx',
}
}
})
```
<a id="pino-logfmt"></a>
### pino-logfmt
[pino-logfmt](https://github.com/botflux/pino-logfmt) is a Pino v7+ transport that formats logs into [logfmt](https://brandur.org/logfmt). This transport can output the formatted logs to stdout or file.
```js
import pino from 'pino'
const logger = pino({
transport: {
target: 'pino-logfmt'
}
})
```
<a id="communication-between-pino-and-transport"></a>
## Communication between Pino and Transports
Here we discuss some technical details of how Pino communicates with its [worker threads](https://nodejs.org/api/worker_threads.html).
Pino uses [`thread-stream`](https://github.com/pinojs/thread-stream) to create a stream for transports.
When we create a stream with `thread-stream`, `thread-stream` spawns a [worker](https://github.com/pinojs/thread-stream/blob/f19ac8dbd602837d2851e17fbc7dfc5bbc51083f/index.js#L50-L60) (an independent JavaScript execution thread).
### Error messages
How are error messages propagated from a transport worker to Pino?
Let's assume we have a transport with an error listener:
```js
// index.js
const transport = pino.transport({
target: './transport.js'
})
transport.on('error', err => {
console.error('error caught', err)
})
const log = pino(transport)
```
When our worker emits an error event, the worker has listeners for it: [error](https://github.com/pinojs/thread-stream/blob/f19ac8dbd602837d2851e17fbc7dfc5bbc51083f/lib/worker.js#L59-L70) and [unhandledRejection](https://github.com/pinojs/thread-stream/blob/f19ac8dbd602837d2851e17fbc7dfc5bbc51083f/lib/worker.js#L135-L141). These listeners send the error message to the main thread where Pino is present.
When Pino receives the error message, it further [emits](https://github.com/pinojs/thread-stream/blob/f19ac8dbd602837d2851e17fbc7dfc5bbc51083f/index.js#L349) the error message. Finally, the error message arrives at our `index.js` and is caught by our error listener.
+259
View File
@@ -0,0 +1,259 @@
# Web Frameworks
Since HTTP logging is a primary use case, Pino has first-class support for the Node.js
web framework ecosystem.
- [Web Frameworks](#web-frameworks)
- [Pino with Fastify](#pino-with-fastify)
- [Pino with Express](#pino-with-express)
- [Pino with Hapi](#pino-with-hapi)
- [Pino with Restify](#pino-with-restify)
- [Pino with Koa](#pino-with-koa)
- [Pino with Node core `http`](#pino-with-node-core-http)
- [Pino with Nest](#pino-with-nest)
- [Pino with H3](#pino-with-h3)
<a id="fastify"></a>
## Pino with Fastify
The Fastify web framework comes bundled with Pino by default, simply set Fastify's
`logger` option to `true` and use `request.log` or `reply.log` for log messages that correspond
to each request:
```js
const fastify = require('fastify')({
logger: true
})
fastify.get('/', async (request, reply) => {
request.log.info('something')
return { hello: 'world' }
})
fastify.listen({ port: 3000 }, (err) => {
if (err) {
fastify.log.error(err)
process.exit(1)
}
})
```
The `logger` option can also be set to an object, which will be passed through directly
as the [`pino` options object](/docs/api.md#options-object).
See the [fastify documentation](https://www.fastify.io/docs/latest/Reference/Logging/) for more information.
<a id="express"></a>
## Pino with Express
```sh
npm install pino-http
```
```js
const app = require('express')()
const pino = require('pino-http')()
app.use(pino)
app.get('/', function (req, res) {
req.log.info('something')
res.send('hello world')
})
app.listen(3000)
```
See the [pino-http README](https://npm.im/pino-http) for more info.
<a id="hapi"></a>
## Pino with Hapi
```sh
npm install hapi-pino
```
```js
'use strict'
const Hapi = require('@hapi/hapi')
const Pino = require('hapi-pino');
async function start () {
// Create a server with a host and port
const server = Hapi.server({
host: 'localhost',
port: 3000
})
// Add the route
server.route({
method: 'GET',
path: '/',
handler: async function (request, h) {
// request.log is HAPI's standard way of logging
request.log(['a', 'b'], 'Request into hello world')
// a pino instance can also be used, which will be faster
request.logger.info('In handler %s', request.path)
return 'hello world'
}
})
await server.register(Pino)
// also as a decorated API
server.logger.info('another way for accessing it')
// and through Hapi standard logging system
server.log(['subsystem'], 'third way for accessing it')
await server.start()
return server
}
start().catch((err) => {
console.log(err)
process.exit(1)
})
```
See the [hapi-pino README](https://npm.im/hapi-pino) for more info.
<a id="restify"></a>
## Pino with Restify
```sh
npm install restify-pino-logger
```
```js
const server = require('restify').createServer({name: 'server'})
const pino = require('restify-pino-logger')()
server.use(pino)
server.get('/', function (req, res) {
req.log.info('something')
res.send('hello world')
})
server.listen(3000)
```
See the [restify-pino-logger README](https://npm.im/restify-pino-logger) for more info.
<a id="koa"></a>
## Pino with Koa
```sh
npm install koa-pino-logger
```
```js
const Koa = require('koa')
const app = new Koa()
const pino = require('koa-pino-logger')()
app.use(pino)
app.use((ctx) => {
ctx.log.info('something else')
ctx.body = 'hello world'
})
app.listen(3000)
```
See the [koa-pino-logger README](https://github.com/pinojs/koa-pino-logger) for more info.
<a id="http"></a>
## Pino with Node core `http`
```sh
npm install pino-http
```
```js
const http = require('http')
const server = http.createServer(handle)
const logger = require('pino-http')()
function handle (req, res) {
logger(req, res)
req.log.info('something else')
res.end('hello world')
}
server.listen(3000)
```
See the [pino-http README](https://npm.im/pino-http) for more info.
<a id="nest"></a>
## Pino with Nest
```sh
npm install nestjs-pino
```
```ts
import { NestFactory } from '@nestjs/core'
import { Controller, Get, Module } from '@nestjs/common'
import { LoggerModule, Logger } from 'nestjs-pino'
@Controller()
export class AppController {
constructor(private readonly logger: Logger) {}
@Get()
getHello() {
this.logger.log('something')
return `Hello world`
}
}
@Module({
controllers: [AppController],
imports: [LoggerModule.forRoot()]
})
class MyModule {}
async function bootstrap() {
const app = await NestFactory.create(MyModule)
await app.listen(3000)
}
bootstrap()
```
See the [nestjs-pino README](https://npm.im/nestjs-pino) for more info.
<a id="h3"></a>
## Pino with H3
```sh
npm install pino-http
```
```js
import { createServer } from 'http'
import { createApp } from 'h3'
import pino from 'pino-http'
const app = createApp()
app.use(pino())
app.use('/', (req) => {
req.log.info('something')
return 'hello world'
})
createServer(app).listen(process.env.PORT || 3000)
```
See the [pino-http README](https://npm.im/pino-http) for more info.
+26
View File
@@ -0,0 +1,26 @@
* [Readme](/)
* [API](/docs/api.md)
* [Browser API](/docs/browser.md)
* [Redaction](/docs/redaction.md)
* [Child Loggers](/docs/child-loggers.md)
* [Transports](/docs/transports.md)
* [Web Frameworks](/docs/web.md)
* [Pretty Printing](/docs/pretty.md)
* [Asynchronous Logging](/docs/asynchronous.md)
* [Ecosystem](/docs/ecosystem.md)
* [Benchmarks](/docs/benchmarks.md)
* [Long Term Support](/docs/lts.md)
* [Help](/docs/help.md)
* [Log rotation](/docs/help.md#rotate)
* [Reopening log files](/docs/help.md#reopening)
* [Saving to multiple files](/docs/help.md#multiple)
* [Log filtering](/docs/help.md#filter-logs)
* [Transports and systemd](/docs/help.md#transport-systemd)
* [Duplicate keys](/docs/help.md#dupe-keys)
* [Log levels as labels instead of numbers](/docs/help.md#level-string)
* [Pino with `debug`](/docs/help.md#debug)
* [Unicode and Windows terminal](/docs/help.md#windows)
* [Mapping Pino Log Levels to Google Cloud Logging (Stackdriver) Severity Levels](/docs/help.md#stackdriver)
* [Avoid Message Conflict](/docs/help.md#avoid-message-conflict)
* [Best performance for logging to `stdout`](/docs/help.md#best-performance-for-stdout)
* [Testing](/docs/help.md#testing)
+43
View File
@@ -0,0 +1,43 @@
'use strict'
// Pino's primary usage writes ndjson to `stdout`:
const pino = require('..')()
// However, if "human readable" output is desired,
// `pino-pretty` can be provided as the destination
// stream by uncommenting the following line in place
// of the previous declaration:
// const pino = require('..')(require('pino-pretty')())
pino.info('hello world')
pino.error('this is at error level')
pino.info('the answer is %d', 42)
pino.info({ obj: 42 }, 'hello world')
pino.info({ obj: 42, b: 2 }, 'hello world')
pino.info({ nested: { obj: 42 } }, 'nested')
setImmediate(() => {
pino.info('after setImmediate')
})
pino.error(new Error('an error'))
const child = pino.child({ a: 'property' })
child.info('hello child!')
const childsChild = child.child({ another: 'property' })
childsChild.info('hello baby..')
pino.debug('this should be mute')
pino.level = 'trace'
pino.debug('this is a debug statement')
pino.child({ another: 'property' }).debug('this is a debug statement via child')
pino.trace('this is a trace statement')
pino.debug('this is a "debug" statement with "')
pino.info(new Error('kaboom'))
pino.info(null)
pino.info(new Error('kaboom'), 'with', 'a', 'message')
+68
View File
@@ -0,0 +1,68 @@
'use strict'
const pino = require('..')
const { tmpdir } = require('os')
const { join } = require('path')
const file = join(tmpdir(), `pino-${process.pid}-example`)
const transport = pino.transport({
targets: [{
level: 'warn',
target: 'pino/file',
options: {
destination: file
}
/*
}, {
level: 'info',
target: 'pino-elasticsearch',
options: {
node: 'http://localhost:9200'
}
*/
}, {
level: 'info',
target: 'pino-pretty'
}]
})
const logger = pino(transport)
logger.info({
file
}, 'logging destination')
logger.info('hello world')
logger.error('this is at error level')
logger.info('the answer is %d', 42)
logger.info({ obj: 42 }, 'hello world')
logger.info({ obj: 42, b: 2 }, 'hello world')
logger.info({ nested: { obj: 42 } }, 'nested')
logger.warn('WARNING!')
setImmediate(() => {
logger.info('after setImmediate')
})
logger.error(new Error('an error'))
const child = logger.child({ a: 'property' })
child.info('hello child!')
const childsChild = child.child({ another: 'property' })
childsChild.info('hello baby..')
logger.debug('this should be mute')
logger.level = 'trace'
logger.debug('this is a debug statement')
logger.child({ another: 'property' }).debug('this is a debug statement via child')
logger.trace('this is a trace statement')
logger.debug('this is a "debug" statement with "')
logger.info(new Error('kaboom'))
logger.info(null)
logger.info(new Error('kaboom'), 'with', 'a', 'message')
Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+12
View File
@@ -0,0 +1,12 @@
'use strict'
const pino = require('./pino')
const { once } = require('events')
module.exports = async function (opts = {}) {
const destOpts = Object.assign({}, opts, { dest: opts.destination || 1, sync: false })
delete destOpts.destination
const destination = pino.destination(destOpts)
await once(destination, 'ready')
return destination
}
+42
View File
@@ -0,0 +1,42 @@
#!/bin/bash
set -e
PATH=./node_modules/.bin:${PATH}
CURRENT_VERSION=$(jq -r .version package.json)
case ${1} in
Major | MAJOR | major)
LEVEL=major
;;
Minor | MINOR | minor)
LEVEL=minor
;;
Patch | PATCH | patch)
LEVEL=patch
;;
*)
LEVEL=patch
;;
esac
NEW_VERSION=$(semver -i ${LEVEL} ${CURRENT_VERSION})
echo "${CURRENT_VERSION} => ${NEW_VERSION}"
read -n 1 -s -r -p "Press any key to continue (ctrl+c to abort)..."
echo ""
echo "Patching package.json..."
cat package.json | \
jq --arg vers "${NEW_VERSION}" '.version = $vers' | \
tee package.json 1>/dev/null
echo "Patching lib/meta.js ..."
SED_SCRIPT=$(printf 's/%s/%s/' ${CURRENT_VERSION//\./\\.} ${NEW_VERSION//\./\\.})
cat ./lib/meta.js | \
sed -e ${SED_SCRIPT} | \
tee ./lib/meta.js 1>/dev/null
echo "Done."
+55
View File
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Pino - Super fast, all natural JSON logger for Node.js</title>
<meta name="description" content="Super fast, all natural JSON logger for Node.js">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<link rel="stylesheet" href="//unpkg.com/docsify-themeable/dist/css/theme-simple.css">
<style>
:root {
--base-font-size: 16px;
--theme-color: rgb(104, 118, 52);
--link-color: rgb(104, 118, 52);
--link-color--hover: rgb(137, 152, 100);
--sidebar-name-margin: 0;
--sidebar-name-padding: 0;
--code-font-size: .9em;
}
.sidebar > h1 {
margin-bottom: -.75em;
margin-top: .75em;
}
.sidebar > h1 img {
height: 4em;
}
.markdown-section a code {
color: var(--link-color)!important;
}
.markdown-section code:not([class*="lang-"]):not([class*="language-"]) {
white-space: unset
}
</style>
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
</head>
<body>
<div id="app"></div>
</body>
<script>
window.$docsify = {
name: 'pino',
logo: './pino-tree.png',
loadSidebar: 'docsify/sidebar.md',
repo: 'https://github.com/pinojs/pino',
auto2top: true,
ga: 'UA-103155139-1'
}
</script>
<script src="//unpkg.com/docsify/lib/docsify.min.js"></script>
<script src="//unpkg.com/docsify/lib/plugins/search.min.js"></script>
<script src="//unpkg.com/docsify/lib/plugins/ga.min.js"></script>
<!-- To enable syntax highlighting on TypeScript codes: -->
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-typescript.min.js"></script>
</html>
+30
View File
@@ -0,0 +1,30 @@
'use strict'
function noOpPrepareStackTrace (_, stack) {
return stack
}
module.exports = function getCallers () {
const originalPrepare = Error.prepareStackTrace
Error.prepareStackTrace = noOpPrepareStackTrace
const stack = new Error().stack
Error.prepareStackTrace = originalPrepare
if (!Array.isArray(stack)) {
return undefined
}
const entries = stack.slice(2)
const fileNames = []
for (const entry of entries) {
if (!entry) {
continue
}
fileNames.push(entry.getFileName())
}
return fileNames
}
+28
View File
@@ -0,0 +1,28 @@
/**
* Represents default log level values
*
* @enum {number}
*/
const DEFAULT_LEVELS = {
trace: 10,
debug: 20,
info: 30,
warn: 40,
error: 50,
fatal: 60
}
/**
* Represents sort order direction: `ascending` or `descending`
*
* @enum {string}
*/
const SORTING_ORDER = {
ASC: 'ASC',
DESC: 'DESC'
}
module.exports = {
DEFAULT_LEVELS,
SORTING_ORDER
}
+8
View File
@@ -0,0 +1,8 @@
'use strict'
const warning = require('process-warning')()
module.exports = warning
// const warnName = 'PinoWarning'
// warning.create(warnName, 'PINODEP010', 'A new deprecation')
+241
View File
@@ -0,0 +1,241 @@
'use strict'
/* eslint no-prototype-builtins: 0 */
const {
lsCacheSym,
levelValSym,
useOnlyCustomLevelsSym,
streamSym,
formattersSym,
hooksSym,
levelCompSym
} = require('./symbols')
const { noop, genLog } = require('./tools')
const { DEFAULT_LEVELS, SORTING_ORDER } = require('./constants')
const levelMethods = {
fatal: (hook) => {
const logFatal = genLog(DEFAULT_LEVELS.fatal, hook)
return function (...args) {
const stream = this[streamSym]
logFatal.call(this, ...args)
if (typeof stream.flushSync === 'function') {
try {
stream.flushSync()
} catch (e) {
// https://github.com/pinojs/pino/pull/740#discussion_r346788313
}
}
}
},
error: (hook) => genLog(DEFAULT_LEVELS.error, hook),
warn: (hook) => genLog(DEFAULT_LEVELS.warn, hook),
info: (hook) => genLog(DEFAULT_LEVELS.info, hook),
debug: (hook) => genLog(DEFAULT_LEVELS.debug, hook),
trace: (hook) => genLog(DEFAULT_LEVELS.trace, hook)
}
const nums = Object.keys(DEFAULT_LEVELS).reduce((o, k) => {
o[DEFAULT_LEVELS[k]] = k
return o
}, {})
const initialLsCache = Object.keys(nums).reduce((o, k) => {
o[k] = '{"level":' + Number(k)
return o
}, {})
function genLsCache (instance) {
const formatter = instance[formattersSym].level
const { labels } = instance.levels
const cache = {}
for (const label in labels) {
const level = formatter(labels[label], Number(label))
cache[label] = JSON.stringify(level).slice(0, -1)
}
instance[lsCacheSym] = cache
return instance
}
function isStandardLevel (level, useOnlyCustomLevels) {
if (useOnlyCustomLevels) {
return false
}
switch (level) {
case 'fatal':
case 'error':
case 'warn':
case 'info':
case 'debug':
case 'trace':
return true
default:
return false
}
}
function setLevel (level) {
const { labels, values } = this.levels
if (typeof level === 'number') {
if (labels[level] === undefined) throw Error('unknown level value' + level)
level = labels[level]
}
if (values[level] === undefined) throw Error('unknown level ' + level)
const preLevelVal = this[levelValSym]
const levelVal = this[levelValSym] = values[level]
const useOnlyCustomLevelsVal = this[useOnlyCustomLevelsSym]
const levelComparison = this[levelCompSym]
const hook = this[hooksSym].logMethod
for (const key in values) {
if (levelComparison(values[key], levelVal) === false) {
this[key] = noop
continue
}
this[key] = isStandardLevel(key, useOnlyCustomLevelsVal) ? levelMethods[key](hook) : genLog(values[key], hook)
}
this.emit(
'level-change',
level,
levelVal,
labels[preLevelVal],
preLevelVal,
this
)
}
function getLevel (level) {
const { levels, levelVal } = this
// protection against potential loss of Pino scope from serializers (edge case with circular refs - https://github.com/pinojs/pino/issues/833)
return (levels && levels.labels) ? levels.labels[levelVal] : ''
}
function isLevelEnabled (logLevel) {
const { values } = this.levels
const logLevelVal = values[logLevel]
return logLevelVal !== undefined && this[levelCompSym](logLevelVal, this[levelValSym])
}
/**
* Determine if the given `current` level is enabled by comparing it
* against the current threshold (`expected`).
*
* @param {SORTING_ORDER} direction comparison direction "ASC" or "DESC"
* @param {number} current current log level number representation
* @param {number} expected threshold value to compare with
* @returns {boolean}
*/
function compareLevel (direction, current, expected) {
if (direction === SORTING_ORDER.DESC) {
return current <= expected
}
return current >= expected
}
/**
* Create a level comparison function based on `levelComparison`
* it could a default function which compares levels either in "ascending" or "descending" order or custom comparison function
*
* @param {SORTING_ORDER | Function} levelComparison sort levels order direction or custom comparison function
* @returns Function
*/
function genLevelComparison (levelComparison) {
if (typeof levelComparison === 'string') {
return compareLevel.bind(null, levelComparison)
}
return levelComparison
}
function mappings (customLevels = null, useOnlyCustomLevels = false) {
const customNums = customLevels
/* eslint-disable */
? Object.keys(customLevels).reduce((o, k) => {
o[customLevels[k]] = k
return o
}, {})
: null
/* eslint-enable */
const labels = Object.assign(
Object.create(Object.prototype, { Infinity: { value: 'silent' } }),
useOnlyCustomLevels ? null : nums,
customNums
)
const values = Object.assign(
Object.create(Object.prototype, { silent: { value: Infinity } }),
useOnlyCustomLevels ? null : DEFAULT_LEVELS,
customLevels
)
return { labels, values }
}
function assertDefaultLevelFound (defaultLevel, customLevels, useOnlyCustomLevels) {
if (typeof defaultLevel === 'number') {
const values = [].concat(
Object.keys(customLevels || {}).map(key => customLevels[key]),
useOnlyCustomLevels ? [] : Object.keys(nums).map(level => +level),
Infinity
)
if (!values.includes(defaultLevel)) {
throw Error(`default level:${defaultLevel} must be included in custom levels`)
}
return
}
const labels = Object.assign(
Object.create(Object.prototype, { silent: { value: Infinity } }),
useOnlyCustomLevels ? null : DEFAULT_LEVELS,
customLevels
)
if (!(defaultLevel in labels)) {
throw Error(`default level:${defaultLevel} must be included in custom levels`)
}
}
function assertNoLevelCollisions (levels, customLevels) {
const { labels, values } = levels
for (const k in customLevels) {
if (k in values) {
throw Error('levels cannot be overridden')
}
if (customLevels[k] in labels) {
throw Error('pre-existing level values cannot be used for new levels')
}
}
}
/**
* Validates whether `levelComparison` is correct
*
* @throws Error
* @param {SORTING_ORDER | Function} levelComparison - value to validate
* @returns
*/
function assertLevelComparison (levelComparison) {
if (typeof levelComparison === 'function') {
return
}
if (typeof levelComparison === 'string' && Object.values(SORTING_ORDER).includes(levelComparison)) {
return
}
throw new Error('Levels comparison should be one of "ASC", "DESC" or "function" type')
}
module.exports = {
initialLsCache,
genLsCache,
levelMethods,
getLevel,
setLevel,
isLevelEnabled,
mappings,
assertNoLevelCollisions,
assertDefaultLevelFound,
genLevelComparison,
assertLevelComparison
}
+3
View File
@@ -0,0 +1,3 @@
'use strict'
module.exports = { version: '8.21.0' }
+188
View File
@@ -0,0 +1,188 @@
'use strict'
const metadata = Symbol.for('pino.metadata')
const { DEFAULT_LEVELS } = require('./constants')
const DEFAULT_INFO_LEVEL = DEFAULT_LEVELS.info
function multistream (streamsArray, opts) {
let counter = 0
streamsArray = streamsArray || []
opts = opts || { dedupe: false }
const streamLevels = Object.create(DEFAULT_LEVELS)
streamLevels.silent = Infinity
if (opts.levels && typeof opts.levels === 'object') {
Object.keys(opts.levels).forEach(i => {
streamLevels[i] = opts.levels[i]
})
}
const res = {
write,
add,
emit,
flushSync,
end,
minLevel: 0,
streams: [],
clone,
[metadata]: true,
streamLevels
}
if (Array.isArray(streamsArray)) {
streamsArray.forEach(add, res)
} else {
add.call(res, streamsArray)
}
// clean this object up
// or it will stay allocated forever
// as it is closed on the following closures
streamsArray = null
return res
// we can exit early because the streams are ordered by level
function write (data) {
let dest
const level = this.lastLevel
const { streams } = this
// for handling situation when several streams has the same level
let recordedLevel = 0
let stream
// if dedupe set to true we send logs to the stream with the highest level
// therefore, we have to change sorting order
for (let i = initLoopVar(streams.length, opts.dedupe); checkLoopVar(i, streams.length, opts.dedupe); i = adjustLoopVar(i, opts.dedupe)) {
dest = streams[i]
if (dest.level <= level) {
if (recordedLevel !== 0 && recordedLevel !== dest.level) {
break
}
stream = dest.stream
if (stream[metadata]) {
const { lastTime, lastMsg, lastObj, lastLogger } = this
stream.lastLevel = level
stream.lastTime = lastTime
stream.lastMsg = lastMsg
stream.lastObj = lastObj
stream.lastLogger = lastLogger
}
stream.write(data)
if (opts.dedupe) {
recordedLevel = dest.level
}
} else if (!opts.dedupe) {
break
}
}
}
function emit (...args) {
for (const { stream } of this.streams) {
if (typeof stream.emit === 'function') {
stream.emit(...args)
}
}
}
function flushSync () {
for (const { stream } of this.streams) {
if (typeof stream.flushSync === 'function') {
stream.flushSync()
}
}
}
function add (dest) {
if (!dest) {
return res
}
// Check that dest implements either StreamEntry or DestinationStream
const isStream = typeof dest.write === 'function' || dest.stream
const stream_ = dest.write ? dest : dest.stream
// This is necessary to provide a meaningful error message, otherwise it throws somewhere inside write()
if (!isStream) {
throw Error('stream object needs to implement either StreamEntry or DestinationStream interface')
}
const { streams, streamLevels } = this
let level
if (typeof dest.levelVal === 'number') {
level = dest.levelVal
} else if (typeof dest.level === 'string') {
level = streamLevels[dest.level]
} else if (typeof dest.level === 'number') {
level = dest.level
} else {
level = DEFAULT_INFO_LEVEL
}
const dest_ = {
stream: stream_,
level,
levelVal: undefined,
id: counter++
}
streams.unshift(dest_)
streams.sort(compareByLevel)
this.minLevel = streams[0].level
return res
}
function end () {
for (const { stream } of this.streams) {
if (typeof stream.flushSync === 'function') {
stream.flushSync()
}
stream.end()
}
}
function clone (level) {
const streams = new Array(this.streams.length)
for (let i = 0; i < streams.length; i++) {
streams[i] = {
level,
stream: this.streams[i].stream
}
}
return {
write,
add,
minLevel: level,
streams,
clone,
emit,
flushSync,
[metadata]: true
}
}
}
function compareByLevel (a, b) {
return a.level - b.level
}
function initLoopVar (length, dedupe) {
return dedupe ? length - 1 : 0
}
function adjustLoopVar (i, dedupe) {
return dedupe ? i - 1 : i + 1
}
function checkLoopVar (i, length, dedupe) {
return dedupe ? i >= 0 : i < length
}
module.exports = multistream
+232
View File
@@ -0,0 +1,232 @@
'use strict'
/* eslint no-prototype-builtins: 0 */
const { EventEmitter } = require('events')
const {
lsCacheSym,
levelValSym,
setLevelSym,
getLevelSym,
chindingsSym,
parsedChindingsSym,
mixinSym,
asJsonSym,
writeSym,
mixinMergeStrategySym,
timeSym,
timeSliceIndexSym,
streamSym,
serializersSym,
formattersSym,
errorKeySym,
messageKeySym,
useOnlyCustomLevelsSym,
needsMetadataGsym,
redactFmtSym,
stringifySym,
formatOptsSym,
stringifiersSym,
msgPrefixSym
} = require('./symbols')
const {
getLevel,
setLevel,
isLevelEnabled,
mappings,
initialLsCache,
genLsCache,
assertNoLevelCollisions
} = require('./levels')
const {
asChindings,
asJson,
buildFormatters,
stringify
} = require('./tools')
const {
version
} = require('./meta')
const redaction = require('./redaction')
// note: use of class is satirical
// https://github.com/pinojs/pino/pull/433#pullrequestreview-127703127
const constructor = class Pino {}
const prototype = {
constructor,
child,
bindings,
setBindings,
flush,
isLevelEnabled,
version,
get level () { return this[getLevelSym]() },
set level (lvl) { this[setLevelSym](lvl) },
get levelVal () { return this[levelValSym] },
set levelVal (n) { throw Error('levelVal is read-only') },
[lsCacheSym]: initialLsCache,
[writeSym]: write,
[asJsonSym]: asJson,
[getLevelSym]: getLevel,
[setLevelSym]: setLevel
}
Object.setPrototypeOf(prototype, EventEmitter.prototype)
// exporting and consuming the prototype object using factory pattern fixes scoping issues with getters when serializing
module.exports = function () {
return Object.create(prototype)
}
const resetChildingsFormatter = bindings => bindings
function child (bindings, options) {
if (!bindings) {
throw Error('missing bindings for child Pino')
}
options = options || {} // default options to empty object
const serializers = this[serializersSym]
const formatters = this[formattersSym]
const instance = Object.create(this)
if (options.hasOwnProperty('serializers') === true) {
instance[serializersSym] = Object.create(null)
for (const k in serializers) {
instance[serializersSym][k] = serializers[k]
}
const parentSymbols = Object.getOwnPropertySymbols(serializers)
/* eslint no-var: off */
for (var i = 0; i < parentSymbols.length; i++) {
const ks = parentSymbols[i]
instance[serializersSym][ks] = serializers[ks]
}
for (const bk in options.serializers) {
instance[serializersSym][bk] = options.serializers[bk]
}
const bindingsSymbols = Object.getOwnPropertySymbols(options.serializers)
for (var bi = 0; bi < bindingsSymbols.length; bi++) {
const bks = bindingsSymbols[bi]
instance[serializersSym][bks] = options.serializers[bks]
}
} else instance[serializersSym] = serializers
if (options.hasOwnProperty('formatters')) {
const { level, bindings: chindings, log } = options.formatters
instance[formattersSym] = buildFormatters(
level || formatters.level,
chindings || resetChildingsFormatter,
log || formatters.log
)
} else {
instance[formattersSym] = buildFormatters(
formatters.level,
resetChildingsFormatter,
formatters.log
)
}
if (options.hasOwnProperty('customLevels') === true) {
assertNoLevelCollisions(this.levels, options.customLevels)
instance.levels = mappings(options.customLevels, instance[useOnlyCustomLevelsSym])
genLsCache(instance)
}
// redact must place before asChindings and only replace if exist
if ((typeof options.redact === 'object' && options.redact !== null) || Array.isArray(options.redact)) {
instance.redact = options.redact // replace redact directly
const stringifiers = redaction(instance.redact, stringify)
const formatOpts = { stringify: stringifiers[redactFmtSym] }
instance[stringifySym] = stringify
instance[stringifiersSym] = stringifiers
instance[formatOptsSym] = formatOpts
}
if (typeof options.msgPrefix === 'string') {
instance[msgPrefixSym] = (this[msgPrefixSym] || '') + options.msgPrefix
}
instance[chindingsSym] = asChindings(instance, bindings)
const childLevel = options.level || this.level
instance[setLevelSym](childLevel)
this.onChild(instance)
return instance
}
function bindings () {
const chindings = this[chindingsSym]
const chindingsJson = `{${chindings.substr(1)}}` // at least contains ,"pid":7068,"hostname":"myMac"
const bindingsFromJson = JSON.parse(chindingsJson)
delete bindingsFromJson.pid
delete bindingsFromJson.hostname
return bindingsFromJson
}
function setBindings (newBindings) {
const chindings = asChindings(this, newBindings)
this[chindingsSym] = chindings
delete this[parsedChindingsSym]
}
/**
* Default strategy for creating `mergeObject` from arguments and the result from `mixin()`.
* Fields from `mergeObject` have higher priority in this strategy.
*
* @param {Object} mergeObject The object a user has supplied to the logging function.
* @param {Object} mixinObject The result of the `mixin` method.
* @return {Object}
*/
function defaultMixinMergeStrategy (mergeObject, mixinObject) {
return Object.assign(mixinObject, mergeObject)
}
function write (_obj, msg, num) {
const t = this[timeSym]()
const mixin = this[mixinSym]
const errorKey = this[errorKeySym]
const messageKey = this[messageKeySym]
const mixinMergeStrategy = this[mixinMergeStrategySym] || defaultMixinMergeStrategy
let obj
if (_obj === undefined || _obj === null) {
obj = {}
} else if (_obj instanceof Error) {
obj = { [errorKey]: _obj }
if (msg === undefined) {
msg = _obj.message
}
} else {
obj = _obj
if (msg === undefined && _obj[messageKey] === undefined && _obj[errorKey]) {
msg = _obj[errorKey].message
}
}
if (mixin) {
obj = mixinMergeStrategy(obj, mixin(obj, num, this))
}
const s = this[asJsonSym](obj, msg, num, t)
const stream = this[streamSym]
if (stream[needsMetadataGsym] === true) {
stream.lastLevel = num
stream.lastObj = obj
stream.lastMsg = msg
stream.lastTime = t.slice(this[timeSliceIndexSym])
stream.lastLogger = this // for child loggers
}
stream.write(s)
}
function noop () {}
function flush (cb) {
if (cb != null && typeof cb !== 'function') {
throw Error('callback must be a function')
}
const stream = this[streamSym]
if (typeof stream.flush === 'function') {
stream.flush(cb || noop)
} else if (cb) cb()
}
+118
View File
@@ -0,0 +1,118 @@
'use strict'
const fastRedact = require('fast-redact')
const { redactFmtSym, wildcardFirstSym } = require('./symbols')
const { rx, validator } = fastRedact
const validate = validator({
ERR_PATHS_MUST_BE_STRINGS: () => 'pino redacted paths must be strings',
ERR_INVALID_PATH: (s) => `pino redact paths array contains an invalid path (${s})`
})
const CENSOR = '[Redacted]'
const strict = false // TODO should this be configurable?
function redaction (opts, serialize) {
const { paths, censor } = handle(opts)
const shape = paths.reduce((o, str) => {
rx.lastIndex = 0
const first = rx.exec(str)
const next = rx.exec(str)
// ns is the top-level path segment, brackets + quoting removed.
let ns = first[1] !== undefined
? first[1].replace(/^(?:"|'|`)(.*)(?:"|'|`)$/, '$1')
: first[0]
if (ns === '*') {
ns = wildcardFirstSym
}
// top level key:
if (next === null) {
o[ns] = null
return o
}
// path with at least two segments:
// if ns is already redacted at the top level, ignore lower level redactions
if (o[ns] === null) {
return o
}
const { index } = next
const nextPath = `${str.substr(index, str.length - 1)}`
o[ns] = o[ns] || []
// shape is a mix of paths beginning with literal values and wildcard
// paths [ "a.b.c", "*.b.z" ] should reduce to a shape of
// { "a": [ "b.c", "b.z" ], *: [ "b.z" ] }
// note: "b.z" is in both "a" and * arrays because "a" matches the wildcard.
// (* entry has wildcardFirstSym as key)
if (ns !== wildcardFirstSym && o[ns].length === 0) {
// first time ns's get all '*' redactions so far
o[ns].push(...(o[wildcardFirstSym] || []))
}
if (ns === wildcardFirstSym) {
// new * path gets added to all previously registered literal ns's.
Object.keys(o).forEach(function (k) {
if (o[k]) {
o[k].push(nextPath)
}
})
}
o[ns].push(nextPath)
return o
}, {})
// the redactor assigned to the format symbol key
// provides top level redaction for instances where
// an object is interpolated into the msg string
const result = {
[redactFmtSym]: fastRedact({ paths, censor, serialize, strict })
}
const topCensor = (...args) => {
return typeof censor === 'function' ? serialize(censor(...args)) : serialize(censor)
}
return [...Object.keys(shape), ...Object.getOwnPropertySymbols(shape)].reduce((o, k) => {
// top level key:
if (shape[k] === null) {
o[k] = (value) => topCensor(value, [k])
} else {
const wrappedCensor = typeof censor === 'function'
? (value, path) => {
return censor(value, [k, ...path])
}
: censor
o[k] = fastRedact({
paths: shape[k],
censor: wrappedCensor,
serialize,
strict
})
}
return o
}, result)
}
function handle (opts) {
if (Array.isArray(opts)) {
opts = { paths: opts, censor: CENSOR }
validate(opts)
return opts
}
let { paths, censor = CENSOR, remove } = opts
if (Array.isArray(paths) === false) { throw Error('pino redact must contain an array of strings') }
if (remove === true) censor = undefined
validate({ paths, censor })
return { paths, censor }
}
module.exports = redaction
+74
View File
@@ -0,0 +1,74 @@
'use strict'
const setLevelSym = Symbol('pino.setLevel')
const getLevelSym = Symbol('pino.getLevel')
const levelValSym = Symbol('pino.levelVal')
const levelCompSym = Symbol('pino.levelComp')
const useLevelLabelsSym = Symbol('pino.useLevelLabels')
const useOnlyCustomLevelsSym = Symbol('pino.useOnlyCustomLevels')
const mixinSym = Symbol('pino.mixin')
const lsCacheSym = Symbol('pino.lsCache')
const chindingsSym = Symbol('pino.chindings')
const asJsonSym = Symbol('pino.asJson')
const writeSym = Symbol('pino.write')
const redactFmtSym = Symbol('pino.redactFmt')
const timeSym = Symbol('pino.time')
const timeSliceIndexSym = Symbol('pino.timeSliceIndex')
const streamSym = Symbol('pino.stream')
const stringifySym = Symbol('pino.stringify')
const stringifySafeSym = Symbol('pino.stringifySafe')
const stringifiersSym = Symbol('pino.stringifiers')
const endSym = Symbol('pino.end')
const formatOptsSym = Symbol('pino.formatOpts')
const messageKeySym = Symbol('pino.messageKey')
const errorKeySym = Symbol('pino.errorKey')
const nestedKeySym = Symbol('pino.nestedKey')
const nestedKeyStrSym = Symbol('pino.nestedKeyStr')
const mixinMergeStrategySym = Symbol('pino.mixinMergeStrategy')
const msgPrefixSym = Symbol('pino.msgPrefix')
const wildcardFirstSym = Symbol('pino.wildcardFirst')
// public symbols, no need to use the same pino
// version for these
const serializersSym = Symbol.for('pino.serializers')
const formattersSym = Symbol.for('pino.formatters')
const hooksSym = Symbol.for('pino.hooks')
const needsMetadataGsym = Symbol.for('pino.metadata')
module.exports = {
setLevelSym,
getLevelSym,
levelValSym,
levelCompSym,
useLevelLabelsSym,
mixinSym,
lsCacheSym,
chindingsSym,
asJsonSym,
writeSym,
serializersSym,
redactFmtSym,
timeSym,
timeSliceIndexSym,
streamSym,
stringifySym,
stringifySafeSym,
stringifiersSym,
endSym,
formatOptsSym,
messageKeySym,
errorKeySym,
nestedKeySym,
wildcardFirstSym,
needsMetadataGsym,
useOnlyCustomLevelsSym,
formattersSym,
hooksSym,
nestedKeyStrSym,
mixinMergeStrategySym,
msgPrefixSym
}
+11
View File
@@ -0,0 +1,11 @@
'use strict'
const nullTime = () => ''
const epochTime = () => `,"time":${Date.now()}`
const unixTime = () => `,"time":${Math.round(Date.now() / 1000.0)}`
const isoTime = () => `,"time":"${new Date(Date.now()).toISOString()}"` // using Date.now() for testability
module.exports = { nullTime, epochTime, unixTime, isoTime }
+394
View File
@@ -0,0 +1,394 @@
'use strict'
/* eslint no-prototype-builtins: 0 */
const format = require('quick-format-unescaped')
const { mapHttpRequest, mapHttpResponse } = require('pino-std-serializers')
const SonicBoom = require('sonic-boom')
const onExit = require('on-exit-leak-free')
const {
lsCacheSym,
chindingsSym,
writeSym,
serializersSym,
formatOptsSym,
endSym,
stringifiersSym,
stringifySym,
stringifySafeSym,
wildcardFirstSym,
nestedKeySym,
formattersSym,
messageKeySym,
errorKeySym,
nestedKeyStrSym,
msgPrefixSym
} = require('./symbols')
const { isMainThread } = require('worker_threads')
const transport = require('./transport')
function noop () {
}
function genLog (level, hook) {
if (!hook) return LOG
return function hookWrappedLog (...args) {
hook.call(this, args, LOG, level)
}
function LOG (o, ...n) {
if (typeof o === 'object') {
let msg = o
if (o !== null) {
if (o.method && o.headers && o.socket) {
o = mapHttpRequest(o)
} else if (typeof o.setHeader === 'function') {
o = mapHttpResponse(o)
}
}
let formatParams
if (msg === null && n.length === 0) {
formatParams = [null]
} else {
msg = n.shift()
formatParams = n
}
// We do not use a coercive check for `msg` as it is
// measurably slower than the explicit checks.
if (typeof this[msgPrefixSym] === 'string' && msg !== undefined && msg !== null) {
msg = this[msgPrefixSym] + msg
}
this[writeSym](o, format(msg, formatParams, this[formatOptsSym]), level)
} else {
let msg = o === undefined ? n.shift() : o
// We do not use a coercive check for `msg` as it is
// measurably slower than the explicit checks.
if (typeof this[msgPrefixSym] === 'string' && msg !== undefined && msg !== null) {
msg = this[msgPrefixSym] + msg
}
this[writeSym](null, format(msg, n, this[formatOptsSym]), level)
}
}
}
// magically escape strings for json
// relying on their charCodeAt
// everything below 32 needs JSON.stringify()
// 34 and 92 happens all the time, so we
// have a fast case for them
function asString (str) {
let result = ''
let last = 0
let found = false
let point = 255
const l = str.length
if (l > 100) {
return JSON.stringify(str)
}
for (var i = 0; i < l && point >= 32; i++) {
point = str.charCodeAt(i)
if (point === 34 || point === 92) {
result += str.slice(last, i) + '\\'
last = i
found = true
}
}
if (!found) {
result = str
} else {
result += str.slice(last)
}
return point < 32 ? JSON.stringify(str) : '"' + result + '"'
}
function asJson (obj, msg, num, time) {
const stringify = this[stringifySym]
const stringifySafe = this[stringifySafeSym]
const stringifiers = this[stringifiersSym]
const end = this[endSym]
const chindings = this[chindingsSym]
const serializers = this[serializersSym]
const formatters = this[formattersSym]
const messageKey = this[messageKeySym]
const errorKey = this[errorKeySym]
let data = this[lsCacheSym][num] + time
// we need the child bindings added to the output first so instance logged
// objects can take precedence when JSON.parse-ing the resulting log line
data = data + chindings
let value
if (formatters.log) {
obj = formatters.log(obj)
}
const wildcardStringifier = stringifiers[wildcardFirstSym]
let propStr = ''
for (const key in obj) {
value = obj[key]
if (Object.prototype.hasOwnProperty.call(obj, key) && value !== undefined) {
if (serializers[key]) {
value = serializers[key](value)
} else if (key === errorKey && serializers.err) {
value = serializers.err(value)
}
const stringifier = stringifiers[key] || wildcardStringifier
switch (typeof value) {
case 'undefined':
case 'function':
continue
case 'number':
/* eslint no-fallthrough: "off" */
if (Number.isFinite(value) === false) {
value = null
}
// this case explicitly falls through to the next one
case 'boolean':
if (stringifier) value = stringifier(value)
break
case 'string':
value = (stringifier || asString)(value)
break
default:
value = (stringifier || stringify)(value, stringifySafe)
}
if (value === undefined) continue
const strKey = asString(key)
propStr += ',' + strKey + ':' + value
}
}
let msgStr = ''
if (msg !== undefined) {
value = serializers[messageKey] ? serializers[messageKey](msg) : msg
const stringifier = stringifiers[messageKey] || wildcardStringifier
switch (typeof value) {
case 'function':
break
case 'number':
/* eslint no-fallthrough: "off" */
if (Number.isFinite(value) === false) {
value = null
}
// this case explicitly falls through to the next one
case 'boolean':
if (stringifier) value = stringifier(value)
msgStr = ',"' + messageKey + '":' + value
break
case 'string':
value = (stringifier || asString)(value)
msgStr = ',"' + messageKey + '":' + value
break
default:
value = (stringifier || stringify)(value, stringifySafe)
msgStr = ',"' + messageKey + '":' + value
}
}
if (this[nestedKeySym] && propStr) {
// place all the obj properties under the specified key
// the nested key is already formatted from the constructor
return data + this[nestedKeyStrSym] + propStr.slice(1) + '}' + msgStr + end
} else {
return data + propStr + msgStr + end
}
}
function asChindings (instance, bindings) {
let value
let data = instance[chindingsSym]
const stringify = instance[stringifySym]
const stringifySafe = instance[stringifySafeSym]
const stringifiers = instance[stringifiersSym]
const wildcardStringifier = stringifiers[wildcardFirstSym]
const serializers = instance[serializersSym]
const formatter = instance[formattersSym].bindings
bindings = formatter(bindings)
for (const key in bindings) {
value = bindings[key]
const valid = key !== 'level' &&
key !== 'serializers' &&
key !== 'formatters' &&
key !== 'customLevels' &&
bindings.hasOwnProperty(key) &&
value !== undefined
if (valid === true) {
value = serializers[key] ? serializers[key](value) : value
value = (stringifiers[key] || wildcardStringifier || stringify)(value, stringifySafe)
if (value === undefined) continue
data += ',"' + key + '":' + value
}
}
return data
}
function hasBeenTampered (stream) {
return stream.write !== stream.constructor.prototype.write
}
const hasNodeCodeCoverage = process.env.NODE_V8_COVERAGE || process.env.V8_COVERAGE
function buildSafeSonicBoom (opts) {
const stream = new SonicBoom(opts)
stream.on('error', filterBrokenPipe)
// If we are sync: false, we must flush on exit
// We must disable this if there is node code coverage due to
// https://github.com/nodejs/node/issues/49344#issuecomment-1741776308.
if (!hasNodeCodeCoverage && !opts.sync && isMainThread) {
onExit.register(stream, autoEnd)
stream.on('close', function () {
onExit.unregister(stream)
})
}
return stream
function filterBrokenPipe (err) {
// Impossible to replicate across all operating systems
/* istanbul ignore next */
if (err.code === 'EPIPE') {
// If we get EPIPE, we should stop logging here
// however we have no control to the consumer of
// SonicBoom, so we just overwrite the write method
stream.write = noop
stream.end = noop
stream.flushSync = noop
stream.destroy = noop
return
}
stream.removeListener('error', filterBrokenPipe)
stream.emit('error', err)
}
}
function autoEnd (stream, eventName) {
// This check is needed only on some platforms
/* istanbul ignore next */
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 {
// For some reason istanbul is not detecting this, but it's there
/* istanbul ignore next */
// We do not have an event loop, so flush synchronously
stream.flushSync()
}
}
function createArgsNormalizer (defaultOptions) {
return function normalizeArgs (instance, caller, opts = {}, stream) {
// support stream as a string
if (typeof opts === 'string') {
stream = buildSafeSonicBoom({ dest: opts })
opts = {}
} else if (typeof stream === 'string') {
if (opts && opts.transport) {
throw Error('only one of option.transport or stream can be specified')
}
stream = buildSafeSonicBoom({ dest: stream })
} else if (opts instanceof SonicBoom || opts.writable || opts._writableState) {
stream = opts
opts = {}
} else if (opts.transport) {
if (opts.transport instanceof SonicBoom || opts.transport.writable || opts.transport._writableState) {
throw Error('option.transport do not allow stream, please pass to option directly. e.g. pino(transport)')
}
if (opts.transport.targets && opts.transport.targets.length && opts.formatters && typeof opts.formatters.level === 'function') {
throw Error('option.transport.targets do not allow custom level formatters')
}
let customLevels
if (opts.customLevels) {
customLevels = opts.useOnlyCustomLevels ? opts.customLevels : Object.assign({}, opts.levels, opts.customLevels)
}
stream = transport({ caller, ...opts.transport, levels: customLevels })
}
opts = Object.assign({}, defaultOptions, opts)
opts.serializers = Object.assign({}, defaultOptions.serializers, opts.serializers)
opts.formatters = Object.assign({}, defaultOptions.formatters, opts.formatters)
if (opts.prettyPrint) {
throw new Error('prettyPrint option is no longer supported, see the pino-pretty package (https://github.com/pinojs/pino-pretty)')
}
const { enabled, onChild } = opts
if (enabled === false) opts.level = 'silent'
if (!onChild) opts.onChild = noop
if (!stream) {
if (!hasBeenTampered(process.stdout)) {
// If process.stdout.fd is undefined, it means that we are running
// in a worker thread. Let's assume we are logging to file descriptor 1.
stream = buildSafeSonicBoom({ fd: process.stdout.fd || 1 })
} else {
stream = process.stdout
}
}
return { opts, stream }
}
}
function stringify (obj, stringifySafeFn) {
try {
return JSON.stringify(obj)
} catch (_) {
try {
const stringify = stringifySafeFn || this[stringifySafeSym]
return stringify(obj)
} catch (_) {
return '"[unable to serialize, circular reference is too complex to analyze]"'
}
}
}
function buildFormatters (level, bindings, log) {
return {
level,
bindings,
log
}
}
/**
* Convert a string integer file descriptor to a proper native integer
* file descriptor.
*
* @param {string} destination The file descriptor string to attempt to convert.
*
* @returns {Number}
*/
function normalizeDestFileDescriptor (destination) {
const fd = Number(destination)
if (typeof destination === 'string' && Number.isFinite(fd)) {
return fd
}
// destination could be undefined if we are in a worker
if (destination === undefined) {
// This is stdout in UNIX systems
return 1
}
return destination
}
module.exports = {
noop,
buildSafeSonicBoom,
asChindings,
asJson,
genLog,
createArgsNormalizer,
stringify,
buildFormatters,
normalizeDestFileDescriptor
}
+50
View File
@@ -0,0 +1,50 @@
'use strict'
const { realImport, realRequire } = require('real-require')
module.exports = loadTransportStreamBuilder
/**
* Loads & returns a function to build transport streams
* @param {string} target
* @returns {function(object): Promise<import('stream').Writable>}
* @throws {Error} In case the target module does not export a function
*/
async function loadTransportStreamBuilder (target) {
let fn
try {
const toLoad = 'file://' + target
if (toLoad.endsWith('.ts') || toLoad.endsWith('.cts')) {
// TODO: add support for the TSM modules loader ( https://github.com/lukeed/tsm ).
if (process[Symbol.for('ts-node.register.instance')]) {
realRequire('ts-node/register')
} else if (process.env && process.env.TS_NODE_DEV) {
realRequire('ts-node-dev')
}
// TODO: Support ES imports once tsc, tap & ts-node provide better compatibility guarantees.
fn = realRequire(decodeURIComponent(target))
} else {
fn = (await realImport(toLoad))
}
} catch (error) {
// See this PR for details: https://github.com/pinojs/thread-stream/pull/34
if ((error.code === 'ENOTDIR' || error.code === 'ERR_MODULE_NOT_FOUND')) {
fn = realRequire(target)
} else if (error.code === undefined) {
// When bundled with pkg, an undefined error is thrown when called with realImport
fn = realRequire(decodeURIComponent(target))
} else {
throw error
}
}
// Depending on how the default export is performed, and on how the code is
// transpiled, we may find cases of two nested "default" objects.
// See https://github.com/pinojs/pino/issues/1243#issuecomment-982774762
if (typeof fn === 'object') fn = fn.default
if (typeof fn === 'object') fn = fn.default
if (typeof fn !== 'function') throw Error('exported worker is not a function')
return fn
}
+153
View File
@@ -0,0 +1,153 @@
'use strict'
const { createRequire } = require('module')
const getCallers = require('./caller')
const { join, isAbsolute, sep } = require('path')
const sleep = require('atomic-sleep')
const onExit = require('on-exit-leak-free')
const ThreadStream = require('thread-stream')
function setupOnExit (stream) {
// This is leak free, it does not leave event handlers
onExit.register(stream, autoEnd)
onExit.registerBeforeExit(stream, flush)
stream.on('close', function () {
onExit.unregister(stream)
})
}
function buildStream (filename, workerData, workerOpts) {
const stream = new ThreadStream({
filename,
workerData,
workerOpts
})
stream.on('ready', onReady)
stream.on('close', function () {
process.removeListener('exit', onExit)
})
process.on('exit', onExit)
function onReady () {
process.removeListener('exit', onExit)
stream.unref()
if (workerOpts.autoEnd !== false) {
setupOnExit(stream)
}
}
function onExit () {
/* istanbul ignore next */
if (stream.closed) {
return
}
stream.flushSync()
// Apparently there is a very sporadic race condition
// that in certain OS would prevent the messages to be flushed
// because the thread might not have been created still.
// Unfortunately we need to sleep(100) in this case.
sleep(100)
stream.end()
}
return stream
}
function autoEnd (stream) {
stream.ref()
stream.flushSync()
stream.end()
stream.once('close', function () {
stream.unref()
})
}
function flush (stream) {
stream.flushSync()
}
function transport (fullOptions) {
const { pipeline, targets, levels, dedupe, options = {}, worker = {}, caller = getCallers() } = fullOptions
// Backwards compatibility
const callers = typeof caller === 'string' ? [caller] : caller
// This will be eventually modified by bundlers
const bundlerOverrides = '__bundlerPathsOverrides' in globalThis ? globalThis.__bundlerPathsOverrides : {}
let target = fullOptions.target
if (target && targets) {
throw new Error('only one of target or targets can be specified')
}
if (targets) {
target = bundlerOverrides['pino-worker'] || join(__dirname, 'worker.js')
options.targets = targets.map((dest) => {
return {
...dest,
target: fixTarget(dest.target)
}
})
} else if (pipeline) {
target = bundlerOverrides['pino-pipeline-worker'] || join(__dirname, 'worker-pipeline.js')
options.targets = pipeline.map((dest) => {
return {
...dest,
target: fixTarget(dest.target)
}
})
}
if (levels) {
options.levels = levels
}
if (dedupe) {
options.dedupe = dedupe
}
options.pinoWillSendConfig = true
return buildStream(fixTarget(target), options, worker)
function fixTarget (origin) {
origin = bundlerOverrides[origin] || origin
if (isAbsolute(origin) || origin.indexOf('file://') === 0) {
return origin
}
if (origin === 'pino/file') {
return join(__dirname, '..', 'file.js')
}
let fixTarget
for (const filePath of callers) {
try {
const context = filePath === 'node:repl'
? process.cwd() + sep
: filePath
fixTarget = createRequire(context).resolve(origin)
break
} catch (err) {
// Silent catch
continue
}
}
if (!fixTarget) {
throw new Error(`unable to determine transport target for "${origin}"`)
}
return fixTarget
}
}
module.exports = transport
+38
View File
@@ -0,0 +1,38 @@
'use strict'
const EE = require('events')
const loadTransportStreamBuilder = require('./transport-stream')
const { pipeline, PassThrough } = require('stream')
// This file is not checked by the code coverage tool,
// as it is not reliable.
/* istanbul ignore file */
module.exports = async function ({ targets }) {
const streams = await Promise.all(targets.map(async (t) => {
const fn = await loadTransportStreamBuilder(t.target)
const stream = await fn(t.options)
return stream
}))
const ee = new EE()
const stream = new PassThrough({
autoDestroy: true,
destroy (_, cb) {
ee.on('error', cb)
ee.on('closed', cb)
}
})
pipeline(stream, ...streams, function (err) {
if (err && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') {
ee.emit('error', err)
return
}
ee.emit('closed')
})
return stream
}
+54
View File
@@ -0,0 +1,54 @@
'use strict'
const pino = require('../pino.js')
const build = require('pino-abstract-transport')
const loadTransportStreamBuilder = require('./transport-stream')
// This file is not checked by the code coverage tool,
// as it is not reliable.
/* istanbul ignore file */
module.exports = async function ({ targets, levels, dedupe }) {
targets = await Promise.all(targets.map(async (t) => {
const fn = await loadTransportStreamBuilder(t.target)
const stream = await fn(t.options)
return {
level: t.level,
stream
}
}))
return build(process, {
parse: 'lines',
metadata: true,
close (err, cb) {
let expected = 0
for (const transport of targets) {
expected++
transport.stream.on('close', closeCb)
transport.stream.end()
}
function closeCb () {
if (--expected === 0) {
cb(err)
}
}
}
})
function process (stream) {
const multi = pino.multistream(targets, { levels, dedupe })
// TODO manage backpressure
stream.on('data', function (chunk) {
const { lastTime, lastMsg, lastObj, lastLevel } = this
multi.lastLevel = lastLevel
multi.lastTime = lastTime
multi.lastMsg = lastMsg
multi.lastObj = lastObj
// TODO handle backpressure
multi.write(chunk + '\n')
})
}
}
+119
View File
@@ -0,0 +1,119 @@
{
"name": "pino",
"version": "8.21.0",
"description": "super fast, all natural json logger",
"main": "pino.js",
"type": "commonjs",
"types": "pino.d.ts",
"browser": "./browser.js",
"scripts": {
"docs": "docsify serve",
"browser-test": "airtap --local 8080 test/browser*test.js",
"lint": "eslint .",
"prepublishOnly": "tap --no-check-coverage test/internals/version.test.js",
"test": "npm run lint && npm run transpile && tap --ts && jest test/jest && npm run test-types",
"test-ci": "npm run lint && npm run transpile && tap --ts --no-check-coverage --coverage-report=lcovonly && npm run test-types",
"test-ci-pnpm": "pnpm run lint && npm run transpile && tap --ts --no-coverage --no-check-coverage && pnpm run test-types",
"test-ci-yarn-pnp": "yarn run lint && npm run transpile && tap --ts --no-check-coverage --coverage-report=lcovonly",
"test-types": "tsc && tsd && ts-node test/types/pino.ts",
"test:smoke": "smoker smoke:pino && smoker smoke:browser && smoker smoke:file",
"smoke:pino": "node ./pino.js",
"smoke:browser": "node ./browser.js",
"smoke:file": "node ./file.js",
"transpile": "node ./test/fixtures/ts/transpile.cjs",
"cov-ui": "tap --ts --coverage-report=html",
"bench": "node benchmarks/utils/runbench all",
"bench-basic": "node benchmarks/utils/runbench basic",
"bench-object": "node benchmarks/utils/runbench object",
"bench-deep-object": "node benchmarks/utils/runbench deep-object",
"bench-multi-arg": "node benchmarks/utils/runbench multi-arg",
"bench-longs-tring": "node benchmarks/utils/runbench long-string",
"bench-child": "node benchmarks/utils/runbench child",
"bench-child-child": "node benchmarks/utils/runbench child-child",
"bench-child-creation": "node benchmarks/utils/runbench child-creation",
"bench-formatters": "node benchmarks/utils/runbench formatters",
"update-bench-doc": "node benchmarks/utils/generate-benchmark-doc > docs/benchmarks.md"
},
"bin": {
"pino": "./bin.js"
},
"precommit": "test",
"repository": {
"type": "git",
"url": "git+https://github.com/pinojs/pino.git"
},
"keywords": [
"fast",
"logger",
"stream",
"json"
],
"author": "Matteo Collina <hello@matteocollina.com>",
"contributors": [
"David Mark Clements <huperekchuno@googlemail.com>",
"James Sumners <james.sumners@gmail.com>",
"Thomas Watson Steen <w@tson.dk> (https://twitter.com/wa7son)"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/pinojs/pino/issues"
},
"homepage": "https://getpino.io",
"devDependencies": {
"@types/flush-write-stream": "^1.0.0",
"@types/node": "^20.2.3",
"@types/tap": "^15.0.6",
"airtap": "4.0.4",
"benchmark": "^2.1.4",
"bole": "^5.0.5",
"bunyan": "^1.8.14",
"debug": "^4.3.4",
"docsify-cli": "^4.4.4",
"eslint": "^8.17.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "15.7.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.0.0",
"execa": "^5.0.0",
"fastbench": "^1.0.1",
"flush-write-stream": "^2.0.0",
"import-fresh": "^3.2.1",
"jest": "^29.0.3",
"log": "^6.0.0",
"loglevel": "^1.6.7",
"midnight-smoker": "1.1.1",
"pino-pretty": "^10.2.1",
"pre-commit": "^1.2.2",
"proxyquire": "^2.1.3",
"pump": "^3.0.0",
"rimraf": "^5.0.1",
"semver": "^7.3.7",
"split2": "^4.0.0",
"steed": "^1.1.3",
"strip-ansi": "^6.0.0",
"tap": "^16.2.0",
"tape": "^5.5.3",
"through2": "^4.0.0",
"ts-node": "^10.9.1",
"tsd": "^0.30.4",
"typescript": "^5.1.3",
"winston": "^3.7.2"
},
"dependencies": {
"atomic-sleep": "^1.0.0",
"fast-redact": "^3.1.1",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^1.2.0",
"pino-std-serializers": "^6.0.0",
"process-warning": "^3.0.0",
"quick-format-unescaped": "^4.0.3",
"real-require": "^0.2.0",
"safe-stable-stringify": "^2.3.1",
"sonic-boom": "^3.7.0",
"thread-stream": "^2.6.0"
},
"tsd": {
"directory": "test/types"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+863
View File
@@ -0,0 +1,863 @@
// Project: https://github.com/pinojs/pino.git, http://getpino.io
// Definitions by: Peter Snider <https://github.com/psnider>
// BendingBender <https://github.com/BendingBender>
// Christian Rackerseder <https://github.com/screendriver>
// GP <https://github.com/paambaati>
// Alex Ferrando <https://github.com/alferpal>
// Oleksandr Sidko <https://github.com/mortiy>
// Harris Lummis <https://github.com/lummish>
// Raoul Jaeckel <https://github.com/raoulus>
// Cory Donkin <https://github.com/Cooryd>
// Adam Vigneaux <https://github.com/AdamVig>
// Austin Beer <https://github.com/austin-beer>
// Michel Nemnom <https://github.com/Pegase745>
// Igor Savin <https://github.com/kibertoad>
// James Bromwell <https://github.com/thw0rted>
// TypeScript Version: 4.4
import type { EventEmitter } from "events";
import * as pinoStdSerializers from "pino-std-serializers";
import type { SonicBoom, SonicBoomOpts } from "sonic-boom";
import type { WorkerOptions } from "worker_threads";
//// Non-exported types and interfaces
// ToDo https://github.com/pinojs/thread-stream/issues/24
type ThreadStream = any
type TimeFn = () => string;
type MixinFn = (mergeObject: object, level: number) => object;
type MixinMergeStrategyFn = (mergeObject: object, mixinObject: object) => object;
type CustomLevelLogger<CustomLevels extends string> = { [level in CustomLevels]: LogFn }
/**
* A synchronous callback that will run on each creation of a new child.
* @param child: The newly created child logger instance.
*/
type OnChildCallback<CustomLevels extends string = never> = (child: pino.Logger<CustomLevels>) => void
export interface redactOptions {
paths: string[];
censor?: string | ((value: any, path: string[]) => any);
remove?: boolean;
}
export interface LoggerExtras<CustomLevels extends string = never> extends EventEmitter {
/**
* Exposes the Pino package version. Also available on the exported pino function.
*/
readonly version: string;
levels: pino.LevelMapping;
/**
* Outputs the level as a string instead of integer.
*/
useLevelLabels: boolean;
/**
* Define additional logging levels.
*/
customLevels: { [level in CustomLevels]: number };
/**
* Use only defined `customLevels` and omit Pino's levels.
*/
useOnlyCustomLevels: boolean;
/**
* Returns the integer value for the logger instance's logging level.
*/
levelVal: number;
/**
* Creates a child logger, setting all key-value pairs in `bindings` as properties in the log lines. All serializers will be applied to the given pair.
* Child loggers use the same output stream as the parent and inherit the current log level of the parent at the time they are spawned.
* From v2.x.x the log level of a child is mutable (whereas in v1.x.x it was immutable), and can be set independently of the parent.
* If a `level` property is present in the object passed to `child` it will override the child logger level.
*
* @param bindings: an object of key-value pairs to include in log lines as properties.
* @param options: an options object that will override child logger inherited options.
* @returns a child logger instance.
*/
child<ChildCustomLevels extends string = never>(bindings: pino.Bindings, options?: ChildLoggerOptions<ChildCustomLevels>): pino.Logger<CustomLevels | ChildCustomLevels>;
/**
* This can be used to modify the callback function on creation of a new child.
*/
onChild: OnChildCallback<CustomLevels>;
/**
* Registers a listener function that is triggered when the level is changed.
* Note: When browserified, this functionality will only be available if the `events` module has been required elsewhere
* (e.g. if you're using streams in the browser). This allows for a trade-off between bundle size and functionality.
*
* @param event: only ever fires the `'level-change'` event
* @param listener: The listener is passed four arguments: `levelLabel`, `levelValue`, `previousLevelLabel`, `previousLevelValue`.
*/
on(event: "level-change", listener: pino.LevelChangeEventListener<CustomLevels>): this;
addListener(event: "level-change", listener: pino.LevelChangeEventListener<CustomLevels>): this;
once(event: "level-change", listener: pino.LevelChangeEventListener<CustomLevels>): this;
prependListener(event: "level-change", listener: pino.LevelChangeEventListener<CustomLevels>): this;
prependOnceListener(event: "level-change", listener: pino.LevelChangeEventListener<CustomLevels>): this;
removeListener(event: "level-change", listener: pino.LevelChangeEventListener<CustomLevels>): this;
/**
* A utility method for determining if a given log level will write to the destination.
*/
isLevelEnabled(level: pino.LevelWithSilentOrString): boolean;
/**
* Returns an object containing all the current bindings, cloned from the ones passed in via logger.child().
*/
bindings(): pino.Bindings;
/**
* Adds to the bindings of this logger instance.
* Note: Does not overwrite bindings. Can potentially result in duplicate keys in log lines.
*
* @param bindings: an object of key-value pairs to include in log lines as properties.
*/
setBindings(bindings: pino.Bindings): void;
/**
* Flushes the content of the buffer when using pino.destination({ sync: false }).
* call the callback when finished
*/
flush(cb?: (err?: Error) => void): void;
}
declare namespace pino {
//// Exported types and interfaces
interface BaseLogger {
/**
* Set this property to the desired logging level. In order of priority, available levels are:
*
* - 'fatal'
* - 'error'
* - 'warn'
* - 'info'
* - 'debug'
* - 'trace'
*
* The logging level is a __minimum__ level. For instance if `logger.level` is `'info'` then all `'fatal'`, `'error'`, `'warn'`,
* and `'info'` logs will be enabled.
*
* You can pass `'silent'` to disable logging.
*/
level: pino.LevelWithSilentOrString;
/**
* Log at `'fatal'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line.
* If more args follows `msg`, these will be used to format `msg` using `util.format`.
*
* @typeParam T: the interface of the object being serialized. Default is object.
* @param obj: object to be serialized
* @param msg: the log message to write
* @param ...args: format string values when `msg` is a format string
*/
fatal: pino.LogFn;
/**
* Log at `'error'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line.
* If more args follows `msg`, these will be used to format `msg` using `util.format`.
*
* @typeParam T: the interface of the object being serialized. Default is object.
* @param obj: object to be serialized
* @param msg: the log message to write
* @param ...args: format string values when `msg` is a format string
*/
error: pino.LogFn;
/**
* Log at `'warn'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line.
* If more args follows `msg`, these will be used to format `msg` using `util.format`.
*
* @typeParam T: the interface of the object being serialized. Default is object.
* @param obj: object to be serialized
* @param msg: the log message to write
* @param ...args: format string values when `msg` is a format string
*/
warn: pino.LogFn;
/**
* Log at `'info'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line.
* If more args follows `msg`, these will be used to format `msg` using `util.format`.
*
* @typeParam T: the interface of the object being serialized. Default is object.
* @param obj: object to be serialized
* @param msg: the log message to write
* @param ...args: format string values when `msg` is a format string
*/
info: pino.LogFn;
/**
* Log at `'debug'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line.
* If more args follows `msg`, these will be used to format `msg` using `util.format`.
*
* @typeParam T: the interface of the object being serialized. Default is object.
* @param obj: object to be serialized
* @param msg: the log message to write
* @param ...args: format string values when `msg` is a format string
*/
debug: pino.LogFn;
/**
* Log at `'trace'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line.
* If more args follows `msg`, these will be used to format `msg` using `util.format`.
*
* @typeParam T: the interface of the object being serialized. Default is object.
* @param obj: object to be serialized
* @param msg: the log message to write
* @param ...args: format string values when `msg` is a format string
*/
trace: pino.LogFn;
/**
* Noop function.
*/
silent: pino.LogFn;
}
type Bindings = Record<string, any>;
type Level = "fatal" | "error" | "warn" | "info" | "debug" | "trace";
type LevelOrString = Level | (string & {});
type LevelWithSilent = pino.Level | "silent";
type LevelWithSilentOrString = LevelWithSilent | (string & {});
type SerializerFn = (value: any) => any;
type WriteFn = (o: object) => void;
type LevelChangeEventListener<CustomLevels extends string = never> = (
lvl: LevelWithSilentOrString,
val: number,
prevLvl: LevelWithSilentOrString,
prevVal: number,
logger: Logger<CustomLevels>
) => void;
type LogDescriptor = Record<string, any>;
type Logger<CustomLevels extends string = never> = BaseLogger & LoggerExtras<CustomLevels> & CustomLevelLogger<CustomLevels>;
type SerializedError = pinoStdSerializers.SerializedError;
type SerializedResponse = pinoStdSerializers.SerializedResponse;
type SerializedRequest = pinoStdSerializers.SerializedRequest;
interface TransportTargetOptions<TransportOptions = Record<string, any>> {
target: string
options?: TransportOptions
level?: LevelWithSilentOrString
}
interface TransportBaseOptions<TransportOptions = Record<string, any>> {
options?: TransportOptions
worker?: WorkerOptions & { autoEnd?: boolean}
}
interface TransportSingleOptions<TransportOptions = Record<string, any>> extends TransportBaseOptions<TransportOptions>{
target: string
}
interface TransportPipelineOptions<TransportOptions = Record<string, any>> extends TransportBaseOptions<TransportOptions>{
pipeline: TransportSingleOptions<TransportOptions>[]
}
interface TransportMultiOptions<TransportOptions = Record<string, any>> extends TransportBaseOptions<TransportOptions>{
targets: readonly TransportTargetOptions<TransportOptions>[],
levels?: Record<string, number>
dedupe?: boolean
}
interface MultiStreamOptions {
levels?: Record<string, number>
dedupe?: boolean
}
interface DestinationStream {
write(msg: string): void;
}
interface DestinationStreamHasMetadata {
[symbols.needsMetadataGsym]: true;
lastLevel: number;
lastTime: string;
lastMsg: string;
lastObj: object;
lastLogger: pino.Logger;
}
type DestinationStreamWithMetadata = DestinationStream & ({ [symbols.needsMetadataGsym]?: false } | DestinationStreamHasMetadata);
interface StreamEntry<TLevel = Level> {
stream: DestinationStream
level?: TLevel
}
interface MultiStreamRes<TOriginLevel = Level> {
write: (data: any) => void,
add: <TLevel = Level>(dest: StreamEntry<TLevel> | DestinationStream) => MultiStreamRes<TOriginLevel & TLevel>,
flushSync: () => void,
minLevel: number,
streams: StreamEntry<TOriginLevel>[],
clone<TLevel = Level>(level: TLevel): MultiStreamRes<TLevel>,
}
interface LevelMapping {
/**
* Returns the mappings of level names to their respective internal number representation.
*/
values: { [level: string]: number };
/**
* Returns the mappings of level internal level numbers to their string representations.
*/
labels: { [level: number]: string };
}
interface LogFn {
// TODO: why is this different from `obj: object` or `obj: any`?
/* tslint:disable:no-unnecessary-generics */
<T extends object>(obj: T, msg?: string, ...args: any[]): void;
(obj: unknown, msg?: string, ...args: any[]): void;
(msg: string, ...args: any[]): void;
}
interface LoggerOptions<CustomLevels extends string = never> {
transport?: TransportSingleOptions | TransportMultiOptions | TransportPipelineOptions
/**
* Avoid error causes by circular references in the object tree. Default: `true`.
*/
safe?: boolean;
/**
* The name of the logger. Default: `undefined`.
*/
name?: string;
/**
* an object containing functions for custom serialization of objects.
* These functions should return an JSONifiable object and they should never throw. When logging an object,
* each top-level property matching the exact key of a serializer will be serialized using the defined serializer.
*/
serializers?: { [key: string]: SerializerFn };
/**
* Enables or disables the inclusion of a timestamp in the log message. If a function is supplied, it must
* synchronously return a JSON string representation of the time. If set to `false`, no timestamp will be included in the output.
* See stdTimeFunctions for a set of available functions for passing in as a value for this option.
* Caution: any sort of formatted time will significantly slow down Pino's performance.
*/
timestamp?: TimeFn | boolean;
/**
* One of the supported levels or `silent` to disable logging. Any other value defines a custom level and
* requires supplying a level value via `levelVal`. Default: 'info'.
*/
level?: LevelWithSilentOrString;
/**
* Use this option to define additional logging levels.
* The keys of the object correspond the namespace of the log level, and the values should be the numerical value of the level.
*/
customLevels?: { [level in CustomLevels]: number };
/**
* Use this option to define custom comparison of log levels.
* Useful to compare custom log levels or non-standard level values.
* Default: "ASC"
*/
levelComparison?: "ASC" | "DESC" | ((current: number, expected: number) => boolean);
/**
* Use this option to only use defined `customLevels` and omit Pino's levels.
* Logger's default `level` must be changed to a value in `customLevels` in order to use `useOnlyCustomLevels`
* Warning: this option may not be supported by downstream transports.
*/
useOnlyCustomLevels?: boolean;
/**
* If provided, the `mixin` function is called each time one of the active logging methods
* is called. The function must synchronously return an object. The properties of the
* returned object will be added to the logged JSON.
*/
mixin?: MixinFn;
/**
* If provided, the `mixinMergeStrategy` function is called each time one of the active
* logging methods is called. The first parameter is the value `mergeObject` or an empty object,
* the second parameter is the value resulting from `mixin()` or an empty object.
* The function must synchronously return an object.
*/
mixinMergeStrategy?: MixinMergeStrategyFn
/**
* As an array, the redact option specifies paths that should have their values redacted from any log output.
*
* Each path must be a string using a syntax which corresponds to JavaScript dot and bracket notation.
*
* If an object is supplied, three options can be specified:
*
* paths (String[]): Required. An array of paths
* censor (String): Optional. A value to overwrite key which are to be redacted. Default: '[Redacted]'
* remove (Boolean): Optional. Instead of censoring the value, remove both the key and the value. Default: false
*/
redact?: string[] | redactOptions;
/**
* When defining a custom log level via level, set to an integer value to define the new level. Default: `undefined`.
*/
levelVal?: number;
/**
* The string key for the 'message' in the JSON object. Default: "msg".
*/
messageKey?: string;
/**
* The string key for the 'error' in the JSON object. Default: "err".
*/
errorKey?: string;
/**
* The string key to place any logged object under.
*/
nestedKey?: string;
/**
* Enables logging. Default: `true`.
*/
enabled?: boolean;
/**
* Browser only, see http://getpino.io/#/docs/browser.
*/
browser?: {
/**
* The `asObject` option will create a pino-like log object instead of passing all arguments to a console
* method. When `write` is set, `asObject` will always be true.
*
* @example
* pino.info('hi') // creates and logs {msg: 'hi', level: 30, time: <ts>}
*/
asObject?: boolean;
/**
* Instead of passing log messages to `console.log` they can be passed to a supplied function. If `write` is
* set to a single function, all logging objects are passed to this function. If `write` is an object, it
* can have methods that correspond to the levels. When a message is logged at a given level, the
* corresponding method is called. If a method isn't present, the logging falls back to using the `console`.
*
* @example
* const pino = require('pino')({
* browser: {
* write: (o) => {
* // do something with o
* }
* }
* })
*
* @example
* const pino = require('pino')({
* browser: {
* write: {
* info: function (o) {
* //process info log object
* },
* error: function (o) {
* //process error log object
* }
* }
* }
* })
*/
write?:
| WriteFn
| ({
fatal?: WriteFn;
error?: WriteFn;
warn?: WriteFn;
info?: WriteFn;
debug?: WriteFn;
trace?: WriteFn;
} & { [logLevel: string]: WriteFn });
/**
* The serializers provided to `pino` are ignored by default in the browser, including the standard
* serializers provided with Pino. Since the default destination for log messages is the console, values
* such as `Error` objects are enhanced for inspection, which they otherwise wouldn't be if the Error
* serializer was enabled. We can turn all serializers on or we can selectively enable them via an array.
*
* When `serialize` is `true` the standard error serializer is also enabled (see
* {@link https://github.com/pinojs/pino/blob/master/docs/api.md#pino-stdserializers}). This is a global
* serializer which will apply to any `Error` objects passed to the logger methods.
*
* If `serialize` is an array the standard error serializer is also automatically enabled, it can be
* explicitly disabled by including a string in the serialize array: `!stdSerializers.err` (see example).
*
* The `serialize` array also applies to any child logger serializers (see
* {@link https://github.com/pinojs/pino/blob/master/docs/api.md#bindingsserializers-object} for how to
* set child-bound serializers).
*
* Unlike server pino the serializers apply to every object passed to the logger method, if the `asObject`
* option is `true`, this results in the serializers applying to the first object (as in server pino).
*
* For more info on serializers see
* {@link https://github.com/pinojs/pino/blob/master/docs/api.md#serializers-object}.
*
* @example
* const pino = require('pino')({
* browser: {
* serialize: true
* }
* })
*
* @example
* const pino = require('pino')({
* serializers: {
* custom: myCustomSerializer,
* another: anotherSerializer
* },
* browser: {
* serialize: ['custom']
* }
* })
* // following will apply myCustomSerializer to the custom property,
* // but will not apply anotherSerializer to another key
* pino.info({custom: 'a', another: 'b'})
*
* @example
* const pino = require('pino')({
* serializers: {
* custom: myCustomSerializer,
* another: anotherSerializer
* },
* browser: {
* serialize: ['!stdSerializers.err', 'custom'] //will not serialize Errors, will serialize `custom` keys
* }
* })
*/
serialize?: boolean | string[];
/**
* Options for transmission of logs.
*
* @example
* const pino = require('pino')({
* browser: {
* transmit: {
* level: 'warn',
* send: function (level, logEvent) {
* if (level === 'warn') {
* // maybe send the logEvent to a separate endpoint
* // or maybe analyse the messages further before sending
* }
* // we could also use the `logEvent.level.value` property to determine
* // numerical value
* if (logEvent.level.value >= 50) { // covers error and fatal
*
* // send the logEvent somewhere
* }
* }
* }
* }
* })
*/
transmit?: {
/**
* Specifies the minimum level (inclusive) of when the `send` function should be called, if not supplied
* the `send` function will be called based on the main logging `level` (set via `options.level`,
* defaulting to `info`).
*/
level?: LevelOrString;
/**
* Remotely record log messages.
*
* @description Called after writing the log message.
*/
send: (level: Level, logEvent: LogEvent) => void;
};
/**
* The disabled option will disable logging in browser if set to true, by default it is set to false.
*
* @example
* const pino = require('pino')({browser: {disabled: true}})
*/
disabled?: boolean;
};
/**
* key-value object added as child logger to each log line. If set to null the base child logger is not added
*/
base?: { [key: string]: any } | null;
/**
* An object containing functions for formatting the shape of the log lines.
* These functions should return a JSONifiable object and should never throw.
* These functions allow for full customization of the resulting log lines.
* For example, they can be used to change the level key name or to enrich the default metadata.
*/
formatters?: {
/**
* Changes the shape of the log level.
* The default shape is { level: number }.
* The function takes two arguments, the label of the level (e.g. 'info') and the numeric value (e.g. 30).
*/
level?: (label: string, number: number) => object;
/**
* Changes the shape of the bindings.
* The default shape is { pid, hostname }.
* The function takes a single argument, the bindings object.
* It will be called every time a child logger is created.
*/
bindings?: (bindings: Bindings) => object;
/**
* Changes the shape of the log object.
* This function will be called every time one of the log methods (such as .info) is called.
* All arguments passed to the log method, except the message, will be pass to this function.
* By default it does not change the shape of the log object.
*/
log?: (object: Record<string, unknown>) => Record<string, unknown>;
};
/**
* A string that would be prefixed to every message (and child message)
*/
msgPrefix?: string
/**
* An object mapping to hook functions. Hook functions allow for customizing internal logger operations.
* Hook functions must be synchronous functions.
*/
hooks?: {
/**
* Allows for manipulating the parameters passed to logger methods. The signature for this hook is
* logMethod (args, method, level) {}, where args is an array of the arguments that were passed to the
* log method and method is the log method itself, and level is the log level. This hook must invoke the method function by
* using apply, like so: method.apply(this, newArgumentsArray).
*/
logMethod?: (this: Logger, args: Parameters<LogFn>, method: LogFn, level: number) => void;
};
/**
* Stringification limit at a specific nesting depth when logging circular object. Default: `5`.
*/
depthLimit?: number
/**
* Stringification limit of properties/elements when logging a specific object/array with circular references. Default: `100`.
*/
edgeLimit?: number
/**
* Optional child creation callback.
*/
onChild?: OnChildCallback<CustomLevels>;
/**
* logs newline delimited JSON with `\r\n` instead of `\n`. Default: `false`.
*/
crlf?: boolean;
}
interface ChildLoggerOptions<CustomLevels extends string = never> {
level?: LevelOrString;
serializers?: { [key: string]: SerializerFn };
customLevels?: { [level in CustomLevels]: number };
formatters?: {
level?: (label: string, number: number) => object;
bindings?: (bindings: Bindings) => object;
log?: (object: object) => object;
};
redact?: string[] | redactOptions;
msgPrefix?: string
}
/**
* A data structure representing a log message, it represents the arguments passed to a logger statement, the level
* at which they were logged and the hierarchy of child bindings.
*
* @description By default serializers are not applied to log output in the browser, but they will always be applied
* to `messages` and `bindings` in the `logEvent` object. This allows us to ensure a consistent format for all
* values between server and client.
*/
interface LogEvent {
/**
* Unix epoch timestamp in milliseconds, the time is taken from the moment the logger method is called.
*/
ts: number;
/**
* All arguments passed to logger method, (for instance `logger.info('a', 'b', 'c')` would result in `messages`
* array `['a', 'b', 'c']`).
*/
messages: any[];
/**
* Represents each child logger (if any), and the relevant bindings.
*
* @description For instance, given `logger.child({a: 1}).child({b: 2}).info({c: 3})`, the bindings array would
* hold `[{a: 1}, {b: 2}]` and the `messages` array would be `[{c: 3}]`. The `bindings` are ordered according to
* their position in the child logger hierarchy, with the lowest index being the top of the hierarchy.
*/
bindings: Bindings[];
/**
* Holds the `label` (for instance `info`), and the corresponding numerical `value` (for instance `30`).
* This could be important in cases where client side level values and labels differ from server side.
*/
level: {
label: string;
value: number;
};
}
//// Top level variable (const) exports
/**
* Provides functions for serializing objects common to many projects.
*/
export const stdSerializers: typeof pinoStdSerializers;
/**
* Holds the current log format version (as output in the v property of each log record).
*/
export const levels: LevelMapping;
export const symbols: {
readonly setLevelSym: unique symbol;
readonly getLevelSym: unique symbol;
readonly levelValSym: unique symbol;
readonly useLevelLabelsSym: unique symbol;
readonly mixinSym: unique symbol;
readonly lsCacheSym: unique symbol;
readonly chindingsSym: unique symbol;
readonly parsedChindingsSym: unique symbol;
readonly asJsonSym: unique symbol;
readonly writeSym: unique symbol;
readonly serializersSym: unique symbol;
readonly redactFmtSym: unique symbol;
readonly timeSym: unique symbol;
readonly timeSliceIndexSym: unique symbol;
readonly streamSym: unique symbol;
readonly stringifySym: unique symbol;
readonly stringifySafeSym: unique symbol;
readonly stringifiersSym: unique symbol;
readonly endSym: unique symbol;
readonly formatOptsSym: unique symbol;
readonly messageKeySym: unique symbol;
readonly errorKeySym: unique symbol;
readonly nestedKeySym: unique symbol;
readonly wildcardFirstSym: unique symbol;
readonly needsMetadataGsym: unique symbol;
readonly useOnlyCustomLevelsSym: unique symbol;
readonly formattersSym: unique symbol;
readonly hooksSym: unique symbol;
};
/**
* Exposes the Pino package version. Also available on the logger instance.
*/
export const version: string;
/**
* Provides functions for generating the timestamp property in the log output. You can set the `timestamp` option during
* initialization to one of these functions to adjust the output format. Alternatively, you can specify your own time function.
* A time function must synchronously return a string that would be a valid component of a JSON string. For example,
* the default function returns a string like `,"time":1493426328206`.
*/
export const stdTimeFunctions: {
/**
* The default time function for Pino. Returns a string like `,"time":1493426328206`.
*/
epochTime: TimeFn;
/*
* Returns the seconds since Unix epoch
*/
unixTime: TimeFn;
/**
* Returns an empty string. This function is used when the `timestamp` option is set to `false`.
*/
nullTime: TimeFn;
/*
* Returns ISO 8601-formatted time in UTC
*/
isoTime: TimeFn;
};
//// Exported functions
/**
* Create a Pino Destination instance: a stream-like object with significantly more throughput (over 30%) than a standard Node.js stream.
* @param [dest]: The `destination` parameter, can be a file descriptor, a file path, or an object with `dest` property pointing to a fd or path.
* An ordinary Node.js `stream` file descriptor can be passed as the destination (such as the result of `fs.createWriteStream`)
* but for peak log writing performance, it is strongly recommended to use `pino.destination` to create the destination stream.
* @returns A Sonic-Boom stream to be used as destination for the pino function
*/
export function destination(
dest?: number | object | string | DestinationStream | NodeJS.WritableStream | SonicBoomOpts,
): SonicBoom;
export function transport<TransportOptions = Record<string, any>>(
options: TransportSingleOptions<TransportOptions> | TransportMultiOptions<TransportOptions> | TransportPipelineOptions<TransportOptions>
): ThreadStream
export function multistream<TLevel = Level>(
streamsArray: (DestinationStream | StreamEntry<TLevel>)[] | DestinationStream | StreamEntry<TLevel>,
opts?: MultiStreamOptions
): MultiStreamRes<TLevel>
}
//// Callable default export
/**
* @param [optionsOrStream]: an options object or a writable stream where the logs will be written. It can also receive some log-line metadata, if the
* relative protocol is enabled. Default: process.stdout
* @returns a new logger instance.
*/
declare function pino<CustomLevels extends string = never>(optionsOrStream?: LoggerOptions<CustomLevels> | DestinationStream): Logger<CustomLevels>;
/**
* @param [options]: an options object
* @param [stream]: a writable stream where the logs will be written. It can also receive some log-line metadata, if the
* relative protocol is enabled. Default: process.stdout
* @returns a new logger instance.
*/
declare function pino<CustomLevels extends string = never>(options: LoggerOptions<CustomLevels>, stream?: DestinationStream | undefined): Logger<CustomLevels>;
// Pass through all the top-level exports, allows `import {version} from "pino"`
// Constants and functions
export const destination: typeof pino.destination;
export const transport: typeof pino.transport;
export const multistream: typeof pino.multistream;
export const levels: typeof pino.levels;
export const stdSerializers: typeof pino.stdSerializers;
export const stdTimeFunctions: typeof pino.stdTimeFunctions;
export const symbols: typeof pino.symbols;
export const version: typeof pino.version;
// Types
export type Bindings = pino.Bindings;
export type DestinationStreamWithMetadata = pino.DestinationStreamWithMetadata;
export type Level = pino.Level;
export type LevelOrString = pino.LevelOrString;
export type LevelWithSilent = pino.LevelWithSilent;
export type LevelWithSilentOrString = pino.LevelWithSilentOrString;
export type LevelChangeEventListener<CustomLevels extends string> = pino.LevelChangeEventListener<CustomLevels>;
export type LogDescriptor = pino.LogDescriptor;
export type Logger<CustomLevels extends string = never> = pino.Logger<CustomLevels>;
export type SerializedError = pino.SerializedError;
export type SerializerFn = pino.SerializerFn;
export type SerializedRequest = pino.SerializedRequest;
export type SerializedResponse = pino.SerializedResponse;
export type WriteFn = pino.WriteFn;
// Interfaces
export interface BaseLogger extends pino.BaseLogger {}
export interface ChildLoggerOptions<CustomLevels extends string = never> extends pino.ChildLoggerOptions<CustomLevels> {}
export interface DestinationStream extends pino.DestinationStream {}
export interface LevelMapping extends pino.LevelMapping {}
export interface LogEvent extends pino.LogEvent {}
export interface LogFn extends pino.LogFn {}
export interface LoggerOptions<CustomLevels extends string = never> extends pino.LoggerOptions<CustomLevels> {}
export interface MultiStreamOptions extends pino.MultiStreamOptions {}
export interface MultiStreamRes<TLevel = Level> extends pino.MultiStreamRes<TLevel> {}
export interface StreamEntry<TLevel = Level> extends pino.StreamEntry<TLevel> {}
export interface TransportBaseOptions extends pino.TransportBaseOptions {}
export interface TransportMultiOptions extends pino.TransportMultiOptions {}
export interface TransportPipelineOptions extends pino.TransportPipelineOptions {}
export interface TransportSingleOptions extends pino.TransportSingleOptions {}
export interface TransportTargetOptions extends pino.TransportTargetOptions {}
// Bundle all top level exports into a namespace, then export namespace both
// as default (`import pino from "pino"`) and named variable
// (`import {pino} from "pino"`).
export { pino as default, pino };
// Export just the type side of the namespace as "P", allows
// `import {P} from "pino"; const log: P.Logger;`.
// (Legacy support for early 7.x releases, remove in 8.x.)
export type { pino as P };
+230
View File
@@ -0,0 +1,230 @@
'use strict'
/* eslint no-prototype-builtins: 0 */
const os = require('os')
const stdSerializers = require('pino-std-serializers')
const caller = require('./lib/caller')
const redaction = require('./lib/redaction')
const time = require('./lib/time')
const proto = require('./lib/proto')
const symbols = require('./lib/symbols')
const { configure } = require('safe-stable-stringify')
const { assertDefaultLevelFound, mappings, genLsCache, genLevelComparison, assertLevelComparison } = require('./lib/levels')
const { DEFAULT_LEVELS, SORTING_ORDER } = require('./lib/constants')
const {
createArgsNormalizer,
asChindings,
buildSafeSonicBoom,
buildFormatters,
stringify,
normalizeDestFileDescriptor,
noop
} = require('./lib/tools')
const { version } = require('./lib/meta')
const {
chindingsSym,
redactFmtSym,
serializersSym,
timeSym,
timeSliceIndexSym,
streamSym,
stringifySym,
stringifySafeSym,
stringifiersSym,
setLevelSym,
endSym,
formatOptsSym,
messageKeySym,
errorKeySym,
nestedKeySym,
mixinSym,
levelCompSym,
useOnlyCustomLevelsSym,
formattersSym,
hooksSym,
nestedKeyStrSym,
mixinMergeStrategySym,
msgPrefixSym
} = symbols
const { epochTime, nullTime } = time
const { pid } = process
const hostname = os.hostname()
const defaultErrorSerializer = stdSerializers.err
const defaultOptions = {
level: 'info',
levelComparison: SORTING_ORDER.ASC,
levels: DEFAULT_LEVELS,
messageKey: 'msg',
errorKey: 'err',
nestedKey: null,
enabled: true,
base: { pid, hostname },
serializers: Object.assign(Object.create(null), {
err: defaultErrorSerializer
}),
formatters: Object.assign(Object.create(null), {
bindings (bindings) {
return bindings
},
level (label, number) {
return { level: number }
}
}),
hooks: {
logMethod: undefined
},
timestamp: epochTime,
name: undefined,
redact: null,
customLevels: null,
useOnlyCustomLevels: false,
depthLimit: 5,
edgeLimit: 100
}
const normalize = createArgsNormalizer(defaultOptions)
const serializers = Object.assign(Object.create(null), stdSerializers)
function pino (...args) {
const instance = {}
const { opts, stream } = normalize(instance, caller(), ...args)
const {
redact,
crlf,
serializers,
timestamp,
messageKey,
errorKey,
nestedKey,
base,
name,
level,
customLevels,
levelComparison,
mixin,
mixinMergeStrategy,
useOnlyCustomLevels,
formatters,
hooks,
depthLimit,
edgeLimit,
onChild,
msgPrefix
} = opts
const stringifySafe = configure({
maximumDepth: depthLimit,
maximumBreadth: edgeLimit
})
const allFormatters = buildFormatters(
formatters.level,
formatters.bindings,
formatters.log
)
const stringifyFn = stringify.bind({
[stringifySafeSym]: stringifySafe
})
const stringifiers = redact ? redaction(redact, stringifyFn) : {}
const formatOpts = redact
? { stringify: stringifiers[redactFmtSym] }
: { stringify: stringifyFn }
const end = '}' + (crlf ? '\r\n' : '\n')
const coreChindings = asChindings.bind(null, {
[chindingsSym]: '',
[serializersSym]: serializers,
[stringifiersSym]: stringifiers,
[stringifySym]: stringify,
[stringifySafeSym]: stringifySafe,
[formattersSym]: allFormatters
})
let chindings = ''
if (base !== null) {
if (name === undefined) {
chindings = coreChindings(base)
} else {
chindings = coreChindings(Object.assign({}, base, { name }))
}
}
const time = (timestamp instanceof Function)
? timestamp
: (timestamp ? epochTime : nullTime)
const timeSliceIndex = time().indexOf(':') + 1
if (useOnlyCustomLevels && !customLevels) throw Error('customLevels is required if useOnlyCustomLevels is set true')
if (mixin && typeof mixin !== 'function') throw Error(`Unknown mixin type "${typeof mixin}" - expected "function"`)
if (msgPrefix && typeof msgPrefix !== 'string') throw Error(`Unknown msgPrefix type "${typeof msgPrefix}" - expected "string"`)
assertDefaultLevelFound(level, customLevels, useOnlyCustomLevels)
const levels = mappings(customLevels, useOnlyCustomLevels)
if (typeof stream.emit === 'function') {
stream.emit('message', { code: 'PINO_CONFIG', config: { levels, messageKey, errorKey } })
}
assertLevelComparison(levelComparison)
const levelCompFunc = genLevelComparison(levelComparison)
Object.assign(instance, {
levels,
[levelCompSym]: levelCompFunc,
[useOnlyCustomLevelsSym]: useOnlyCustomLevels,
[streamSym]: stream,
[timeSym]: time,
[timeSliceIndexSym]: timeSliceIndex,
[stringifySym]: stringify,
[stringifySafeSym]: stringifySafe,
[stringifiersSym]: stringifiers,
[endSym]: end,
[formatOptsSym]: formatOpts,
[messageKeySym]: messageKey,
[errorKeySym]: errorKey,
[nestedKeySym]: nestedKey,
// protect against injection
[nestedKeyStrSym]: nestedKey ? `,${JSON.stringify(nestedKey)}:{` : '',
[serializersSym]: serializers,
[mixinSym]: mixin,
[mixinMergeStrategySym]: mixinMergeStrategy,
[chindingsSym]: chindings,
[formattersSym]: allFormatters,
[hooksSym]: hooks,
silent: noop,
onChild,
[msgPrefixSym]: msgPrefix
})
Object.setPrototypeOf(instance, proto())
genLsCache(instance)
instance[setLevelSym](level)
return instance
}
module.exports = pino
module.exports.destination = (dest = process.stdout.fd) => {
if (typeof dest === 'object') {
dest.dest = normalizeDestFileDescriptor(dest.dest || process.stdout.fd)
return buildSafeSonicBoom(dest)
} else {
return buildSafeSonicBoom({ dest: normalizeDestFileDescriptor(dest), minLength: 0 })
}
}
module.exports.transport = require('./lib/transport')
module.exports.multistream = require('./lib/multistream')
module.exports.levels = mappings()
module.exports.stdSerializers = serializers
module.exports.stdTimeFunctions = Object.assign({}, time)
module.exports.symbols = symbols
module.exports.version = version
// Enables default and name export with TypeScript and Babel
module.exports.default = pino
module.exports.pino = pino
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+874
View File
@@ -0,0 +1,874 @@
'use strict'
const os = require('os')
const { readFileSync } = require('fs')
const { test } = require('tap')
const { sink, check, once, watchFileCreated, file } = require('./helper')
const pino = require('../')
const { version } = require('../package.json')
const { pid } = process
const hostname = os.hostname()
test('pino version is exposed on export', async ({ equal }) => {
equal(pino.version, version)
})
test('pino version is exposed on instance', async ({ equal }) => {
const instance = pino()
equal(instance.version, version)
})
test('child instance exposes pino version', async ({ equal }) => {
const child = pino().child({ foo: 'bar' })
equal(child.version, version)
})
test('bindings are exposed on every instance', async ({ same }) => {
const instance = pino()
same(instance.bindings(), {})
})
test('bindings contain the name and the child bindings', async ({ same }) => {
const instance = pino({ name: 'basicTest', level: 'info' }).child({ foo: 'bar' }).child({ a: 2 })
same(instance.bindings(), { name: 'basicTest', foo: 'bar', a: 2 })
})
test('set bindings on instance', async ({ same }) => {
const instance = pino({ name: 'basicTest', level: 'info' })
instance.setBindings({ foo: 'bar' })
same(instance.bindings(), { name: 'basicTest', foo: 'bar' })
})
test('newly set bindings overwrite old bindings', async ({ same }) => {
const instance = pino({ name: 'basicTest', level: 'info', base: { foo: 'bar' } })
instance.setBindings({ foo: 'baz' })
same(instance.bindings(), { name: 'basicTest', foo: 'baz' })
})
test('set bindings on child instance', async ({ same }) => {
const child = pino({ name: 'basicTest', level: 'info' }).child({})
child.setBindings({ foo: 'bar' })
same(child.bindings(), { name: 'basicTest', foo: 'bar' })
})
test('child should have bindings set by parent', async ({ same }) => {
const instance = pino({ name: 'basicTest', level: 'info' })
instance.setBindings({ foo: 'bar' })
const child = instance.child({})
same(child.bindings(), { name: 'basicTest', foo: 'bar' })
})
test('child should not share bindings of parent set after child creation', async ({ same }) => {
const instance = pino({ name: 'basicTest', level: 'info' })
const child = instance.child({})
instance.setBindings({ foo: 'bar' })
same(instance.bindings(), { name: 'basicTest', foo: 'bar' })
same(child.bindings(), { name: 'basicTest' })
})
function levelTest (name, level) {
test(`${name} logs as ${level}`, async ({ equal }) => {
const stream = sink()
const instance = pino(stream)
instance.level = name
instance[name]('hello world')
check(equal, await once(stream, 'data'), level, 'hello world')
})
test(`passing objects at level ${name}`, async ({ equal, same }) => {
const stream = sink()
const instance = pino(stream)
instance.level = name
const obj = { hello: 'world' }
instance[name](obj)
const result = await once(stream, 'data')
equal(new Date(result.time) <= new Date(), true, 'time is greater than Date.now()')
equal(result.pid, pid)
equal(result.hostname, hostname)
equal(result.level, level)
equal(result.hello, 'world')
same(Object.keys(obj), ['hello'])
})
test(`passing an object and a string at level ${name}`, async ({ equal, same }) => {
const stream = sink()
const instance = pino(stream)
instance.level = name
const obj = { hello: 'world' }
instance[name](obj, 'a string')
const result = await once(stream, 'data')
equal(new Date(result.time) <= new Date(), true, 'time is greater than Date.now()')
delete result.time
same(result, {
pid,
hostname,
level,
msg: 'a string',
hello: 'world'
})
same(Object.keys(obj), ['hello'])
})
test(`passing a undefined and a string at level ${name}`, async ({ equal, same }) => {
const stream = sink()
const instance = pino(stream)
instance.level = name
instance[name](undefined, 'a string')
const result = await once(stream, 'data')
equal(new Date(result.time) <= new Date(), true, 'time is greater than Date.now()')
delete result.time
same(result, {
pid,
hostname,
level,
msg: 'a string'
})
})
test(`overriding object key by string at level ${name}`, async ({ equal, same }) => {
const stream = sink()
const instance = pino(stream)
instance.level = name
instance[name]({ hello: 'world', msg: 'object' }, 'string')
const result = await once(stream, 'data')
equal(new Date(result.time) <= new Date(), true, 'time is greater than Date.now()')
delete result.time
same(result, {
pid,
hostname,
level,
msg: 'string',
hello: 'world'
})
})
test(`formatting logs as ${name}`, async ({ equal }) => {
const stream = sink()
const instance = pino(stream)
instance.level = name
instance[name]('hello %d', 42)
const result = await once(stream, 'data')
check(equal, result, level, 'hello 42')
})
test(`formatting a symbol at level ${name}`, async ({ equal }) => {
const stream = sink()
const instance = pino(stream)
instance.level = name
const sym = Symbol('foo')
instance[name]('hello %s', sym)
const result = await once(stream, 'data')
check(equal, result, level, 'hello Symbol(foo)')
})
test(`passing error with a serializer at level ${name}`, async ({ equal, same }) => {
const stream = sink()
const err = new Error('myerror')
const instance = pino({
serializers: {
err: pino.stdSerializers.err
}
}, stream)
instance.level = name
instance[name]({ err })
const result = await once(stream, 'data')
equal(new Date(result.time) <= new Date(), true, 'time is greater than Date.now()')
delete result.time
same(result, {
pid,
hostname,
level,
err: {
type: 'Error',
message: err.message,
stack: err.stack
},
msg: err.message
})
})
test(`child logger for level ${name}`, async ({ equal, same }) => {
const stream = sink()
const instance = pino(stream)
instance.level = name
const child = instance.child({ hello: 'world' })
child[name]('hello world')
const result = await once(stream, 'data')
equal(new Date(result.time) <= new Date(), true, 'time is greater than Date.now()')
delete result.time
same(result, {
pid,
hostname,
level,
msg: 'hello world',
hello: 'world'
})
})
}
levelTest('fatal', 60)
levelTest('error', 50)
levelTest('warn', 40)
levelTest('info', 30)
levelTest('debug', 20)
levelTest('trace', 10)
test('serializers can return undefined to strip field', async ({ equal }) => {
const stream = sink()
const instance = pino({
serializers: {
test () { return undefined }
}
}, stream)
instance.info({ test: 'sensitive info' })
const result = await once(stream, 'data')
equal('test' in result, false)
})
test('streams receive a message event with PINO_CONFIG', ({ match, end }) => {
const stream = sink()
stream.once('message', (message) => {
match(message, {
code: 'PINO_CONFIG',
config: {
errorKey: 'err',
levels: {
labels: {
10: 'trace',
20: 'debug',
30: 'info',
40: 'warn',
50: 'error',
60: 'fatal'
},
values: {
debug: 20,
error: 50,
fatal: 60,
info: 30,
trace: 10,
warn: 40
}
},
messageKey: 'msg'
}
})
end()
})
pino(stream)
})
test('does not explode with a circular ref', async ({ doesNotThrow }) => {
const stream = sink()
const instance = pino(stream)
const b = {}
const a = {
hello: b
}
b.a = a // circular ref
doesNotThrow(() => instance.info(a))
})
test('set the name', async ({ equal, same }) => {
const stream = sink()
const instance = pino({
name: 'hello'
}, stream)
instance.fatal('this is fatal')
const result = await once(stream, 'data')
equal(new Date(result.time) <= new Date(), true, 'time is greater than Date.now()')
delete result.time
same(result, {
pid,
hostname,
level: 60,
name: 'hello',
msg: 'this is fatal'
})
})
test('set the messageKey', async ({ equal, same }) => {
const stream = sink()
const message = 'hello world'
const messageKey = 'fooMessage'
const instance = pino({
messageKey
}, stream)
instance.info(message)
const result = await once(stream, 'data')
equal(new Date(result.time) <= new Date(), true, 'time is greater than Date.now()')
delete result.time
same(result, {
pid,
hostname,
level: 30,
fooMessage: message
})
})
test('set the nestedKey', async ({ equal, same }) => {
const stream = sink()
const object = { hello: 'world' }
const nestedKey = 'stuff'
const instance = pino({
nestedKey
}, stream)
instance.info(object)
const result = await once(stream, 'data')
equal(new Date(result.time) <= new Date(), true, 'time is greater than Date.now()')
delete result.time
same(result, {
pid,
hostname,
level: 30,
stuff: object
})
})
test('set undefined properties', async ({ equal, same }) => {
const stream = sink()
const instance = pino(stream)
instance.info({ hello: 'world', property: undefined })
const result = await once(stream, 'data')
equal(new Date(result.time) <= new Date(), true, 'time is greater than Date.now()')
delete result.time
same(result, {
pid,
hostname,
level: 30,
hello: 'world'
})
})
test('prototype properties are not logged', async ({ equal }) => {
const stream = sink()
const instance = pino(stream)
instance.info(Object.create({ hello: 'world' }))
const { hello } = await once(stream, 'data')
equal(hello, undefined)
})
test('set the base', async ({ equal, same }) => {
const stream = sink()
const instance = pino({
base: {
a: 'b'
}
}, stream)
instance.fatal('this is fatal')
const result = await once(stream, 'data')
equal(new Date(result.time) <= new Date(), true, 'time is greater than Date.now()')
delete result.time
same(result, {
a: 'b',
level: 60,
msg: 'this is fatal'
})
})
test('set the base to null', async ({ equal, same }) => {
const stream = sink()
const instance = pino({
base: null
}, stream)
instance.fatal('this is fatal')
const result = await once(stream, 'data')
equal(new Date(result.time) <= new Date(), true, 'time is greater than Date.now()')
delete result.time
same(result, {
level: 60,
msg: 'this is fatal'
})
})
test('set the base to null and use a formatter', async ({ equal, same }) => {
const stream = sink()
const instance = pino({
base: null,
formatters: {
log (input) {
return Object.assign({}, input, { additionalMessage: 'using pino' })
}
}
}, stream)
instance.fatal('this is fatal too')
const result = await once(stream, 'data')
equal(new Date(result.time) <= new Date(), true, 'time is greater than Date.now()')
delete result.time
same(result, {
level: 60,
msg: 'this is fatal too',
additionalMessage: 'using pino'
})
})
test('throw if creating child without bindings', async ({ equal, fail }) => {
const stream = sink()
const instance = pino(stream)
try {
instance.child()
fail('it should throw')
} catch (err) {
equal(err.message, 'missing bindings for child Pino')
}
})
test('correctly escapes msg strings with stray double quote at end', async ({ same }) => {
const stream = sink()
const instance = pino({
name: 'hello'
}, stream)
instance.fatal('this contains "')
const result = await once(stream, 'data')
delete result.time
same(result, {
pid,
hostname,
level: 60,
name: 'hello',
msg: 'this contains "'
})
})
test('correctly escape msg strings with unclosed double quote', async ({ same }) => {
const stream = sink()
const instance = pino({
name: 'hello'
}, stream)
instance.fatal('" this contains')
const result = await once(stream, 'data')
delete result.time
same(result, {
pid,
hostname,
level: 60,
name: 'hello',
msg: '" this contains'
})
})
test('correctly escape quote in a key', async ({ same }) => {
const stream = sink()
const instance = pino(stream)
const obj = { 'some"obj': 'world' }
instance.info(obj, 'a string')
const result = await once(stream, 'data')
delete result.time
same(result, {
level: 30,
pid,
hostname,
msg: 'a string',
'some"obj': 'world'
})
same(Object.keys(obj), ['some"obj'])
})
// https://github.com/pinojs/pino/issues/139
test('object and format string', async ({ same }) => {
const stream = sink()
const instance = pino(stream)
instance.info({}, 'foo %s', 'bar')
const result = await once(stream, 'data')
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'foo bar'
})
})
test('object and format string property', async ({ same }) => {
const stream = sink()
const instance = pino(stream)
instance.info({ answer: 42 }, 'foo %s', 'bar')
const result = await once(stream, 'data')
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'foo bar',
answer: 42
})
})
test('correctly strip undefined when returned from toJSON', async ({ equal }) => {
const stream = sink()
const instance = pino({
test: 'this'
}, stream)
instance.fatal({ test: { toJSON () { return undefined } } })
const result = await once(stream, 'data')
equal('test' in result, false)
})
test('correctly supports stderr', async ({ same }) => {
// stderr inherits from Stream, rather than Writable
const dest = {
writable: true,
write (result) {
result = JSON.parse(result)
delete result.time
same(result, {
pid,
hostname,
level: 60,
msg: 'a message'
})
}
}
const instance = pino(dest)
instance.fatal('a message')
})
test('normalize number to string', async ({ same }) => {
const stream = sink()
const instance = pino(stream)
instance.info(1)
const result = await once(stream, 'data')
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: '1'
})
})
test('normalize number to string with an object', async ({ same }) => {
const stream = sink()
const instance = pino(stream)
instance.info({ answer: 42 }, 1)
const result = await once(stream, 'data')
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: '1',
answer: 42
})
})
test('handles objects with null prototype', async ({ same }) => {
const stream = sink()
const instance = pino(stream)
const o = Object.create(null)
o.test = 'test'
instance.info(o)
const result = await once(stream, 'data')
delete result.time
same(result, {
pid,
hostname,
level: 30,
test: 'test'
})
})
test('pino.destination', async ({ same }) => {
const tmp = file()
const instance = pino(pino.destination(tmp))
instance.info('hello')
await watchFileCreated(tmp)
const result = JSON.parse(readFileSync(tmp).toString())
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
test('auto pino.destination with a string', async ({ same }) => {
const tmp = file()
const instance = pino(tmp)
instance.info('hello')
await watchFileCreated(tmp)
const result = JSON.parse(readFileSync(tmp).toString())
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
test('auto pino.destination with a string as second argument', async ({ same }) => {
const tmp = file()
const instance = pino(null, tmp)
instance.info('hello')
await watchFileCreated(tmp)
const result = JSON.parse(readFileSync(tmp).toString())
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
test('does not override opts with a string as second argument', async ({ same }) => {
const tmp = file()
const instance = pino({
timestamp: () => ',"time":"none"'
}, tmp)
instance.info('hello')
await watchFileCreated(tmp)
const result = JSON.parse(readFileSync(tmp).toString())
same(result, {
pid,
hostname,
level: 30,
time: 'none',
msg: 'hello'
})
})
// https://github.com/pinojs/pino/issues/222
test('children with same names render in correct order', async ({ equal }) => {
const stream = sink()
const root = pino(stream)
root.child({ a: 1 }).child({ a: 2 }).info({ a: 3 })
const { a } = await once(stream, 'data')
equal(a, 3, 'last logged object takes precedence')
})
test('use `safe-stable-stringify` to avoid circular dependencies', async ({ same }) => {
const stream = sink()
const root = pino(stream)
// circular depth
const obj = {}
obj.a = obj
root.info(obj)
const { a } = await once(stream, 'data')
same(a, { a: '[Circular]' })
})
test('correctly log non circular objects', async ({ same }) => {
const stream = sink()
const root = pino(stream)
const obj = {}
let parent = obj
for (let i = 0; i < 10; i++) {
parent.node = {}
parent = parent.node
}
root.info(obj)
const { node } = await once(stream, 'data')
same(node, { node: { node: { node: { node: { node: { node: { node: { node: { node: {} } } } } } } } } })
})
test('safe-stable-stringify must be used when interpolating', async (t) => {
const stream = sink()
const instance = pino(stream)
const o = { a: { b: {} } }
o.a.b.c = o.a.b
instance.info('test %j', o)
const { msg } = await once(stream, 'data')
t.equal(msg, 'test {"a":{"b":{"c":"[Circular]"}}}')
})
test('throws when setting useOnlyCustomLevels without customLevels', async ({ throws }) => {
throws(() => {
pino({
useOnlyCustomLevels: true
})
}, 'customLevels is required if useOnlyCustomLevels is set true')
})
test('correctly log Infinity', async (t) => {
const stream = sink()
const instance = pino(stream)
const o = { num: Infinity }
instance.info(o)
const { num } = await once(stream, 'data')
t.equal(num, null)
})
test('correctly log -Infinity', async (t) => {
const stream = sink()
const instance = pino(stream)
const o = { num: -Infinity }
instance.info(o)
const { num } = await once(stream, 'data')
t.equal(num, null)
})
test('correctly log NaN', async (t) => {
const stream = sink()
const instance = pino(stream)
const o = { num: NaN }
instance.info(o)
const { num } = await once(stream, 'data')
t.equal(num, null)
})
test('offers a .default() method to please typescript', async ({ equal }) => {
equal(pino.default, pino)
const stream = sink()
const instance = pino.default(stream)
instance.info('hello world')
check(equal, await once(stream, 'data'), 30, 'hello world')
})
test('correctly skip function', async (t) => {
const stream = sink()
const instance = pino(stream)
const o = { num: NaN }
instance.info(o, () => {})
const { msg } = await once(stream, 'data')
t.equal(msg, undefined)
})
test('correctly skip Infinity', async (t) => {
const stream = sink()
const instance = pino(stream)
const o = { num: NaN }
instance.info(o, Infinity)
const { msg } = await once(stream, 'data')
t.equal(msg, null)
})
test('correctly log number', async (t) => {
const stream = sink()
const instance = pino(stream)
const o = { num: NaN }
instance.info(o, 42)
const { msg } = await once(stream, 'data')
t.equal(msg, 42)
})
test('nestedKey should not be used for non-objects', async ({ strictSame }) => {
const stream = sink()
const message = 'hello'
const nestedKey = 'stuff'
const instance = pino({
nestedKey
}, stream)
instance.info(message)
const result = await once(stream, 'data')
delete result.time
strictSame(result, {
pid,
hostname,
level: 30,
msg: message
})
})
test('throws if prettyPrint is passed in as an option', async (t) => {
t.throws(() => {
pino({
prettyPrint: true
})
}, new Error('prettyPrint option is no longer supported, see the pino-pretty package (https://github.com/pinojs/pino-pretty)'))
})
test('Should invoke `onChild` with the newly created child', async ({ equal }) => {
let innerChild
const child = pino({
onChild: (instance) => {
innerChild = instance
}
}).child({ foo: 'bar' })
equal(child, innerChild)
})
test('logger message should have the prefix message that defined in the logger creation', async ({ equal }) => {
const stream = sink()
const logger = pino({
msgPrefix: 'My name is Bond '
}, stream)
logger.info('James Bond')
const { msg } = await once(stream, 'data')
equal(msg, 'My name is Bond James Bond')
})
test('child message should have the prefix message that defined in the child creation', async ({ equal }) => {
const stream = sink()
const instance = pino(stream)
const child = instance.child({}, { msgPrefix: 'My name is Bond ' })
child.info('James Bond')
const { msg } = await once(stream, 'data')
equal(msg, 'My name is Bond James Bond')
})
test('child message should have the prefix message that defined in the child creation when logging with log meta', async ({ equal }) => {
const stream = sink()
const instance = pino(stream)
const child = instance.child({}, { msgPrefix: 'My name is Bond ' })
child.info({ hello: 'world' }, 'James Bond')
const { msg, hello } = await once(stream, 'data')
equal(hello, 'world')
equal(msg, 'My name is Bond James Bond')
})
test('logged message should not have the prefix when not providing any message', async ({ equal }) => {
const stream = sink()
const instance = pino(stream)
const child = instance.child({}, { msgPrefix: 'This should not be shown ' })
child.info({ hello: 'world' })
const { msg, hello } = await once(stream, 'data')
equal(hello, 'world')
equal(msg, undefined)
})
test('child message should append parent prefix to current prefix that defined in the child creation', async ({ equal }) => {
const stream = sink()
const instance = pino({
msgPrefix: 'My name is Bond '
}, stream)
const child = instance.child({}, { msgPrefix: 'James ' })
child.info('Bond')
const { msg } = await once(stream, 'data')
equal(msg, 'My name is Bond James Bond')
})
test('child message should inherent parent prefix', async ({ equal }) => {
const stream = sink()
const instance = pino({
msgPrefix: 'My name is Bond '
}, stream)
const child = instance.child({})
child.info('James Bond')
const { msg } = await once(stream, 'data')
equal(msg, 'My name is Bond James Bond')
})
test('grandchild message should inherent parent prefix', async ({ equal }) => {
const stream = sink()
const instance = pino(stream)
const child = instance.child({}, { msgPrefix: 'My name is Bond ' })
const grandchild = child.child({})
grandchild.info('James Bond')
const { msg } = await once(stream, 'data')
equal(msg, 'My name is Bond James Bond')
})
+57
View File
@@ -0,0 +1,57 @@
'use strict'
const t = require('tap')
const { join } = require('path')
const { fork } = require('child_process')
const { once } = require('./helper')
const pino = require('..')
if (process.platform === 'win32') {
t.skip('skipping on windows')
process.exit(0)
}
if (process.env.CITGM) {
// This looks like a some form of limitations of the CITGM test runner
// or the HW/SW we run it on. This file can hang on Node.js v18.x.
// The failure does not reproduce locally or on our CI.
// Skipping it is the only way to keep pino in CITGM.
// https://github.com/nodejs/citgm/pull/1002#issuecomment-1751942988
t.skip('Skipping on Node.js core CITGM because it hangs on v18.x')
process.exit(0)
}
function test (file) {
file = join('fixtures', 'broken-pipe', file)
t.test(file, { parallel: true }, async ({ equal }) => {
const child = fork(join(__dirname, file), { silent: true })
child.stdout.destroy()
child.stderr.pipe(process.stdout)
const res = await once(child, 'close')
equal(res, 0) // process exits successfully
})
}
t.jobs = 42
test('basic.js')
test('destination.js')
test('syncfalse.js')
t.test('let error pass through', ({ equal, plan }) => {
plan(3)
const stream = pino.destination({ sync: true })
// side effect of the pino constructor is that it will set an
// event handler for error
pino(stream)
process.nextTick(() => stream.emit('error', new Error('kaboom')))
process.nextTick(() => stream.emit('error', new Error('kaboom')))
stream.on('error', (err) => {
equal(err.message, 'kaboom')
})
})
+106
View File
@@ -0,0 +1,106 @@
'use strict'
const test = require('tape')
const pino = require('../browser')
test('child has parent level', ({ end, same, is }) => {
const instance = pino({
level: 'error',
browser: {}
})
const child = instance.child({})
same(child.level, instance.level)
end()
})
test('changing child level does not affect parent', ({ end, same, is }) => {
const instance = pino({
level: 'error',
browser: {}
})
const child = instance.child({})
child.level = 'info'
same(instance.level, 'error')
end()
})
test('child should log, if its own level allows it', ({ end, same, is }) => {
const expected = [
{
level: 30,
msg: 'this is info'
},
{
level: 40,
msg: 'this is warn'
},
{
level: 50,
msg: 'this is an error'
}
]
const instance = pino({
level: 'error',
browser: {
write (actual) {
checkLogObjects(is, same, actual, expected.shift())
}
}
})
const child = instance.child({})
child.level = 'info'
child.debug('this is debug')
child.info('this is info')
child.warn('this is warn')
child.error('this is an error')
same(expected.length, 0, 'not all messages were read')
end()
})
test('changing child log level should not affect parent log behavior', ({ end, same, is }) => {
const expected = [
{
level: 50,
msg: 'this is an error'
},
{
level: 60,
msg: 'this is fatal'
}
]
const instance = pino({
level: 'error',
browser: {
write (actual) {
checkLogObjects(is, same, actual, expected.shift())
}
}
})
const child = instance.child({})
child.level = 'info'
instance.warn('this is warn')
instance.error('this is an error')
instance.fatal('this is fatal')
same(expected.length, 0, 'not all messages were read')
end()
})
function checkLogObjects (is, same, actual, expected) {
is(actual.time <= Date.now(), true, 'time is greater than Date.now()')
const actualCopy = Object.assign({}, actual)
const expectedCopy = Object.assign({}, expected)
delete actualCopy.time
delete expectedCopy.time
same(actualCopy, expectedCopy)
}
@@ -0,0 +1,87 @@
'use strict'
const test = require('tape')
const pino = require('../browser')
test('set browser opts disabled to true', ({ end, same }) => {
const instance = pino({
browser: {
disabled: true,
write (actual) {
checkLogObjects(same, actual, [])
}
}
})
instance.info('hello world')
instance.error('this is an error')
instance.fatal('this is fatal')
end()
})
test('set browser opts disabled to false', ({ end, same }) => {
const expected = [
{
level: 30,
msg: 'hello world'
},
{
level: 50,
msg: 'this is an error'
},
{
level: 60,
msg: 'this is fatal'
}
]
const instance = pino({
browser: {
disabled: false,
write (actual) {
checkLogObjects(same, actual, expected.shift())
}
}
})
instance.info('hello world')
instance.error('this is an error')
instance.fatal('this is fatal')
end()
})
test('disabled is not set in browser opts', ({ end, same }) => {
const expected = [
{
level: 30,
msg: 'hello world'
},
{
level: 50,
msg: 'this is an error'
},
{
level: 60,
msg: 'this is fatal'
}
]
const instance = pino({
browser: {
write (actual) {
checkLogObjects(same, actual, expected.shift())
}
}
})
instance.info('hello world')
instance.error('this is an error')
instance.fatal('this is fatal')
end()
})
function checkLogObjects (same, actual, expected, is) {
const actualCopy = Object.assign({}, actual)
const expectedCopy = Object.assign({}, expected)
delete actualCopy.time
delete expectedCopy.time
same(actualCopy, expectedCopy)
}
@@ -0,0 +1,12 @@
'use strict'
Object.freeze(console)
const test = require('tape')
const pino = require('../browser')
test('silent level', ({ end, fail, pass }) => {
pino({
level: 'silent',
browser: { }
})
end()
})
+241
View File
@@ -0,0 +1,241 @@
'use strict'
const test = require('tape')
const pino = require('../browser')
test('set the level by string', ({ end, same, is }) => {
const expected = [
{
level: 50,
msg: 'this is an error'
},
{
level: 60,
msg: 'this is fatal'
}
]
const instance = pino({
browser: {
write (actual) {
checkLogObjects(is, same, actual, expected.shift())
}
}
})
instance.level = 'error'
instance.info('hello world')
instance.error('this is an error')
instance.fatal('this is fatal')
end()
})
test('set the level by string. init with silent', ({ end, same, is }) => {
const expected = [
{
level: 50,
msg: 'this is an error'
},
{
level: 60,
msg: 'this is fatal'
}
]
const instance = pino({
level: 'silent',
browser: {
write (actual) {
checkLogObjects(is, same, actual, expected.shift())
}
}
})
instance.level = 'error'
instance.info('hello world')
instance.error('this is an error')
instance.fatal('this is fatal')
end()
})
test('set the level by string. init with silent and transmit', ({ end, same, is }) => {
const expected = [
{
level: 50,
msg: 'this is an error'
},
{
level: 60,
msg: 'this is fatal'
}
]
const instance = pino({
level: 'silent',
browser: {
write (actual) {
checkLogObjects(is, same, actual, expected.shift())
}
},
transmit: {
send () {}
}
})
instance.level = 'error'
instance.info('hello world')
instance.error('this is an error')
instance.fatal('this is fatal')
end()
})
test('set the level via constructor', ({ end, same, is }) => {
const expected = [
{
level: 50,
msg: 'this is an error'
},
{
level: 60,
msg: 'this is fatal'
}
]
const instance = pino({
level: 'error',
browser: {
write (actual) {
checkLogObjects(is, same, actual, expected.shift())
}
}
})
instance.info('hello world')
instance.error('this is an error')
instance.fatal('this is fatal')
end()
})
test('set custom level and use it', ({ end, same, is }) => {
const expected = [
{
level: 31,
msg: 'this is a custom level'
}
]
const instance = pino({
customLevels: {
success: 31
},
browser: {
write (actual) {
checkLogObjects(is, same, actual, expected.shift())
}
}
})
instance.success('this is a custom level')
end()
})
test('the wrong level throws', ({ end, throws }) => {
const instance = pino()
throws(() => {
instance.level = 'kaboom'
})
end()
})
test('the wrong level by number throws', ({ end, throws }) => {
const instance = pino()
throws(() => {
instance.levelVal = 55
})
end()
})
test('exposes level string mappings', ({ end, is }) => {
is(pino.levels.values.error, 50)
end()
})
test('exposes level number mappings', ({ end, is }) => {
is(pino.levels.labels[50], 'error')
end()
})
test('returns level integer', ({ end, is }) => {
const instance = pino({ level: 'error' })
is(instance.levelVal, 50)
end()
})
test('silent level via constructor', ({ end, fail }) => {
const instance = pino({
level: 'silent',
browser: {
write () {
fail('no data should be logged')
}
}
})
Object.keys(pino.levels.values).forEach((level) => {
instance[level]('hello world')
})
end()
})
test('silent level by string', ({ end, fail }) => {
const instance = pino({
browser: {
write () {
fail('no data should be logged')
}
}
})
instance.level = 'silent'
Object.keys(pino.levels.values).forEach((level) => {
instance[level]('hello world')
})
end()
})
test('exposed levels', ({ end, same }) => {
same(Object.keys(pino.levels.values), [
'fatal',
'error',
'warn',
'info',
'debug',
'trace'
])
end()
})
test('exposed labels', ({ end, same }) => {
same(Object.keys(pino.levels.labels), [
'10',
'20',
'30',
'40',
'50',
'60'
])
end()
})
function checkLogObjects (is, same, actual, expected) {
is(actual.time <= Date.now(), true, 'time is greater than Date.now()')
const actualCopy = Object.assign({}, actual)
const expectedCopy = Object.assign({}, expected)
delete actualCopy.time
delete expectedCopy.time
same(actualCopy, expectedCopy)
}
@@ -0,0 +1,354 @@
'use strict'
// eslint-disable-next-line
if (typeof $1 !== 'undefined') $1 = arguments.callee.caller.arguments[0]
const test = require('tape')
const fresh = require('import-fresh')
const pino = require('../browser')
const parentSerializers = {
test: () => 'parent'
}
const childSerializers = {
test: () => 'child'
}
test('serializers override values', ({ end, is }) => {
const parent = pino({
serializers: parentSerializers,
browser: {
serialize: true,
write (o) {
is(o.test, 'parent')
end()
}
}
})
parent.fatal({ test: 'test' })
})
test('without the serialize option, serializers do not override values', ({ end, is }) => {
const parent = pino({
serializers: parentSerializers,
browser: {
write (o) {
is(o.test, 'test')
end()
}
}
})
parent.fatal({ test: 'test' })
})
if (process.title !== 'browser') {
test('if serialize option is true, standard error serializer is auto enabled', ({ end, same }) => {
const err = Error('test')
err.code = 'test'
err.type = 'Error' // get that cov
const expect = pino.stdSerializers.err(err)
const consoleError = console.error
console.error = function (err) {
same(err, expect)
}
const logger = fresh('../browser')({
browser: { serialize: true }
})
console.error = consoleError
logger.fatal(err)
end()
})
test('if serialize option is array, standard error serializer is auto enabled', ({ end, same }) => {
const err = Error('test')
err.code = 'test'
const expect = pino.stdSerializers.err(err)
const consoleError = console.error
console.error = function (err) {
same(err, expect)
}
const logger = fresh('../browser', require)({
browser: { serialize: [] }
})
console.error = consoleError
logger.fatal(err)
end()
})
test('if serialize option is array containing !stdSerializers.err, standard error serializer is disabled', ({ end, is }) => {
const err = Error('test')
err.code = 'test'
const expect = err
const consoleError = console.error
console.error = function (err) {
is(err, expect)
}
const logger = fresh('../browser', require)({
browser: { serialize: ['!stdSerializers.err'] }
})
console.error = consoleError
logger.fatal(err)
end()
})
test('in browser, serializers apply to all objects', ({ end, is }) => {
const consoleError = console.error
console.error = function (test, test2, test3, test4, test5) {
is(test.key, 'serialized')
is(test2.key2, 'serialized2')
is(test5.key3, 'serialized3')
}
const logger = fresh('../browser', require)({
serializers: {
key: () => 'serialized',
key2: () => 'serialized2',
key3: () => 'serialized3'
},
browser: { serialize: true }
})
console.error = consoleError
logger.fatal({ key: 'test' }, { key2: 'test' }, 'str should skip', [{ foo: 'array should skip' }], { key3: 'test' })
end()
})
test('serialize can be an array of selected serializers', ({ end, is }) => {
const consoleError = console.error
console.error = function (test, test2, test3, test4, test5) {
is(test.key, 'test')
is(test2.key2, 'serialized2')
is(test5.key3, 'test')
}
const logger = fresh('../browser', require)({
serializers: {
key: () => 'serialized',
key2: () => 'serialized2',
key3: () => 'serialized3'
},
browser: { serialize: ['key2'] }
})
console.error = consoleError
logger.fatal({ key: 'test' }, { key2: 'test' }, 'str should skip', [{ foo: 'array should skip' }], { key3: 'test' })
end()
})
test('serialize filter applies to child loggers', ({ end, is }) => {
const consoleError = console.error
console.error = function (binding, test, test2, test3, test4, test5) {
is(test.key, 'test')
is(test2.key2, 'serialized2')
is(test5.key3, 'test')
}
const logger = fresh('../browser', require)({
browser: { serialize: ['key2'] }
})
console.error = consoleError
logger.child({
aBinding: 'test'
}, {
serializers: {
key: () => 'serialized',
key2: () => 'serialized2',
key3: () => 'serialized3'
}
}).fatal({ key: 'test' }, { key2: 'test' }, 'str should skip', [{ foo: 'array should skip' }], { key3: 'test' })
end()
})
test('serialize filter applies to child loggers through bindings', ({ end, is }) => {
const consoleError = console.error
console.error = function (binding, test, test2, test3, test4, test5) {
is(test.key, 'test')
is(test2.key2, 'serialized2')
is(test5.key3, 'test')
}
const logger = fresh('../browser', require)({
browser: { serialize: ['key2'] }
})
console.error = consoleError
logger.child({
aBinding: 'test',
serializers: {
key: () => 'serialized',
key2: () => 'serialized2',
key3: () => 'serialized3'
}
}).fatal({ key: 'test' }, { key2: 'test' }, 'str should skip', [{ foo: 'array should skip' }], { key3: 'test' })
end()
})
test('parent serializers apply to child bindings', ({ end, is }) => {
const consoleError = console.error
console.error = function (binding) {
is(binding.key, 'serialized')
}
const logger = fresh('../browser', require)({
serializers: {
key: () => 'serialized'
},
browser: { serialize: true }
})
console.error = consoleError
logger.child({ key: 'test' }).fatal({ test: 'test' })
end()
})
test('child serializers apply to child bindings', ({ end, is }) => {
const consoleError = console.error
console.error = function (binding) {
is(binding.key, 'serialized')
}
const logger = fresh('../browser', require)({
browser: { serialize: true }
})
console.error = consoleError
logger.child({
key: 'test'
}, {
serializers: {
key: () => 'serialized'
}
}).fatal({ test: 'test' })
end()
})
}
test('child does not overwrite parent serializers', ({ end, is }) => {
let c = 0
const parent = pino({
serializers: parentSerializers,
browser: {
serialize: true,
write (o) {
c++
if (c === 1) is(o.test, 'parent')
if (c === 2) {
is(o.test, 'child')
end()
}
}
}
})
const child = parent.child({}, { serializers: childSerializers })
parent.fatal({ test: 'test' })
child.fatal({ test: 'test' })
})
test('children inherit parent serializers', ({ end, is }) => {
const parent = pino({
serializers: parentSerializers,
browser: {
serialize: true,
write (o) {
is(o.test, 'parent')
}
}
})
const child = parent.child({ a: 'property' })
child.fatal({ test: 'test' })
end()
})
test('children serializers get called', ({ end, is }) => {
const parent = pino({
test: 'this',
browser: {
serialize: true,
write (o) {
is(o.test, 'child')
}
}
})
const child = parent.child({ a: 'property' }, { serializers: childSerializers })
child.fatal({ test: 'test' })
end()
})
test('children serializers get called when inherited from parent', ({ end, is }) => {
const parent = pino({
test: 'this',
serializers: parentSerializers,
browser: {
serialize: true,
write: (o) => {
is(o.test, 'pass')
}
}
})
const child = parent.child({}, { serializers: { test: () => 'pass' } })
child.fatal({ test: 'fail' })
end()
})
test('non overridden serializers are available in the children', ({ end, is }) => {
const pSerializers = {
onlyParent: () => 'parent',
shared: () => 'parent'
}
const cSerializers = {
shared: () => 'child',
onlyChild: () => 'child'
}
let c = 0
const parent = pino({
serializers: pSerializers,
browser: {
serialize: true,
write (o) {
c++
if (c === 1) is(o.shared, 'child')
if (c === 2) is(o.onlyParent, 'parent')
if (c === 3) is(o.onlyChild, 'child')
if (c === 4) is(o.onlyChild, 'test')
}
}
})
const child = parent.child({}, { serializers: cSerializers })
child.fatal({ shared: 'test' })
child.fatal({ onlyParent: 'test' })
child.fatal({ onlyChild: 'test' })
parent.fatal({ onlyChild: 'test' })
end()
})
@@ -0,0 +1,88 @@
'use strict'
const test = require('tape')
const pino = require('../browser')
Date.now = () => 1599400603614
test('null timestamp', ({ end, is }) => {
const instance = pino({
timestamp: pino.stdTimeFunctions.nullTime,
browser: {
asObject: true,
write: function (o) {
is(o.time, undefined)
}
}
})
instance.info('hello world')
end()
})
test('iso timestamp', ({ end, is }) => {
const instance = pino({
timestamp: pino.stdTimeFunctions.isoTime,
browser: {
asObject: true,
write: function (o) {
is(o.time, '2020-09-06T13:56:43.614Z')
}
}
})
instance.info('hello world')
end()
})
test('epoch timestamp', ({ end, is }) => {
const instance = pino({
timestamp: pino.stdTimeFunctions.epochTime,
browser: {
asObject: true,
write: function (o) {
is(o.time, 1599400603614)
}
}
})
instance.info('hello world')
end()
})
test('unix timestamp', ({ end, is }) => {
const instance = pino({
timestamp: pino.stdTimeFunctions.unixTime,
browser: {
asObject: true,
write: function (o) {
is(o.time, Math.round(1599400603614 / 1000.0))
}
}
})
instance.info('hello world')
end()
})
test('epoch timestamp by default', ({ end, is }) => {
const instance = pino({
browser: {
asObject: true,
write: function (o) {
is(o.time, 1599400603614)
}
}
})
instance.info('hello world')
end()
})
test('not print timestamp if the option is false', ({ end, is }) => {
const instance = pino({
timestamp: false,
browser: {
asObject: true,
write: function (o) {
is(o.time, undefined)
}
}
})
instance.info('hello world')
end()
})
+349
View File
@@ -0,0 +1,349 @@
'use strict'
const test = require('tape')
const pino = require('../browser')
function noop () {}
test('throws if transmit object does not have send function', ({ end, throws }) => {
throws(() => {
pino({ browser: { transmit: {} } })
})
throws(() => {
pino({ browser: { transmit: { send: 'not a func' } } })
})
end()
})
test('calls send function after write', ({ end, is }) => {
let c = 0
const logger = pino({
browser: {
write: () => {
c++
},
transmit: {
send () { is(c, 1) }
}
}
})
logger.fatal({ test: 'test' })
end()
})
test('passes send function the logged level', ({ end, is }) => {
const logger = pino({
browser: {
write () {},
transmit: {
send (level) {
is(level, 'fatal')
}
}
}
})
logger.fatal({ test: 'test' })
end()
})
test('passes send function message strings in logEvent object when asObject is not set', ({ end, same, is }) => {
const logger = pino({
browser: {
write: noop,
transmit: {
send (level, { messages }) {
is(messages[0], 'test')
is(messages[1], 'another test')
}
}
}
})
logger.fatal('test', 'another test')
end()
})
test('passes send function message objects in logEvent object when asObject is not set', ({ end, same, is }) => {
const logger = pino({
browser: {
write: noop,
transmit: {
send (level, { messages }) {
same(messages[0], { test: 'test' })
is(messages[1], 'another test')
}
}
}
})
logger.fatal({ test: 'test' }, 'another test')
end()
})
test('passes send function message strings in logEvent object when asObject is set', ({ end, same, is }) => {
const logger = pino({
browser: {
asObject: true,
write: noop,
transmit: {
send (level, { messages }) {
is(messages[0], 'test')
is(messages[1], 'another test')
}
}
}
})
logger.fatal('test', 'another test')
end()
})
test('passes send function message objects in logEvent object when asObject is set', ({ end, same, is }) => {
const logger = pino({
browser: {
asObject: true,
write: noop,
transmit: {
send (level, { messages }) {
same(messages[0], { test: 'test' })
is(messages[1], 'another test')
}
}
}
})
logger.fatal({ test: 'test' }, 'another test')
end()
})
test('supplies a timestamp (ts) in logEvent object which is exactly the same as the `time` property in asObject mode', ({ end, is }) => {
let expected
const logger = pino({
browser: {
asObject: true, // implicit because `write`, but just to be explicit
write (o) {
expected = o.time
},
transmit: {
send (level, logEvent) {
is(logEvent.ts, expected)
}
}
}
})
logger.fatal('test')
end()
})
test('passes send function child bindings via logEvent object', ({ end, same, is }) => {
const logger = pino({
browser: {
write: noop,
transmit: {
send (level, logEvent) {
const messages = logEvent.messages
const bindings = logEvent.bindings
same(bindings[0], { first: 'binding' })
same(bindings[1], { second: 'binding2' })
same(messages[0], { test: 'test' })
is(messages[1], 'another test')
}
}
}
})
logger
.child({ first: 'binding' })
.child({ second: 'binding2' })
.fatal({ test: 'test' }, 'another test')
end()
})
test('passes send function level:{label, value} via logEvent object', ({ end, is }) => {
const logger = pino({
browser: {
write: noop,
transmit: {
send (level, logEvent) {
const label = logEvent.level.label
const value = logEvent.level.value
is(label, 'fatal')
is(value, 60)
}
}
}
})
logger.fatal({ test: 'test' }, 'another test')
end()
})
test('calls send function according to transmit.level', ({ end, is }) => {
let c = 0
const logger = pino({
browser: {
write: noop,
transmit: {
level: 'error',
send (level) {
c++
if (c === 1) is(level, 'error')
if (c === 2) is(level, 'fatal')
}
}
}
})
logger.warn('ignored')
logger.error('test')
logger.fatal('test')
end()
})
test('transmit.level defaults to logger level', ({ end, is }) => {
let c = 0
const logger = pino({
level: 'error',
browser: {
write: noop,
transmit: {
send (level) {
c++
if (c === 1) is(level, 'error')
if (c === 2) is(level, 'fatal')
}
}
}
})
logger.warn('ignored')
logger.error('test')
logger.fatal('test')
end()
})
test('transmit.level is effective even if lower than logger level', ({ end, is }) => {
let c = 0
const logger = pino({
level: 'error',
browser: {
write: noop,
transmit: {
level: 'info',
send (level) {
c++
if (c === 1) is(level, 'warn')
if (c === 2) is(level, 'error')
if (c === 3) is(level, 'fatal')
}
}
}
})
logger.warn('ignored')
logger.error('test')
logger.fatal('test')
end()
})
test('applies all serializers to messages and bindings (serialize:false - default)', ({ end, same, is }) => {
const logger = pino({
serializers: {
first: () => 'first',
second: () => 'second',
test: () => 'serialize it'
},
browser: {
write: noop,
transmit: {
send (level, logEvent) {
const messages = logEvent.messages
const bindings = logEvent.bindings
same(bindings[0], { first: 'first' })
same(bindings[1], { second: 'second' })
same(messages[0], { test: 'serialize it' })
is(messages[1].type, 'Error')
}
}
}
})
logger
.child({ first: 'binding' })
.child({ second: 'binding2' })
.fatal({ test: 'test' }, Error())
end()
})
test('applies all serializers to messages and bindings (serialize:true)', ({ end, same, is }) => {
const logger = pino({
serializers: {
first: () => 'first',
second: () => 'second',
test: () => 'serialize it'
},
browser: {
serialize: true,
write: noop,
transmit: {
send (level, logEvent) {
const messages = logEvent.messages
const bindings = logEvent.bindings
same(bindings[0], { first: 'first' })
same(bindings[1], { second: 'second' })
same(messages[0], { test: 'serialize it' })
is(messages[1].type, 'Error')
}
}
}
})
logger
.child({ first: 'binding' })
.child({ second: 'binding2' })
.fatal({ test: 'test' }, Error())
end()
})
test('extracts correct bindings and raw messages over multiple transmits', ({ end, same, is }) => {
let messages = null
let bindings = null
const logger = pino({
browser: {
write: noop,
transmit: {
send (level, logEvent) {
messages = logEvent.messages
bindings = logEvent.bindings
}
}
}
})
const child = logger.child({ child: true })
const grandchild = child.child({ grandchild: true })
logger.fatal({ test: 'parent:test1' })
logger.fatal({ test: 'parent:test2' })
same([], bindings)
same([{ test: 'parent:test2' }], messages)
child.fatal({ test: 'child:test1' })
child.fatal({ test: 'child:test2' })
same([{ child: true }], bindings)
same([{ test: 'child:test2' }], messages)
grandchild.fatal({ test: 'grandchild:test1' })
grandchild.fatal({ test: 'grandchild:test2' })
same([{ child: true }, { grandchild: true }], bindings)
same([{ test: 'grandchild:test2' }], messages)
end()
})
+594
View File
@@ -0,0 +1,594 @@
'use strict'
const test = require('tape')
const fresh = require('import-fresh')
const pinoStdSerializers = require('pino-std-serializers')
const pino = require('../browser')
levelTest('fatal')
levelTest('error')
levelTest('warn')
levelTest('info')
levelTest('debug')
levelTest('trace')
test('silent level', ({ end, fail, pass }) => {
const instance = pino({
level: 'silent',
browser: { write: fail }
})
instance.info('test')
const child = instance.child({ test: 'test' })
child.info('msg-test')
// use setTimeout because setImmediate isn't supported in most browsers
setTimeout(() => {
pass()
end()
}, 0)
})
test('enabled false', ({ end, fail, pass }) => {
const instance = pino({
enabled: false,
browser: { write: fail }
})
instance.info('test')
const child = instance.child({ test: 'test' })
child.info('msg-test')
// use setTimeout because setImmediate isn't supported in most browsers
setTimeout(() => {
pass()
end()
}, 0)
})
test('throw if creating child without bindings', ({ end, throws }) => {
const instance = pino()
throws(() => instance.child())
end()
})
test('stubs write, flush and ee methods on instance', ({ end, ok, is }) => {
const instance = pino()
ok(isFunc(instance.setMaxListeners))
ok(isFunc(instance.getMaxListeners))
ok(isFunc(instance.emit))
ok(isFunc(instance.addListener))
ok(isFunc(instance.on))
ok(isFunc(instance.prependListener))
ok(isFunc(instance.once))
ok(isFunc(instance.prependOnceListener))
ok(isFunc(instance.removeListener))
ok(isFunc(instance.removeAllListeners))
ok(isFunc(instance.listeners))
ok(isFunc(instance.listenerCount))
ok(isFunc(instance.eventNames))
ok(isFunc(instance.write))
ok(isFunc(instance.flush))
is(instance.on(), undefined)
end()
})
test('exposes levels object', ({ end, same }) => {
same(pino.levels, {
values: {
fatal: 60,
error: 50,
warn: 40,
info: 30,
debug: 20,
trace: 10
},
labels: {
10: 'trace',
20: 'debug',
30: 'info',
40: 'warn',
50: 'error',
60: 'fatal'
}
})
end()
})
test('exposes faux stdSerializers', ({ end, ok, same }) => {
ok(pino.stdSerializers)
// make sure faux stdSerializers match pino-std-serializers
for (const serializer in pinoStdSerializers) {
ok(pino.stdSerializers[serializer], `pino.stdSerializers.${serializer}`)
}
// confirm faux methods return empty objects
same(pino.stdSerializers.req(), {})
same(pino.stdSerializers.mapHttpRequest(), {})
same(pino.stdSerializers.mapHttpResponse(), {})
same(pino.stdSerializers.res(), {})
// confirm wrapping function is a passthrough
const noChange = { foo: 'bar', fuz: 42 }
same(pino.stdSerializers.wrapRequestSerializer(noChange), noChange)
same(pino.stdSerializers.wrapResponseSerializer(noChange), noChange)
end()
})
test('exposes err stdSerializer', ({ end, ok }) => {
ok(pino.stdSerializers.err)
ok(pino.stdSerializers.err(Error()))
end()
})
consoleMethodTest('error')
consoleMethodTest('fatal', 'error')
consoleMethodTest('warn')
consoleMethodTest('info')
consoleMethodTest('debug')
consoleMethodTest('trace')
absentConsoleMethodTest('error', 'log')
absentConsoleMethodTest('warn', 'error')
absentConsoleMethodTest('info', 'log')
absentConsoleMethodTest('debug', 'log')
absentConsoleMethodTest('trace', 'log')
// do not run this with airtap
if (process.title !== 'browser') {
test('in absence of console, log methods become noops', ({ end, ok }) => {
const console = global.console
delete global.console
const instance = fresh('../browser')()
global.console = console
ok(fnName(instance.log).match(/noop/))
ok(fnName(instance.fatal).match(/noop/))
ok(fnName(instance.error).match(/noop/))
ok(fnName(instance.warn).match(/noop/))
ok(fnName(instance.info).match(/noop/))
ok(fnName(instance.debug).match(/noop/))
ok(fnName(instance.trace).match(/noop/))
end()
})
}
test('opts.browser.asObject logs pino-like object to console', ({ end, ok, is }) => {
const info = console.info
console.info = function (o) {
is(o.level, 30)
is(o.msg, 'test')
ok(o.time)
console.info = info
}
const instance = require('../browser')({
browser: {
asObject: true
}
})
instance.info('test')
end()
})
test('opts.browser.formatters logs pino-like object to console', ({ end, ok, is }) => {
const info = console.info
console.info = function (o) {
is(o.level, 30)
is(o.label, 'info')
is(o.msg, 'test')
ok(o.time)
console.info = info
}
const instance = require('../browser')({
browser: {
formatters: {
level (label, number) {
return { label, level: number }
}
}
}
})
instance.info('test')
end()
})
test('opts.browser.formatters logs pino-like object to console', ({ end, ok, is }) => {
const info = console.info
console.info = function (o) {
is(o.level, 30)
is(o.msg, 'test')
is(o.hello, 'world')
is(o.newField, 'test')
ok(o.time, `Logged at ${o.time}`)
console.info = info
}
const instance = require('../browser')({
browser: {
formatters: {
log (o) {
return { ...o, newField: 'test', time: `Logged at ${o.time}` }
}
}
}
})
instance.info({ hello: 'world' }, 'test')
end()
})
test('opts.browser.write func log single string', ({ end, ok, is }) => {
const instance = pino({
browser: {
write: function (o) {
is(o.level, 30)
is(o.msg, 'test')
ok(o.time)
}
}
})
instance.info('test')
end()
})
test('opts.browser.write func string joining', ({ end, ok, is }) => {
const instance = pino({
browser: {
write: function (o) {
is(o.level, 30)
is(o.msg, 'test test2 test3')
ok(o.time)
}
}
})
instance.info('test %s %s', 'test2', 'test3')
end()
})
test('opts.browser.write func string joining when asObject is true', ({ end, ok, is }) => {
const instance = pino({
browser: {
asObject: true,
write: function (o) {
is(o.level, 30)
is(o.msg, 'test test2 test3')
ok(o.time)
}
}
})
instance.info('test %s %s', 'test2', 'test3')
end()
})
test('opts.browser.write func string joining when asObject is true', ({ end, ok, is }) => {
const instance = pino({
browser: {
asObject: true,
write: function (o) {
is(o.level, 30)
is(o.msg, 'test test2 test3')
ok(o.time)
}
}
})
instance.info('test %s %s', 'test2', 'test3')
end()
})
test('opts.browser.write func string object joining', ({ end, ok, is }) => {
const instance = pino({
browser: {
write: function (o) {
is(o.level, 30)
is(o.msg, 'test {"test":"test2"} {"test":"test3"}')
ok(o.time)
}
}
})
instance.info('test %j %j', { test: 'test2' }, { test: 'test3' })
end()
})
test('opts.browser.write func string object joining when asObject is true', ({ end, ok, is }) => {
const instance = pino({
browser: {
asObject: true,
write: function (o) {
is(o.level, 30)
is(o.msg, 'test {"test":"test2"} {"test":"test3"}')
ok(o.time)
}
}
})
instance.info('test %j %j', { test: 'test2' }, { test: 'test3' })
end()
})
test('opts.browser.write func string interpolation', ({ end, ok, is }) => {
const instance = pino({
browser: {
write: function (o) {
is(o.level, 30)
is(o.msg, 'test2 test ({"test":"test3"})')
ok(o.time)
}
}
})
instance.info('%s test (%j)', 'test2', { test: 'test3' })
end()
})
test('opts.browser.write func number', ({ end, ok, is }) => {
const instance = pino({
browser: {
write: function (o) {
is(o.level, 30)
is(o.msg, 1)
ok(o.time)
}
}
})
instance.info(1)
end()
})
test('opts.browser.write func log single object', ({ end, ok, is }) => {
const instance = pino({
browser: {
write: function (o) {
is(o.level, 30)
is(o.test, 'test')
ok(o.time)
}
}
})
instance.info({ test: 'test' })
end()
})
test('opts.browser.write obj writes to methods corresponding to level', ({ end, ok, is }) => {
const instance = pino({
browser: {
write: {
error: function (o) {
is(o.level, 50)
is(o.test, 'test')
ok(o.time)
}
}
}
})
instance.error({ test: 'test' })
end()
})
test('opts.browser.asObject/write supports child loggers', ({ end, ok, is }) => {
const instance = pino({
browser: {
write (o) {
is(o.level, 30)
is(o.test, 'test')
is(o.msg, 'msg-test')
ok(o.time)
}
}
})
const child = instance.child({ test: 'test' })
child.info('msg-test')
end()
})
test('opts.browser.asObject/write supports child child loggers', ({ end, ok, is }) => {
const instance = pino({
browser: {
write (o) {
is(o.level, 30)
is(o.test, 'test')
is(o.foo, 'bar')
is(o.msg, 'msg-test')
ok(o.time)
}
}
})
const child = instance.child({ test: 'test' }).child({ foo: 'bar' })
child.info('msg-test')
end()
})
test('opts.browser.asObject/write supports child child child loggers', ({ end, ok, is }) => {
const instance = pino({
browser: {
write (o) {
is(o.level, 30)
is(o.test, 'test')
is(o.foo, 'bar')
is(o.baz, 'bop')
is(o.msg, 'msg-test')
ok(o.time)
}
}
})
const child = instance.child({ test: 'test' }).child({ foo: 'bar' }).child({ baz: 'bop' })
child.info('msg-test')
end()
})
test('opts.browser.asObject defensively mitigates naughty numbers', ({ end, pass }) => {
const instance = pino({
browser: { asObject: true, write: () => {} }
})
const child = instance.child({ test: 'test' })
child._childLevel = -10
child.info('test')
pass() // if we reached here, there was no infinite loop, so, .. pass.
end()
})
test('opts.browser.write obj falls back to console where a method is not supplied', ({ end, ok, is }) => {
const info = console.info
console.info = (o) => {
is(o.level, 30)
is(o.msg, 'test')
ok(o.time)
console.info = info
}
const instance = require('../browser')({
browser: {
write: {
error (o) {
is(o.level, 50)
is(o.test, 'test')
ok(o.time)
}
}
}
})
instance.error({ test: 'test' })
instance.info('test')
end()
})
function levelTest (name) {
test(name + ' logs', ({ end, is }) => {
const msg = 'hello world'
sink(name, (args) => {
is(args[0], msg)
end()
})
pino({ level: name })[name](msg)
})
test('passing objects at level ' + name, ({ end, is }) => {
const msg = { hello: 'world' }
sink(name, (args) => {
is(args[0], msg)
end()
})
pino({ level: name })[name](msg)
})
test('passing an object and a string at level ' + name, ({ end, is }) => {
const a = { hello: 'world' }
const b = 'a string'
sink(name, (args) => {
is(args[0], a)
is(args[1], b)
end()
})
pino({ level: name })[name](a, b)
})
test('formatting logs as ' + name, ({ end, is }) => {
sink(name, (args) => {
is(args[0], 'hello %d')
is(args[1], 42)
end()
})
pino({ level: name })[name]('hello %d', 42)
})
test('passing error at level ' + name, ({ end, is }) => {
const err = new Error('myerror')
sink(name, (args) => {
is(args[0], err)
end()
})
pino({ level: name })[name](err)
})
test('passing error with a serializer at level ' + name, ({ end, is }) => {
// in browser - should have no effect (should not crash)
const err = new Error('myerror')
sink(name, (args) => {
is(args[0].err, err)
end()
})
const instance = pino({
level: name,
serializers: {
err: pino.stdSerializers.err
}
})
instance[name]({ err })
})
test('child logger for level ' + name, ({ end, is }) => {
const msg = 'hello world'
const parent = { hello: 'world' }
sink(name, (args) => {
is(args[0], parent)
is(args[1], msg)
end()
})
const instance = pino({ level: name })
const child = instance.child(parent)
child[name](msg)
})
test('child-child logger for level ' + name, ({ end, is }) => {
const msg = 'hello world'
const grandParent = { hello: 'world' }
const parent = { hello: 'you' }
sink(name, (args) => {
is(args[0], grandParent)
is(args[1], parent)
is(args[2], msg)
end()
})
const instance = pino({ level: name })
const child = instance.child(grandParent).child(parent)
child[name](msg)
})
}
function consoleMethodTest (level, method) {
if (!method) method = level
test('pino().' + level + ' uses console.' + method, ({ end, is }) => {
sink(method, (args) => {
is(args[0], 'test')
end()
})
const instance = require('../browser')({ level })
instance[level]('test')
})
}
function absentConsoleMethodTest (method, fallback) {
test('in absence of console.' + method + ', console.' + fallback + ' is used', ({ end, is }) => {
const fn = console[method]
console[method] = undefined
sink(fallback, function (args) {
is(args[0], 'test')
end()
console[method] = fn
})
const instance = require('../browser')({ level: method })
instance[method]('test')
})
}
function isFunc (fn) { return typeof fn === 'function' }
function fnName (fn) {
const rx = /^\s*function\s*([^(]*)/i
const match = rx.exec(fn)
return match && match[1]
}
function sink (method, fn) {
if (method === 'fatal') method = 'error'
const orig = console[method]
console[method] = function () {
console[method] = orig
fn(Array.prototype.slice.call(arguments))
}
}
+34
View File
@@ -0,0 +1,34 @@
'use strict'
const { test } = require('tap')
const { sink, once } = require('./helper')
const { PassThrough } = require('stream')
const pino = require('../')
test('Proxy and stream objects', async ({ equal }) => {
const s = new PassThrough()
s.resume()
s.write('', () => {})
const obj = { s, p: new Proxy({}, { get () { throw new Error('kaboom') } }) }
const stream = sink()
const instance = pino(stream)
instance.info({ obj })
const result = await once(stream, 'data')
equal(result.obj, '[unable to serialize, circular reference is too complex to analyze]')
})
test('Proxy and stream objects', async ({ equal }) => {
const s = new PassThrough()
s.resume()
s.write('', () => {})
const obj = { s, p: new Proxy({}, { get () { throw new Error('kaboom') } }) }
const stream = sink()
const instance = pino(stream)
instance.info(obj)
const result = await once(stream, 'data')
equal(result.p, '[unable to serialize, circular reference is too complex to analyze]')
})
+32
View File
@@ -0,0 +1,32 @@
'use strict'
const { test } = require('tap')
const writer = require('flush-write-stream')
const pino = require('../')
function capture () {
const ws = writer((chunk, enc, cb) => {
ws.data += chunk.toString()
cb()
})
ws.data = ''
return ws
}
test('pino uses LF by default', async ({ ok }) => {
const stream = capture()
const logger = pino(stream)
logger.info('foo')
logger.error('bar')
ok(/foo[^\r\n]+\n[^\r\n]+bar[^\r\n]+\n/.test(stream.data))
})
test('pino can log CRLF', async ({ ok }) => {
const stream = capture()
const logger = pino({
crlf: true
}, stream)
logger.info('foo')
logger.error('bar')
ok(/foo[^\n]+\r\n[^\n]+bar[^\n]+\r\n/.test(stream.data))
})
+253
View File
@@ -0,0 +1,253 @@
'use strict'
/* eslint no-prototype-builtins: 0 */
const { test } = require('tap')
const { sink, once } = require('./helper')
const pino = require('../')
// Silence all warnings for this test
process.removeAllListeners('warning')
process.on('warning', () => {})
test('adds additional levels', async ({ equal }) => {
const stream = sink()
const logger = pino({
customLevels: {
foo: 35,
bar: 45
}
}, stream)
logger.foo('test')
const { level } = await once(stream, 'data')
equal(level, 35)
})
test('custom levels does not override default levels', async ({ equal }) => {
const stream = sink()
const logger = pino({
customLevels: {
foo: 35
}
}, stream)
logger.info('test')
const { level } = await once(stream, 'data')
equal(level, 30)
})
test('default levels can be redefined using custom levels', async ({ equal }) => {
const stream = sink()
const logger = pino({
customLevels: {
info: 35,
debug: 45
},
useOnlyCustomLevels: true
}, stream)
equal(logger.hasOwnProperty('info'), true)
logger.info('test')
const { level } = await once(stream, 'data')
equal(level, 35)
})
test('custom levels overrides default level label if use useOnlyCustomLevels', async ({ equal }) => {
const stream = sink()
const logger = pino({
customLevels: {
foo: 35
},
useOnlyCustomLevels: true,
level: 'foo'
}, stream)
equal(logger.hasOwnProperty('info'), false)
})
test('custom levels overrides default level value if use useOnlyCustomLevels', async ({ equal }) => {
const stream = sink()
const logger = pino({
customLevels: {
foo: 35
},
useOnlyCustomLevels: true,
level: 35
}, stream)
equal(logger.hasOwnProperty('info'), false)
})
test('custom levels are inherited by children', async ({ equal }) => {
const stream = sink()
const logger = pino({
customLevels: {
foo: 35
}
}, stream)
logger.child({ childMsg: 'ok' }).foo('test')
const { msg, childMsg, level } = await once(stream, 'data')
equal(level, 35)
equal(childMsg, 'ok')
equal(msg, 'test')
})
test('custom levels can be specified on child bindings', async ({ equal }) => {
const stream = sink()
const logger = pino(stream).child({
childMsg: 'ok'
}, {
customLevels: {
foo: 35
}
})
logger.foo('test')
const { msg, childMsg, level } = await once(stream, 'data')
equal(level, 35)
equal(childMsg, 'ok')
equal(msg, 'test')
})
test('customLevels property child bindings does not get logged', async ({ equal }) => {
const stream = sink()
const logger = pino(stream).child({
childMsg: 'ok'
}, {
customLevels: {
foo: 35
}
})
logger.foo('test')
const { customLevels } = await once(stream, 'data')
equal(customLevels, undefined)
})
test('throws when specifying pre-existing parent labels via child bindings', async ({ throws }) => {
const stream = sink()
throws(() => pino({
customLevels: {
foo: 35
}
}, stream).child({}, {
customLevels: {
foo: 45
}
}), 'levels cannot be overridden')
})
test('throws when specifying pre-existing parent values via child bindings', async ({ throws }) => {
const stream = sink()
throws(() => pino({
customLevels: {
foo: 35
}
}, stream).child({}, {
customLevels: {
bar: 35
}
}), 'pre-existing level values cannot be used for new levels')
})
test('throws when specifying core values via child bindings', async ({ throws }) => {
const stream = sink()
throws(() => pino(stream).child({}, {
customLevels: {
foo: 30
}
}), 'pre-existing level values cannot be used for new levels')
})
test('throws when useOnlyCustomLevels is set true without customLevels', async ({ throws }) => {
const stream = sink()
throws(() => pino({
useOnlyCustomLevels: true
}, stream), 'customLevels is required if useOnlyCustomLevels is set true')
})
test('custom level on one instance does not affect other instances', async ({ equal }) => {
pino({
customLevels: {
foo: 37
}
})
equal(typeof pino().foo, 'undefined')
})
test('setting level below or at custom level will successfully log', async ({ equal }) => {
const stream = sink()
const instance = pino({ customLevels: { foo: 35 } }, stream)
instance.level = 'foo'
instance.info('nope')
instance.foo('bar')
const { msg } = await once(stream, 'data')
equal(msg, 'bar')
})
test('custom level below level threshold will not log', async ({ equal }) => {
const stream = sink()
const instance = pino({ customLevels: { foo: 15 } }, stream)
instance.level = 'info'
instance.info('bar')
instance.foo('nope')
const { msg } = await once(stream, 'data')
equal(msg, 'bar')
})
test('does not share custom level state across siblings', async ({ doesNotThrow }) => {
const stream = sink()
const logger = pino(stream)
logger.child({}, {
customLevels: { foo: 35 }
})
doesNotThrow(() => {
logger.child({}, {
customLevels: { foo: 35 }
})
})
})
test('custom level does not affect the levels serializer', async ({ equal }) => {
const stream = sink()
const logger = pino({
customLevels: {
foo: 35,
bar: 45
},
formatters: {
level (label, number) {
return { priority: number }
}
}
}, stream)
logger.foo('test')
const { priority } = await once(stream, 'data')
equal(priority, 35)
})
test('When useOnlyCustomLevels is set to true, the level formatter should only get custom levels', async ({ equal }) => {
const stream = sink()
const logger = pino({
customLevels: {
answer: 42
},
useOnlyCustomLevels: true,
level: 42,
formatters: {
level (label, number) {
equal(label, 'answer')
equal(number, 42)
return { level: number }
}
}
}, stream)
logger.answer('test')
const { level } = await once(stream, 'data')
equal(level, 42)
})
+398
View File
@@ -0,0 +1,398 @@
'use strict'
/* eslint no-prototype-builtins: 0 */
const os = require('os')
const { test } = require('tap')
const { sink, once } = require('./helper')
const pino = require('../')
const { pid } = process
const hostname = os.hostname()
const level = 50
const name = 'error'
test('err is serialized with additional properties set on the Error object', async ({ ok, same }) => {
const stream = sink()
const err = Object.assign(new Error('myerror'), { foo: 'bar' })
const instance = pino(stream)
instance.level = name
instance[name](err)
const result = await once(stream, 'data')
ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()')
delete result.time
same(result, {
pid,
hostname,
level,
err: {
type: 'Error',
message: err.message,
stack: err.stack,
foo: err.foo
},
msg: err.message
})
})
test('type should be detected based on constructor', async ({ ok, same }) => {
class Bar extends Error {}
const stream = sink()
const err = new Bar('myerror')
const instance = pino(stream)
instance.level = name
instance[name](err)
const result = await once(stream, 'data')
ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()')
delete result.time
same(result, {
pid,
hostname,
level,
err: {
type: 'Bar',
message: err.message,
stack: err.stack
},
msg: err.message
})
})
test('type, message and stack should be first level properties', async ({ ok, same }) => {
const stream = sink()
const err = Object.assign(new Error('foo'), { foo: 'bar' })
const instance = pino(stream)
instance.level = name
instance[name](err)
const result = await once(stream, 'data')
ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()')
delete result.time
same(result, {
pid,
hostname,
level,
err: {
type: 'Error',
message: err.message,
stack: err.stack,
foo: err.foo
},
msg: err.message
})
})
test('err serializer', async ({ ok, same }) => {
const stream = sink()
const err = Object.assign(new Error('myerror'), { foo: 'bar' })
const instance = pino({
serializers: {
err: pino.stdSerializers.err
}
}, stream)
instance.level = name
instance[name]({ err })
const result = await once(stream, 'data')
ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()')
delete result.time
same(result, {
pid,
hostname,
level,
err: {
type: 'Error',
message: err.message,
stack: err.stack,
foo: err.foo
},
msg: err.message
})
})
test('an error with statusCode property is not confused for a http response', async ({ ok, same }) => {
const stream = sink()
const err = Object.assign(new Error('StatusCodeErr'), { statusCode: 500 })
const instance = pino(stream)
instance.level = name
instance[name](err)
const result = await once(stream, 'data')
ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()')
delete result.time
same(result, {
pid,
hostname,
level,
err: {
type: 'Error',
message: err.message,
stack: err.stack,
statusCode: err.statusCode
},
msg: err.message
})
})
test('stack is omitted if it is not set on err', t => {
t.plan(2)
const err = new Error('myerror')
delete err.stack
const instance = pino(sink(function (chunk, enc, cb) {
t.ok(new Date(chunk.time) <= new Date(), 'time is greater than Date.now()')
delete chunk.time
t.equal(chunk.hasOwnProperty('stack'), false)
cb()
}))
instance.level = name
instance[name](err)
})
test('correctly ignores toString on errors', async ({ same }) => {
const err = new Error('myerror')
err.toString = () => undefined
const stream = sink()
const instance = pino({
test: 'this'
}, stream)
instance.fatal(err)
const result = await once(stream, 'data')
delete result.time
same(result, {
pid,
hostname,
level: 60,
err: {
type: 'Error',
message: err.message,
stack: err.stack
},
msg: err.message
})
})
test('assign mixin()', async ({ same }) => {
const err = new Error('myerror')
const stream = sink()
const instance = pino({
mixin () {
return { hello: 'world' }
}
}, stream)
instance.fatal(err)
const result = await once(stream, 'data')
delete result.time
same(result, {
pid,
hostname,
level: 60,
hello: 'world',
err: {
type: 'Error',
message: err.message,
stack: err.stack
},
msg: err.message
})
})
test('no err serializer', async ({ same }) => {
const err = new Error('myerror')
const stream = sink()
const instance = pino({
serializers: {}
}, stream)
instance.fatal(err)
const result = await once(stream, 'data')
delete result.time
same(result, {
pid,
hostname,
level: 60,
err: {
type: 'Error',
message: err.message,
stack: err.stack
},
msg: err.message
})
})
test('empty serializer', async ({ same }) => {
const err = new Error('myerror')
const stream = sink()
const instance = pino({
serializers: {
err () {}
}
}, stream)
instance.fatal(err)
const result = await once(stream, 'data')
delete result.time
same(result, {
pid,
hostname,
level: 60,
msg: err.message
})
})
test('assign mixin()', async ({ same }) => {
const err = new Error('myerror')
const stream = sink()
const instance = pino({
mixin () {
return { hello: 'world' }
}
}, stream)
instance.fatal(err)
const result = await once(stream, 'data')
delete result.time
same(result, {
pid,
hostname,
level: 60,
hello: 'world',
err: {
type: 'Error',
message: err.message,
stack: err.stack
},
msg: err.message
})
})
test('no err serializer', async ({ same }) => {
const err = new Error('myerror')
const stream = sink()
const instance = pino({
serializers: {}
}, stream)
instance.fatal(err)
const result = await once(stream, 'data')
delete result.time
same(result, {
pid,
hostname,
level: 60,
err: {
type: 'Error',
message: err.message,
stack: err.stack
},
msg: err.message
})
})
test('empty serializer', async ({ same }) => {
const err = new Error('myerror')
const stream = sink()
const instance = pino({
serializers: {
err () {}
}
}, stream)
instance.fatal(err)
const result = await once(stream, 'data')
delete result.time
same(result, {
pid,
hostname,
level: 60,
msg: err.message
})
})
test('correctly adds error information when nestedKey is used', async ({ same }) => {
const err = new Error('myerror')
err.toString = () => undefined
const stream = sink()
const instance = pino({
test: 'this',
nestedKey: 'obj'
}, stream)
instance.fatal(err)
const result = await once(stream, 'data')
delete result.time
same(result, {
pid,
hostname,
level: 60,
obj: {
err: {
type: 'Error',
stack: err.stack,
message: err.message
}
},
msg: err.message
})
})
test('correctly adds msg on error when nestedKey is used', async ({ same }) => {
const err = new Error('myerror')
err.toString = () => undefined
const stream = sink()
const instance = pino({
test: 'this',
nestedKey: 'obj'
}, stream)
instance.fatal(err, 'msg message')
const result = await once(stream, 'data')
delete result.time
same(result, {
pid,
hostname,
level: 60,
obj: {
err: {
type: 'Error',
stack: err.stack,
message: err.message
}
},
msg: 'msg message'
})
})
test('msg should take precedence over error message on mergingObject', async ({ same }) => {
const err = new Error('myerror')
const stream = sink()
const instance = pino(stream)
instance.error({ msg: 'my message', err })
const result = await once(stream, 'data')
delete result.time
same(result, {
pid,
hostname,
level: 50,
err: {
type: 'Error',
stack: err.stack,
message: err.message
},
msg: 'my message'
})
})
test('considers messageKey when giving msg precedence over error', async ({ same }) => {
const err = new Error('myerror')
const stream = sink()
const instance = pino({ messageKey: 'message' }, stream)
instance.error({ message: 'my message', err })
const result = await once(stream, 'data')
delete result.time
same(result, {
pid,
hostname,
level: 50,
err: {
type: 'Error',
stack: err.stack,
message: err.message
},
message: 'my message'
})
})
+34
View File
@@ -0,0 +1,34 @@
'use strict'
const { test } = require('tap')
const { sink, once } = require('./helper')
const stdSerializers = require('pino-std-serializers')
const pino = require('../')
test('set the errorKey with error serializer', async ({ equal, same }) => {
const stream = sink()
const errorKey = 'error'
const instance = pino({
errorKey,
serializers: { [errorKey]: stdSerializers.err }
}, stream)
instance.error(new ReferenceError('test'))
const o = await once(stream, 'data')
equal(typeof o[errorKey], 'object')
equal(o[errorKey].type, 'ReferenceError')
equal(o[errorKey].message, 'test')
equal(typeof o[errorKey].stack, 'string')
})
test('set the errorKey without error serializer', async ({ equal, same }) => {
const stream = sink()
const errorKey = 'error'
const instance = pino({
errorKey
}, stream)
instance.error(new ReferenceError('test'))
const o = await once(stream, 'data')
equal(typeof o[errorKey], 'object')
equal(o[errorKey].type, 'ReferenceError')
equal(o[errorKey].message, 'test')
equal(typeof o[errorKey].stack, 'string')
})
+91
View File
@@ -0,0 +1,91 @@
'use strict'
const os = require('os')
const { test } = require('tap')
const { sink, once } = require('./helper')
const pino = require('../')
const { pid } = process
const hostname = os.hostname()
function testEscape (ch, key) {
test('correctly escape ' + ch, async ({ same }) => {
const stream = sink()
const instance = pino({
name: 'hello'
}, stream)
instance.fatal('this contains ' + key)
const result = await once(stream, 'data')
delete result.time
same(result, {
pid,
hostname,
level: 60,
name: 'hello',
msg: 'this contains ' + key
})
})
}
testEscape('\\n', '\n')
testEscape('\\/', '/')
testEscape('\\\\', '\\')
testEscape('\\r', '\r')
testEscape('\\t', '\t')
testEscape('\\b', '\b')
const toEscape = [
'\u0000', // NUL Null character
'\u0001', // SOH Start of Heading
'\u0002', // STX Start of Text
'\u0003', // ETX End-of-text character
'\u0004', // EOT End-of-transmission character
'\u0005', // ENQ Enquiry character
'\u0006', // ACK Acknowledge character
'\u0007', // BEL Bell character
'\u0008', // BS Backspace
'\u0009', // HT Horizontal tab
'\u000A', // LF Line feed
'\u000B', // VT Vertical tab
'\u000C', // FF Form feed
'\u000D', // CR Carriage return
'\u000E', // SO Shift Out
'\u000F', // SI Shift In
'\u0010', // DLE Data Link Escape
'\u0011', // DC1 Device Control 1
'\u0012', // DC2 Device Control 2
'\u0013', // DC3 Device Control 3
'\u0014', // DC4 Device Control 4
'\u0015', // NAK Negative-acknowledge character
'\u0016', // SYN Synchronous Idle
'\u0017', // ETB End of Transmission Block
'\u0018', // CAN Cancel character
'\u0019', // EM End of Medium
'\u001A', // SUB Substitute character
'\u001B', // ESC Escape character
'\u001C', // FS File Separator
'\u001D', // GS Group Separator
'\u001E', // RS Record Separator
'\u001F' // US Unit Separator
]
toEscape.forEach((key) => {
testEscape(JSON.stringify(key), key)
})
test('correctly escape `hello \\u001F world \\n \\u0022`', async ({ same }) => {
const stream = sink()
const instance = pino({
name: 'hello'
}, stream)
instance.fatal('hello \u001F world \n \u0022')
const result = await once(stream, 'data')
delete result.time
same(result, {
pid,
hostname,
level: 60,
name: 'hello',
msg: 'hello \u001F world \n \u0022'
})
})
+12
View File
@@ -0,0 +1,12 @@
import t from 'tap'
import pino from '../../pino.js'
import helper from '../helper.js'
const { sink, check, once } = helper
t.test('esm support', async ({ equal }) => {
const stream = sink()
const instance = pino(stream)
instance.info('hello world')
check(equal, await once(stream, 'data'), 30, 'hello world')
})
+34
View File
@@ -0,0 +1,34 @@
'use strict'
const t = require('tap')
const semver = require('semver')
const { isYarnPnp } = require('../helper')
if (!semver.satisfies(process.versions.node, '^13.3.0 || ^12.10.0 || >= 14.0.0') || isYarnPnp) {
t.skip('Skip esm because not supported by Node')
} else {
// Node v8 throw a `SyntaxError: Unexpected token import`
// even if this branch is never touch in the code,
// by using `eval` we can avoid this issue.
// eslint-disable-next-line
new Function('module', 'return import(module)')('./esm.mjs').catch((err) => {
process.nextTick(() => {
throw err
})
})
}
if (!semver.satisfies(process.versions.node, '>= 14.13.0 || ^12.20.0') || isYarnPnp) {
t.skip('Skip named exports because not supported by Node')
} else {
// Node v8 throw a `SyntaxError: Unexpected token import`
// even if this branch is never touch in the code,
// by using `eval` we can avoid this issue.
// eslint-disable-next-line
new Function('module', 'return import(module)')('./named-exports.mjs').catch((err) => {
process.nextTick(() => {
throw err
})
})
}
+28
View File
@@ -0,0 +1,28 @@
import { tmpdir, hostname } from 'os'
import t from 'tap'
import { sink, check, once, watchFileCreated, file } from '../helper.js'
import { pino, destination } from '../../pino.js'
import { join } from 'path'
import { readFileSync } from 'fs'
t.test('named exports support', async ({ equal }) => {
const stream = sink()
const instance = pino(stream)
instance.info('hello world')
check(equal, await once(stream, 'data'), 30, 'hello world')
})
t.test('destination', async ({ same }) => {
const tmp = file()
const instance = pino(destination(tmp))
instance.info('hello')
await watchFileCreated(tmp)
const result = JSON.parse(readFileSync(tmp).toString())
delete result.time
same(result, {
pid: process.pid,
hostname,
level: 30,
msg: 'hello'
})
})
+77
View File
@@ -0,0 +1,77 @@
'use strict'
const { test } = require('tap')
const { join } = require('path')
const execa = require('execa')
const writer = require('flush-write-stream')
const { once } = require('./helper')
// https://github.com/pinojs/pino/issues/542
test('pino.destination log everything when calling process.exit(0)', async ({ not }) => {
let actual = ''
const child = execa(process.argv[0], [join(__dirname, 'fixtures', 'destination-exit.js')])
child.stdout.pipe(writer((s, enc, cb) => {
actual += s
cb()
}))
await once(child, 'close')
not(actual.match(/hello/), null)
not(actual.match(/world/), null)
})
test('pino with no args log everything when calling process.exit(0)', async ({ not }) => {
let actual = ''
const child = execa(process.argv[0], [join(__dirname, 'fixtures', 'default-exit.js')])
child.stdout.pipe(writer((s, enc, cb) => {
actual += s
cb()
}))
await once(child, 'close')
not(actual.match(/hello/), null)
not(actual.match(/world/), null)
})
test('sync false logs everything when calling process.exit(0)', async ({ not }) => {
let actual = ''
const child = execa(process.argv[0], [join(__dirname, 'fixtures', 'syncfalse-exit.js')])
child.stdout.pipe(writer((s, enc, cb) => {
actual += s
cb()
}))
await once(child, 'close')
not(actual.match(/hello/), null)
not(actual.match(/world/), null)
})
test('sync false logs everything when calling flushSync', async ({ not }) => {
let actual = ''
const child = execa(process.argv[0], [join(__dirname, 'fixtures', 'syncfalse-flush-exit.js')])
child.stdout.pipe(writer((s, enc, cb) => {
actual += s
cb()
}))
await once(child, 'close')
not(actual.match(/hello/), null)
not(actual.match(/world/), null)
})
test('transports exits gracefully when logging in exit', async ({ equal }) => {
const child = execa(process.argv[0], [join(__dirname, 'fixtures', 'transport-with-on-exit.js')])
child.stdout.resume()
const code = await once(child, 'close')
equal(code, 0)
})
@@ -0,0 +1,9 @@
'use strict'
global.process = { __proto__: process, pid: 123456 }
Date.now = function () { return 1459875739796 }
require('os').hostname = function () { return 'abcdefghijklmnopqr' }
const pino = require('../../..')()
pino.info('hello world')
@@ -0,0 +1,10 @@
'use strict'
global.process = { __proto__: process, pid: 123456 }
Date.now = function () { return 1459875739796 }
require('os').hostname = function () { return 'abcdefghijklmnopqr' }
const pino = require('../../..')
const logger = pino(pino.destination())
logger.info('hello world')
@@ -0,0 +1,12 @@
'use strict'
global.process = { __proto__: process, pid: 123456 }
Date.now = function () { return 1459875739796 }
require('os').hostname = function () { return 'abcdefghijklmnopqr' }
const pino = require('../../..')
const logger = pino(pino.destination({ sync: false }))
for (var i = 0; i < 1000; i++) {
logger.info('hello world')
}
@@ -0,0 +1,13 @@
const { Writable } = require('stream')
module.exports = (options) => {
const myTransportStream = new Writable({
autoDestroy: true,
write (chunk, enc, cb) {
// apply a transform and send to stdout
console.log(chunk.toString().toUpperCase())
cb()
}
})
return myTransportStream
}
@@ -0,0 +1,8 @@
global.process = { __proto__: process, pid: 123456 }
Date.now = function () { return 1459875739796 }
require('os').hostname = function () { return 'abcdefghijklmnopqr' }
const pino = require(require.resolve('./../../'))
const logger = pino()
logger.info('hello')
logger.info('world')
process.exit(0)
@@ -0,0 +1,8 @@
global.process = { __proto__: process, pid: 123456 }
Date.now = function () { return 1459875739796 }
require('os').hostname = function () { return 'abcdefghijklmnopqr' }
const pino = require(require.resolve('./../../'))
const logger = pino({}, pino.destination(1))
logger.info('hello')
logger.info('world')
process.exit(0)
+13
View File
@@ -0,0 +1,13 @@
/* eslint-disable no-eval */
eval(`
const pino = require('../../../')
const logger = pino(
pino.transport({
target: 'pino/file'
})
)
logger.info('done!')
`)
@@ -0,0 +1,3 @@
const file1 = require("./file1.js")
file1()
@@ -0,0 +1,3 @@
const file12 = require("./file12.js")
file12()
@@ -0,0 +1,5 @@
const file2 = require("./file2.js")
module.exports = function () {
file2()
}
@@ -0,0 +1,5 @@
const file11 = require("./file11.js")
module.exports = function () {
file11()
}
@@ -0,0 +1,5 @@
const file12 = require("./file12.js")
module.exports = function () {
file12()
}
@@ -0,0 +1,5 @@
const file13 = require("./file13.js")
module.exports = function () {
file13()
}
@@ -0,0 +1,5 @@
const file14 = require("./file14.js")
module.exports = function () {
file14()
}
@@ -0,0 +1,11 @@
const pino = require("../../../../");
module.exports = function() {
const logger = pino(
pino.transport({
target: 'pino/file'
})
)
logger.info('done!')
}
@@ -0,0 +1,5 @@
const file3 = require("./file3.js")
module.exports = function () {
file3()
}
@@ -0,0 +1,5 @@
const file4 = require("./file4.js")
module.exports = function () {
file4()
}
@@ -0,0 +1,5 @@
const file5 = require("./file5.js")
module.exports = function () {
file5()
}
@@ -0,0 +1,5 @@
const file6 = require("./file6.js")
module.exports = function () {
file6()
}
@@ -0,0 +1,5 @@
const file7 = require("./file7.js")
module.exports = function () {
file7()
}
@@ -0,0 +1,5 @@
const file8 = require("./file8.js")
module.exports = function () {
file8()
}
@@ -0,0 +1,5 @@
const file9 = require("./file9.js")
module.exports = function () {
file9()
}
@@ -0,0 +1,5 @@
const file10 = require("./file10.js")
module.exports = function () {
file10()
}
@@ -0,0 +1,10 @@
const { Writable } = require('stream')
module.exports = () => {
return new Writable({
autoDestroy: true,
write (chunk, enc, cb) {
cb()
}
})
}
@@ -0,0 +1,8 @@
global.process = { __proto__: process, pid: 123456 }
Date.now = function () { return 1459875739796 }
require('os').hostname = function () { return 'abcdefghijklmnopqr' }
const pino = require(require.resolve('./../../../'))
const log = pino({ prettyPrint: true })
const obj = Object.create(null)
Object.assign(obj, { foo: 'bar' })
log.info(obj, 'hello')
@@ -0,0 +1,11 @@
global.process = { __proto__: process, pid: 123456 }
const write = process.stdout.write.bind(process.stdout)
process.stdout.write = function (chunk) {
write('hack ' + chunk)
}
Date.now = function () { return 1459875739796 }
require('os').hostname = function () { return 'abcdefghijklmnopqr' }
const pino = require(require.resolve('../../'))()
pino.info('me')
@@ -0,0 +1,6 @@
global.process = { __proto__: process, pid: 123456 }
Date.now = function () { return 1459875739796 }
require('os').hostname = function () { return 'abcdefghijklmnopqr' }
const pino = require(require.resolve('./../../'))
const asyncLogger = pino(pino.destination({ sync: false })).child({ hello: 'world' })
asyncLogger.info('h')
@@ -0,0 +1,9 @@
global.process = { __proto__: process, pid: 123456 }
Date.now = function () { return 1459875739796 }
require('os').hostname = function () { return 'abcdefghijklmnopqr' }
const pino = require(require.resolve('./../../'))
const dest = pino.destination({ dest: 1, minLength: 4096, sync: false })
const logger = pino({}, dest)
logger.info('hello')
logger.info('world')
process.exit(0)
@@ -0,0 +1,10 @@
global.process = { __proto__: process, pid: 123456 }
Date.now = function () { return 1459875739796 }
require('os').hostname = function () { return 'abcdefghijklmnopqr' }
const pino = require(require.resolve('./../../'))
const dest = pino.destination({ dest: 1, minLength: 4096, sync: false })
const logger = pino({}, dest)
logger.info('hello')
logger.info('world')
dest.flushSync()
process.exit(0)
+6
View File
@@ -0,0 +1,6 @@
global.process = { __proto__: process, pid: 123456 }
Date.now = function () { return 1459875739796 }
require('os').hostname = function () { return 'abcdefghijklmnopqr' }
const pino = require(require.resolve('./../../'))
const asyncLogger = pino(pino.destination({ minLength: 4096, sync: false }))
asyncLogger.info('h')
@@ -0,0 +1,20 @@
'use strict'
const fs = require('fs')
const { once } = require('events')
const { Transform } = require('stream')
async function run (opts) {
if (!opts.destination) throw new Error('kaboom')
const stream = fs.createWriteStream(opts.destination)
await once(stream, 'open')
const t = new Transform({
transform (chunk, enc, cb) {
setImmediate(cb, null, chunk.toString().toUpperCase())
}
})
t.pipe(stream)
return t
}
module.exports = run
@@ -0,0 +1,13 @@
'use strict'
const fs = require('fs')
const { once } = require('events')
async function run (opts) {
if (!opts.destination) throw new Error('kaboom')
const stream = fs.createWriteStream(opts.destination)
await once(stream, 'open')
return stream
}
module.exports = run
@@ -0,0 +1,8 @@
import { createWriteStream } from 'fs'
import { once } from 'events'
export default async function run (opts) {
const stream = createWriteStream(opts.destination)
await once(stream, 'open')
return stream
}
@@ -0,0 +1,16 @@
'use strict'
const pino = require('../..')
const transport = pino.transport({
target: './to-file-transport-with-transform.js',
options: {
destination: process.argv[2]
}
})
const logger = pino(transport)
logger.info('Hello')
logger.info('World')
process.exit(0)
@@ -0,0 +1,11 @@
'use strict'
const pino = require('../..')
const transport = pino.transport({
target: 'pino/file'
})
const logger = pino(transport)
logger.info('Hello')
process.exit(0)
@@ -0,0 +1,12 @@
'use strict'
const pino = require('../..')
const transport = pino.transport({
target: 'pino/file'
})
const logger = pino(transport)
transport.on('ready', function () {
logger.info('Hello')
process.exit(0)
})
@@ -0,0 +1,9 @@
'use strict'
const { join } = require('path')
const pino = require('../..')
const transport = pino.transport({
target: join(__dirname, 'transport-worker.js')
})
const logger = pino(transport)
logger.info('Hello')
@@ -0,0 +1,29 @@
'use strict'
const pino = require('../..')
const transport = pino.transport({
targets: [{
level: 'info',
target: 'pino/file',
options: {
destination: process.argv[2]
}
}]
})
const logger = pino(transport)
const toWrite = 1000000
transport.on('ready', run)
let total = 0
function run () {
if (total++ === 8) {
return
}
for (let i = 0; i < toWrite; i++) {
logger.info(`hello ${i}`)
}
transport.once('drain', run)
}
@@ -0,0 +1,9 @@
'use strict'
const pino = require('../..')
const transport = pino.transport({
target: 'pino/file',
options: { destination: '1' }
})
const logger = pino(transport)
logger.info('Hello')
@@ -0,0 +1,21 @@
'use strict'
const build = require('pino-abstract-transport')
const { pipeline, Transform } = require('stream')
module.exports = (options) => {
return build(function (source) {
const myTransportStream = new Transform({
autoDestroy: true,
objectMode: true,
transform (chunk, enc, cb) {
chunk.service = 'pino'
this.push(JSON.stringify(chunk))
cb()
}
})
pipeline(source, myTransportStream, () => {})
return myTransportStream
}, {
enablePipelining: true
})
}
@@ -0,0 +1,33 @@
'use strict'
const build = require('pino-abstract-transport')
const { pipeline, Transform } = require('stream')
module.exports = () => {
return build(function (source) {
const myTransportStream = new Transform({
autoDestroy: true,
objectMode: true,
transform (chunk, enc, cb) {
const {
time,
level,
[source.messageKey]: body,
[source.errorKey]: error,
...attributes
} = chunk
this.push(JSON.stringify({
severityText: source.levels.labels[level],
body,
attributes,
...(error && { error })
}))
cb()
}
})
pipeline(source, myTransportStream, () => {})
return myTransportStream
}, {
enablePipelining: true,
expectPinoConfig: true
})
}
@@ -0,0 +1,12 @@
'use strict'
const pino = require('../..')
const log = pino({
transport: {
target: 'pino/file',
options: { destination: 1 }
}
})
log.info('hello world!')
process.on('exit', (code) => {
log.info('Exiting peacefully')
})
@@ -0,0 +1,19 @@
'use strict'
const { parentPort, workerData } = require('worker_threads')
const { Writable } = require('stream')
module.exports = (options) => {
const myTransportStream = new Writable({
autoDestroy: true,
write (chunk, enc, cb) {
parentPort.postMessage({
code: 'EVENT',
name: 'workerData',
args: [workerData]
})
cb()
}
})
return myTransportStream
}
@@ -0,0 +1,15 @@
'use strict'
const { Writable } = require('stream')
const fs = require('fs')
module.exports = (options) => {
const myTransportStream = new Writable({
autoDestroy: true,
write (chunk, enc, cb) {
// Bypass console.log() to avoid flakiness
fs.writeSync(1, chunk.toString())
cb()
}
})
return myTransportStream
}
@@ -0,0 +1,3 @@
module.exports = {
completelyUnrelatedProperty: 'Just a very incorrect transport worker implementation'
}
@@ -0,0 +1,12 @@
'use strict'
const fs = require('fs')
const { once } = require('events')
async function run (opts) {
const stream = fs.createWriteStream(opts.destination)
await once(stream, 'open')
return stream
}
module.exports = run
@@ -0,0 +1,5 @@
{
"name": "transport",
"version": "0.0.1",
"main": "./index.js"
}
@@ -0,0 +1,18 @@
import * as fs from 'fs'
import { once } from 'events'
import { Transform } from 'stream'
async function run (opts: { destination?: fs.PathLike }): Promise<Transform> {
if (!opts.destination) throw new Error('kaboom')
const stream = fs.createWriteStream(opts.destination)
await once(stream, 'open')
const t = new Transform({
transform (chunk, enc, cb) {
setImmediate(cb, null, chunk.toString().toUpperCase())
}
})
t.pipe(stream)
return t
}
export default run
@@ -0,0 +1,11 @@
import * as fs from 'fs'
import { once } from 'events'
async function run (opts: { destination?: fs.PathLike }): Promise<fs.WriteStream> {
if (!opts.destination) throw new Error('kaboom')
const stream = fs.createWriteStream(opts.destination, { encoding: 'utf8' })
await once(stream, 'open')
return stream
}
export default run
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env node
const execa = require('execa')
const fs = require('fs')
const existsSync = fs.existsSync
const stat = fs.promises.stat
// Hardcoded parameters
const esVersions = ['es5', 'es6', 'es2017', 'esnext']
const filesToTranspile = ['to-file-transport.ts']
async function transpile () {
process.chdir(__dirname)
for (const sourceFileName of filesToTranspile) {
const sourceStat = await stat(sourceFileName)
for (const esVersion of esVersions) {
const intermediateFileName = sourceFileName.replace(/\.ts$/, '.js')
const targetFileName = sourceFileName.replace(/\.ts$/, `.${esVersion}.cjs`)
const shouldTranspile = !existsSync(targetFileName) || (await stat(targetFileName)).mtimeMs < sourceStat.mtimeMs
if (shouldTranspile) {
await execa('tsc', ['--target', esVersion, '--module', 'commonjs', sourceFileName])
await execa('mv', [intermediateFileName, targetFileName])
}
}
}
}
transpile().catch(err => {
process.exitCode = 1
throw err
})
@@ -0,0 +1,15 @@
import pino from '../../..'
import { join } from 'path'
const transport = pino.transport({
target: join(__dirname, 'to-file-transport-with-transform.ts'),
options: {
destination: process.argv[2]
}
})
const logger = pino(transport)
logger.info('Hello')
logger.info('World')
process.exit(0)
@@ -0,0 +1,10 @@
import pino from '../../..'
const transport = pino.transport({
target: 'pino/file'
})
const logger = pino(transport)
logger.info('Hello')
process.exit(0)
@@ -0,0 +1,11 @@
import pino from '../../..'
const transport = pino.transport({
target: 'pino/file'
})
const logger = pino(transport)
transport.on('ready', function () {
logger.info('Hello')
process.exit(0)
})
@@ -0,0 +1,8 @@
import { join } from 'path'
import pino from '../../..'
const transport = pino.transport({
target: join(__dirname, 'transport-worker.ts')
})
const logger = pino(transport)
logger.info('Hello')
@@ -0,0 +1,8 @@
import pino from '../../..'
const transport = pino.transport({
target: 'pino/file',
options: { destination: '1' }
})
const logger = pino(transport)
logger.info('Hello')
@@ -0,0 +1,14 @@
import { Writable } from 'stream'
export default (): Writable => {
const myTransportStream = new Writable({
autoDestroy: true,
write (chunk, _enc, cb) {
console.log(chunk.toString())
cb()
},
defaultEncoding: 'utf8'
})
return myTransportStream
}
+355
View File
@@ -0,0 +1,355 @@
'use strict'
/* eslint no-prototype-builtins: 0 */
const { hostname } = require('os')
const { join } = require('path')
const { readFile } = require('fs').promises
const { test } = require('tap')
const { sink, once, watchFileCreated, file } = require('./helper')
const pino = require('../')
test('level formatter', async ({ match }) => {
const stream = sink()
const logger = pino({
formatters: {
level (label, number) {
return {
log: {
level: label
}
}
}
}
}, stream)
const o = once(stream, 'data')
logger.info('hello world')
match(await o, {
log: {
level: 'info'
}
})
})
test('bindings formatter', async ({ match }) => {
const stream = sink()
const logger = pino({
formatters: {
bindings (bindings) {
return {
process: {
pid: bindings.pid
},
host: {
name: bindings.hostname
}
}
}
}
}, stream)
const o = once(stream, 'data')
logger.info('hello world')
match(await o, {
process: {
pid: process.pid
},
host: {
name: hostname()
}
})
})
test('no bindings formatter', async ({ match, notOk }) => {
const stream = sink()
const logger = pino({
formatters: {
bindings (bindings) {
return null
}
}
}, stream)
const o = once(stream, 'data')
logger.info('hello world')
const log = await o
notOk(log.hasOwnProperty('pid'))
notOk(log.hasOwnProperty('hostname'))
match(log, { msg: 'hello world' })
})
test('log formatter', async ({ match, equal }) => {
const stream = sink()
const logger = pino({
formatters: {
log (obj) {
equal(obj.hasOwnProperty('msg'), false)
return { hello: 'world', ...obj }
}
}
}, stream)
const o = once(stream, 'data')
logger.info({ foo: 'bar', nested: { object: true } }, 'hello world')
match(await o, {
hello: 'world',
foo: 'bar',
nested: { object: true }
})
})
test('Formatters combined', async ({ match }) => {
const stream = sink()
const logger = pino({
formatters: {
level (label, number) {
return {
log: {
level: label
}
}
},
bindings (bindings) {
return {
process: {
pid: bindings.pid
},
host: {
name: bindings.hostname
}
}
},
log (obj) {
return { hello: 'world', ...obj }
}
}
}, stream)
const o = once(stream, 'data')
logger.info({ foo: 'bar', nested: { object: true } }, 'hello world')
match(await o, {
log: {
level: 'info'
},
process: {
pid: process.pid
},
host: {
name: hostname()
},
hello: 'world',
foo: 'bar',
nested: { object: true }
})
})
test('Formatters in child logger', async ({ match }) => {
const stream = sink()
const logger = pino({
formatters: {
level (label, number) {
return {
log: {
level: label
}
}
},
bindings (bindings) {
return {
process: {
pid: bindings.pid
},
host: {
name: bindings.hostname
}
}
},
log (obj) {
return { hello: 'world', ...obj }
}
}
}, stream)
const child = logger.child({
foo: 'bar',
nested: { object: true }
}, {
formatters: {
bindings (bindings) {
return { ...bindings, faz: 'baz' }
}
}
})
const o = once(stream, 'data')
child.info('hello world')
match(await o, {
log: {
level: 'info'
},
process: {
pid: process.pid
},
host: {
name: hostname()
},
hello: 'world',
foo: 'bar',
nested: { object: true },
faz: 'baz'
})
})
test('Formatters without bindings in child logger', async ({ match }) => {
const stream = sink()
const logger = pino({
formatters: {
level (label, number) {
return {
log: {
level: label
}
}
},
bindings (bindings) {
return {
process: {
pid: bindings.pid
},
host: {
name: bindings.hostname
}
}
},
log (obj) {
return { hello: 'world', ...obj }
}
}
}, stream)
const child = logger.child({
foo: 'bar',
nested: { object: true }
}, {
formatters: {
log (obj) {
return { other: 'stuff', ...obj }
}
}
})
const o = once(stream, 'data')
child.info('hello world')
match(await o, {
log: {
level: 'info'
},
process: {
pid: process.pid
},
host: {
name: hostname()
},
foo: 'bar',
other: 'stuff',
nested: { object: true }
})
})
test('elastic common schema format', async ({ match, type }) => {
const stream = sink()
const ecs = {
formatters: {
level (label, number) {
return {
log: {
level: label,
logger: 'pino'
}
}
},
bindings (bindings) {
return {
process: {
pid: bindings.pid
},
host: {
name: bindings.hostname
}
}
},
log (obj) {
return { ecs: { version: '1.4.0' }, ...obj }
}
},
messageKey: 'message',
timestamp: () => `,"@timestamp":"${new Date(Date.now()).toISOString()}"`
}
const logger = pino({ ...ecs }, stream)
const o = once(stream, 'data')
logger.info({ foo: 'bar' }, 'hello world')
const log = await o
type(log['@timestamp'], 'string')
match(log, {
log: { level: 'info', logger: 'pino' },
process: { pid: process.pid },
host: { name: hostname() },
ecs: { version: '1.4.0' },
foo: 'bar',
message: 'hello world'
})
})
test('formatter with transport', async ({ match, equal }) => {
const destination = file()
const logger = pino({
formatters: {
log (obj) {
equal(obj.hasOwnProperty('msg'), false)
return { hello: 'world', ...obj }
}
},
transport: {
targets: [
{
target: join(__dirname, 'fixtures', 'to-file-transport.js'),
options: { destination }
}
]
}
})
logger.info({ foo: 'bar', nested: { object: true } }, 'hello world')
await watchFileCreated(destination)
const result = JSON.parse(await readFile(destination))
delete result.time
match(result, {
hello: 'world',
foo: 'bar',
nested: { object: true }
})
})
test('throws when custom level formatter is used with transport.targets', async ({ throws }) => {
throws(() => {
pino({
formatters: {
level (label) {
return label
}
},
transport: {
targets: [
{
target: 'pino/file',
options: { destination: 'foo.log' }
}
]
}
}
)
},
Error('option.transport.targets do not allow custom level formatters'))
})
+4
View File
@@ -0,0 +1,4 @@
import { PathLike } from 'fs'
export declare function watchFileCreated(filename: PathLike): Promise<void>
export declare function watchForWrite(filename: PathLike, testString: string): Promise<void>
+128
View File
@@ -0,0 +1,128 @@
'use strict'
const crypto = require('crypto')
const os = require('os')
const writer = require('flush-write-stream')
const split = require('split2')
const { existsSync, readFileSync, statSync, unlinkSync } = require('fs')
const pid = process.pid
const hostname = os.hostname()
const t = require('tap')
const { join } = require('path')
const { tmpdir } = os
const isWin = process.platform === 'win32'
const isYarnPnp = process.versions.pnp !== undefined
function getPathToNull () {
return isWin ? '\\\\.\\NUL' : '/dev/null'
}
function once (emitter, name) {
return new Promise((resolve, reject) => {
if (name !== 'error') emitter.once('error', reject)
emitter.once(name, (...args) => {
emitter.removeListener('error', reject)
resolve(...args)
})
})
}
function sink (func) {
const result = split((data) => {
try {
return JSON.parse(data)
} catch (err) {
console.log(err)
console.log(data)
}
})
if (func) result.pipe(writer.obj(func))
return result
}
function check (is, chunk, level, msg) {
is(new Date(chunk.time) <= new Date(), true, 'time is greater than Date.now()')
delete chunk.time
is(chunk.pid, pid)
is(chunk.hostname, hostname)
is(chunk.level, level)
is(chunk.msg, msg)
}
function sleep (ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
function watchFileCreated (filename) {
return new Promise((resolve, reject) => {
const TIMEOUT = process.env.PINO_TEST_WAIT_WATCHFILE_TIMEOUT || 10000
const INTERVAL = 100
const threshold = TIMEOUT / INTERVAL
let counter = 0
const interval = setInterval(() => {
const exists = existsSync(filename)
// On some CI runs file is created but not filled
if (exists && statSync(filename).size !== 0) {
clearInterval(interval)
resolve()
} else if (counter <= threshold) {
counter++
} else {
clearInterval(interval)
reject(new Error(
`${filename} hasn't been created within ${TIMEOUT} ms. ` +
(exists ? 'File exist, but still empty.' : 'File not yet created.')
))
}
}, INTERVAL)
})
}
function watchForWrite (filename, testString) {
return new Promise((resolve, reject) => {
const TIMEOUT = process.env.PINO_TEST_WAIT_WRITE_TIMEOUT || 10000
const INTERVAL = 100
const threshold = TIMEOUT / INTERVAL
let counter = 0
const interval = setInterval(() => {
if (readFileSync(filename).includes(testString)) {
clearInterval(interval)
resolve()
} else if (counter <= threshold) {
counter++
} else {
clearInterval(interval)
reject(new Error(`'${testString}' hasn't been written to ${filename} within ${TIMEOUT} ms.`))
}
}, INTERVAL)
})
}
let files = []
function file () {
const hash = crypto.randomBytes(12).toString('hex')
const file = join(tmpdir(), `pino-${pid}-${hash}`)
files.push(file)
return file
}
process.on('beforeExit', () => {
if (files.length === 0) return
t.comment('unlink files')
for (const file of files) {
try {
t.comment(`unliking ${file}`)
unlinkSync(file)
} catch (e) {
console.log(e)
}
}
files = []
t.comment('unlink completed')
})
module.exports = { getPathToNull, sink, check, once, sleep, watchFileCreated, watchForWrite, isWin, isYarnPnp, file }
+97
View File
@@ -0,0 +1,97 @@
'use strict'
const tap = require('tap')
const { sink, once } = require('./helper')
const pino = require('../')
tap.test('log method hook', t => {
t.test('gets invoked', async t => {
t.plan(8)
const stream = sink()
const logger = pino({
hooks: {
logMethod (args, method, level) {
t.type(args, Array)
t.type(level, 'number')
t.equal(args.length, 3)
t.equal(level, this.levels.values.info)
t.same(args, ['a', 'b', 'c'])
t.type(method, Function)
t.equal(method.name, 'LOG')
method.apply(this, [args.join('-')])
}
}
}, stream)
const o = once(stream, 'data')
logger.info('a', 'b', 'c')
t.match(await o, { msg: 'a-b-c' })
})
t.test('fatal method invokes hook', async t => {
t.plan(2)
const stream = sink()
const logger = pino({
hooks: {
logMethod (args, method) {
t.pass()
method.apply(this, [args.join('-')])
}
}
}, stream)
const o = once(stream, 'data')
logger.fatal('a')
t.match(await o, { msg: 'a' })
})
t.test('children get the hook', async t => {
t.plan(4)
const stream = sink()
const root = pino({
hooks: {
logMethod (args, method) {
t.pass()
method.apply(this, [args.join('-')])
}
}
}, stream)
const child = root.child({ child: 'one' })
const grandchild = child.child({ child: 'two' })
let o = once(stream, 'data')
child.info('a', 'b')
t.match(await o, { msg: 'a-b' })
o = once(stream, 'data')
grandchild.info('c', 'd')
t.match(await o, { msg: 'c-d' })
})
t.test('get log level', async t => {
t.plan(3)
const stream = sink()
const logger = pino({
hooks: {
logMethod (args, method, level) {
t.type(level, 'number')
t.equal(level, this.levels.values.error)
method.apply(this, [args.join('-')])
}
}
}, stream)
const o = once(stream, 'data')
logger.error('a')
t.match(await o, { msg: 'a' })
})
t.end()
})
+242
View File
@@ -0,0 +1,242 @@
'use strict'
const http = require('http')
const os = require('os')
const semver = require('semver')
const { test, skip } = require('tap')
const { sink, once } = require('./helper')
const pino = require('../')
const { pid } = process
const hostname = os.hostname()
test('http request support', async ({ ok, same, error, teardown }) => {
let originalReq
const instance = pino(sink((chunk, enc) => {
ok(new Date(chunk.time) <= new Date(), 'time is greater than Date.now()')
delete chunk.time
same(chunk, {
pid,
hostname,
level: 30,
msg: 'my request',
req: {
method: originalReq.method,
url: originalReq.url,
headers: originalReq.headers,
remoteAddress: originalReq.socket.remoteAddress,
remotePort: originalReq.socket.remotePort
}
})
}))
const server = http.createServer((req, res) => {
originalReq = req
instance.info(req, 'my request')
res.end('hello')
})
server.unref()
server.listen()
const err = await once(server, 'listening')
error(err)
const res = await once(http.get('http://localhost:' + server.address().port), 'response')
res.resume()
server.close()
})
test('http request support via serializer', async ({ ok, same, error, teardown }) => {
let originalReq
const instance = pino({
serializers: {
req: pino.stdSerializers.req
}
}, sink((chunk, enc) => {
ok(new Date(chunk.time) <= new Date(), 'time is greater than Date.now()')
delete chunk.time
same(chunk, {
pid,
hostname,
level: 30,
msg: 'my request',
req: {
method: originalReq.method,
url: originalReq.url,
headers: originalReq.headers,
remoteAddress: originalReq.socket.remoteAddress,
remotePort: originalReq.socket.remotePort
}
})
}))
const server = http.createServer(function (req, res) {
originalReq = req
instance.info({ req }, 'my request')
res.end('hello')
})
server.unref()
server.listen()
const err = await once(server, 'listening')
error(err)
const res = await once(http.get('http://localhost:' + server.address().port), 'response')
res.resume()
server.close()
})
// skipped because request connection is deprecated since v13, and request socket is always available
skip('http request support via serializer without request connection', async ({ ok, same, error, teardown }) => {
let originalReq
const instance = pino({
serializers: {
req: pino.stdSerializers.req
}
}, sink((chunk, enc) => {
ok(new Date(chunk.time) <= new Date(), 'time is greater than Date.now()')
delete chunk.time
const expected = {
pid,
hostname,
level: 30,
msg: 'my request',
req: {
method: originalReq.method,
url: originalReq.url,
headers: originalReq.headers
}
}
if (semver.gte(process.version, '13.0.0')) {
expected.req.remoteAddress = originalReq.socket.remoteAddress
expected.req.remotePort = originalReq.socket.remotePort
}
same(chunk, expected)
}))
const server = http.createServer(function (req, res) {
originalReq = req
delete req.connection
instance.info({ req }, 'my request')
res.end('hello')
})
server.unref()
server.listen()
const err = await once(server, 'listening')
error(err)
const res = await once(http.get('http://localhost:' + server.address().port), 'response')
res.resume()
server.close()
})
test('http response support', async ({ ok, same, error, teardown }) => {
let originalRes
const instance = pino(sink((chunk, enc) => {
ok(new Date(chunk.time) <= new Date(), 'time is greater than Date.now()')
delete chunk.time
same(chunk, {
pid,
hostname,
level: 30,
msg: 'my response',
res: {
statusCode: originalRes.statusCode,
headers: originalRes.getHeaders()
}
})
}))
const server = http.createServer(function (req, res) {
originalRes = res
res.end('hello')
instance.info(res, 'my response')
})
server.unref()
server.listen()
const err = await once(server, 'listening')
error(err)
const res = await once(http.get('http://localhost:' + server.address().port), 'response')
res.resume()
server.close()
})
test('http response support via a serializer', async ({ ok, same, error, teardown }) => {
const instance = pino({
serializers: {
res: pino.stdSerializers.res
}
}, sink((chunk, enc) => {
ok(new Date(chunk.time) <= new Date(), 'time is greater than Date.now()')
delete chunk.time
same(chunk, {
pid,
hostname,
level: 30,
msg: 'my response',
res: {
statusCode: 200,
headers: {
'x-single': 'y',
'x-multi': [1, 2]
}
}
})
}))
const server = http.createServer(function (req, res) {
res.setHeader('x-single', 'y')
res.setHeader('x-multi', [1, 2])
res.end('hello')
instance.info({ res }, 'my response')
})
server.unref()
server.listen()
const err = await once(server, 'listening')
error(err)
const res = await once(http.get('http://localhost:' + server.address().port), 'response')
res.resume()
server.close()
})
test('http request support via serializer in a child', async ({ ok, same, error, teardown }) => {
let originalReq
const instance = pino({
serializers: {
req: pino.stdSerializers.req
}
}, sink((chunk, enc) => {
ok(new Date(chunk.time) <= new Date(), 'time is greater than Date.now()')
delete chunk.time
same(chunk, {
pid,
hostname,
level: 30,
msg: 'my request',
req: {
method: originalReq.method,
url: originalReq.url,
headers: originalReq.headers,
remoteAddress: originalReq.socket.remoteAddress,
remotePort: originalReq.socket.remotePort
}
})
}))
const server = http.createServer(function (req, res) {
originalReq = req
const child = instance.child({ req })
child.info('my request')
res.end('hello')
})
server.unref()
server.listen()
const err = await once(server, 'listening')
error(err)
const res = await once(http.get('http://localhost:' + server.address().port), 'response')
res.resume()
server.close()
})
@@ -0,0 +1,15 @@
'use strict'
const fs = require('fs')
const path = require('path')
const t = require('tap')
const test = t.test
const pino = require('../..')()
test('should be the same as package.json', t => {
t.plan(1)
const json = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json')).toString('utf8'))
t.equal(pino.version, json.version)
})
+185
View File
@@ -0,0 +1,185 @@
'use strict'
const { test } = require('tap')
const pino = require('../')
const descLevels = {
trace: 60,
debug: 50,
info: 40,
warn: 30,
error: 20,
fatal: 10
}
const ascLevels = {
trace: 10,
debug: 20,
info: 30,
warn: 40,
error: 50,
fatal: 60
}
test('Default levels suite', ({ test, end }) => {
test('can check if current level enabled', async ({ equal }) => {
const log = pino({ level: 'debug' })
equal(true, log.isLevelEnabled('debug'))
})
test('can check if level enabled after level set', async ({ equal }) => {
const log = pino()
equal(false, log.isLevelEnabled('debug'))
log.level = 'debug'
equal(true, log.isLevelEnabled('debug'))
})
test('can check if higher level enabled', async ({ equal }) => {
const log = pino({ level: 'debug' })
equal(true, log.isLevelEnabled('error'))
})
test('can check if lower level is disabled', async ({ equal }) => {
const log = pino({ level: 'error' })
equal(false, log.isLevelEnabled('trace'))
})
test('ASC: can check if child has current level enabled', async ({ equal }) => {
const log = pino().child({}, { level: 'debug' })
equal(true, log.isLevelEnabled('debug'))
equal(true, log.isLevelEnabled('error'))
equal(false, log.isLevelEnabled('trace'))
})
test('can check if custom level is enabled', async ({ equal }) => {
const log = pino({
customLevels: { foo: 35 },
level: 'debug'
})
equal(true, log.isLevelEnabled('foo'))
equal(true, log.isLevelEnabled('error'))
equal(false, log.isLevelEnabled('trace'))
})
end()
})
test('Ascending levels suite', ({ test, end }) => {
const customLevels = ascLevels
const levelComparison = 'ASC'
test('can check if current level enabled', async ({ equal }) => {
const log = pino({ level: 'debug', levelComparison, customLevels, useOnlyCustomLevels: true })
equal(true, log.isLevelEnabled('debug'))
})
test('can check if level enabled after level set', async ({ equal }) => {
const log = pino({ levelComparison, customLevels, useOnlyCustomLevels: true })
equal(false, log.isLevelEnabled('debug'))
log.level = 'debug'
equal(true, log.isLevelEnabled('debug'))
})
test('can check if higher level enabled', async ({ equal }) => {
const log = pino({ level: 'debug', levelComparison, customLevels, useOnlyCustomLevels: true })
equal(true, log.isLevelEnabled('error'))
})
test('can check if lower level is disabled', async ({ equal }) => {
const log = pino({ level: 'error', customLevels, useOnlyCustomLevels: true })
equal(false, log.isLevelEnabled('trace'))
})
test('can check if child has current level enabled', async ({ equal }) => {
const log = pino().child({ levelComparison, customLevels, useOnlyCustomLevels: true }, { level: 'debug' })
equal(true, log.isLevelEnabled('debug'))
equal(true, log.isLevelEnabled('error'))
equal(false, log.isLevelEnabled('trace'))
})
test('can check if custom level is enabled', async ({ equal }) => {
const log = pino({
levelComparison,
useOnlyCustomLevels: true,
customLevels: { foo: 35, ...customLevels },
level: 'debug'
})
equal(true, log.isLevelEnabled('foo'))
equal(true, log.isLevelEnabled('error'))
equal(false, log.isLevelEnabled('trace'))
})
end()
})
test('Descending levels suite', ({ test, end }) => {
const customLevels = descLevels
const levelComparison = 'DESC'
test('can check if current level enabled', async ({ equal }) => {
const log = pino({ level: 'debug', levelComparison, customLevels, useOnlyCustomLevels: true })
equal(true, log.isLevelEnabled('debug'))
})
test('can check if level enabled after level set', async ({ equal }) => {
const log = pino({ levelComparison, customLevels, useOnlyCustomLevels: true })
equal(false, log.isLevelEnabled('debug'))
log.level = 'debug'
equal(true, log.isLevelEnabled('debug'))
})
test('can check if higher level enabled', async ({ equal }) => {
const log = pino({ level: 'debug', levelComparison, customLevels, useOnlyCustomLevels: true })
equal(true, log.isLevelEnabled('error'))
})
test('can check if lower level is disabled', async ({ equal }) => {
const log = pino({ level: 'error', levelComparison, customLevels, useOnlyCustomLevels: true })
equal(false, log.isLevelEnabled('trace'))
})
test('can check if child has current level enabled', async ({ equal }) => {
const log = pino({ levelComparison, customLevels, useOnlyCustomLevels: true }).child({}, { level: 'debug' })
equal(true, log.isLevelEnabled('debug'))
equal(true, log.isLevelEnabled('error'))
equal(false, log.isLevelEnabled('trace'))
})
test('can check if custom level is enabled', async ({ equal }) => {
const log = pino({
levelComparison,
customLevels: { foo: 35, ...customLevels },
useOnlyCustomLevels: true,
level: 'debug'
})
equal(true, log.isLevelEnabled('foo'))
equal(true, log.isLevelEnabled('error'))
equal(false, log.isLevelEnabled('trace'))
})
end()
})
test('Custom levels comparison', async ({ test, end }) => {
test('Custom comparison returns true cause level is enabled', async ({ equal }) => {
const log = pino({ level: 'error', levelComparison: () => true })
equal(true, log.isLevelEnabled('debug'))
})
test('Custom comparison returns false cause level is disabled', async ({ equal }) => {
const log = pino({ level: 'error', levelComparison: () => false })
equal(false, log.isLevelEnabled('debug'))
})
test('Custom comparison returns true cause child level is enabled', async ({ equal }) => {
const log = pino({ levelComparison: () => true }).child({ level: 'error' })
equal(true, log.isLevelEnabled('debug'))
})
test('Custom comparison returns false cause child level is disabled', async ({ equal }) => {
const log = pino({ levelComparison: () => false }).child({ level: 'error' })
equal(false, log.isLevelEnabled('debug'))
})
end()
})
+10
View File
@@ -0,0 +1,10 @@
/* global test */
const pino = require('../../pino')
test('transport should work in jest', function () {
pino({
transport: {
target: 'pino-pretty'
}
})
})
+706
View File
@@ -0,0 +1,706 @@
'use strict'
const { test } = require('tap')
const { sink, once, check } = require('./helper')
const pino = require('../')
const levelsLib = require('../lib/levels')
// Silence all warnings for this test
process.removeAllListeners('warning')
process.on('warning', () => {})
test('set the level by string', async ({ equal }) => {
const expected = [{
level: 50,
msg: 'this is an error'
}, {
level: 60,
msg: 'this is fatal'
}]
const stream = sink()
const instance = pino(stream)
instance.level = 'error'
instance.info('hello world')
instance.error('this is an error')
instance.fatal('this is fatal')
const result = await once(stream, 'data')
const current = expected.shift()
check(equal, result, current.level, current.msg)
})
test('the wrong level throws', async ({ throws }) => {
const instance = pino()
throws(() => {
instance.level = 'kaboom'
})
})
test('set the level by number', async ({ equal }) => {
const expected = [{
level: 50,
msg: 'this is an error'
}, {
level: 60,
msg: 'this is fatal'
}]
const stream = sink()
const instance = pino(stream)
instance.level = 50
instance.info('hello world')
instance.error('this is an error')
instance.fatal('this is fatal')
const result = await once(stream, 'data')
const current = expected.shift()
check(equal, result, current.level, current.msg)
})
test('exposes level string mappings', async ({ equal }) => {
equal(pino.levels.values.error, 50)
})
test('exposes level number mappings', async ({ equal }) => {
equal(pino.levels.labels[50], 'error')
})
test('returns level integer', async ({ equal }) => {
const instance = pino({ level: 'error' })
equal(instance.levelVal, 50)
})
test('child returns level integer', async ({ equal }) => {
const parent = pino({ level: 'error' })
const child = parent.child({ foo: 'bar' })
equal(child.levelVal, 50)
})
test('set the level via exported pino function', async ({ equal }) => {
const expected = [{
level: 50,
msg: 'this is an error'
}, {
level: 60,
msg: 'this is fatal'
}]
const stream = sink()
const instance = pino({ level: 'error' }, stream)
instance.info('hello world')
instance.error('this is an error')
instance.fatal('this is fatal')
const result = await once(stream, 'data')
const current = expected.shift()
check(equal, result, current.level, current.msg)
})
test('level-change event', async ({ equal }) => {
const instance = pino()
function handle (lvl, val, prevLvl, prevVal, logger) {
equal(lvl, 'trace')
equal(val, 10)
equal(prevLvl, 'info')
equal(prevVal, 30)
equal(logger, instance)
}
instance.on('level-change', handle)
instance.level = 'trace'
instance.removeListener('level-change', handle)
instance.level = 'info'
let count = 0
const l1 = () => count++
const l2 = () => count++
const l3 = () => count++
instance.on('level-change', l1)
instance.on('level-change', l2)
instance.on('level-change', l3)
instance.level = 'trace'
instance.removeListener('level-change', l3)
instance.level = 'fatal'
instance.removeListener('level-change', l1)
instance.level = 'debug'
instance.removeListener('level-change', l2)
instance.level = 'info'
equal(count, 6)
instance.once('level-change', (lvl, val, prevLvl, prevVal, logger) => equal(logger, instance))
instance.level = 'info'
const child = instance.child({})
instance.once('level-change', (lvl, val, prevLvl, prevVal, logger) => equal(logger, child))
child.level = 'trace'
})
test('enable', async ({ fail }) => {
const instance = pino({
level: 'trace',
enabled: false
}, sink((result, enc) => {
fail('no data should be logged')
}))
Object.keys(pino.levels.values).forEach((level) => {
instance[level]('hello world')
})
})
test('silent level', async ({ fail }) => {
const instance = pino({
level: 'silent'
}, sink((result, enc) => {
fail('no data should be logged')
}))
Object.keys(pino.levels.values).forEach((level) => {
instance[level]('hello world')
})
})
test('set silent via Infinity', async ({ fail }) => {
const instance = pino({
level: Infinity
}, sink((result, enc) => {
fail('no data should be logged')
}))
Object.keys(pino.levels.values).forEach((level) => {
instance[level]('hello world')
})
})
test('exposed levels', async ({ same }) => {
same(Object.keys(pino.levels.values), [
'trace',
'debug',
'info',
'warn',
'error',
'fatal'
])
})
test('exposed labels', async ({ same }) => {
same(Object.keys(pino.levels.labels), [
'10',
'20',
'30',
'40',
'50',
'60'
])
})
test('setting level in child', async ({ equal }) => {
const expected = [{
level: 50,
msg: 'this is an error'
}, {
level: 60,
msg: 'this is fatal'
}]
const instance = pino(sink((result, enc, cb) => {
const current = expected.shift()
check(equal, result, current.level, current.msg)
cb()
})).child({ level: 30 })
instance.level = 'error'
instance.info('hello world')
instance.error('this is an error')
instance.fatal('this is fatal')
})
test('setting level by assigning a number to level', async ({ equal }) => {
const instance = pino()
equal(instance.levelVal, 30)
equal(instance.level, 'info')
instance.level = 50
equal(instance.levelVal, 50)
equal(instance.level, 'error')
})
test('setting level by number to unknown value results in a throw', async ({ throws }) => {
const instance = pino()
throws(() => { instance.level = 973 })
})
test('setting level by assigning a known label to level', async ({ equal }) => {
const instance = pino()
equal(instance.levelVal, 30)
equal(instance.level, 'info')
instance.level = 'error'
equal(instance.levelVal, 50)
equal(instance.level, 'error')
})
test('levelVal is read only', async ({ throws }) => {
const instance = pino()
throws(() => { instance.levelVal = 20 })
})
test('produces labels when told to', async ({ equal }) => {
const expected = [{
level: 'info',
msg: 'hello world'
}]
const instance = pino({
formatters: {
level (label, number) {
return { level: label }
}
}
}, sink((result, enc, cb) => {
const current = expected.shift()
check(equal, result, current.level, current.msg)
cb()
}))
instance.info('hello world')
})
test('resets levels from labels to numbers', async ({ equal }) => {
const expected = [{
level: 30,
msg: 'hello world'
}]
pino({ useLevelLabels: true })
const instance = pino({ useLevelLabels: false }, sink((result, enc, cb) => {
const current = expected.shift()
check(equal, result, current.level, current.msg)
cb()
}))
instance.info('hello world')
})
test('changes label naming when told to', async ({ equal }) => {
const expected = [{
priority: 30,
msg: 'hello world'
}]
const instance = pino({
formatters: {
level (label, number) {
return { priority: number }
}
}
}, sink((result, enc, cb) => {
const current = expected.shift()
equal(result.priority, current.priority)
equal(result.msg, current.msg)
cb()
}))
instance.info('hello world')
})
test('children produce labels when told to', async ({ equal }) => {
const expected = [
{
level: 'info',
msg: 'child 1'
},
{
level: 'info',
msg: 'child 2'
}
]
const instance = pino({
formatters: {
level (label, number) {
return { level: label }
}
}
}, sink((result, enc, cb) => {
const current = expected.shift()
check(equal, result, current.level, current.msg)
cb()
}))
const child1 = instance.child({ name: 'child1' })
const child2 = child1.child({ name: 'child2' })
child1.info('child 1')
child2.info('child 2')
})
test('produces labels for custom levels', async ({ equal }) => {
const expected = [
{
level: 'info',
msg: 'hello world'
},
{
level: 'foo',
msg: 'foobar'
}
]
const opts = {
formatters: {
level (label, number) {
return { level: label }
}
},
customLevels: {
foo: 35
}
}
const instance = pino(opts, sink((result, enc, cb) => {
const current = expected.shift()
check(equal, result, current.level, current.msg)
cb()
}))
instance.info('hello world')
instance.foo('foobar')
})
test('setting levelKey does not affect labels when told to', async ({ equal }) => {
const instance = pino(
{
formatters: {
level (label, number) {
return { priority: label }
}
}
},
sink((result, enc, cb) => {
equal(result.priority, 'info')
cb()
})
)
instance.info('hello world')
})
test('throws when creating a default label that does not exist in logger levels', async ({ throws }) => {
const defaultLevel = 'foo'
throws(() => {
pino({
customLevels: {
bar: 5
},
level: defaultLevel
})
}, `default level:${defaultLevel} must be included in custom levels`)
})
test('throws when creating a default value that does not exist in logger levels', async ({ throws }) => {
const defaultLevel = 15
throws(() => {
pino({
customLevels: {
bar: 5
},
level: defaultLevel
})
}, `default level:${defaultLevel} must be included in custom levels`)
})
test('throws when creating a default value that does not exist in logger levels', async ({ equal, throws }) => {
throws(() => {
pino({
customLevels: {
foo: 5
},
useOnlyCustomLevels: true
})
}, 'default level:info must be included in custom levels')
})
test('passes when creating a default value that exists in logger levels', async ({ equal, throws }) => {
pino({
level: 30
})
})
test('log null value when message is null', async ({ equal }) => {
const expected = {
msg: null,
level: 30
}
const stream = sink()
const instance = pino(stream)
instance.level = 'info'
instance.info(null)
const result = await once(stream, 'data')
check(equal, result, expected.level, expected.msg)
})
test('formats when base param is null', async ({ equal }) => {
const expected = {
msg: 'a string',
level: 30
}
const stream = sink()
const instance = pino(stream)
instance.level = 'info'
instance.info(null, 'a %s', 'string')
const result = await once(stream, 'data')
check(equal, result, expected.level, expected.msg)
})
test('fatal method sync-flushes the destination if sync flushing is available', async ({ pass, doesNotThrow, plan }) => {
plan(2)
const stream = sink()
stream.flushSync = () => {
pass('destination flushed')
}
const instance = pino(stream)
instance.fatal('this is fatal')
await once(stream, 'data')
doesNotThrow(() => {
stream.flushSync = undefined
instance.fatal('this is fatal')
})
})
test('fatal method should call async when sync-flushing fails', ({ equal, fail, doesNotThrow, plan }) => {
plan(2)
const messages = [
'this is fatal 1'
]
const stream = sink((result) => equal(result.msg, messages.shift()))
stream.flushSync = () => { throw new Error('Error') }
stream.flush = () => fail('flush should be called')
const instance = pino(stream)
doesNotThrow(() => instance.fatal(messages[0]))
})
test('calling silent method on logger instance', async ({ fail }) => {
const instance = pino({ level: 'silent' }, sink((result, enc) => {
fail('no data should be logged')
}))
instance.silent('hello world')
})
test('calling silent method on child logger', async ({ fail }) => {
const child = pino({ level: 'silent' }, sink((result, enc) => {
fail('no data should be logged')
})).child({})
child.silent('hello world')
})
test('changing level from info to silent and back to info', async ({ equal }) => {
const expected = {
level: 30,
msg: 'hello world'
}
const stream = sink()
const instance = pino({ level: 'info' }, stream)
instance.level = 'silent'
instance.info('hello world')
let result = stream.read()
equal(result, null)
instance.level = 'info'
instance.info('hello world')
result = await once(stream, 'data')
check(equal, result, expected.level, expected.msg)
})
test('changing level from info to silent and back to info in child logger', async ({ equal }) => {
const expected = {
level: 30,
msg: 'hello world'
}
const stream = sink()
const child = pino({ level: 'info' }, stream).child({})
child.level = 'silent'
child.info('hello world')
let result = stream.read()
equal(result, null)
child.level = 'info'
child.info('hello world')
result = await once(stream, 'data')
check(equal, result, expected.level, expected.msg)
})
test('changing level respects level comparison set to', async ({ test, end }) => {
const ascLevels = {
debug: 1,
info: 2,
warn: 3
}
const descLevels = {
debug: 3,
info: 2,
warn: 1
}
const expected = {
level: 2,
msg: 'hello world'
}
test('ASC in parent logger', async ({ equal }) => {
const customLevels = ascLevels
const levelComparison = 'ASC'
const stream = sink()
const logger = pino({ levelComparison, customLevels, useOnlyCustomLevels: true, level: 'info' }, stream)
logger.level = 'warn'
logger.info('hello world')
let result = stream.read()
equal(result, null)
logger.level = 'debug'
logger.info('hello world')
result = await once(stream, 'data')
check(equal, result, expected.level, expected.msg)
})
test('DESC in parent logger', async ({ equal }) => {
const customLevels = descLevels
const levelComparison = 'DESC'
const stream = sink()
const logger = pino({ levelComparison, customLevels, useOnlyCustomLevels: true, level: 'info' }, stream)
logger.level = 'warn'
logger.info('hello world')
let result = stream.read()
equal(result, null)
logger.level = 'debug'
logger.info('hello world')
result = await once(stream, 'data')
check(equal, result, expected.level, expected.msg)
})
test('custom function in parent logger', async ({ equal }) => {
const customLevels = {
info: 2,
debug: 345,
warn: 789
}
const levelComparison = (current, expected) => {
if (expected === customLevels.warn) return false
return true
}
const stream = sink()
const logger = pino({ levelComparison, customLevels, useOnlyCustomLevels: true, level: 'info' }, stream)
logger.level = 'warn'
logger.info('hello world')
let result = stream.read()
equal(result, null)
logger.level = 'debug'
logger.info('hello world')
result = await once(stream, 'data')
check(equal, result, expected.level, expected.msg)
})
test('ASC in child logger', async ({ equal }) => {
const customLevels = ascLevels
const levelComparison = 'ASC'
const stream = sink()
const logger = pino({ levelComparison, customLevels, useOnlyCustomLevels: true, level: 'info' }, stream).child({ })
logger.level = 'warn'
logger.info('hello world')
let result = stream.read()
equal(result, null)
logger.level = 'debug'
logger.info('hello world')
result = await once(stream, 'data')
check(equal, result, expected.level, expected.msg)
})
test('DESC in parent logger', async ({ equal }) => {
const customLevels = descLevels
const levelComparison = 'DESC'
const stream = sink()
const logger = pino({ levelComparison, customLevels, useOnlyCustomLevels: true, level: 'info' }, stream).child({ })
logger.level = 'warn'
logger.info('hello world')
let result = stream.read()
equal(result, null)
logger.level = 'debug'
logger.info('hello world')
result = await once(stream, 'data')
check(equal, result, expected.level, expected.msg)
})
test('custom function in child logger', async ({ equal }) => {
const customLevels = {
info: 2,
debug: 345,
warn: 789
}
const levelComparison = (current, expected) => {
if (expected === customLevels.warn) return false
return true
}
const stream = sink()
const logger = pino({ levelComparison, customLevels, useOnlyCustomLevels: true, level: 'info' }, stream).child({ })
logger.level = 'warn'
logger.info('hello world')
let result = stream.read()
equal(result, null)
logger.level = 'debug'
logger.info('hello world')
result = await once(stream, 'data')
check(equal, result, expected.level, expected.msg)
})
end()
})
test('changing level respects level comparison DESC', async ({ equal }) => {
const customLevels = {
warn: 1,
info: 2,
debug: 3
}
const levelComparison = 'DESC'
const expected = {
level: 2,
msg: 'hello world'
}
const stream = sink()
const logger = pino({ levelComparison, customLevels, useOnlyCustomLevels: true, level: 'info' }, stream)
logger.level = 'warn'
logger.info('hello world')
let result = stream.read()
equal(result, null)
logger.level = 'debug'
logger.info('hello world')
result = await once(stream, 'data')
check(equal, result, expected.level, expected.msg)
})
// testing for potential loss of Pino constructor scope from serializers - an edge case with circular refs see: https://github.com/pinojs/pino/issues/833
test('trying to get levels when `this` is no longer a Pino instance returns an empty string', async ({ equal }) => {
const notPinoInstance = { some: 'object', getLevel: levelsLib.getLevel }
const blankedLevelValue = notPinoInstance.getLevel()
equal(blankedLevelValue, '')
})
+106
View File
@@ -0,0 +1,106 @@
'use strict'
const os = require('os')
const { test } = require('tap')
const pino = require('../')
const { pid } = process
const hostname = os.hostname()
test('metadata works', async ({ ok, same, equal }) => {
const now = Date.now()
const instance = pino({}, {
[Symbol.for('pino.metadata')]: true,
write (chunk) {
equal(instance, this.lastLogger)
equal(30, this.lastLevel)
equal('a msg', this.lastMsg)
ok(Number(this.lastTime) >= now)
same(this.lastObj, { hello: 'world' })
const result = JSON.parse(chunk)
ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()')
delete result.time
same(result, {
pid,
hostname,
level: 30,
hello: 'world',
msg: 'a msg'
})
}
})
instance.info({ hello: 'world' }, 'a msg')
})
test('child loggers works', async ({ ok, same, equal }) => {
const instance = pino({}, {
[Symbol.for('pino.metadata')]: true,
write (chunk) {
equal(child, this.lastLogger)
equal(30, this.lastLevel)
equal('a msg', this.lastMsg)
same(this.lastObj, { from: 'child' })
const result = JSON.parse(chunk)
ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()')
delete result.time
same(result, {
pid,
hostname,
level: 30,
hello: 'world',
from: 'child',
msg: 'a msg'
})
}
})
const child = instance.child({ hello: 'world' })
child.info({ from: 'child' }, 'a msg')
})
test('without object', async ({ ok, same, equal }) => {
const instance = pino({}, {
[Symbol.for('pino.metadata')]: true,
write (chunk) {
equal(instance, this.lastLogger)
equal(30, this.lastLevel)
equal('a msg', this.lastMsg)
same({ }, this.lastObj)
const result = JSON.parse(chunk)
ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()')
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'a msg'
})
}
})
instance.info('a msg')
})
test('without msg', async ({ ok, same, equal }) => {
const instance = pino({}, {
[Symbol.for('pino.metadata')]: true,
write (chunk) {
equal(instance, this.lastLogger)
equal(30, this.lastLevel)
equal(undefined, this.lastMsg)
same({ hello: 'world' }, this.lastObj)
const result = JSON.parse(chunk)
ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()')
delete result.time
same(result, {
pid,
hostname,
level: 30,
hello: 'world'
})
}
})
instance.info({ hello: 'world' })
})
@@ -0,0 +1,55 @@
'use strict'
const { test } = require('tap')
const { sink, once } = require('./helper')
const pino = require('../')
const level = 50
const name = 'error'
test('default merge strategy', async ({ ok, same }) => {
const stream = sink()
const instance = pino({
base: {},
mixin () {
return { tag: 'k8s' }
}
}, stream)
instance.level = name
instance[name]({
tag: 'local'
}, 'test')
const result = await once(stream, 'data')
ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()')
delete result.time
same(result, {
level,
msg: 'test',
tag: 'local'
})
})
test('custom merge strategy with mixin priority', async ({ ok, same }) => {
const stream = sink()
const instance = pino({
base: {},
mixin () {
return { tag: 'k8s' }
},
mixinMergeStrategy (mergeObject, mixinObject) {
return Object.assign(mergeObject, mixinObject)
}
}, stream)
instance.level = name
instance[name]({
tag: 'local'
}, 'test')
const result = await once(stream, 'data')
ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()')
delete result.time
same(result, {
level,
msg: 'test',
tag: 'k8s'
})
})
+218
View File
@@ -0,0 +1,218 @@
'use strict'
const os = require('os')
const { test } = require('tap')
const { sink, once } = require('./helper')
const pino = require('../')
const { pid } = process
const hostname = os.hostname()
const level = 50
const name = 'error'
test('mixin object is included', async ({ ok, same }) => {
let n = 0
const stream = sink()
const instance = pino({
mixin () {
return { hello: ++n }
}
}, stream)
instance.level = name
instance[name]('test')
const result = await once(stream, 'data')
ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()')
delete result.time
same(result, {
pid,
hostname,
level,
msg: 'test',
hello: 1
})
})
test('mixin object is new every time', async ({ plan, ok, same }) => {
plan(6)
let n = 0
const stream = sink()
const instance = pino({
mixin () {
return { hello: n }
}
}, stream)
instance.level = name
while (++n < 4) {
const msg = `test #${n}`
stream.pause()
instance[name](msg)
stream.resume()
const result = await once(stream, 'data')
ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()')
delete result.time
same(result, {
pid,
hostname,
level,
msg,
hello: n
})
}
})
test('mixin object is not called if below log level', async ({ ok }) => {
const stream = sink()
const instance = pino({
mixin () {
ok(false, 'should not call mixin function')
}
}, stream)
instance.level = 'error'
instance.info('test')
})
test('mixin object + logged object', async ({ ok, same }) => {
const stream = sink()
const instance = pino({
mixin () {
return { foo: 1, bar: 2 }
}
}, stream)
instance.level = name
instance[name]({ bar: 3, baz: 4 })
const result = await once(stream, 'data')
ok(new Date(result.time) <= new Date(), 'time is greater than Date.now()')
delete result.time
same(result, {
pid,
hostname,
level,
foo: 1,
bar: 3,
baz: 4
})
})
test('mixin not a function', async ({ throws }) => {
const stream = sink()
throws(function () {
pino({ mixin: 'not a function' }, stream)
})
})
test('mixin can use context', async ({ ok, same }) => {
const stream = sink()
const instance = pino({
mixin (context) {
ok(context !== null, 'context should be defined')
ok(context !== undefined, 'context should be defined')
same(context, {
message: '123',
stack: 'stack'
})
return Object.assign({
error: context.message,
stack: context.stack
})
}
}, stream)
instance.level = name
instance[name]({
message: '123',
stack: 'stack'
}, 'test')
})
test('mixin works without context', async ({ ok, same }) => {
const stream = sink()
const instance = pino({
mixin (context) {
ok(context !== null, 'context is still defined w/o passing mergeObject')
ok(context !== undefined, 'context is still defined w/o passing mergeObject')
same(context, {})
return {
something: true
}
}
}, stream)
instance.level = name
instance[name]('test')
})
test('mixin can use level number', async ({ ok, same }) => {
const stream = sink()
const instance = pino({
mixin (context, num) {
ok(num !== null, 'level should be defined')
ok(num !== undefined, 'level should be defined')
same(num, level)
return Object.assign({
error: context.message,
stack: context.stack
})
}
}, stream)
instance.level = name
instance[name]({
message: '123',
stack: 'stack'
}, 'test')
})
test('mixin receives logger as third parameter', async ({ ok, same }) => {
const stream = sink()
const instance = pino({
mixin (context, num, logger) {
ok(logger !== null, 'logger should be defined')
ok(logger !== undefined, 'logger should be defined')
same(logger, instance)
return { ...context, num }
}
}, stream)
instance.level = name
instance[name]({
message: '123'
}, 'test')
})
test('mixin receives child logger', async ({ ok, same }) => {
const stream = sink()
let child = null
const instance = pino({
mixin (context, num, logger) {
ok(logger !== null, 'logger should be defined')
ok(logger !== undefined, 'logger should be defined')
same(logger.expected, child.expected)
return { ...context, num }
}
}, stream)
instance.level = name
instance.expected = false
child = instance.child({})
child.expected = true
child[name]({
message: '123'
}, 'test')
})
test('mixin receives logger even if child exists', async ({ ok, same }) => {
const stream = sink()
let child = null
const instance = pino({
mixin (context, num, logger) {
ok(logger !== null, 'logger should be defined')
ok(logger !== undefined, 'logger should be defined')
same(logger.expected, instance.expected)
return { ...context, num }
}
}, stream)
instance.level = name
instance.expected = false
child = instance.child({})
child.expected = true
instance[name]({
message: '123'
}, 'test')
})
+673
View File
@@ -0,0 +1,673 @@
'use strict'
const writeStream = require('flush-write-stream')
const { readFileSync } = require('fs')
const { join } = require('path')
const test = require('tap').test
const pino = require('../')
const multistream = pino.multistream
const proxyquire = require('proxyquire')
const strip = require('strip-ansi')
const { file, sink } = require('./helper')
test('sends to multiple streams using string levels', function (t) {
let messageCount = 0
const stream = writeStream(function (data, enc, cb) {
messageCount += 1
cb()
})
const streams = [
{ stream },
{ level: 'debug', stream },
{ level: 'trace', stream },
{ level: 'fatal', stream },
{ level: 'silent', stream }
]
const log = pino({
level: 'trace'
}, multistream(streams))
log.info('info stream')
log.debug('debug stream')
log.fatal('fatal stream')
t.equal(messageCount, 9)
t.end()
})
test('sends to multiple streams using custom levels', function (t) {
let messageCount = 0
const stream = writeStream(function (data, enc, cb) {
messageCount += 1
cb()
})
const streams = [
{ stream },
{ level: 'debug', stream },
{ level: 'trace', stream },
{ level: 'fatal', stream },
{ level: 'silent', stream }
]
const log = pino({
level: 'trace'
}, multistream(streams))
log.info('info stream')
log.debug('debug stream')
log.fatal('fatal stream')
t.equal(messageCount, 9)
t.end()
})
test('sends to multiple streams using optionally predefined levels', function (t) {
let messageCount = 0
const stream = writeStream(function (data, enc, cb) {
messageCount += 1
cb()
})
const opts = {
levels: {
silent: Infinity,
fatal: 60,
error: 50,
warn: 50,
info: 30,
debug: 20,
trace: 10
}
}
const streams = [
{ stream },
{ level: 'trace', stream },
{ level: 'debug', stream },
{ level: 'info', stream },
{ level: 'warn', stream },
{ level: 'error', stream },
{ level: 'fatal', stream },
{ level: 'silent', stream }
]
const mstream = multistream(streams, opts)
const log = pino({
level: 'trace'
}, mstream)
log.trace('trace stream')
log.debug('debug stream')
log.info('info stream')
log.warn('warn stream')
log.error('error stream')
log.fatal('fatal stream')
log.silent('silent stream')
t.equal(messageCount, 24)
t.end()
})
test('sends to multiple streams using number levels', function (t) {
let messageCount = 0
const stream = writeStream(function (data, enc, cb) {
messageCount += 1
cb()
})
const streams = [
{ stream },
{ level: 20, stream },
{ level: 60, stream }
]
const log = pino({
level: 'debug'
}, multistream(streams))
log.info('info stream')
log.debug('debug stream')
log.fatal('fatal stream')
t.equal(messageCount, 6)
t.end()
})
test('level include higher levels', function (t) {
let messageCount = 0
const stream = writeStream(function (data, enc, cb) {
messageCount += 1
cb()
})
const log = pino({}, multistream([{ level: 'info', stream }]))
log.fatal('message')
t.equal(messageCount, 1)
t.end()
})
test('supports multiple arguments', function (t) {
const messages = []
const stream = writeStream(function (data, enc, cb) {
messages.push(JSON.parse(data))
if (messages.length === 2) {
const msg1 = messages[0]
t.equal(msg1.msg, 'foo bar baz foobar')
const msg2 = messages[1]
t.equal(msg2.msg, 'foo bar baz foobar barfoo foofoo')
t.end()
}
cb()
})
const log = pino({}, multistream({ stream }))
log.info('%s %s %s %s', 'foo', 'bar', 'baz', 'foobar') // apply not invoked
log.info('%s %s %s %s %s %s', 'foo', 'bar', 'baz', 'foobar', 'barfoo', 'foofoo') // apply invoked
})
test('supports children', function (t) {
const stream = writeStream(function (data, enc, cb) {
const input = JSON.parse(data)
t.equal(input.msg, 'child stream')
t.equal(input.child, 'one')
t.end()
cb()
})
const streams = [
{ stream }
]
const log = pino({}, multistream(streams)).child({ child: 'one' })
log.info('child stream')
})
test('supports grandchildren', function (t) {
const messages = []
const stream = writeStream(function (data, enc, cb) {
messages.push(JSON.parse(data))
if (messages.length === 3) {
const msg1 = messages[0]
t.equal(msg1.msg, 'grandchild stream')
t.equal(msg1.child, 'one')
t.equal(msg1.grandchild, 'two')
const msg2 = messages[1]
t.equal(msg2.msg, 'grandchild stream')
t.equal(msg2.child, 'one')
t.equal(msg2.grandchild, 'two')
const msg3 = messages[2]
t.equal(msg3.msg, 'debug grandchild')
t.equal(msg3.child, 'one')
t.equal(msg3.grandchild, 'two')
t.end()
}
cb()
})
const streams = [
{ stream },
{ level: 'debug', stream }
]
const log = pino({
level: 'debug'
}, multistream(streams)).child({ child: 'one' }).child({ grandchild: 'two' })
log.info('grandchild stream')
log.debug('debug grandchild')
})
test('supports custom levels', function (t) {
const stream = writeStream(function (data, enc, cb) {
t.equal(JSON.parse(data).msg, 'bar')
t.end()
})
const log = pino({
customLevels: {
foo: 35
}
}, multistream([{ level: 35, stream }]))
log.foo('bar')
})
test('supports pretty print', function (t) {
t.plan(2)
const stream = writeStream(function (data, enc, cb) {
t.not(strip(data.toString()).match(/INFO.*: pretty print/), null)
cb()
})
const safeBoom = proxyquire('pino-pretty/lib/utils/build-safe-sonic-boom.js', {
'sonic-boom': function () {
t.pass('sonic created')
stream.flushSync = () => {}
stream.flush = () => {}
return stream
}
})
const nested = proxyquire('pino-pretty/lib/utils/index.js', {
'./build-safe-sonic-boom.js': safeBoom
})
const pretty = proxyquire('pino-pretty', {
'./lib/utils/index.js': nested
})
const log = pino({
level: 'debug',
name: 'helloName'
}, multistream([
{ stream: pretty() }
]))
log.info('pretty print')
})
test('emit propagates events to each stream', function (t) {
t.plan(3)
const handler = function (data) {
t.equal(data.msg, 'world')
}
const streams = [sink(), sink(), sink()]
streams.forEach(function (s) {
s.once('hello', handler)
})
const stream = multistream(streams)
stream.emit('hello', { msg: 'world' })
})
test('children support custom levels', function (t) {
const stream = writeStream(function (data, enc, cb) {
t.equal(JSON.parse(data).msg, 'bar')
t.end()
})
const parent = pino({
customLevels: {
foo: 35
}
}, multistream([{ level: 35, stream }]))
const child = parent.child({ child: 'yes' })
child.foo('bar')
})
test('levelVal overrides level', function (t) {
let messageCount = 0
const stream = writeStream(function (data, enc, cb) {
messageCount += 1
cb()
})
const streams = [
{ stream },
{ level: 'blabla', levelVal: 15, stream },
{ level: 60, stream }
]
const log = pino({
level: 'debug'
}, multistream(streams))
log.info('info stream')
log.debug('debug stream')
log.fatal('fatal stream')
t.equal(messageCount, 6)
t.end()
})
test('forwards metadata', function (t) {
t.plan(4)
const streams = [
{
stream: {
[Symbol.for('pino.metadata')]: true,
write (chunk) {
t.equal(log, this.lastLogger)
t.equal(30, this.lastLevel)
t.same({ hello: 'world' }, this.lastObj)
t.same('a msg', this.lastMsg)
}
}
}
]
const log = pino({
level: 'debug'
}, multistream(streams))
log.info({ hello: 'world' }, 'a msg')
t.end()
})
test('forward name', function (t) {
t.plan(2)
const streams = [
{
stream: {
[Symbol.for('pino.metadata')]: true,
write (chunk) {
const line = JSON.parse(chunk)
t.equal(line.name, 'helloName')
t.equal(line.hello, 'world')
}
}
}
]
const log = pino({
level: 'debug',
name: 'helloName'
}, multistream(streams))
log.info({ hello: 'world' }, 'a msg')
t.end()
})
test('forward name with child', function (t) {
t.plan(3)
const streams = [
{
stream: {
write (chunk) {
const line = JSON.parse(chunk)
t.equal(line.name, 'helloName')
t.equal(line.hello, 'world')
t.equal(line.component, 'aComponent')
}
}
}
]
const log = pino({
level: 'debug',
name: 'helloName'
}, multistream(streams)).child({ component: 'aComponent' })
log.info({ hello: 'world' }, 'a msg')
t.end()
})
test('clone generates a new multistream with all stream at the same level', function (t) {
let messageCount = 0
const stream = writeStream(function (data, enc, cb) {
messageCount += 1
cb()
})
const streams = [
{ stream },
{ level: 'debug', stream },
{ level: 'trace', stream },
{ level: 'fatal', stream }
]
const ms = multistream(streams)
const clone = ms.clone(30)
t.not(clone, ms)
clone.streams.forEach((s, i) => {
t.not(s, streams[i])
t.equal(s.stream, streams[i].stream)
t.equal(s.level, 30)
})
const log = pino({
level: 'trace'
}, clone)
log.info('info stream')
log.debug('debug message not counted')
log.fatal('fatal stream')
t.equal(messageCount, 8)
t.end()
})
test('one stream', function (t) {
let messageCount = 0
const stream = writeStream(function (data, enc, cb) {
messageCount += 1
cb()
})
const log = pino({
level: 'trace'
}, multistream({ stream, level: 'fatal' }))
log.info('info stream')
log.debug('debug stream')
log.fatal('fatal stream')
t.equal(messageCount, 1)
t.end()
})
test('dedupe', function (t) {
let messageCount = 0
const stream1 = writeStream(function (data, enc, cb) {
messageCount -= 1
cb()
})
const stream2 = writeStream(function (data, enc, cb) {
messageCount += 1
cb()
})
const streams = [
{
stream: stream1,
level: 'info'
},
{
stream: stream2,
level: 'fatal'
}
]
const log = pino({
level: 'trace'
}, multistream(streams, { dedupe: true }))
log.info('info stream')
log.fatal('fatal stream')
log.fatal('fatal stream')
t.equal(messageCount, 1)
t.end()
})
test('dedupe when logs have different levels', function (t) {
let messageCount = 0
const stream1 = writeStream(function (data, enc, cb) {
messageCount += 1
cb()
})
const stream2 = writeStream(function (data, enc, cb) {
messageCount += 2
cb()
})
const streams = [
{
stream: stream1,
level: 'info'
},
{
stream: stream2,
level: 'error'
}
]
const log = pino({
level: 'trace'
}, multistream(streams, { dedupe: true }))
log.info('info stream')
log.warn('warn stream')
log.error('error streams')
log.fatal('fatal streams')
t.equal(messageCount, 6)
t.end()
})
test('dedupe when some streams has the same level', function (t) {
let messageCount = 0
const stream1 = writeStream(function (data, enc, cb) {
messageCount -= 1
cb()
})
const stream2 = writeStream(function (data, enc, cb) {
messageCount += 1
cb()
})
const stream3 = writeStream(function (data, enc, cb) {
messageCount += 1
cb()
})
const streams = [
{
stream: stream1,
level: 'info'
},
{
stream: stream2,
level: 'fatal'
},
{
stream: stream3,
level: 'fatal'
}
]
const log = pino({
level: 'trace'
}, multistream(streams, { dedupe: true }))
log.info('info stream')
log.fatal('fatal streams')
log.fatal('fatal streams')
t.equal(messageCount, 3)
t.end()
})
test('no stream', function (t) {
const log = pino({
level: 'trace'
}, multistream())
log.info('info stream')
log.debug('debug stream')
log.fatal('fatal stream')
t.end()
})
test('one stream', function (t) {
let messageCount = 0
const stream = writeStream(function (data, enc, cb) {
messageCount += 1
cb()
})
const log = pino({
level: 'trace'
}, multistream(stream))
log.info('info stream')
log.debug('debug stream')
log.fatal('fatal stream')
t.equal(messageCount, 2)
t.end()
})
test('add a stream', function (t) {
let messageCount = 0
const stream = writeStream(function (data, enc, cb) {
messageCount += 1
cb()
})
const log = pino({
level: 'trace'
}, multistream().add(stream))
log.info('info stream')
log.debug('debug stream')
log.fatal('fatal stream')
t.equal(messageCount, 2)
t.end()
})
test('multistream.add throws if not a stream', function (t) {
try {
pino({
level: 'trace'
}, multistream().add({}))
} catch (_) {
t.end()
}
})
test('multistream throws if not a stream', function (t) {
try {
pino({
level: 'trace'
}, multistream({}))
} catch (_) {
t.end()
}
})
test('multistream.write should not throw if one stream fails', function (t) {
let messageCount = 0
const stream = writeStream(function (data, enc, cb) {
messageCount += 1
cb()
})
const noopStream = pino.transport({
target: join(__dirname, 'fixtures', 'noop-transport.js')
})
// eslint-disable-next-line
noopStream.on('error', function (err) {
// something went wrong while writing to noop stream, ignoring!
})
const log = pino({
level: 'trace'
},
multistream([
{
level: 'trace',
stream
},
{
level: 'debug',
stream: noopStream
}
])
)
log.debug('0')
noopStream.end()
// noop stream is ending, should emit an error but not throw
log.debug('1')
log.debug('2')
t.equal(messageCount, 3)
t.end()
})
test('flushSync', function (t) {
const tmp = file()
const destination = pino.destination({ dest: tmp, sync: false, minLength: 4096 })
const stream = multistream([{ level: 'info', stream: destination }])
const log = pino({ level: 'info' }, stream)
destination.on('ready', () => {
log.info('foo')
log.info('bar')
stream.flushSync()
t.equal(readFileSync(tmp, { encoding: 'utf-8' }).split('\n').length - 1, 2)
log.info('biz')
stream.flushSync()
t.equal(readFileSync(tmp, { encoding: 'utf-8' }).split('\n').length - 1, 3)
t.end()
})
})
test('ends all streams', function (t) {
t.plan(7)
const stream = writeStream(function (data, enc, cb) {
t.pass('message')
cb()
})
stream.flushSync = function () {
t.pass('flushSync')
}
// stream2 has no flushSync
const stream2 = writeStream(function (data, enc, cb) {
t.pass('message2')
cb()
})
const streams = [
{ stream },
{ level: 'debug', stream },
{ level: 'trace', stream: stream2 },
{ level: 'fatal', stream },
{ level: 'silent', stream }
]
const multi = multistream(streams)
const log = pino({
level: 'trace'
}, multi)
log.info('info stream')
multi.end()
})
+847
View File
@@ -0,0 +1,847 @@
'use strict'
const { test } = require('tap')
const { sink, once } = require('./helper')
const pino = require('../')
test('redact option throws if not array', async ({ throws }) => {
throws(() => {
pino({ redact: 'req.headers.cookie' })
})
})
test('redact option throws if array does not only contain strings', async ({ throws }) => {
throws(() => {
pino({ redact: ['req.headers.cookie', {}] })
})
})
test('redact option throws if array contains an invalid path', async ({ throws }) => {
throws(() => {
pino({ redact: ['req,headers.cookie'] })
})
})
test('redact.paths option throws if not array', async ({ throws }) => {
throws(() => {
pino({ redact: { paths: 'req.headers.cookie' } })
})
})
test('redact.paths option throws if array does not only contain strings', async ({ throws }) => {
throws(() => {
pino({ redact: { paths: ['req.headers.cookie', {}] } })
})
})
test('redact.paths option throws if array contains an invalid path', async ({ throws }) => {
throws(() => {
pino({ redact: { paths: ['req,headers.cookie'] } })
})
})
test('redact option top level key', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['key'] }, stream)
instance.info({
key: { redact: 'me' }
})
const { key } = await once(stream, 'data')
equal(key, '[Redacted]')
})
test('redact option top level key next level key', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['key', 'key.foo'] }, stream)
instance.info({
key: { redact: 'me' }
})
const { key } = await once(stream, 'data')
equal(key, '[Redacted]')
})
test('redact option next level key then top level key', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['key.foo', 'key'] }, stream)
instance.info({
key: { redact: 'me' }
})
const { key } = await once(stream, 'data')
equal(key, '[Redacted]')
})
test('redact option object', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['req.headers.cookie'] }, stream)
instance.info({
req: {
id: 7915,
method: 'GET',
url: '/',
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
},
remoteAddress: '::ffff:127.0.0.1',
remotePort: 58022
}
})
const { req } = await once(stream, 'data')
equal(req.headers.cookie, '[Redacted]')
})
test('redact option child object', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['req.headers.cookie'] }, stream)
instance.child({
req: {
id: 7915,
method: 'GET',
url: '/',
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
},
remoteAddress: '::ffff:127.0.0.1',
remotePort: 58022
}
}).info('message completed')
const { req } = await once(stream, 'data')
equal(req.headers.cookie, '[Redacted]')
})
test('redact option interpolated object', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['req.headers.cookie'] }, stream)
instance.info('test %j', {
req: {
id: 7915,
method: 'GET',
url: '/',
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
},
remoteAddress: '::ffff:127.0.0.1',
remotePort: 58022
}
})
const { msg } = await once(stream, 'data')
equal(JSON.parse(msg.replace(/test /, '')).req.headers.cookie, '[Redacted]')
})
test('redact.paths option object', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: { paths: ['req.headers.cookie'] } }, stream)
instance.info({
req: {
id: 7915,
method: 'GET',
url: '/',
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
},
remoteAddress: '::ffff:127.0.0.1',
remotePort: 58022
}
})
const { req } = await once(stream, 'data')
equal(req.headers.cookie, '[Redacted]')
})
test('redact.paths option child object', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: { paths: ['req.headers.cookie'] } }, stream)
instance.child({
req: {
id: 7915,
method: 'GET',
url: '/',
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
},
remoteAddress: '::ffff:127.0.0.1',
remotePort: 58022
}
}).info('message completed')
const { req } = await once(stream, 'data')
equal(req.headers.cookie, '[Redacted]')
})
test('redact.paths option interpolated object', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: { paths: ['req.headers.cookie'] } }, stream)
instance.info('test %j', {
req: {
id: 7915,
method: 'GET',
url: '/',
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
},
remoteAddress: '::ffff:127.0.0.1',
remotePort: 58022
}
})
const { msg } = await once(stream, 'data')
equal(JSON.parse(msg.replace(/test /, '')).req.headers.cookie, '[Redacted]')
})
test('redact.censor option sets the redact value', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: { paths: ['req.headers.cookie'], censor: 'test' } }, stream)
instance.info({
req: {
id: 7915,
method: 'GET',
url: '/',
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
},
remoteAddress: '::ffff:127.0.0.1',
remotePort: 58022
}
})
const { req } = await once(stream, 'data')
equal(req.headers.cookie, 'test')
})
test('redact.censor option can be a function that accepts value and path arguments', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: { paths: ['topLevel'], censor: (value, path) => value + ' ' + path.join('.') } }, stream)
instance.info({
topLevel: 'test'
})
const { topLevel } = await once(stream, 'data')
equal(topLevel, 'test topLevel')
})
test('redact.censor option can be a function that accepts value and path arguments (nested path)', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: { paths: ['req.headers.cookie'], censor: (value, path) => value + ' ' + path.join('.') } }, stream)
instance.info({
req: {
id: 7915,
method: 'GET',
url: '/',
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
},
remoteAddress: '::ffff:127.0.0.1',
remotePort: 58022
}
})
const { req } = await once(stream, 'data')
equal(req.headers.cookie, 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1; req.headers.cookie')
})
test('redact.remove option removes both key and value', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: { paths: ['req.headers.cookie'], remove: true } }, stream)
instance.info({
req: {
id: 7915,
method: 'GET',
url: '/',
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
},
remoteAddress: '::ffff:127.0.0.1',
remotePort: 58022
}
})
const { req } = await once(stream, 'data')
equal('cookie' in req.headers, false)
})
test('redact.remove top level key - object value', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: { paths: ['key'], remove: true } }, stream)
instance.info({
key: { redact: 'me' }
})
const o = await once(stream, 'data')
equal('key' in o, false)
})
test('redact.remove top level key - number value', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: { paths: ['key'], remove: true } }, stream)
instance.info({
key: 1
})
const o = await once(stream, 'data')
equal('key' in o, false)
})
test('redact.remove top level key - boolean value', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: { paths: ['key'], remove: true } }, stream)
instance.info({
key: false
})
const o = await once(stream, 'data')
equal('key' in o, false)
})
test('redact.remove top level key in child logger', async ({ equal }) => {
const stream = sink()
const opts = { redact: { paths: ['key'], remove: true } }
const instance = pino(opts, stream).child({ key: { redact: 'me' } })
instance.info('test')
const o = await once(stream, 'data')
equal('key' in o, false)
})
test('redact.paths preserves original object values after the log write', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['req.headers.cookie'] }, stream)
const obj = {
req: {
id: 7915,
method: 'GET',
url: '/',
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
},
remoteAddress: '::ffff:127.0.0.1',
remotePort: 58022
}
}
instance.info(obj)
const o = await once(stream, 'data')
equal(o.req.headers.cookie, '[Redacted]')
equal(obj.req.headers.cookie, 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;')
})
test('redact.paths preserves original object values after the log write', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: { paths: ['req.headers.cookie'] } }, stream)
const obj = {
req: {
id: 7915,
method: 'GET',
url: '/',
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
},
remoteAddress: '::ffff:127.0.0.1',
remotePort: 58022
}
}
instance.info(obj)
const o = await once(stream, 'data')
equal(o.req.headers.cookie, '[Redacted]')
equal(obj.req.headers.cookie, 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;')
})
test('redact.censor preserves original object values after the log write', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: { paths: ['req.headers.cookie'], censor: 'test' } }, stream)
const obj = {
req: {
id: 7915,
method: 'GET',
url: '/',
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
},
remoteAddress: '::ffff:127.0.0.1',
remotePort: 58022
}
}
instance.info(obj)
const o = await once(stream, 'data')
equal(o.req.headers.cookie, 'test')
equal(obj.req.headers.cookie, 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;')
})
test('redact.remove preserves original object values after the log write', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: { paths: ['req.headers.cookie'], remove: true } }, stream)
const obj = {
req: {
id: 7915,
method: 'GET',
url: '/',
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
},
remoteAddress: '::ffff:127.0.0.1',
remotePort: 58022
}
}
instance.info(obj)
const o = await once(stream, 'data')
equal('cookie' in o.req.headers, false)
equal('cookie' in obj.req.headers, true)
})
test('redact supports last position wildcard paths', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['req.headers.*'] }, stream)
instance.info({
req: {
id: 7915,
method: 'GET',
url: '/',
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
},
remoteAddress: '::ffff:127.0.0.1',
remotePort: 58022
}
})
const { req } = await once(stream, 'data')
equal(req.headers.cookie, '[Redacted]')
equal(req.headers.host, '[Redacted]')
equal(req.headers.connection, '[Redacted]')
})
test('redact supports first position wildcard paths', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['*.headers'] }, stream)
instance.info({
req: {
id: 7915,
method: 'GET',
url: '/',
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
},
remoteAddress: '::ffff:127.0.0.1',
remotePort: 58022
}
})
const { req } = await once(stream, 'data')
equal(req.headers, '[Redacted]')
})
test('redact supports first position wildcards before other paths', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['*.headers.cookie', 'req.id'] }, stream)
instance.info({
req: {
id: 7915,
method: 'GET',
url: '/',
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
},
remoteAddress: '::ffff:127.0.0.1',
remotePort: 58022
}
})
const { req } = await once(stream, 'data')
equal(req.headers.cookie, '[Redacted]')
equal(req.id, '[Redacted]')
})
test('redact supports first position wildcards after other paths', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['req.id', '*.headers.cookie'] }, stream)
instance.info({
req: {
id: 7915,
method: 'GET',
url: '/',
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
},
remoteAddress: '::ffff:127.0.0.1',
remotePort: 58022
}
})
const { req } = await once(stream, 'data')
equal(req.headers.cookie, '[Redacted]')
equal(req.id, '[Redacted]')
})
test('redact supports first position wildcards after top level keys', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['key', '*.headers.cookie'] }, stream)
instance.info({
req: {
id: 7915,
method: 'GET',
url: '/',
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
},
remoteAddress: '::ffff:127.0.0.1',
remotePort: 58022
}
})
const { req } = await once(stream, 'data')
equal(req.headers.cookie, '[Redacted]')
})
test('redact supports top level wildcard', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['*'] }, stream)
instance.info({
req: {
id: 7915,
method: 'GET',
url: '/',
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
},
remoteAddress: '::ffff:127.0.0.1',
remotePort: 58022
}
})
const { req } = await once(stream, 'data')
equal(req, '[Redacted]')
})
test('redact supports top level wildcard with a censor function', async ({ equal }) => {
const stream = sink()
const instance = pino({
redact: {
paths: ['*'],
censor: () => '[Redacted]'
}
}, stream)
instance.info({
req: {
id: 7915,
method: 'GET',
url: '/',
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
},
remoteAddress: '::ffff:127.0.0.1',
remotePort: 58022
}
})
const { req } = await once(stream, 'data')
equal(req, '[Redacted]')
})
test('redact supports top level wildcard and leading wildcard', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['*', '*.req'] }, stream)
instance.info({
req: {
id: 7915,
method: 'GET',
url: '/',
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
},
remoteAddress: '::ffff:127.0.0.1',
remotePort: 58022
}
})
const { req } = await once(stream, 'data')
equal(req, '[Redacted]')
})
test('redact supports intermediate wildcard paths', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['req.*.cookie'] }, stream)
instance.info({
req: {
id: 7915,
method: 'GET',
url: '/',
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
},
remoteAddress: '::ffff:127.0.0.1',
remotePort: 58022
}
})
const { req } = await once(stream, 'data')
equal(req.headers.cookie, '[Redacted]')
})
test('redacts numbers at the top level', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['id'] }, stream)
const obj = {
id: 7915
}
instance.info(obj)
const o = await once(stream, 'data')
equal(o.id, '[Redacted]')
})
test('redacts booleans at the top level', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['maybe'] }, stream)
const obj = {
maybe: true
}
instance.info(obj)
const o = await once(stream, 'data')
equal(o.maybe, '[Redacted]')
})
test('redacts strings at the top level', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['s'] }, stream)
const obj = {
s: 's'
}
instance.info(obj)
const o = await once(stream, 'data')
equal(o.s, '[Redacted]')
})
test('does not redact primitives if not objects', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['a.b'] }, stream)
const obj = {
a: 42
}
instance.info(obj)
const o = await once(stream, 'data')
equal(o.a, 42)
})
test('redacts null at the top level', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['n'] }, stream)
const obj = {
n: null
}
instance.info(obj)
const o = await once(stream, 'data')
equal(o.n, '[Redacted]')
})
test('supports bracket notation', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['a["b.b"]'] }, stream)
const obj = {
a: { 'b.b': 'c' }
}
instance.info(obj)
const o = await once(stream, 'data')
equal(o.a['b.b'], '[Redacted]')
})
test('supports bracket notation with further nesting', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['a["b.b"].c'] }, stream)
const obj = {
a: { 'b.b': { c: 'd' } }
}
instance.info(obj)
const o = await once(stream, 'data')
equal(o.a['b.b'].c, '[Redacted]')
})
test('supports bracket notation with empty string as path segment', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['a[""].c'] }, stream)
const obj = {
a: { '': { c: 'd' } }
}
instance.info(obj)
const o = await once(stream, 'data')
equal(o.a[''].c, '[Redacted]')
})
test('supports leading bracket notation (single quote)', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['[\'a.a\'].b'] }, stream)
const obj = {
'a.a': { b: 'c' }
}
instance.info(obj)
const o = await once(stream, 'data')
equal(o['a.a'].b, '[Redacted]')
})
test('supports leading bracket notation (double quote)', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['["a.a"].b'] }, stream)
const obj = {
'a.a': { b: 'c' }
}
instance.info(obj)
const o = await once(stream, 'data')
equal(o['a.a'].b, '[Redacted]')
})
test('supports leading bracket notation (backtick quote)', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['[`a.a`].b'] }, stream)
const obj = {
'a.a': { b: 'c' }
}
instance.info(obj)
const o = await once(stream, 'data')
equal(o['a.a'].b, '[Redacted]')
})
test('supports leading bracket notation (single-segment path)', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['[`a.a`]'] }, stream)
const obj = {
'a.a': { b: 'c' }
}
instance.info(obj)
const o = await once(stream, 'data')
equal(o['a.a'], '[Redacted]')
})
test('supports leading bracket notation (single-segment path, wildcard)', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['[*]'] }, stream)
const obj = {
'a.a': { b: 'c' }
}
instance.info(obj)
const o = await once(stream, 'data')
equal(o['a.a'], '[Redacted]')
})
test('child bindings are redacted using wildcard path', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['*.headers.cookie'] }, stream)
instance.child({
req: {
method: 'GET',
url: '/',
headers: {
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
}
}
}).info('message completed')
const { req } = await once(stream, 'data')
equal(req.headers.cookie, '[Redacted]')
})
test('child bindings are redacted using wildcard and plain path keys', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['req.method', '*.headers.cookie'] }, stream)
instance.child({
req: {
method: 'GET',
url: '/',
headers: {
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
}
}
}).info('message completed')
const { req } = await once(stream, 'data')
equal(req.headers.cookie, '[Redacted]')
equal(req.method, '[Redacted]')
})
test('redacts boolean at the top level', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['msg'] }, stream)
const obj = {
s: 's'
}
instance.info(obj, true)
const o = await once(stream, 'data')
equal(o.s, 's')
equal(o.msg, '[Redacted]')
})
test('child can customize redact', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['req.method', '*.headers.cookie'] }, stream)
instance.child({
req: {
method: 'GET',
url: '/',
headers: {
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
}
}
}, {
redact: ['req.url']
}).info('message completed')
const { req } = await once(stream, 'data')
equal(req.headers.cookie, 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;')
equal(req.method, 'GET')
equal(req.url, '[Redacted]')
})
test('child can remove parent redact by array', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: ['req.method', '*.headers.cookie'] }, stream)
instance.child({
req: {
method: 'GET',
url: '/',
headers: {
cookie: 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;'
}
}
}, {
redact: []
}).info('message completed')
const { req } = await once(stream, 'data')
equal(req.headers.cookie, 'SESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;')
equal(req.method, 'GET')
})
test('redact safe stringify', async ({ equal }) => {
const stream = sink()
const instance = pino({ redact: { paths: ['that.secret'] } }, stream)
instance.info({
that: {
secret: 'please hide me',
myBigInt: 123n
},
other: {
mySecondBigInt: 222n
}
})
const { that, other } = await once(stream, 'data')
equal(that.secret, '[Redacted]')
equal(that.myBigInt, 123)
equal(other.mySecondBigInt, 222)
})
+253
View File
@@ -0,0 +1,253 @@
'use strict'
const { test } = require('tap')
const { sink, once } = require('./helper')
const stdSerializers = require('pino-std-serializers')
const pino = require('../')
const parentSerializers = {
test: () => 'parent'
}
const childSerializers = {
test: () => 'child'
}
test('default err namespace error serializer', async ({ equal }) => {
const stream = sink()
const parent = pino(stream)
parent.info({ err: ReferenceError('test') })
const o = await once(stream, 'data')
equal(typeof o.err, 'object')
equal(o.err.type, 'ReferenceError')
equal(o.err.message, 'test')
equal(typeof o.err.stack, 'string')
})
test('custom serializer overrides default err namespace error serializer', async ({ equal }) => {
const stream = sink()
const parent = pino({
serializers: {
err: (e) => ({
t: e.constructor.name,
m: e.message,
s: e.stack
})
}
}, stream)
parent.info({ err: ReferenceError('test') })
const o = await once(stream, 'data')
equal(typeof o.err, 'object')
equal(o.err.t, 'ReferenceError')
equal(o.err.m, 'test')
equal(typeof o.err.s, 'string')
})
test('custom serializer overrides default err namespace error serializer when nestedKey is on', async ({ equal }) => {
const stream = sink()
const parent = pino({
nestedKey: 'obj',
serializers: {
err: (e) => {
return {
t: e.constructor.name,
m: e.message,
s: e.stack
}
}
}
}, stream)
parent.info({ err: ReferenceError('test') })
const o = await once(stream, 'data')
equal(typeof o.obj.err, 'object')
equal(o.obj.err.t, 'ReferenceError')
equal(o.obj.err.m, 'test')
equal(typeof o.obj.err.s, 'string')
})
test('null overrides default err namespace error serializer', async ({ equal }) => {
const stream = sink()
const parent = pino({ serializers: { err: null } }, stream)
parent.info({ err: ReferenceError('test') })
const o = await once(stream, 'data')
equal(typeof o.err, 'object')
equal(typeof o.err.type, 'undefined')
equal(typeof o.err.message, 'undefined')
equal(typeof o.err.stack, 'undefined')
})
test('undefined overrides default err namespace error serializer', async ({ equal }) => {
const stream = sink()
const parent = pino({ serializers: { err: undefined } }, stream)
parent.info({ err: ReferenceError('test') })
const o = await once(stream, 'data')
equal(typeof o.err, 'object')
equal(typeof o.err.type, 'undefined')
equal(typeof o.err.message, 'undefined')
equal(typeof o.err.stack, 'undefined')
})
test('serializers override values', async ({ equal }) => {
const stream = sink()
const parent = pino({ serializers: parentSerializers }, stream)
parent.child({}, { serializers: childSerializers })
parent.fatal({ test: 'test' })
const o = await once(stream, 'data')
equal(o.test, 'parent')
})
test('child does not overwrite parent serializers', async ({ equal }) => {
const stream = sink()
const parent = pino({ serializers: parentSerializers }, stream)
const child = parent.child({}, { serializers: childSerializers })
parent.fatal({ test: 'test' })
const o = once(stream, 'data')
equal((await o).test, 'parent')
const o2 = once(stream, 'data')
child.fatal({ test: 'test' })
equal((await o2).test, 'child')
})
test('Symbol.for(\'pino.serializers\')', async ({ equal, same, not }) => {
const stream = sink()
const expected = Object.assign({
err: stdSerializers.err
}, parentSerializers)
const parent = pino({ serializers: parentSerializers }, stream)
const child = parent.child({ a: 'property' })
same(parent[Symbol.for('pino.serializers')], expected)
same(child[Symbol.for('pino.serializers')], expected)
equal(parent[Symbol.for('pino.serializers')], child[Symbol.for('pino.serializers')])
const child2 = parent.child({}, {
serializers: {
a
}
})
function a () {
return 'hello'
}
not(child2[Symbol.for('pino.serializers')], parentSerializers)
equal(child2[Symbol.for('pino.serializers')].a, a)
equal(child2[Symbol.for('pino.serializers')].test, parentSerializers.test)
})
test('children inherit parent serializers', async ({ equal }) => {
const stream = sink()
const parent = pino({ serializers: parentSerializers }, stream)
const child = parent.child({ a: 'property' })
child.fatal({ test: 'test' })
const o = await once(stream, 'data')
equal(o.test, 'parent')
})
test('children inherit parent Symbol serializers', async ({ equal, same, not }) => {
const stream = sink()
const symbolSerializers = {
[Symbol.for('b')]: b
}
const expected = Object.assign({
err: stdSerializers.err
}, symbolSerializers)
const parent = pino({ serializers: symbolSerializers }, stream)
same(parent[Symbol.for('pino.serializers')], expected)
const child = parent.child({}, {
serializers: {
[Symbol.for('a')]: a,
a
}
})
function a () {
return 'hello'
}
function b () {
return 'world'
}
same(child[Symbol.for('pino.serializers')].a, a)
same(child[Symbol.for('pino.serializers')][Symbol.for('b')], b)
same(child[Symbol.for('pino.serializers')][Symbol.for('a')], a)
})
test('children serializers get called', async ({ equal }) => {
const stream = sink()
const parent = pino({
test: 'this'
}, stream)
const child = parent.child({ a: 'property' }, { serializers: childSerializers })
child.fatal({ test: 'test' })
const o = await once(stream, 'data')
equal(o.test, 'child')
})
test('children serializers get called when inherited from parent', async ({ equal }) => {
const stream = sink()
const parent = pino({
test: 'this',
serializers: parentSerializers
}, stream)
const child = parent.child({}, { serializers: { test: function () { return 'pass' } } })
child.fatal({ test: 'fail' })
const o = await once(stream, 'data')
equal(o.test, 'pass')
})
test('non-overridden serializers are available in the children', async ({ equal }) => {
const stream = sink()
const pSerializers = {
onlyParent: function () { return 'parent' },
shared: function () { return 'parent' }
}
const cSerializers = {
shared: function () { return 'child' },
onlyChild: function () { return 'child' }
}
const parent = pino({ serializers: pSerializers }, stream)
const child = parent.child({}, { serializers: cSerializers })
const o = once(stream, 'data')
child.fatal({ shared: 'test' })
equal((await o).shared, 'child')
const o2 = once(stream, 'data')
child.fatal({ onlyParent: 'test' })
equal((await o2).onlyParent, 'parent')
const o3 = once(stream, 'data')
child.fatal({ onlyChild: 'test' })
equal((await o3).onlyChild, 'child')
const o4 = once(stream, 'data')
parent.fatal({ onlyChild: 'test' })
equal((await o4).onlyChild, 'test')
})
test('custom serializer for messageKey', async (t) => {
const stream = sink()
const instance = pino({ serializers: { msg: () => '422' } }, stream)
const o = { num: NaN }
instance.info(o, 42)
const { msg } = await once(stream, 'data')
t.equal(msg, '422')
})
@@ -0,0 +1,39 @@
'use strict'
const { test } = require('tap')
const { join } = require('path')
const { fork } = require('child_process')
const { once } = require('./helper')
const writer = require('flush-write-stream')
const pino = require('..')
test('do not use SonicBoom is someone tampered with process.stdout.write', async ({ not }) => {
let actual = ''
const child = fork(join(__dirname, 'fixtures', 'stdout-hack-protection.js'), { silent: true })
child.stdout.pipe(writer((s, enc, cb) => {
actual += s
cb()
}))
await once(child, 'close')
not(actual.match(/^hack/), null)
})
test('do not use SonicBoom is someone has passed process.stdout to pino', async ({ equal }) => {
const logger = pino(process.stdout)
equal(logger[pino.symbols.streamSym], process.stdout)
})
test('do not crash if process.stdout has no fd', async ({ teardown }) => {
const fd = process.stdout.fd
delete process.stdout.fd
teardown(function () { process.stdout.fd = fd })
pino()
})
test('use fd=1 if process.stdout has no fd in pino.destination() (worker case)', async ({ teardown }) => {
const fd = process.stdout.fd
delete process.stdout.fd
teardown(function () { process.stdout.fd = fd })
pino.destination()
})
+191
View File
@@ -0,0 +1,191 @@
'use strict'
const os = require('os')
const {
createWriteStream
} = require('fs')
const { readFile } = require('fs').promises
const { join } = require('path')
const { test } = require('tap')
const { fork } = require('child_process')
const writer = require('flush-write-stream')
const {
once,
getPathToNull,
file,
watchFileCreated
} = require('./helper')
const { promisify } = require('util')
const sleep = promisify(setTimeout)
test('asynchronous logging', async ({
equal,
teardown
}) => {
const now = Date.now
const hostname = os.hostname
const proc = process
global.process = {
__proto__: process,
pid: 123456
}
Date.now = () => 1459875739796
os.hostname = () => 'abcdefghijklmnopqr'
delete require.cache[require.resolve('../')]
const pino = require('../')
let expected = ''
let actual = ''
const normal = pino(writer((s, enc, cb) => {
expected += s
cb()
}))
const dest = createWriteStream(getPathToNull())
dest.write = (s) => {
actual += s
}
const asyncLogger = pino(dest)
let i = 44
while (i--) {
normal.info('h')
asyncLogger.info('h')
}
const expected2 = expected.split('\n')[0]
let actual2 = ''
const child = fork(join(__dirname, '/fixtures/syncfalse.js'), { silent: true })
child.stdout.pipe(writer((s, enc, cb) => {
actual2 += s
cb()
}))
await once(child, 'close')
// Wait for the last write to be flushed
await sleep(100)
equal(actual, expected)
equal(actual2.trim(), expected2)
teardown(() => {
os.hostname = hostname
Date.now = now
global.process = proc
})
})
test('sync false with child', async ({
equal,
teardown
}) => {
const now = Date.now
const hostname = os.hostname
const proc = process
global.process = {
__proto__: process,
pid: 123456
}
Date.now = function () {
return 1459875739796
}
os.hostname = function () {
return 'abcdefghijklmnopqr'
}
delete require.cache[require.resolve('../')]
const pino = require('../')
let expected = ''
let actual = ''
const normal = pino(writer((s, enc, cb) => {
expected += s
cb()
})).child({ hello: 'world' })
const dest = createWriteStream(getPathToNull())
dest.write = function (s) {
actual += s
}
const asyncLogger = pino(dest).child({ hello: 'world' })
let i = 500
while (i--) {
normal.info('h')
asyncLogger.info('h')
}
asyncLogger.flush()
const expected2 = expected.split('\n')[0]
let actual2 = ''
const child = fork(join(__dirname, '/fixtures/syncfalse-child.js'), { silent: true })
child.stdout.pipe(writer((s, enc, cb) => {
actual2 += s
cb()
}))
await once(child, 'close')
equal(actual, expected)
equal(actual2.trim(), expected2)
teardown(() => {
os.hostname = hostname
Date.now = now
global.process = proc
})
})
test('flush does nothing with sync true (default)', async ({ equal }) => {
const instance = require('..')()
equal(instance.flush(), undefined)
})
test('should still call flush callback even when does nothing with sync true (default)', (t) => {
t.plan(3)
const instance = require('..')()
instance.flush((...args) => {
t.ok('flush called')
t.same(args, [])
// next tick to make flush not called more than once
process.nextTick(() => {
t.ok('flush next tick called')
})
})
})
test('should call the flush callback when flushed the data for async logger', async (t) => {
const outputPath = file()
async function getOutputLogLines () {
return (await readFile(outputPath)).toString().trim().split('\n').map(JSON.parse)
}
const pino = require('../')
const instance = pino({}, pino.destination({
dest: outputPath,
// to make sure it does not flush on its own
minLength: 4096
}))
const flushPromise = promisify(instance.flush).bind(instance)
instance.info('hello')
await flushPromise()
await watchFileCreated(outputPath)
const [firstFlushData] = await getOutputLogLines()
t.equal(firstFlushData.msg, 'hello')
// should not flush this as no data accumulated that's bigger than min length
instance.info('world')
// Making sure data is not flushed yet
const afterLogData = await getOutputLogLines()
t.equal(afterLogData.length, 1)
await flushPromise()
// Making sure data is not flushed yet
const afterSecondFlush = (await getOutputLogLines())[1]
t.equal(afterSecondFlush.msg, 'world')
})
+121
View File
@@ -0,0 +1,121 @@
'use strict'
/* eslint no-prototype-builtins: 0 */
const { test } = require('tap')
const { sink, once } = require('./helper')
const pino = require('../')
test('pino exposes standard time functions', async ({ ok }) => {
ok(pino.stdTimeFunctions)
ok(pino.stdTimeFunctions.epochTime)
ok(pino.stdTimeFunctions.unixTime)
ok(pino.stdTimeFunctions.nullTime)
ok(pino.stdTimeFunctions.isoTime)
})
test('pino accepts external time functions', async ({ equal }) => {
const opts = {
timestamp: () => ',"time":"none"'
}
const stream = sink()
const instance = pino(opts, stream)
instance.info('foobar')
const result = await once(stream, 'data')
equal(result.hasOwnProperty('time'), true)
equal(result.time, 'none')
})
test('pino accepts external time functions with custom label', async ({ equal }) => {
const opts = {
timestamp: () => ',"custom-time-label":"none"'
}
const stream = sink()
const instance = pino(opts, stream)
instance.info('foobar')
const result = await once(stream, 'data')
equal(result.hasOwnProperty('custom-time-label'), true)
equal(result['custom-time-label'], 'none')
})
test('inserts timestamp by default', async ({ ok, equal }) => {
const stream = sink()
const instance = pino(stream)
instance.info('foobar')
const result = await once(stream, 'data')
equal(result.hasOwnProperty('time'), true)
ok(new Date(result.time) <= new Date(), 'time is greater than timestamp')
equal(result.msg, 'foobar')
})
test('omits timestamp when timestamp option is false', async ({ equal }) => {
const stream = sink()
const instance = pino({ timestamp: false }, stream)
instance.info('foobar')
const result = await once(stream, 'data')
equal(result.hasOwnProperty('time'), false)
equal(result.msg, 'foobar')
})
test('inserts timestamp when timestamp option is true', async ({ ok, equal }) => {
const stream = sink()
const instance = pino({ timestamp: true }, stream)
instance.info('foobar')
const result = await once(stream, 'data')
equal(result.hasOwnProperty('time'), true)
ok(new Date(result.time) <= new Date(), 'time is greater than timestamp')
equal(result.msg, 'foobar')
})
test('child inserts timestamp by default', async ({ ok, equal }) => {
const stream = sink()
const logger = pino(stream)
const instance = logger.child({ component: 'child' })
instance.info('foobar')
const result = await once(stream, 'data')
equal(result.hasOwnProperty('time'), true)
ok(new Date(result.time) <= new Date(), 'time is greater than timestamp')
equal(result.msg, 'foobar')
})
test('child omits timestamp with option', async ({ equal }) => {
const stream = sink()
const logger = pino({ timestamp: false }, stream)
const instance = logger.child({ component: 'child' })
instance.info('foobar')
const result = await once(stream, 'data')
equal(result.hasOwnProperty('time'), false)
equal(result.msg, 'foobar')
})
test('pino.stdTimeFunctions.unixTime returns seconds based timestamps', async ({ equal }) => {
const opts = {
timestamp: pino.stdTimeFunctions.unixTime
}
const stream = sink()
const instance = pino(opts, stream)
const now = Date.now
Date.now = () => 1531069919686
instance.info('foobar')
const result = await once(stream, 'data')
equal(result.hasOwnProperty('time'), true)
equal(result.time, 1531069920)
Date.now = now
})
test('pino.stdTimeFunctions.isoTime returns ISO 8601 timestamps', async ({ equal }) => {
const opts = {
timestamp: pino.stdTimeFunctions.isoTime
}
const stream = sink()
const instance = pino(opts, stream)
const ms = 1531069919686
const now = Date.now
Date.now = () => ms
const iso = new Date(ms).toISOString()
instance.info('foobar')
const result = await once(stream, 'data')
equal(result.hasOwnProperty('time'), true)
equal(result.time, iso)
Date.now = now
})
+43
View File
@@ -0,0 +1,43 @@
'use strict'
const { test } = require('tap')
const { join } = require('path')
const { createReadStream } = require('fs')
const { promisify } = require('util')
const execa = require('execa')
const split = require('split2')
const stream = require('stream')
const { file } = require('../helper')
const pipeline = promisify(stream.pipeline)
const { Writable } = stream
const sleep = promisify(setTimeout)
const skip = process.env.CI || process.env.CITGM
test('eight million lines', { skip }, async ({ equal, comment }) => {
const destination = file()
await execa(process.argv[0], [join(__dirname, '..', 'fixtures', 'transport-many-lines.js'), destination])
if (process.platform !== 'win32') {
try {
await execa('sync') // Wait for the file to be written to disk
} catch {
// Just a fallback, this should be unreachable
}
}
await sleep(1000) // It seems that sync is not enough (even in POSIX systems)
const toWrite = 8 * 1000000
let count = 0
await pipeline(createReadStream(destination), split(), new Writable({
write (chunk, enc, cb) {
if (count % (toWrite / 10) === 0) {
comment(`read ${count}`)
}
count++
cb()
}
}))
equal(count, toWrite)
})
@@ -0,0 +1,127 @@
'use strict'
const os = require('os')
const { join } = require('path')
const { readFile } = require('fs').promises
const { watchFileCreated, file } = require('../helper')
const { test } = require('tap')
const pino = require('../../pino')
const { pid } = process
const hostname = os.hostname()
test('pino.transport with destination overridden by bundler', async ({ same, teardown }) => {
globalThis.__bundlerPathsOverrides = {
foobar: join(__dirname, '..', 'fixtures', 'to-file-transport.js')
}
const destination = file()
const transport = pino.transport({
target: 'foobar',
options: { destination }
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
instance.info('hello')
await watchFileCreated(destination)
const result = JSON.parse(await readFile(destination))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
globalThis.__bundlerPathsOverrides = undefined
})
test('pino.transport with worker destination overridden by bundler', async ({ same, teardown }) => {
globalThis.__bundlerPathsOverrides = {
'pino-worker': join(__dirname, '..', '..', 'lib/worker.js')
}
const destination = file()
const transport = pino.transport({
targets: [
{
target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'),
options: { destination }
}
]
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
instance.info('hello')
await watchFileCreated(destination)
const result = JSON.parse(await readFile(destination))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
globalThis.__bundlerPathsOverrides = undefined
})
test('pino.transport with worker destination overridden by bundler and mjs transport', async ({ same, teardown }) => {
globalThis.__bundlerPathsOverrides = {
'pino-worker': join(__dirname, '..', '..', 'lib/worker.js')
}
const destination = file()
const transport = pino.transport({
targets: [
{
target: join(__dirname, '..', 'fixtures', 'ts', 'to-file-transport.es2017.cjs'),
options: { destination }
}
]
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
instance.info('hello')
await watchFileCreated(destination)
const result = JSON.parse(await readFile(destination))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
globalThis.__bundlerPathsOverrides = undefined
})
test('pino.transport with worker-pipeline destination overridden by bundler', async ({ same, teardown }) => {
globalThis.__bundlerPathsOverrides = {
'pino-pipeline-worker': join(__dirname, '..', '..', 'lib/worker-pipeline.js')
}
const destination = file()
const transport = pino.transport({
pipeline: [
{
target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'),
options: { destination }
}
]
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
instance.info('hello')
await watchFileCreated(destination)
const result = JSON.parse(await readFile(destination))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
globalThis.__bundlerPathsOverrides = undefined
})
@@ -0,0 +1,23 @@
'use strict'
const { join } = require('path')
const { test } = require('tap')
const execa = require('execa')
test('when using a custom transport outside node_modules, the first file outside node_modules should be used', async function (t) {
const evalApp = join(__dirname, '../', '/fixtures/eval/index.js')
const { stdout } = await execa(process.argv[0], [evalApp])
t.match(stdout, /done!/)
})
test('when using a custom transport where some files in stacktrace are in the node_modules, the first file outside node_modules should be used', async function (t) {
const evalApp = join(__dirname, '../', '/fixtures/eval/node_modules/2-files.js')
const { stdout } = await execa(process.argv[0], [evalApp])
t.match(stdout, /done!/)
})
test('when using a custom transport where all files in stacktrace are in the node_modules, the first file inside node_modules should be used', async function (t) {
const evalApp = join(__dirname, '../', '/fixtures/eval/node_modules/14-files.js')
const { stdout } = await execa(process.argv[0], [evalApp])
t.match(stdout, /done!/)
})
+624
View File
@@ -0,0 +1,624 @@
'use strict'
const os = require('os')
const { join } = require('path')
const { once } = require('events')
const { readFile, writeFile } = require('fs').promises
const { watchFileCreated, watchForWrite, file } = require('../helper')
const { test } = require('tap')
const pino = require('../../')
const url = require('url')
const strip = require('strip-ansi')
const execa = require('execa')
const writer = require('flush-write-stream')
const rimraf = require('rimraf')
const { promisify } = require('util')
const { tmpdir } = os
const immediate = promisify(setImmediate)
const pid = process.pid
const hostname = os.hostname()
test('pino.transport with file', async ({ same, teardown }) => {
const destination = file()
const transport = pino.transport({
target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'),
options: { destination }
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
instance.info('hello')
await watchFileCreated(destination)
const result = JSON.parse(await readFile(destination))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
test('pino.transport with file (no options + error handling)', async ({ equal }) => {
const transport = pino.transport({
target: join(__dirname, '..', 'fixtures', 'to-file-transport.js')
})
const [err] = await once(transport, 'error')
equal(err.message, 'kaboom')
})
test('pino.transport with file URL', async ({ same, teardown }) => {
const destination = file()
const transport = pino.transport({
target: url.pathToFileURL(join(__dirname, '..', 'fixtures', 'to-file-transport.js')).href,
options: { destination }
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
instance.info('hello')
await watchFileCreated(destination)
const result = JSON.parse(await readFile(destination))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
test('pino.transport errors if file does not exists', ({ plan, pass }) => {
plan(1)
const instance = pino.transport({
target: join(__dirname, '..', 'fixtures', 'non-existent-file'),
worker: {
stdin: true,
stdout: true,
stderr: true
}
})
instance.on('error', function () {
pass('error received')
})
})
test('pino.transport errors if transport worker module does not export a function', ({ plan, equal }) => {
// TODO: add case for non-pipelined single target (needs changes in thread-stream)
plan(2)
const manyTargetsInstance = pino.transport({
targets: [{
level: 'info',
target: join(__dirname, '..', 'fixtures', 'transport-wrong-export-type.js')
}, {
level: 'info',
target: join(__dirname, '..', 'fixtures', 'transport-wrong-export-type.js')
}]
})
manyTargetsInstance.on('error', function (e) {
equal(e.message, 'exported worker is not a function')
})
const pipelinedInstance = pino.transport({
pipeline: [{
target: join(__dirname, '..', 'fixtures', 'transport-wrong-export-type.js')
}]
})
pipelinedInstance.on('error', function (e) {
equal(e.message, 'exported worker is not a function')
})
})
test('pino.transport with esm', async ({ same, teardown }) => {
const destination = file()
const transport = pino.transport({
target: join(__dirname, '..', 'fixtures', 'to-file-transport.mjs'),
options: { destination }
})
const instance = pino(transport)
teardown(transport.end.bind(transport))
instance.info('hello')
await watchFileCreated(destination)
const result = JSON.parse(await readFile(destination))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
test('pino.transport with two files', async ({ same, teardown }) => {
const dest1 = file()
const dest2 = file()
const transport = pino.transport({
targets: [{
level: 'info',
target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'),
options: { destination: dest1 }
}, {
level: 'info',
target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'),
options: { destination: dest2 }
}]
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
instance.info('hello')
await Promise.all([watchFileCreated(dest1), watchFileCreated(dest2)])
const result1 = JSON.parse(await readFile(dest1))
delete result1.time
same(result1, {
pid,
hostname,
level: 30,
msg: 'hello'
})
const result2 = JSON.parse(await readFile(dest2))
delete result2.time
same(result2, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
test('pino.transport with two files and custom levels', async ({ same, teardown }) => {
const dest1 = file()
const dest2 = file()
const transport = pino.transport({
targets: [{
level: 'info',
target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'),
options: { destination: dest1 }
}, {
level: 'foo',
target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'),
options: { destination: dest2 }
}],
levels: { trace: 10, debug: 20, info: 30, warn: 40, error: 50, fatal: 60, foo: 25 }
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
instance.info('hello')
await Promise.all([watchFileCreated(dest1), watchFileCreated(dest2)])
const result1 = JSON.parse(await readFile(dest1))
delete result1.time
same(result1, {
pid,
hostname,
level: 30,
msg: 'hello'
})
const result2 = JSON.parse(await readFile(dest2))
delete result2.time
same(result2, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
test('pino.transport without specifying default levels', async ({ same, teardown }) => {
const dest = file()
const transport = pino.transport({
targets: [{
level: 'foo',
target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'),
options: { destination: dest }
}],
levels: { foo: 25 }
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
instance.info('hello')
await Promise.all([watchFileCreated(dest)])
const result1 = JSON.parse(await readFile(dest))
delete result1.time
same(result1, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
test('pino.transport with two files and dedupe', async ({ same, teardown }) => {
const dest1 = file()
const dest2 = file()
const transport = pino.transport({
dedupe: true,
targets: [{
level: 'info',
target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'),
options: { destination: dest1 }
}, {
level: 'error',
target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'),
options: { destination: dest2 }
}]
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
instance.info('hello')
instance.error('world')
await Promise.all([watchFileCreated(dest1), watchFileCreated(dest2)])
const result1 = JSON.parse(await readFile(dest1))
delete result1.time
same(result1, {
pid,
hostname,
level: 30,
msg: 'hello'
})
const result2 = JSON.parse(await readFile(dest2))
delete result2.time
same(result2, {
pid,
hostname,
level: 50,
msg: 'world'
})
})
test('pino.transport with an array including a pino-pretty destination', async ({ same, match, teardown }) => {
const dest1 = file()
const dest2 = file()
const transport = pino.transport({
targets: [{
level: 'info',
target: 'pino/file',
options: {
destination: dest1
}
}, {
level: 'info',
target: 'pino-pretty',
options: {
destination: dest2
}
}]
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
instance.info('hello')
await Promise.all([watchFileCreated(dest1), watchFileCreated(dest2)])
const result1 = JSON.parse(await readFile(dest1))
delete result1.time
same(result1, {
pid,
hostname,
level: 30,
msg: 'hello'
})
const actual = (await readFile(dest2)).toString()
match(strip(actual), /\[.*\] INFO.*hello/)
})
test('no transport.end()', async ({ same, teardown }) => {
const destination = file()
const transport = pino.transport({
target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'),
options: { destination }
})
const instance = pino(transport)
instance.info('hello')
await watchFileCreated(destination)
const result = JSON.parse(await readFile(destination))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
test('autoEnd = false', async ({ equal, same, teardown }) => {
const destination = file()
const count = process.listenerCount('exit')
const transport = pino.transport({
target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'),
options: { destination },
worker: { autoEnd: false }
})
teardown(transport.end.bind(transport))
await once(transport, 'ready')
const instance = pino(transport)
instance.info('hello')
await watchFileCreated(destination)
equal(count, process.listenerCount('exit'))
const result = JSON.parse(await readFile(destination))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
test('pino.transport with target and targets', async ({ fail, equal }) => {
try {
pino.transport({
target: '/a/file',
targets: [{
target: '/a/file'
}]
})
fail('must throw')
} catch (err) {
equal(err.message, 'only one of target or targets can be specified')
}
})
test('pino.transport with target pino/file', async ({ same, teardown }) => {
const destination = file()
const transport = pino.transport({
target: 'pino/file',
options: { destination }
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
instance.info('hello')
await watchFileCreated(destination)
const result = JSON.parse(await readFile(destination))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
test('pino.transport with target pino/file and mkdir option', async ({ same, teardown }) => {
const folder = join(tmpdir(), `pino-${process.pid}-mkdir-transport-file`)
const destination = join(folder, 'log.txt')
teardown(() => {
try {
rimraf.sync(folder)
} catch (err) {
// ignore
}
})
const transport = pino.transport({
target: 'pino/file',
options: { destination, mkdir: true }
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
instance.info('hello')
await watchFileCreated(destination)
const result = JSON.parse(await readFile(destination))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
test('pino.transport with target pino/file and append option', async ({ same, teardown }) => {
const destination = file()
await writeFile(destination, JSON.stringify({ pid, hostname, time: Date.now(), level: 30, msg: 'hello' }))
const transport = pino.transport({
target: 'pino/file',
options: { destination, append: false }
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
instance.info('goodbye')
await watchForWrite(destination, '"goodbye"')
const result = JSON.parse(await readFile(destination))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'goodbye'
})
})
test('pino.transport should error with unknown target', async ({ fail, equal }) => {
try {
pino.transport({
target: 'origin',
caller: 'unknown-file.js'
})
fail('must throw')
} catch (err) {
equal(err.message, 'unable to determine transport target for "origin"')
}
})
test('pino.transport with target pino-pretty', async ({ match, teardown }) => {
const destination = file()
const transport = pino.transport({
target: 'pino-pretty',
options: { destination }
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
instance.info('hello')
await watchFileCreated(destination)
const actual = await readFile(destination, 'utf8')
match(strip(actual), /\[.*\] INFO.*hello/)
})
test('sets worker data informing the transport that pino will send its config', ({ match, plan, teardown }) => {
plan(1)
const transport = pino.transport({
target: join(__dirname, '..', 'fixtures', 'transport-worker-data.js')
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
transport.once('workerData', (workerData) => {
match(workerData.workerData, { pinoWillSendConfig: true })
})
instance.info('hello')
})
test('stdout in worker', async ({ not }) => {
let actual = ''
const child = execa(process.argv[0], [join(__dirname, '..', 'fixtures', 'transport-main.js')])
for await (const chunk of child.stdout) {
actual += chunk
}
not(strip(actual).match(/Hello/), null)
})
test('log and exit on ready', async ({ not }) => {
let actual = ''
const child = execa(process.argv[0], [join(__dirname, '..', 'fixtures', 'transport-exit-on-ready.js')])
child.stdout.pipe(writer((s, enc, cb) => {
actual += s
cb()
}))
await once(child, 'close')
await immediate()
not(strip(actual).match(/Hello/), null)
})
test('log and exit before ready', async ({ not }) => {
let actual = ''
const child = execa(process.argv[0], [join(__dirname, '..', 'fixtures', 'transport-exit-immediately.js')])
child.stdout.pipe(writer((s, enc, cb) => {
actual += s
cb()
}))
await once(child, 'close')
await immediate()
not(strip(actual).match(/Hello/), null)
})
test('log and exit before ready with async dest', async ({ not }) => {
const destination = file()
const child = execa(process.argv[0], [join(__dirname, '..', 'fixtures', 'transport-exit-immediately-with-async-dest.js'), destination])
await once(child, 'exit')
const actual = await readFile(destination, 'utf8')
not(strip(actual).match(/HELLO/), null)
not(strip(actual).match(/WORLD/), null)
})
test('string integer destination', async ({ not }) => {
let actual = ''
const child = execa(process.argv[0], [join(__dirname, '..', 'fixtures', 'transport-string-stdout.js')])
child.stdout.pipe(writer((s, enc, cb) => {
actual += s
cb()
}))
await once(child, 'close')
await immediate()
not(strip(actual).match(/Hello/), null)
})
test('pino transport options with target', async ({ teardown, same }) => {
const destination = file()
const instance = pino({
transport: {
target: 'pino/file',
options: { destination }
}
})
const transportStream = instance[pino.symbols.streamSym]
teardown(transportStream.end.bind(transportStream))
instance.info('transport option test')
await watchFileCreated(destination)
const result = JSON.parse(await readFile(destination))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'transport option test'
})
})
test('pino transport options with targets', async ({ teardown, same }) => {
const dest1 = file()
const dest2 = file()
const instance = pino({
transport: {
targets: [
{ target: 'pino/file', options: { destination: dest1 } },
{ target: 'pino/file', options: { destination: dest2 } }
]
}
})
const transportStream = instance[pino.symbols.streamSym]
teardown(transportStream.end.bind(transportStream))
instance.info('transport option test')
await Promise.all([watchFileCreated(dest1), watchFileCreated(dest2)])
const result1 = JSON.parse(await readFile(dest1))
delete result1.time
same(result1, {
pid,
hostname,
level: 30,
msg: 'transport option test'
})
const result2 = JSON.parse(await readFile(dest2))
delete result2.time
same(result2, {
pid,
hostname,
level: 30,
msg: 'transport option test'
})
})
test('transport options with target and targets', async ({ fail, equal }) => {
try {
pino({
transport: {
target: {},
targets: {}
}
})
fail('must throw')
} catch (err) {
equal(err.message, 'only one of target or targets can be specified')
}
})
test('transport options with target and stream', async ({ fail, equal }) => {
try {
pino({
transport: {
target: {}
}
}, '/log/null')
fail('must throw')
} catch (err) {
equal(err.message, 'only one of option.transport or stream can be specified')
}
})
test('transport options with stream', async ({ fail, equal, teardown }) => {
try {
const dest1 = file()
const transportStream = pino.transport({ target: 'pino/file', options: { destination: dest1 } })
teardown(transportStream.end.bind(transportStream))
pino({
transport: transportStream
})
fail('must throw')
} catch (err) {
equal(err.message, 'option.transport do not allow stream, please pass to option directly. e.g. pino(transport)')
}
})
+236
View File
@@ -0,0 +1,236 @@
import * as os from 'os'
import { join } from 'path'
import { once } from 'events'
import fs from 'fs'
import { watchFileCreated } from '../helper'
import { test } from 'tap'
import pino from '../../'
import * as url from 'url'
import { default as strip } from 'strip-ansi'
import execa from 'execa'
import writer from 'flush-write-stream'
if (process.platform === 'win32') {
// TODO: Implement .ts files loading support for Windows
process.exit()
}
const readFile = fs.promises.readFile
const { pid } = process
const hostname = os.hostname()
test('pino.transport with file', async ({ same, teardown }) => {
const destination = join(
os.tmpdir(),
'_' + Math.random().toString(36).substr(2, 9)
)
const transport = pino.transport({
target: join(__dirname, '..', 'fixtures', 'ts', 'to-file-transport.ts'),
options: { destination }
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
instance.info('hello')
await watchFileCreated(destination)
const result = JSON.parse(await readFile(destination, { encoding: 'utf8' }))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
test('pino.transport with file (no options + error handling)', async ({ equal }) => {
const transport = pino.transport({
target: join(__dirname, '..', 'fixtures', 'ts', 'to-file-transport.ts')
})
const [err] = await once(transport, 'error')
equal(err.message, 'kaboom')
})
test('pino.transport with file URL', async ({ same, teardown }) => {
const destination = join(
os.tmpdir(),
'_' + Math.random().toString(36).substr(2, 9)
)
const transport = pino.transport({
target: url.pathToFileURL(join(__dirname, '..', 'fixtures', 'ts', 'to-file-transport.ts')).href,
options: { destination }
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
instance.info('hello')
await watchFileCreated(destination)
const result = JSON.parse(await readFile(destination, { encoding: 'utf8' }))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
test('pino.transport with two files', async ({ same, teardown }) => {
const dest1 = join(
os.tmpdir(),
'_' + Math.random().toString(36).substr(2, 9)
)
const dest2 = join(
os.tmpdir(),
'_' + Math.random().toString(36).substr(2, 9)
)
const transport = pino.transport({
targets: [{
level: 'info',
target: join(__dirname, '..', 'fixtures', 'ts', 'to-file-transport.ts'),
options: { destination: dest1 }
}, {
level: 'info',
target: join(__dirname, '..', 'fixtures', 'ts', 'to-file-transport.ts'),
options: { destination: dest2 }
}]
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
instance.info('hello')
await Promise.all([watchFileCreated(dest1), watchFileCreated(dest2)])
const result1 = JSON.parse(await readFile(dest1, { encoding: 'utf8' }))
delete result1.time
same(result1, {
pid,
hostname,
level: 30,
msg: 'hello'
})
const result2 = JSON.parse(await readFile(dest2, { encoding: 'utf8' }))
delete result2.time
same(result2, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
test('no transport.end()', async ({ same, teardown }) => {
const destination = join(
os.tmpdir(),
'_' + Math.random().toString(36).substr(2, 9)
)
const transport = pino.transport({
target: join(__dirname, '..', 'fixtures', 'ts', 'to-file-transport.ts'),
options: { destination }
})
const instance = pino(transport)
instance.info('hello')
await watchFileCreated(destination)
const result = JSON.parse(await readFile(destination, { encoding: 'utf8' }))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
test('autoEnd = false', async ({ equal, same, teardown }) => {
const destination = join(
os.tmpdir(),
'_' + Math.random().toString(36).substr(2, 9)
)
const count = process.listenerCount('exit')
const transport = pino.transport({
target: join(__dirname, '..', 'fixtures', 'ts', 'to-file-transport.ts'),
options: { destination },
worker: { autoEnd: false }
})
teardown(transport.end.bind(transport))
await once(transport, 'ready')
const instance = pino(transport)
instance.info('hello')
await watchFileCreated(destination)
equal(count, process.listenerCount('exit'))
const result = JSON.parse(await readFile(destination, { encoding: 'utf8' }))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
test('stdout in worker', async ({ not }) => {
let actual = ''
const child = execa(process.argv[0], ['-r', 'ts-node/register', join(__dirname, '..', 'fixtures', 'ts', 'transport-main.ts')])
child.stdout?.pipe(writer((s, enc, cb) => {
actual += s
cb()
}))
await once(child, 'close')
not(strip(actual).match(/Hello/), null)
})
test('log and exit on ready', async ({ not }) => {
let actual = ''
const child = execa(process.argv[0], ['-r', 'ts-node/register', join(__dirname, '..', 'fixtures', 'ts', 'transport-exit-on-ready.ts')])
child.stdout?.pipe(writer((s, enc, cb) => {
actual += s
cb()
}))
await once(child, 'close')
not(strip(actual).match(/Hello/), null)
})
test('log and exit before ready', async ({ not }) => {
let actual = ''
const child = execa(process.argv[0], ['-r', 'ts-node/register', join(__dirname, '..', 'fixtures', 'ts', 'transport-exit-immediately.ts')])
child.stdout?.pipe(writer((s, enc, cb) => {
actual += s
cb()
}))
await once(child, 'close')
not(strip(actual).match(/Hello/), null)
})
test('log and exit before ready with async dest', async ({ not }) => {
const destination = join(
os.tmpdir(),
'_' + Math.random().toString(36).substr(2, 9)
)
const child = execa(process.argv[0], ['-r', 'ts-node/register', join(__dirname, '..', 'fixtures', 'ts', 'transport-exit-immediately-with-async-dest.ts'), destination])
await once(child, 'exit')
const actual = await readFile(destination, { encoding: 'utf8' })
not(strip(actual).match(/HELLO/), null)
not(strip(actual).match(/WORLD/), null)
})
test('string integer destination', async ({ not }) => {
let actual = ''
const child = execa(process.argv[0], ['-r', 'ts-node/register', join(__dirname, '..', 'fixtures', 'ts', 'transport-string-stdout.ts')])
child.stdout?.pipe(writer((s, enc, cb) => {
actual += s
cb()
}))
await once(child, 'close')
not(strip(actual).match(/Hello/), null)
})
@@ -0,0 +1,116 @@
import * as os from 'os'
import { join } from 'path'
import { once } from 'events'
import fs from 'fs'
import { watchFileCreated } from '../helper'
import { test } from 'tap'
import pino from '../../'
import * as url from 'url'
import { default as strip } from 'strip-ansi'
import execa from 'execa'
import writer from 'flush-write-stream'
const readFile = fs.promises.readFile
const { pid } = process
const hostname = os.hostname()
// A subset of the test from core.test.js, we don't need all of them to check for compatibility
function runTests(esVersion: string): void {
test(`(ts -> ${esVersion}) pino.transport with file`, async ({ same, teardown }) => {
const destination = join(
os.tmpdir(),
'_' + Math.random().toString(36).substr(2, 9)
)
const transport = pino.transport({
target: join(__dirname, '..', 'fixtures', 'ts', `to-file-transport.${esVersion}.cjs`),
options: { destination }
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
instance.info('hello')
await watchFileCreated(destination)
const result = JSON.parse(await readFile(destination, { encoding: 'utf8' }))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
test(`(ts -> ${esVersion}) pino.transport with file URL`, async ({ same, teardown }) => {
const destination = join(
os.tmpdir(),
'_' + Math.random().toString(36).substr(2, 9)
)
const transport = pino.transport({
target: url.pathToFileURL(join(__dirname, '..', 'fixtures', 'ts', `to-file-transport.${esVersion}.cjs`)).href,
options: { destination }
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
instance.info('hello')
await watchFileCreated(destination)
const result = JSON.parse(await readFile(destination, { encoding: 'utf8' }))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
test(`(ts -> ${esVersion}) pino.transport with two files`, async ({ same, teardown }) => {
const dest1 = join(
os.tmpdir(),
'_' + Math.random().toString(36).substr(2, 9)
)
const dest2 = join(
os.tmpdir(),
'_' + Math.random().toString(36).substr(2, 9)
)
const transport = pino.transport({
targets: [{
level: 'info',
target: join(__dirname, '..', 'fixtures', 'ts', `to-file-transport.${esVersion}.cjs`),
options: { destination: dest1 }
}, {
level: 'info',
target: join(__dirname, '..', 'fixtures', 'ts', `to-file-transport.${esVersion}.cjs`),
options: { destination: dest2 }
}]
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
instance.info('hello')
await Promise.all([watchFileCreated(dest1), watchFileCreated(dest2)])
const result1 = JSON.parse(await readFile(dest1, { encoding: 'utf8' }))
delete result1.time
same(result1, {
pid,
hostname,
level: 30,
msg: 'hello'
})
const result2 = JSON.parse(await readFile(dest2, { encoding: 'utf8' }))
delete result2.time
same(result2, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
}
runTests('es5')
runTests('es6')
runTests('es2017')
runTests('esnext')
@@ -0,0 +1,239 @@
'use strict'
const os = require('os')
const { join } = require('path')
const { readFile, symlink, unlink, mkdir, writeFile } = require('fs').promises
const { test } = require('tap')
const { isWin, isYarnPnp, watchFileCreated, file } = require('../helper')
const { once } = require('events')
const execa = require('execa')
const pino = require('../../')
const rimraf = require('rimraf')
const { pid } = process
const hostname = os.hostname()
async function installTransportModule (target) {
if (isYarnPnp) {
return
}
try {
await uninstallTransportModule()
} catch {}
if (!target) {
target = join(__dirname, '..', '..')
}
await symlink(
join(__dirname, '..', 'fixtures', 'transport'),
join(target, 'node_modules', 'transport')
)
}
async function uninstallTransportModule () {
if (isYarnPnp) {
return
}
await unlink(join(__dirname, '..', '..', 'node_modules', 'transport'))
}
// TODO make this test pass on Windows
test('pino.transport with package', { skip: isWin }, async ({ same, teardown }) => {
const destination = file()
await installTransportModule()
const transport = pino.transport({
target: 'transport',
options: { destination }
})
teardown(async () => {
await uninstallTransportModule()
transport.end()
})
const instance = pino(transport)
instance.info('hello')
await watchFileCreated(destination)
const result = JSON.parse(await readFile(destination))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
// TODO make this test pass on Windows
test('pino.transport with package as a target', { skip: isWin }, async ({ same, teardown }) => {
const destination = file()
await installTransportModule()
const transport = pino.transport({
targets: [{
target: 'transport',
options: { destination }
}]
})
teardown(async () => {
await uninstallTransportModule()
transport.end()
})
const instance = pino(transport)
instance.info('hello')
await watchFileCreated(destination)
const result = JSON.parse(await readFile(destination))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
// TODO make this test pass on Windows
test('pino({ transport })', { skip: isWin || isYarnPnp }, async ({ same, teardown }) => {
const folder = join(
os.tmpdir(),
'_' + Math.random().toString(36).substr(2, 9)
)
teardown(() => {
rimraf.sync(folder)
})
const destination = join(folder, 'output')
await mkdir(join(folder, 'node_modules'), { recursive: true })
// Link pino
await symlink(
join(__dirname, '..', '..'),
join(folder, 'node_modules', 'pino')
)
await installTransportModule(folder)
const toRun = join(folder, 'index.js')
const toRunContent = `
const pino = require('pino')
const logger = pino({
transport: {
target: 'transport',
options: { destination: '${destination}' }
}
})
logger.info('hello')
`
await writeFile(toRun, toRunContent)
const child = execa(process.argv[0], [toRun])
await once(child, 'close')
const result = JSON.parse(await readFile(destination))
delete result.time
same(result, {
pid: child.pid,
hostname,
level: 30,
msg: 'hello'
})
})
// TODO make this test pass on Windows
test('pino({ transport }) from a wrapped dependency', { skip: isWin || isYarnPnp }, async ({ same, teardown }) => {
const folder = join(
os.tmpdir(),
'_' + Math.random().toString(36).substr(2, 9)
)
const wrappedFolder = join(
os.tmpdir(),
'_' + Math.random().toString(36).substr(2, 9)
)
const destination = join(folder, 'output')
await mkdir(join(folder, 'node_modules'), { recursive: true })
await mkdir(join(wrappedFolder, 'node_modules'), { recursive: true })
teardown(() => {
rimraf.sync(wrappedFolder)
rimraf.sync(folder)
})
// Link pino
await symlink(
join(__dirname, '..', '..'),
join(wrappedFolder, 'node_modules', 'pino')
)
// Link get-caller-file
await symlink(
join(__dirname, '..', '..', 'node_modules', 'get-caller-file'),
join(wrappedFolder, 'node_modules', 'get-caller-file')
)
// Link wrapped
await symlink(
wrappedFolder,
join(folder, 'node_modules', 'wrapped')
)
await installTransportModule(folder)
const pkgjsonContent = {
name: 'pino'
}
await writeFile(join(wrappedFolder, 'package.json'), JSON.stringify(pkgjsonContent))
const wrapped = join(wrappedFolder, 'index.js')
const wrappedContent = `
const pino = require('pino')
const getCaller = require('get-caller-file')
module.exports = function build () {
const logger = pino({
transport: {
caller: getCaller(),
target: 'transport',
options: { destination: '${destination}' }
}
})
return logger
}
`
await writeFile(wrapped, wrappedContent)
const toRun = join(folder, 'index.js')
const toRunContent = `
const logger = require('wrapped')()
logger.info('hello')
`
await writeFile(toRun, toRunContent)
const child = execa(process.argv[0], [toRun])
await once(child, 'close')
const result = JSON.parse(await readFile(destination))
delete result.time
same(result, {
pid: child.pid,
hostname,
level: 30,
msg: 'hello'
})
})
@@ -0,0 +1,36 @@
'use strict'
const os = require('os')
const { join } = require('path')
const { readFile } = require('fs').promises
const { watchFileCreated, file } = require('../helper')
const { test } = require('tap')
const pino = require('../../')
const { pid } = process
const hostname = os.hostname()
test('pino.transport with a pipeline', async ({ same, teardown }) => {
const destination = file()
const transport = pino.transport({
pipeline: [{
target: join(__dirname, '..', 'fixtures', 'transport-transform.js')
}, {
target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'),
options: { destination }
}]
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
instance.info('hello')
await watchFileCreated(destination)
const result = JSON.parse(await readFile(destination))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello',
service: 'pino' // this property was added by the transform
})
})
+14
View File
@@ -0,0 +1,14 @@
'use strict'
const { doesNotThrow, test } = require('tap')
const proxyquire = require('proxyquire')
test('pino.transport resolves targets in REPL', async ({ same }) => {
// Arrange
const transport = proxyquire('../../lib/transport', {
'./caller': () => ['node:repl']
})
// Act / Assert
doesNotThrow(() => transport({ target: 'pino-pretty' }))
})
@@ -0,0 +1,68 @@
'use strict'
const os = require('os')
const pino = require('../..')
const { join } = require('path')
const { test } = require('tap')
const { readFile } = require('fs').promises
const { watchFileCreated, file } = require('../helper')
const { promisify } = require('util')
const { pid } = process
const hostname = os.hostname()
test('thread-stream async flush', async ({ equal, same }) => {
const destination = file()
const transport = pino.transport({
target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'),
options: { destination }
})
const instance = pino(transport)
instance.info('hello')
equal(instance.flush(), undefined)
await watchFileCreated(destination)
const result = JSON.parse(await readFile(destination))
delete result.time
same(result, {
pid,
hostname,
level: 30,
msg: 'hello'
})
})
test('thread-stream async flush should call the passed callback', async (t) => {
const outputPath = file()
async function getOutputLogLines () {
return (await readFile(outputPath)).toString().trim().split('\n').map(JSON.parse)
}
const transport = pino.transport({
target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'),
options: { destination: outputPath }
})
const instance = pino(transport)
const flushPromise = promisify(instance.flush).bind(instance)
instance.info('hello')
await flushPromise()
await watchFileCreated(outputPath)
const [firstFlushData] = await getOutputLogLines()
t.equal(firstFlushData.msg, 'hello')
// should not flush this as no data accumulated that's bigger than min length
instance.info('world')
// Making sure data is not flushed yet
const afterLogData = await getOutputLogLines()
t.equal(afterLogData.length, 1)
await flushPromise()
// Making sure data is not flushed yet
const afterSecondFlush = (await getOutputLogLines())[1]
t.equal(afterSecondFlush.msg, 'world')
})
@@ -0,0 +1,28 @@
'use strict'
const { test } = require('tap')
const proxyquire = require('proxyquire')
const Writable = require('stream').Writable
test('file-target mocked', async function ({ equal, same, plan, pass }) {
plan(1)
let ret
const fileTarget = proxyquire('../../file', {
'./pino': {
destination (opts) {
same(opts, { dest: 1, sync: false })
ret = new Writable()
ret.fd = opts.dest
process.nextTick(() => {
ret.emit('ready')
})
return ret
}
}
})
await fileTarget()
})
@@ -0,0 +1,167 @@
'use strict'
const os = require('os')
const { join } = require('path')
const { readFile } = require('fs').promises
const writeStream = require('flush-write-stream')
const { watchFileCreated, file } = require('../helper')
const { test } = require('tap')
const pino = require('../../')
const { pid } = process
const hostname = os.hostname()
function serializeError (error) {
return {
type: error.name,
message: error.message,
stack: error.stack
}
}
function parseLogs (buffer) {
return JSON.parse(`[${buffer.toString().replace(/}{/g, '},{')}]`)
}
test('transport uses pino config', async ({ same, teardown, plan }) => {
plan(1)
const destination = file()
const transport = pino.transport({
pipeline: [{
target: join(__dirname, '..', 'fixtures', 'transport-uses-pino-config.js')
}, {
target: 'pino/file',
options: { destination }
}]
})
teardown(transport.end.bind(transport))
const instance = pino({
messageKey: 'customMessageKey',
errorKey: 'customErrorKey',
customLevels: { custom: 35 }
}, transport)
const error = new Error('bar')
instance.custom('foo')
instance.error(error)
await watchFileCreated(destination)
const result = parseLogs(await readFile(destination))
same(result, [{
severityText: 'custom',
body: 'foo',
attributes: {
pid,
hostname
}
}, {
severityText: 'error',
body: 'bar',
attributes: {
pid,
hostname
},
error: serializeError(error)
}])
})
test('transport uses pino config without customizations', async ({ same, teardown, plan }) => {
plan(1)
const destination = file()
const transport = pino.transport({
pipeline: [{
target: join(__dirname, '..', 'fixtures', 'transport-uses-pino-config.js')
}, {
target: 'pino/file',
options: { destination }
}]
})
teardown(transport.end.bind(transport))
const instance = pino(transport)
const error = new Error('qux')
instance.info('baz')
instance.error(error)
await watchFileCreated(destination)
const result = parseLogs(await readFile(destination))
same(result, [{
severityText: 'info',
body: 'baz',
attributes: {
pid,
hostname
}
}, {
severityText: 'error',
body: 'qux',
attributes: {
pid,
hostname
},
error: serializeError(error)
}])
})
test('transport uses pino config with multistream', async ({ same, teardown, plan }) => {
plan(2)
const destination = file()
const messages = []
const stream = writeStream(function (data, enc, cb) {
const message = JSON.parse(data)
delete message.time
messages.push(message)
cb()
})
const transport = pino.transport({
pipeline: [{
target: join(__dirname, '..', 'fixtures', 'transport-uses-pino-config.js')
}, {
target: 'pino/file',
options: { destination }
}]
})
teardown(transport.end.bind(transport))
const instance = pino({
messageKey: 'customMessageKey',
errorKey: 'customErrorKey',
customLevels: { custom: 35 }
}, pino.multistream([transport, { stream }]))
const error = new Error('buzz')
const serializedError = serializeError(error)
instance.custom('fizz')
instance.error(error)
await watchFileCreated(destination)
const result = parseLogs(await readFile(destination))
same(result, [{
severityText: 'custom',
body: 'fizz',
attributes: {
pid,
hostname
}
}, {
severityText: 'error',
body: 'buzz',
attributes: {
pid,
hostname
},
error: serializedError
}])
same(messages, [{
level: 35,
pid,
hostname,
customMessageKey: 'fizz'
}, {
level: 50,
pid,
hostname,
customErrorKey: serializedError,
customMessageKey: 'buzz'
}])
})
@@ -0,0 +1,29 @@
import { expectType } from "tsd";
import pino from '../../pino';
import { pino as pinoNamed, P } from "../../pino";
import * as pinoStar from "../../pino";
import pinoCjsImport = require ("../../pino");
const pinoCjs = require("../../pino");
const { P: pinoCjsNamed } = require('pino')
const log = pino();
expectType<P.LogFn>(log.info);
expectType<P.LogFn>(log.error);
expectType<pino.Logger>(pinoNamed());
expectType<P.Logger>(pinoNamed());
expectType<pino.Logger>(pinoStar.default());
expectType<pino.Logger>(pinoStar.pino());
expectType<pino.Logger>(pinoCjsImport.default());
expectType<pino.Logger>(pinoCjsImport.pino());
expectType<any>(pinoCjsNamed());
expectType<any>(pinoCjs());
const levelChangeEventListener: P.LevelChangeEventListener = (
lvl: P.LevelWithSilent | string,
val: number,
prevLvl: P.LevelWithSilent | string,
prevVal: number,
) => {}
expectType<P.LevelChangeEventListener>(levelChangeEventListener)
@@ -0,0 +1,28 @@
import { expectType } from 'tsd'
import { createWriteStream } from 'fs'
import pino, { multistream } from '../../pino'
const streams = [
{ stream: process.stdout },
{ stream: createWriteStream('') },
{ level: 'error' as const, stream: process.stderr },
{ level: 'fatal' as const, stream: process.stderr },
]
expectType<pino.MultiStreamRes>(pino.multistream(process.stdout))
expectType<pino.MultiStreamRes>(pino.multistream([createWriteStream('')]))
expectType<pino.MultiStreamRes<'error'>>(pino.multistream({ level: 'error' as const, stream: process.stderr }))
expectType<pino.MultiStreamRes<'fatal'>>(pino.multistream([{ level: 'fatal' as const, stream: createWriteStream('') }]))
expectType<pino.MultiStreamRes<'error' | 'fatal'>>(pino.multistream(streams))
expectType<pino.MultiStreamRes<'error' | 'fatal'>>(pino.multistream(streams, {}))
expectType<pino.MultiStreamRes<'error' | 'fatal'>>(pino.multistream(streams, { levels: { 'info': 30 } }))
expectType<pino.MultiStreamRes<'error' | 'fatal'>>(pino.multistream(streams, { dedupe: true }))
expectType<pino.MultiStreamRes<'error' | 'fatal'>>(pino.multistream(streams[0]).add(streams[1]))
expectType<pino.MultiStreamRes<'error' | 'fatal'>>(multistream(streams))
expectType<pino.MultiStreamRes<'error'>>(multistream(streams).clone('error'))
expectType<pino.MultiStreamRes>(multistream(process.stdout));
@@ -0,0 +1,35 @@
import { expectType, expectAssignable } from 'tsd'
import type { SonicBoom } from "sonic-boom";
import {
destination,
LevelMapping,
levels,
Logger,
multistream,
MultiStreamRes,
SerializedError,
stdSerializers,
stdTimeFunctions,
symbols,
transport,
version,
} from "../../pino";
import pino from "../../pino";
expectType<SonicBoom>(destination(""));
expectType<LevelMapping>(levels);
expectType<MultiStreamRes>(multistream(process.stdout));
expectType<SerializedError>(stdSerializers.err({} as any));
expectType<string>(stdTimeFunctions.isoTime());
expectType<string>(version);
// Can't test against `unique symbol`, see https://github.com/SamVerschueren/tsd/issues/49
expectAssignable<Symbol>(symbols.endSym);
// TODO: currently returns (aliased) `any`, waiting for strong typed `thread-stream`
transport({
target: '#pino/pretty',
options: { some: 'options for', the: 'transport' }
});
@@ -0,0 +1,145 @@
import { pino } from '../../pino'
import { expectType } from "tsd";
// Single
const transport = pino.transport({
target: '#pino/pretty',
options: { some: 'options for', the: 'transport' }
})
pino(transport)
expectType<pino.Logger>(pino({
transport: {
target: 'pino-pretty'
},
}))
// Multiple
const transports = pino.transport({targets: [
{
level: 'info',
target: '#pino/pretty',
options: { some: 'options for', the: 'transport' }
},
{
level: 'trace',
target: '#pino/file',
options: { destination: './test.log' }
}
]})
pino(transports)
expectType<pino.Logger>(pino({
transport: {targets: [
{
level: 'info',
target: '#pino/pretty',
options: { some: 'options for', the: 'transport' }
},
{
level: 'trace',
target: '#pino/file',
options: { destination: './test.log' }
}
]},
}))
const transportsWithCustomLevels = pino.transport({targets: [
{
level: 'info',
target: '#pino/pretty',
options: { some: 'options for', the: 'transport' }
},
{
level: 'foo',
target: '#pino/file',
options: { destination: './test.log' }
}
], levels: { foo: 35 }})
pino(transports)
expectType<pino.Logger>(pino({
transport: {targets: [
{
level: 'info',
target: '#pino/pretty',
options: { some: 'options for', the: 'transport' }
},
{
level: 'trace',
target: '#pino/file',
options: { destination: './test.log' }
}
], levels: { foo: 35 }
},
}))
const transportsWithoutOptions = pino.transport({
targets: [
{ target: '#pino/pretty' },
{ target: '#pino/file' }
], levels: { foo: 35 }
})
pino(transports)
expectType<pino.Logger>(pino({
transport: {
targets: [
{ target: '#pino/pretty' },
{ target: '#pino/file' }
], levels: { foo: 35 }
},
}))
const pipelineTransport = pino.transport({
pipeline: [{
target: './my-transform.js'
}, {
// Use target: 'pino/file' to write to stdout
// without any change.
target: 'pino-pretty'
}]
})
pino(pipelineTransport)
expectType<pino.Logger>(pino({
transport: {
pipeline: [{
target: './my-transform.js'
}, {
// Use target: 'pino/file' to write to stdout
// without any change.
target: 'pino-pretty'
}]
}
}))
type TransportConfig = {
id: string
}
// Custom transport params
const customTransport = pino.transport<TransportConfig>({
target: 'custom',
options: { id: 'abc' }
})
pino(customTransport)
// Worker
pino.transport({
target: 'custom',
worker: {
argv: ['a', 'b'],
stdin: false,
stderr: true,
stdout: false,
autoEnd: true,
},
options: { id: 'abc' }
})
// Dedupe
pino.transport({
targets: [],
dedupe: true,
})
@@ -0,0 +1,48 @@
import { expectAssignable, expectType, expectNotAssignable } from "tsd";
import pino from "../../";
import type {LevelWithSilent, Logger, LogFn, P, DestinationStreamWithMetadata, Level, LevelOrString, LevelWithSilentOrString, LoggerExtras } from "../../pino";
// NB: can also use `import * as pino`, but that form is callable as `pino()`
// under `esModuleInterop: false` or `pino.default()` under `esModuleInterop: true`.
const log = pino();
expectAssignable<LoggerExtras>(log);
expectType<Logger>(log);
expectType<LogFn>(log.info);
expectType<P.Logger>(log);
expectType<P.LogFn>(log.info);
expectType<Parameters<typeof log.isLevelEnabled>>([log.level]);
const level: Level = 'debug';
expectAssignable<string>(level);
expectAssignable<P.Level>(level);
const levelWithSilent: LevelWithSilent = 'silent';
expectAssignable<string>(levelWithSilent);
expectAssignable<P.LevelWithSilent>(levelWithSilent);
const levelOrString: LevelOrString = "myCustomLevel";
expectAssignable<string>(levelOrString);
expectNotAssignable<pino.Level>(levelOrString);
expectNotAssignable<pino.LevelWithSilent>(levelOrString);
expectAssignable<pino.LevelWithSilentOrString>(levelOrString);
const levelWithSilentOrString: LevelWithSilentOrString = "myCustomLevel";
expectAssignable<string>(levelWithSilentOrString);
expectNotAssignable<pino.Level>(levelWithSilentOrString);
expectNotAssignable<pino.LevelWithSilent>(levelWithSilentOrString);
expectAssignable<pino.LevelOrString>(levelWithSilentOrString);
function createStream(): DestinationStreamWithMetadata {
return { write() {} };
}
const stream = createStream();
// Argh. TypeScript doesn't seem to narrow unless we assign the symbol like so, and tsd seems to
// break without annotating the type explicitly
const needsMetadata: typeof pino.symbols.needsMetadataGsym = pino.symbols.needsMetadataGsym;
if (stream[needsMetadata]) {
expectType<number>(stream.lastLevel);
}
+441
View File
@@ -0,0 +1,441 @@
import { IncomingMessage, ServerResponse } from "http";
import { Socket } from "net";
import { expectError, expectType } from 'tsd';
import P, { pino } from "../../";
import Logger = P.Logger;
const log = pino();
const info = log.info;
const error = log.error;
info("hello world");
error("this is at error level");
info("the answer is %d", 42);
info({ obj: 42 }, "hello world");
info({ obj: 42, b: 2 }, "hello world");
info({ obj: { aa: "bbb" } }, "another");
setImmediate(info, "after setImmediate");
error(new Error("an error"));
const writeSym = pino.symbols.writeSym;
const testUniqSymbol = {
[pino.symbols.needsMetadataGsym]: true,
}[pino.symbols.needsMetadataGsym];
const log2: P.Logger = pino({
name: "myapp",
safe: true,
serializers: {
req: pino.stdSerializers.req,
res: pino.stdSerializers.res,
err: pino.stdSerializers.err,
},
});
pino({
write(o) {},
});
pino({
mixin() {
return { customName: "unknown", customId: 111 };
},
});
pino({
mixin: () => ({ customName: "unknown", customId: 111 }),
});
pino({
mixin: (context: object) => ({ customName: "unknown", customId: 111 }),
});
pino({
mixin: (context: object, level: number) => ({ customName: "unknown", customId: 111 }),
});
pino({
redact: { paths: [], censor: "SECRET" },
});
pino({
redact: { paths: [], censor: () => "SECRET" },
});
pino({
redact: { paths: [], censor: (value) => value },
});
pino({
redact: { paths: [], censor: (value, path) => path.join() },
});
pino({
depthLimit: 1
});
pino({
edgeLimit: 1
});
pino({
browser: {
write(o) {},
},
});
pino({
browser: {
write: {
info(o) {},
error(o) {},
},
serialize: true,
asObject: true,
transmit: {
level: "fatal",
send: (level, logEvent) => {
level;
logEvent.bindings;
logEvent.level;
logEvent.ts;
logEvent.messages;
},
},
disabled: false
},
});
pino({}, undefined);
pino({ base: null });
if ("pino" in log) console.log(`pino version: ${log.pino}`);
expectType<void>(log.flush());
log.flush((err?: Error) => undefined);
log.child({ a: "property" }).info("hello child!");
log.level = "error";
log.info("nope");
const child = log.child({ foo: "bar" });
child.info("nope again");
child.level = "info";
child.info("hooray");
log.info("nope nope nope");
log.child({ foo: "bar" }, { level: "debug" }).debug("debug!");
child.bindings();
const customSerializers = {
test() {
return "this is my serializer";
},
};
pino().child({}, { serializers: customSerializers }).info({ test: "should not show up" });
const child2 = log.child({ father: true });
const childChild = child2.child({ baby: true });
const childRedacted = pino().child({}, { redact: ["path"] })
childRedacted.info({
msg: "logged with redacted properties",
path: "Not shown",
});
const childAnotherRedacted = pino().child({}, {
redact: {
paths: ["anotherPath"],
censor: "Not the log you\re looking for",
}
})
childAnotherRedacted.info({
msg: "another logged with redacted properties",
anotherPath: "Not shown",
});
log.level = "info";
if (log.levelVal === 30) {
console.log("logger level is `info`");
}
const listener = (lvl: any, val: any, prevLvl: any, prevVal: any) => {
console.log(lvl, val, prevLvl, prevVal);
};
log.on("level-change", (lvl, val, prevLvl, prevVal, logger) => {
console.log(lvl, val, prevLvl, prevVal);
});
log.level = "trace";
log.removeListener("level-change", listener);
log.level = "info";
pino.levels.values.error === 50;
pino.levels.labels[50] === "error";
const logstderr: pino.Logger = pino(process.stderr);
logstderr.error("on stderr instead of stdout");
log.useLevelLabels = true;
log.info("lol");
log.level === "info";
const isEnabled: boolean = log.isLevelEnabled("info");
const redacted = pino({
redact: ["path"],
});
redacted.info({
msg: "logged with redacted properties",
path: "Not shown",
});
const anotherRedacted = pino({
redact: {
paths: ["anotherPath"],
censor: "Not the log you\re looking for",
},
});
anotherRedacted.info({
msg: "another logged with redacted properties",
anotherPath: "Not shown",
});
const withTimeFn = pino({
timestamp: pino.stdTimeFunctions.isoTime,
});
const withNestedKey = pino({
nestedKey: "payload",
});
const withHooks = pino({
hooks: {
logMethod(args, method, level) {
expectType<pino.Logger>(this);
return method.apply(this, args);
},
},
});
// Properties/types imported from pino-std-serializers
const wrappedErrSerializer = pino.stdSerializers.wrapErrorSerializer((err: pino.SerializedError) => {
return { ...err, newProp: "foo" };
});
const wrappedReqSerializer = pino.stdSerializers.wrapRequestSerializer((req: pino.SerializedRequest) => {
return { ...req, newProp: "foo" };
});
const wrappedResSerializer = pino.stdSerializers.wrapResponseSerializer((res: pino.SerializedResponse) => {
return { ...res, newProp: "foo" };
});
const socket = new Socket();
const incomingMessage = new IncomingMessage(socket);
const serverResponse = new ServerResponse(incomingMessage);
const mappedHttpRequest: { req: pino.SerializedRequest } = pino.stdSerializers.mapHttpRequest(incomingMessage);
const mappedHttpResponse: { res: pino.SerializedResponse } = pino.stdSerializers.mapHttpResponse(serverResponse);
const serializedErr: pino.SerializedError = pino.stdSerializers.err(new Error());
const serializedReq: pino.SerializedRequest = pino.stdSerializers.req(incomingMessage);
const serializedRes: pino.SerializedResponse = pino.stdSerializers.res(serverResponse);
/**
* Destination static method
*/
const destinationViaDefaultArgs = pino.destination();
const destinationViaStrFileDescriptor = pino.destination("/log/path");
const destinationViaNumFileDescriptor = pino.destination(2);
const destinationViaStream = pino.destination(process.stdout);
const destinationViaOptionsObject = pino.destination({ dest: "/log/path", sync: false });
pino(destinationViaDefaultArgs);
pino({ name: "my-logger" }, destinationViaDefaultArgs);
pino(destinationViaStrFileDescriptor);
pino({ name: "my-logger" }, destinationViaStrFileDescriptor);
pino(destinationViaNumFileDescriptor);
pino({ name: "my-logger" }, destinationViaNumFileDescriptor);
pino(destinationViaStream);
pino({ name: "my-logger" }, destinationViaStream);
pino(destinationViaOptionsObject);
pino({ name: "my-logger" }, destinationViaOptionsObject);
try {
throw new Error('Some error')
} catch (err) {
log.error(err)
}
interface StrictShape {
activity: string;
err?: unknown;
}
info<StrictShape>({
activity: "Required property",
});
const logLine: pino.LogDescriptor = {
level: 20,
msg: "A log message",
time: new Date().getTime(),
aCustomProperty: true,
};
interface CustomLogger extends pino.Logger {
customMethod(msg: string, ...args: unknown[]): void;
}
const serializerFunc: pino.SerializerFn = () => {}
const writeFunc: pino.WriteFn = () => {}
interface CustomBaseLogger extends pino.BaseLogger {
child(): CustomBaseLogger
}
const customBaseLogger: CustomBaseLogger = {
level: 'info',
fatal() {},
error() {},
warn() {},
info() {},
debug() {},
trace() {},
silent() {},
child() { return this }
}
// custom levels
const log3 = pino({ customLevels: { myLevel: 100 } })
expectError(log3.log())
log3.level = 'myLevel'
log3.myLevel('')
log3.child({}).myLevel('')
log3.on('level-change', (lvl, val, prevLvl, prevVal, instance) => {
instance.myLevel('foo');
});
const clog3 = log3.child({}, { customLevels: { childLevel: 120 } })
// child inherit parent
clog3.myLevel('')
// child itself
clog3.childLevel('')
const cclog3 = clog3.child({}, { customLevels: { childLevel2: 130 } })
// child inherit root
cclog3.myLevel('')
// child inherit parent
cclog3.childLevel('')
// child itself
cclog3.childLevel2('')
const ccclog3 = clog3.child({})
expectError(ccclog3.nonLevel(''))
const withChildCallback = pino({
onChild: (child: Logger) => {}
})
withChildCallback.onChild = (child: Logger) => {}
pino({
crlf: true,
});
const customLevels = { foo: 99, bar: 42 }
const customLevelLogger = pino({ customLevels });
type CustomLevelLogger = typeof customLevelLogger
type CustomLevelLoggerLevels = pino.Level | keyof typeof customLevels
const fn = (logger: Pick<CustomLevelLogger, CustomLevelLoggerLevels>) => {}
const customLevelChildLogger = customLevelLogger.child({ name: "child" })
fn(customLevelChildLogger); // missing foo typing
// unknown option
expectError(
pino({
hello: 'world'
})
);
// unknown option
expectError(
pino({
hello: 'world',
customLevels: {
'log': 30
}
})
);
function dangerous () {
throw Error('foo')
}
try {
dangerous()
} catch (err) {
log.error(err)
}
try {
dangerous()
} catch (err) {
log.error({ err })
}
const bLogger = pino({
customLevels: {
log: 5,
},
level: 'log',
transport: {
target: 'pino-pretty',
options: {
colorize: true,
},
},
});
expectType<Logger<'log'>>(pino({
customLevels: {
log: 5,
},
level: 'log',
transport: {
target: 'pino-pretty',
options: {
colorize: true,
},
},
}))
const parentLogger1 = pino({
customLevels: { myLevel: 90 },
onChild: (child) => { const a = child.myLevel; }
}, process.stdout)
parentLogger1.onChild = (child) => { child.myLevel(''); }
const childLogger1 = parentLogger1.child({});
childLogger1.myLevel('');
expectError(childLogger1.doesntExist(''));
const parentLogger2 = pino({}, process.stdin);
expectError(parentLogger2.onChild = (child) => { const b = child.doesntExist; });
const childLogger2 = parentLogger2.child({});
expectError(childLogger2.doesntExist);
expectError(pino({
onChild: (child) => { const a = child.doesntExist; }
}, process.stdout));
const pinoWithoutLevelsSorting = pino({});
const pinoWithDescSortingLevels = pino({ levelComparison: 'DESC' });
const pinoWithAscSortingLevels = pino({ levelComparison: 'ASC' });
const pinoWithCustomSortingLevels = pino({ levelComparison: () => false });
// with wrong level comparison direction
expectError(pino({ levelComparison: 'SOME'}), process.stdout);
// with wrong level comparison type
expectError(pino({ levelComparison: 123}), process.stdout);
// with wrong custom level comparison return type
expectError(pino({ levelComparison: () => null }), process.stdout);
expectError(pino({ levelComparison: () => 1 }), process.stdout);
expectError(pino({ levelComparison: () => 'string' }), process.stdout);
+78
View File
@@ -0,0 +1,78 @@
import { StreamEntry, pino } from '../../pino'
import { join } from 'path'
import { tmpdir } from 'os'
import pinoPretty from 'pino-pretty'
const destination = join(
tmpdir(),
'_' + Math.random().toString(36).substr(2, 9)
)
// Single
const transport = pino.transport({
target: 'pino-pretty',
options: { some: 'options for', the: 'transport' }
})
const logger = pino(transport)
logger.setBindings({ some: 'bindings' })
logger.info('test2')
logger.flush()
const transport2 = pino.transport({
target: 'pino-pretty',
})
const logger2 = pino(transport2)
logger2.info('test2')
// Multiple
const transports = pino.transport({targets: [
{
level: 'info',
target: 'pino-pretty',
options: { some: 'options for', the: 'transport' }
},
{
level: 'trace',
target: 'pino/file',
options: { destination }
}
]})
const loggerMulti = pino(transports)
loggerMulti.info('test2')
// custom levels
const customLevels = {
debug : 1,
info : 2,
network : 3,
error : 4,
};
type CustomLevels = keyof typeof customLevels;
const pinoOpts = {
level: 'debug',
useOnlyCustomLevels: true,
customLevels: customLevels,
};
const multistreamOpts = {
dedupe: true,
levels: customLevels
};
const streams: StreamEntry<CustomLevels>[] = [
{ level : 'debug', stream : pinoPretty() },
{ level : 'info', stream : pinoPretty() },
{ level : 'network', stream : pinoPretty() },
{ level : 'error', stream : pinoPretty() },
];
const loggerCustomLevel = pino(pinoOpts, pino.multistream(streams, multistreamOpts));
loggerCustomLevel.debug('test3')
loggerCustomLevel.info('test4')
loggerCustomLevel.error('test5')
loggerCustomLevel.network('test6')
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es6",
"lib": [ "es2015", "dom" ],
"module": "commonjs",
"noEmit": true,
"strict": true,
"esModuleInterop": true,
},
"exclude": [
"./test/types/*.test-d.ts",
"./*.d.ts"
]
}