├── .github
├── dependabot.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── .taprc
├── LICENSE
├── README.md
├── examples
├── tee-esm.mjs
├── tee-pretty.js
├── tee-transport.js
└── tee.js
├── package.json
├── tee.js
├── test
├── api.js
├── bin-multiple.js
├── bin-stderr.js
├── bin.js
├── transport.js
└── util.js
├── transport.js
└── util.js
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "monthly"
7 | open-pull-requests-limit: 10
8 |
9 | - package-ecosystem: "npm"
10 | directory: "/"
11 | schedule:
12 | interval: "weekly"
13 | open-pull-requests-limit: 10
14 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | paths-ignore:
6 | - 'docs/**'
7 | - '*.md'
8 | pull_request:
9 | paths-ignore:
10 | - 'docs/**'
11 | - '*.md'
12 |
13 | env:
14 | CI: true
15 |
16 | jobs:
17 | build:
18 | runs-on: ${{ matrix.os }}
19 |
20 | permissions:
21 | contents: read
22 |
23 | strategy:
24 | matrix:
25 | node-version: [14, 16, 18]
26 | os: [ubuntu-latest, windows-latest, macOS-latest]
27 |
28 | steps:
29 | - uses: actions/checkout@v3
30 |
31 | - name: Use Node.js
32 | uses: actions/setup-node@v3
33 | with:
34 | node-version: ${{ matrix.node-version }}
35 |
36 | - name: Install
37 | run: npm install --ignore-scripts
38 |
39 | - name: Run tests
40 | run: npm run test
41 |
42 | automerge:
43 | needs: build
44 | runs-on: ubuntu-latest
45 | permissions:
46 | pull-requests: write
47 | contents: write
48 | steps:
49 | - uses: fastify/github-action-merge-dependabot@v3
50 | with:
51 | github-token: ${{ secrets.GITHUB_TOKEN }}
52 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # nyc test coverage
18 | .nyc_output
19 |
20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
21 | .grunt
22 |
23 | # node-waf configuration
24 | .lock-wscript
25 |
26 | # Compiled binary addons (http://nodejs.org/api/addons.html)
27 | build/Release
28 |
29 | # Dependency directories
30 | node_modules
31 | jspm_packages
32 |
33 | # Optional npm cache directory
34 | .npm
35 |
36 | # Optional REPL history
37 | .node_repl_history
38 |
39 | .vscode
40 | package-lock.json
41 |
--------------------------------------------------------------------------------
/.taprc:
--------------------------------------------------------------------------------
1 | check-coverage: false
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 pino
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pino-tee [](https://github.com/pinojs/pino-tee/actions/workflows/ci.yml)
2 |
3 | Tee [pino](https://github.com/pinojs/pino) logs into multiple files,
4 | according to the given levels.
5 | Works with any newline delimited json stream.
6 |
7 | ## Install
8 |
9 | ```bash
10 | npm i pino-tee -g
11 | ```
12 |
13 | ## Usage
14 |
15 | ##### CLI
16 |
17 | Specify a minimum log level to write to file.
18 |
19 | The following writes **info**, **warn** and **error** level logs to `./info-warn-error-log`, and all output of `app.js` to `./all-logs`:
20 |
21 | ```bash
22 | node app.js | pino-tee info ./info-warn-error-logs | tee -a ./all-logs
23 | ```
24 |
25 | (using `tee -a ./all-logs` will both write to `./all-logs` and `stdout`, enabling piping of more pino transports)
26 |
27 | ##### Pino V7+
28 | ```javascript
29 | const pino = require('pino')
30 |
31 | const pinoTee = pino.transport({
32 | target: 'pino-tee',
33 | options: {
34 | filters: {
35 | info: 'info.log',
36 | warn: 'warn.log'
37 | }
38 | }
39 | })
40 |
41 | const logger = pino(pinoTee)
42 |
43 | logger.info('example info log')
44 | logger.error('example error log')
45 | ```
46 |
47 | ##### NodeJS
48 |
49 | You can log to multiple files by spawning a child process. In the following example pino-tee writes into three different files for warn, error & fatal log levels.
50 |
51 | ```javascript
52 | const pino = require('pino')
53 | const childProcess = require('child_process')
54 | const stream = require('stream')
55 |
56 | // Environment variables
57 | const cwd = process.cwd()
58 | const { env } = process
59 | const logPath = `${cwd}/log`
60 |
61 | // Create a stream where the logs will be written
62 | const logThrough = new stream.PassThrough()
63 | const log = pino({ name: 'project' }, logThrough)
64 |
65 | // Log to multiple files using a separate process
66 | const child = childProcess.spawn(process.execPath, [
67 | require.resolve('pino-tee'),
68 | 'warn', `${logPath}/warn.log`,
69 | 'error', `${logPath}/error.log`,
70 | 'fatal', `${logPath}/fatal.log`
71 | ], { cwd, env, stdio: ['pipe', 'inherit', 'inherit'] })
72 |
73 | logThrough.pipe(child.stdin)
74 |
75 | // Writing some test logs
76 | log.warn('WARNING 1')
77 | log.error('ERROR 1')
78 | log.fatal('FATAL 1')
79 | ```
80 |
81 | This prints raw logs into log files, you can also print pretty logs to the console for development purposes. For that, you need to use [pino-multi-stream](http://npm.im/pino-multi-stream). See the example below
82 |
83 | ```js
84 | const pinoms = require('pino-multi-stream')
85 | const childProcess = require('child_process')
86 | const stream = require('stream')
87 |
88 | const cwd = process.cwd()
89 | const { env } = process
90 |
91 | const logThrough = new stream.PassThrough()
92 | const prettyStream = pinoms.prettyStream()
93 | const streams = [
94 | { stream: logThrough },
95 | { stream: prettyStream }
96 | ]
97 | const log = pinoms(pinoms.multistream(streams))
98 |
99 | const child = childProcess.spawn(process.execPath, [
100 | require.resolve('pino-tee'),
101 | 'warn', `${__dirname}/warn`,
102 | 'error', `${__dirname}/error`,
103 | 'fatal', `${__dirname}/fatal`
104 | ], { cwd, env })
105 |
106 | logThrough.pipe(child.stdin)
107 |
108 | // Writing some test logs
109 | log.warn('WARNING 1')
110 | log.error('ERROR 1')
111 | log.fatal('FATAL 1')
112 | ```
113 |
114 | Here, we're tapping into the write stream that pino-tee gets and manually formatting the log line using `pino-pretty` to write on the `stdout`. Note that the pretty printing is typically only done while developing.
115 |
116 | ## API
117 |
118 | ### pinoTee(source)
119 |
120 | Create a new `tee` instance from source. It is an extended instance of
121 | [`cloneable-readable`](https://github.com/mcollina/cloneable-readable).
122 |
123 | Example:
124 |
125 | ```js
126 | const { tee } = require('pino-tee')
127 | const fs = require('fs')
128 | const stream = tee(process.stdin)
129 | stream.tee(fs.createWriteStream('errors'), line => line.level >= 50)
130 | stream.pipe(process.stdout)
131 | ```
132 |
133 | ### stream.tee(dest, [filter])
134 |
135 | Create a new stream that will filter a given line based on some
136 | parameters. Each line is automatically parsed, or skipped if it is not
137 | a newline delimited json.
138 |
139 | The filter can be a `function` with signature `filter(line)`, where
140 | `line` is a parsed JSON object. The filter can also be one of the
141 | [pino levels](https://github.com/pinojs/pino#loggerlevel) either
142 | as text or as a custom level number, in that case _all log lines with
143 | that level or greater will be written_.
144 |
145 |
146 |
147 | ## Acknowledgements
148 |
149 | This project was kindly sponsored by [nearForm](http://nearform.com).
150 |
151 | ## License
152 |
153 | MIT
154 |
--------------------------------------------------------------------------------
/examples/tee-esm.mjs:
--------------------------------------------------------------------------------
1 | import pino from 'pino'
2 | import childProcess from 'child_process'
3 | import stream from 'stream'
4 |
5 | // Environment variables
6 | const cwd = process.cwd()
7 | const { env } = process
8 | const logPath = `${cwd}/log`
9 |
10 | // Create a stream where the logs will be written
11 | const logThrough = new stream.PassThrough()
12 | const log = pino({ name: 'project' }, logThrough)
13 |
14 | // Log to multiple files using a separate process
15 | const child = childProcess.spawn(process.execPath, [
16 | '../tee', // Or path to pino-tee
17 | 'warn', `${logPath}/warn.log`,
18 | 'error', `${logPath}/error.log`,
19 | 'fatal', `${logPath}/fatal.log`
20 | ], { cwd, env })
21 |
22 | logThrough.pipe(child.stdin)
23 |
24 | // Writing some test logs
25 | log.warn('WARNING 1')
26 | log.error('ERROR 1')
27 | log.fatal('FATAL 1')
28 |
--------------------------------------------------------------------------------
/examples/tee-pretty.js:
--------------------------------------------------------------------------------
1 | const pinoms = require('pino-multi-stream')
2 | const childProcess = require('child_process')
3 | const stream = require('stream')
4 |
5 | const cwd = process.cwd()
6 | const { env } = process
7 | const logPath = `${cwd}/log`
8 |
9 | const logThrough = new stream.PassThrough()
10 | const prettyStream = pinoms.prettyStream()
11 | const streams = [
12 | { stream: logThrough },
13 | { stream: prettyStream }
14 | ]
15 | const log = pinoms(pinoms.multistream(streams))
16 |
17 | const child = childProcess.spawn(process.execPath, [
18 | '../tee', // Or require.resolve('pino-tee')
19 | 'warn', `${logPath}/warn.log`,
20 | 'error', `${logPath}/error.log`,
21 | 'fatal', `${logPath}/fatal.log`
22 | ], { cwd, env })
23 |
24 | logThrough.pipe(child.stdin)
25 |
26 | // Writing some test logs
27 | log.warn('WARNING 1')
28 | log.error('ERROR 1')
29 | log.fatal('FATAL 1')
30 |
--------------------------------------------------------------------------------
/examples/tee-transport.js:
--------------------------------------------------------------------------------
1 | const pino = require('pino')
2 |
3 | const pinoTee = pino.transport({
4 | target: 'pino-tee',
5 | options: {
6 | filters: {
7 | info: 'info.log',
8 | warn: 'warn.log'
9 | }
10 | }
11 | })
12 |
13 | const logger = pino(pinoTee)
14 |
15 | logger.info('example info log')
16 | logger.error('example error log')
17 |
--------------------------------------------------------------------------------
/examples/tee.js:
--------------------------------------------------------------------------------
1 | const pino = require('pino')
2 | const childProcess = require('child_process')
3 | const stream = require('stream')
4 |
5 | // Environment variables
6 | const cwd = process.cwd()
7 | const { env } = process
8 | const logPath = `${cwd}/log`
9 |
10 | // Create a stream where the logs will be written
11 | const logThrough = new stream.PassThrough()
12 | const log = pino({ name: 'project' }, logThrough)
13 |
14 | // Log to multiple files using a separate process
15 | const child = childProcess.spawn(process.execPath, [
16 | '../tee', // Or require.resolve('pino-tee')
17 | 'warn', `${logPath}/warn.log`,
18 | 'error', `${logPath}/error.log`,
19 | 'fatal', `${logPath}/fatal.log`
20 | ], { cwd, env })
21 |
22 | logThrough.pipe(child.stdin)
23 |
24 | // Writing some test logs
25 | log.warn('WARNING 1')
26 | log.error('ERROR 1')
27 | log.fatal('FATAL 1')
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pino-tee",
3 | "version": "0.3.0",
4 | "description": "Tee pino logs into a file, with multiple levels",
5 | "main": "tee.js",
6 | "scripts": {
7 | "test": "standard | snazzy && tap test/*.js"
8 | },
9 | "bin": {
10 | "pino-tee": "./tee.js"
11 | },
12 | "precommit": "test",
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/pinojs/pino-tee.git"
16 | },
17 | "keywords": [
18 | "pino",
19 | "logger",
20 | "tee",
21 | "level",
22 | "multiple",
23 | "multi",
24 | "stream"
25 | ],
26 | "author": "Matteo Collina ",
27 | "contributors": [
28 | {
29 | "name": "Salman Mitha",
30 | "email": "salmanmitha@gmail.com"
31 | }
32 | ],
33 | "license": "MIT",
34 | "bugs": {
35 | "url": "https://github.com/pinojs/pino-tee/issues"
36 | },
37 | "homepage": "https://github.com/pinojs/pino-tee#readme",
38 | "devDependencies": {
39 | "pre-commit": "^1.2.2",
40 | "sinon": "^15.0.0",
41 | "snazzy": "^9.0.0",
42 | "standard": "^17.0.0",
43 | "tap": "^16.1.0",
44 | "tmp": "0.2.1"
45 | },
46 | "dependencies": {
47 | "cloneable-readable": "^3.0.0",
48 | "fast-json-parse": "^1.0.3",
49 | "minimist": "^1.2.5",
50 | "pump": "^3.0.0",
51 | "pino": "^8.4.0",
52 | "readable-stream": "^4.0.0",
53 | "split2": "^4.0.0"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/tee.js:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 | 'use strict'
3 |
4 | const split = require('split2')
5 | const cloneable = require('cloneable-readable')
6 | const pump = require('pump')
7 | const Parse = require('fast-json-parse')
8 | const minimist = require('minimist')
9 | const pino = require('pino')
10 | const transport = require('./transport')
11 | const fs = require('fs')
12 |
13 | function tee (origin) {
14 | const clone = cloneable(origin)
15 |
16 | clone.tee = function (dest, filter) {
17 | filter = filter || alwaysTrue
18 | pump(this.clone(), buildFilter(filter), dest)
19 | return dest
20 | }
21 |
22 | return clone
23 | }
24 |
25 | function buildFilter (filter) {
26 | if (typeof filter === 'number') {
27 | const num = filter
28 | filter = function (v) { return v.level >= num }
29 | } else if (typeof filter === 'string') {
30 | const num = pino.levels.values[filter]
31 |
32 | if (typeof num === 'number' && isFinite(num)) {
33 | filter = function (v) { return v.level >= num }
34 | } else {
35 | throw new Error('no such level')
36 | }
37 | }
38 |
39 | return split(function (line) {
40 | const res = new Parse(line)
41 | if (res.value && filter(res.value)) {
42 | return JSON.stringify(res.value) + '\n'
43 | } else {
44 | return undefined
45 | }
46 | })
47 | }
48 |
49 | function alwaysTrue () {
50 | return true
51 | }
52 |
53 | module.exports = transport
54 |
55 | module.exports.tee = tee
56 |
57 | if (require.main === module) {
58 | start()
59 | }
60 |
61 | function start () {
62 | const args = minimist(process.argv.slice(2))
63 | const pairs = []
64 | let i
65 |
66 | if (args._.length % 2) {
67 | console.error('pino-tee requires an even number of args\nUsage: pino-tee [filter dest].')
68 | process.exit(1)
69 | }
70 |
71 | for (i = 0; i < args._.length; i += 2) {
72 | let dest = args._[i + 1]
73 | if (dest === ':2') {
74 | dest = process.stderr
75 | } else {
76 | dest = fs.createWriteStream(dest, { flags: 'a' })
77 | }
78 |
79 | pairs.push({
80 | filter: args._[i],
81 | dest
82 | })
83 | }
84 |
85 | const instance = tee(process.stdin)
86 | pairs.forEach(pair => instance.tee(pair.dest, pair.filter))
87 | instance.pipe(process.stdout)
88 | }
89 |
--------------------------------------------------------------------------------
/test/api.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const split = require('split2')
4 | const { test } = require('tap')
5 | const PassThrough = require('readable-stream').PassThrough
6 | const { tee } = require('..')
7 |
8 | test('tee some logs into another stream', function (t) {
9 | t.plan(4)
10 |
11 | const lines = [{
12 | level: 30,
13 | msg: 'hello world'
14 | }, {
15 | level: 40,
16 | msg: 'a warning'
17 | }]
18 |
19 | const lines2 = Array.from(lines)
20 | const lines3 = Array.from(lines)
21 |
22 | const teed = split(JSON.parse)
23 | const dest = split(JSON.parse)
24 | const origin = new PassThrough()
25 |
26 | const instance = tee(origin)
27 |
28 | instance.pipe(dest)
29 | instance.tee(teed)
30 |
31 | dest.on('data', function (d) {
32 | t.same(d, lines2.shift())
33 | })
34 |
35 | teed.on('data', function (d) {
36 | t.same(d, lines3.shift())
37 | })
38 |
39 | lines.forEach(line => origin.write(JSON.stringify(line) + '\n'))
40 | })
41 |
42 | test('tee some logs into another stream after a while', function (t) {
43 | t.plan(4)
44 |
45 | const lines = [{
46 | level: 30,
47 | msg: 'hello world'
48 | }, {
49 | level: 40,
50 | msg: 'a warning'
51 | }]
52 |
53 | const lines2 = Array.from(lines)
54 | const lines3 = Array.from(lines)
55 |
56 | const teed = split(JSON.parse)
57 | const dest = split(JSON.parse)
58 | const origin = new PassThrough()
59 |
60 | const instance = tee(origin)
61 |
62 | setImmediate(function () {
63 | instance.tee(teed)
64 |
65 | setImmediate(function () {
66 | instance.pipe(dest)
67 | })
68 | })
69 |
70 | dest.on('data', function (d) {
71 | t.same(d, lines2.shift())
72 | })
73 |
74 | teed.on('data', function (d) {
75 | t.same(d, lines3.shift())
76 | })
77 |
78 | lines.forEach(line => origin.write(JSON.stringify(line) + '\n'))
79 | })
80 |
81 | test('filters data', function (t) {
82 | t.plan(3)
83 |
84 | const lines = [{
85 | level: 30,
86 | msg: 'hello world'
87 | }, {
88 | level: 40,
89 | msg: 'a warning'
90 | }]
91 |
92 | const lines2 = Array.from(lines)
93 | const lines3 = [lines[1]]
94 |
95 | const teed = split(JSON.parse)
96 | const dest = split(JSON.parse)
97 | const origin = new PassThrough()
98 |
99 | const instance = tee(origin)
100 |
101 | instance.pipe(dest)
102 | instance.tee(teed, line => line.level > 30)
103 |
104 | dest.on('data', function (d) {
105 | t.same(d, lines2.shift())
106 | })
107 |
108 | teed.on('data', function (d) {
109 | t.same(d, lines3.shift())
110 | })
111 |
112 | lines.forEach(line => origin.write(JSON.stringify(line) + '\n'))
113 | })
114 |
115 | test('skip non-json lines', function (t) {
116 | t.plan(3)
117 |
118 | const lines = [JSON.stringify({
119 | level: 30,
120 | msg: 'hello world'
121 | }), 'muahahha']
122 |
123 | const lines2 = Array.from(lines)
124 | const lines3 = [lines[0]]
125 |
126 | const teed = split()
127 | const dest = split()
128 | const origin = new PassThrough()
129 |
130 | const instance = tee(origin)
131 |
132 | instance.pipe(dest)
133 | instance.tee(teed)
134 |
135 | dest.on('data', function (d) {
136 | t.same(d, lines2.shift())
137 | })
138 |
139 | teed.on('data', function (d) {
140 | t.same(d, lines3.shift())
141 | })
142 |
143 | lines.forEach(line => origin.write(line + '\n'))
144 | })
145 |
146 | test('filters data using a level name', function (t) {
147 | t.plan(5)
148 |
149 | const lines = [{
150 | level: 30,
151 | msg: 'hello world'
152 | }, {
153 | level: 20,
154 | msg: 'a debug statement'
155 | }, {
156 | level: 40,
157 | msg: 'a warning'
158 | }]
159 |
160 | const lines2 = Array.from(lines)
161 | const lines3 = [lines[0], lines[2]]
162 |
163 | const teed = split(JSON.parse)
164 | const dest = split(JSON.parse)
165 | const origin = new PassThrough()
166 |
167 | const instance = tee(origin)
168 |
169 | instance.pipe(dest)
170 | instance.tee(teed, 'info')
171 |
172 | dest.on('data', function (d) {
173 | t.same(d, lines2.shift())
174 | })
175 |
176 | teed.on('data', function (d) {
177 | t.same(d, lines3.shift())
178 | })
179 |
180 | lines.forEach(line => origin.write(JSON.stringify(line) + '\n'))
181 | })
182 |
183 | test('filters data using a wrong level name', function (t) {
184 | t.plan(1)
185 |
186 | const teed = split(JSON.parse)
187 | const dest = split(JSON.parse)
188 | const origin = new PassThrough()
189 |
190 | const instance = tee(origin)
191 |
192 | instance.pipe(dest)
193 | t.throws(() => instance.tee(teed, 'unknown'))
194 | })
195 |
196 | test('filters data using a custon level number', function (t) {
197 | t.plan(9)
198 |
199 | const lines = [{
200 | level: 30,
201 | msg: 'hello world'
202 | }, {
203 | level: 20,
204 | msg: 'a debug statement'
205 | }, {
206 | level: 35,
207 | msg: 'a custom level log line'
208 | }, {
209 | level: 40,
210 | msg: 'a warning'
211 | }]
212 |
213 | const lines2 = Array.from(lines)
214 | const lines30 = lines.filter(x => x.level >= 30)
215 | const lines35 = lines.filter(x => x.level >= 35)
216 |
217 | const teed30 = split(JSON.parse)
218 | const teed35 = split(JSON.parse)
219 | const dest = split(JSON.parse)
220 | const origin = new PassThrough()
221 |
222 | const instance = tee(origin)
223 |
224 | instance.pipe(dest)
225 | instance.tee(teed30, 30)
226 | instance.tee(teed35, 35)
227 |
228 | dest.on('data', function (d) {
229 | t.same(d, lines2.shift())
230 | })
231 |
232 | teed30.on('data', function (d) {
233 | t.same(d, lines30.shift())
234 | })
235 |
236 | teed35.on('data', function (d) {
237 | t.same(d, lines35.shift())
238 | })
239 |
240 | lines.forEach(line => origin.write(JSON.stringify(line) + '\n'))
241 | })
242 |
--------------------------------------------------------------------------------
/test/bin-multiple.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const fs = require('fs')
4 | const split = require('split2')
5 | const t = require('tap')
6 | const tmp = require('tmp')
7 | const path = require('path')
8 | const childProcess = require('child_process')
9 | const file1 = tmp.fileSync()
10 | const file2 = tmp.fileSync()
11 |
12 | t.test('bin-multiple', t => {
13 | t.plan(5)
14 |
15 | const args = [
16 | path.join(__dirname, '../tee.js'),
17 | 'info',
18 | file1.name,
19 | 'warn',
20 | file2.name
21 | ]
22 |
23 | const child = childProcess.spawn(process.execPath, args, {
24 | cwd: path.join(__dirname),
25 | env: process.env,
26 | stdio: ['pipe', 'pipe', 'pipe'],
27 | detached: false
28 | })
29 |
30 | t.teardown(() => {
31 | child.stdin.end()
32 | child.kill()
33 | })
34 |
35 | const messages = [{
36 | level: 30,
37 | msg: 'hello'
38 | }, {
39 | level: 20,
40 | msg: 'should not be seen'
41 | }, {
42 | level: 40,
43 | msg: 'a warning'
44 | }]
45 |
46 | const expected1 = [messages[0], messages[2]]
47 | const expected2 = [messages[2]]
48 |
49 | messages.forEach(line => child.stdin.write(JSON.stringify(line) + '\n'))
50 | child.stderr.pipe(process.stderr)
51 |
52 | child.stdout.pipe(split(JSON.parse)).on('data', function (data) {
53 | t.same(data, messages.shift())
54 | if (messages.length === 0) {
55 | checkFile('info', file1, expected1)
56 | checkFile('warn', file2, expected2)
57 | }
58 | })
59 |
60 | function checkFile (level, file, expected) {
61 | t.test('checking ' + level + ' file', function (t) {
62 | t.plan(expected.length)
63 |
64 | fs.createReadStream(file.name)
65 | .pipe(split(JSON.parse))
66 | .on('data', function (data) {
67 | t.same(data, expected.shift())
68 | })
69 | })
70 | }
71 | })
72 |
--------------------------------------------------------------------------------
/test/bin-stderr.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const split = require('split2')
4 | const t = require('tap')
5 | const path = require('path')
6 | const childProcess = require('child_process')
7 |
8 | t.plan(4)
9 |
10 | const args = [
11 | path.join(__dirname, '../tee.js'),
12 | 'error',
13 | ':2'
14 | ]
15 |
16 | const child = childProcess.spawn(process.execPath, args, {
17 | cwd: path.join(__dirname),
18 | env: process.env,
19 | stdio: ['pipe', 'pipe', 'pipe'],
20 | detached: false
21 | })
22 |
23 | t.teardown(() => {
24 | child.stdin.end()
25 | child.kill()
26 | })
27 |
28 | const messages = [{
29 | level: 30,
30 | msg: 'hello'
31 | }, {
32 | level: 20,
33 | msg: 'should not be seen'
34 | }, {
35 | level: 50,
36 | msg: 'an error'
37 | }]
38 |
39 | const expected = [messages[2]]
40 |
41 | messages.forEach(line => child.stdin.write(JSON.stringify(line) + '\n'))
42 |
43 | child.stdout.pipe(split(JSON.parse)).on('data', function (data) {
44 | t.same(data, messages.shift())
45 | })
46 |
47 | child
48 | .stderr
49 | .pipe(split(JSON.parse))
50 | .on('data', function (data) {
51 | t.same(data, expected.shift())
52 | })
53 |
--------------------------------------------------------------------------------
/test/bin.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const fs = require('fs')
4 | const split = require('split2')
5 | const { test } = require('tap')
6 | const tmp = require('tmp')
7 | const path = require('path')
8 | const childProcess = require('child_process')
9 |
10 | test('invoked with correct args', (t) => {
11 | t.plan(5)
12 |
13 | const file = tmp.fileSync()
14 | const args = [
15 | path.join(__dirname, '../tee.js'),
16 | 'info',
17 | file.name
18 | ]
19 |
20 | const child = childProcess.spawn(process.execPath, args, {
21 | cwd: path.join(__dirname),
22 | env: process.env,
23 | stdio: ['pipe', 'pipe', 'pipe'],
24 | detached: false
25 | })
26 |
27 | t.teardown(() => {
28 | child.stdin.end()
29 | child.kill()
30 | })
31 |
32 | const messages = [{
33 | level: 30,
34 | msg: 'hello'
35 | }, {
36 | level: 20,
37 | msg: 'should not be seen'
38 | }, {
39 | level: 40,
40 | msg: 'a warning'
41 | }]
42 |
43 | const expected = [messages[0], messages[2]]
44 |
45 | messages.forEach(line => child.stdin.write(JSON.stringify(line) + '\n'))
46 | child.stderr.pipe(process.stderr)
47 |
48 | child.stdout.pipe(split(JSON.parse)).on('data', function (data) {
49 | t.same(data, messages.shift())
50 | if (messages.length === 0) {
51 | checkFile()
52 | }
53 | })
54 |
55 | function checkFile () {
56 | fs.createReadStream(file.name)
57 | .pipe(split(JSON.parse))
58 | .on('data', function (data) {
59 | t.same(data, expected.shift())
60 | })
61 | }
62 | })
63 |
64 | test('invoked with incorrect args', (t) => {
65 | t.plan(2)
66 |
67 | const args = [
68 | path.join('..', 'tee.js'),
69 | 'info',
70 | 'info.log',
71 | 'warn'
72 | // 'no file name'
73 | ]
74 |
75 | const child = childProcess.spawn(process.execPath, args, {
76 | cwd: path.join(__dirname),
77 | env: process.env,
78 | stdio: ['pipe', 'pipe', 'pipe'],
79 | detached: false
80 | })
81 |
82 | const arr = []
83 | child.stderr.on('data', (d) => {
84 | arr.push(d.toString())
85 | })
86 |
87 | child.on('close', (code) => {
88 | t.same(arr, [
89 | 'pino-tee requires an even number of args\nUsage: pino-tee [filter dest].\n'
90 | ])
91 | t.equal(code, 1)
92 | })
93 | })
94 |
--------------------------------------------------------------------------------
/test/transport.js:
--------------------------------------------------------------------------------
1 | const tap = require('tap')
2 | const sinon = require('sinon')
3 |
4 | const filters = {
5 | info: './info.log',
6 | error: './error.log',
7 | warn: './warn.log'
8 | }
9 |
10 | const streamStub = () => ({ write: sinon.stub() })
11 |
12 | const destinationStreamStubs = {
13 | [filters.info]: streamStub(),
14 | [filters.error]: streamStub(),
15 | [filters.warn]: streamStub()
16 | }
17 |
18 | const { teeTransport } = tap.mock('../transport', {
19 | '../util': {
20 | getLevelNumber: require('../util').getLevelNumber,
21 | getDestinationStream: (dest) => {
22 | return destinationStreamStubs[dest]
23 | }
24 | }
25 | })
26 |
27 | tap.test('should write to info destination stream correctly', async (t) => {
28 | const lineParser = teeTransport({ filters })
29 |
30 | const infoMsg = JSON.stringify({ level: 30, time: 1522431328992, msg: 'info-msg' })
31 | lineParser(infoMsg)
32 | sinon.assert.calledWith(destinationStreamStubs[filters.info].write, infoMsg + '\n')
33 | })
34 |
35 | tap.test('should write to warn destination stream correctly', async (t) => {
36 | const lineParser = teeTransport({ filters })
37 |
38 | const warnMsg = JSON.stringify({ level: 40, time: 1522431328992, msg: 'warn-msg' })
39 | lineParser(warnMsg)
40 | sinon.assert.calledWith(destinationStreamStubs[filters.warn].write, warnMsg + '\n')
41 | })
42 |
43 | tap.test('should write to error destination stream correctly', async (t) => {
44 | const lineParser = teeTransport({ filters })
45 |
46 | const errorMsg = JSON.stringify({ level: 50, time: 1522431328992, msg: 'error-msg' })
47 | lineParser(errorMsg)
48 | sinon.assert.calledWith(destinationStreamStubs[filters.error].write, errorMsg + '\n')
49 | })
50 |
51 | tap.test('should throw when invalid json is supplied', async (t) => {
52 | const lineParser = teeTransport({ filters })
53 |
54 | t.throws(() => lineParser('invalid-json'))
55 | })
56 |
57 | tap.test('should return writable stream from default export ', async (t) => {
58 | const teeTransport = require('../transport')
59 |
60 | const stream = teeTransport({ filters })
61 |
62 | t.equal(typeof stream, 'object')
63 | t.ok(stream.write)
64 | })
65 |
--------------------------------------------------------------------------------
/test/util.js:
--------------------------------------------------------------------------------
1 | const tap = require('tap')
2 | const sinon = require('sinon')
3 |
4 | const { getLevelNumber, getDestinationStream } = require('../util')
5 |
6 | tap.test('getLevelNumber should return the correct level', async function (t) {
7 | t.equal(getLevelNumber(40), 40)
8 | t.equal(getLevelNumber('40'), 40)
9 | t.equal(getLevelNumber('info'), 30)
10 | t.equal(getLevelNumber('warn'), 40)
11 | t.equal(getLevelNumber('error'), 50)
12 | })
13 |
14 | tap.test('getLevelNumber should throw if an invalid level is provided', async function (t) {
15 | t.throws(() => {
16 | getLevelNumber('invalid-level')
17 | })
18 | t.throws(() => {
19 | getLevelNumber(() => {})
20 | })
21 | })
22 |
23 | tap.test('getDestinationStream should call createWriteStream with appropriate params for filepath', async function (t) {
24 | const createWriteStreamStub = sinon.stub()
25 |
26 | const { getDestinationStream } = t.mock('../util', {
27 | fs: { createWriteStream: createWriteStreamStub }
28 | })
29 |
30 | getDestinationStream('./filepath')
31 | sinon.assert.calledWith(createWriteStreamStub, './filepath', { flags: 'a' })
32 | })
33 |
34 | tap.test('getDestinationStream should return process.stderr when', async function (t) {
35 | const stream = getDestinationStream(':2')
36 |
37 | t.ok(stream._isStdio)
38 | t.equal(stream.fd, 2)
39 | })
40 |
--------------------------------------------------------------------------------
/transport.js:
--------------------------------------------------------------------------------
1 | const split = require('split2')
2 | const Parse = require('fast-json-parse')
3 | const { getDestinationStream, getLevelNumber } = require('./util')
4 |
5 | const teeTransport = options => {
6 | const filters = Object
7 | .entries(options.filters)
8 | .map(([level, dest]) => [level, getDestinationStream(dest)])
9 |
10 | return line => {
11 | const res = new Parse(line)
12 |
13 | if (!res.value) {
14 | throw new Error('Failed to parse line: ', line)
15 | }
16 |
17 | filters
18 | .filter(([level]) => res.value.level >= getLevelNumber(level))
19 | .forEach(([, destination]) => destination.write(line + '\n'))
20 | }
21 | }
22 |
23 | module.exports = (options) => {
24 | const splitter = split(teeTransport(options))
25 | // https://github.com/pinojs/thread-stream/issues/36#issuecomment-1008939471
26 | // This fixes the stream hanging and timing out at 10s
27 | splitter.end = splitter.destroy
28 | return splitter
29 | }
30 |
31 | module.exports.teeTransport = teeTransport
32 |
--------------------------------------------------------------------------------
/util.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const pino = require('pino')
3 |
4 | function getDestinationStream (destination) {
5 | return (destination === ':2')
6 | ? process.stderr
7 | : fs.createWriteStream(destination, { flags: 'a' })
8 | }
9 |
10 | function getLevelNumber (level) {
11 | if (typeof level === 'number' || !isNaN(Number(level))) {
12 | return Number(level)
13 | }
14 |
15 | if (typeof level === 'string') {
16 | const num = pino.levels.values[level]
17 | if (typeof num === 'number' && isFinite(num)) {
18 | return num
19 | } else {
20 | throw new Error('no such level')
21 | }
22 | }
23 |
24 | throw new Error('no such level')
25 | }
26 |
27 | module.exports = {
28 | getLevelNumber,
29 | getDestinationStream
30 | }
31 |
--------------------------------------------------------------------------------