├── .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  [![Build Status](https://github.com/pinojs/pino-tee/actions/workflows/ci.yml/badge.svg?branch=master)](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 | --------------------------------------------------------------------------------