UNPKG

pino

Version:

super fast, all natural json logger

734 lines (674 loc) 20.2 kB
'use strict' const test = require('node:test') const assert = require('node:assert') const os = require('node:os') const { join } = require('node:path') const { once } = require('node:events') const { setImmediate: immediate } = require('node:timers/promises') const { readFile, writeFile } = require('node:fs').promises const url = require('url') const strip = require('strip-ansi') const execa = require('execa') const writer = require('flush-write-stream') const rimraf = require('rimraf') const tspl = require('@matteo.collina/tspl') const { match, watchFileCreated, watchForWrite, file } = require('../helper') const pino = require('../../') const { tmpdir } = os const pid = process.pid const hostname = os.hostname() test('pino.transport with file', async (t) => { const destination = file() const transport = pino.transport({ target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'), options: { destination } }) t.after(transport.end.bind(transport)) const instance = pino(transport) instance.info('hello') await watchFileCreated(destination) const result = JSON.parse(await readFile(destination)) delete result.time assert.deepEqual(result, { pid, hostname, level: 30, msg: 'hello' }) }) test('pino.transport with file (no options + error handling)', async () => { const transport = pino.transport({ target: join(__dirname, '..', 'fixtures', 'to-file-transport.js') }) const [err] = await once(transport, 'error') assert.equal(err.message, 'kaboom') }) test('pino.transport with file URL', async (t) => { const destination = file() const transport = pino.transport({ target: url.pathToFileURL(join(__dirname, '..', 'fixtures', 'to-file-transport.js')).href, options: { destination } }) t.after(transport.end.bind(transport)) const instance = pino(transport) instance.info('hello') await watchFileCreated(destination) const result = JSON.parse(await readFile(destination)) delete result.time assert.deepEqual(result, { pid, hostname, level: 30, msg: 'hello' }) }) test('pino.transport errors if file does not exists', (t, end) => { const instance = pino.transport({ target: join(__dirname, '..', 'fixtures', 'non-existent-file'), worker: { stdin: true, stdout: true, stderr: true } }) instance.on('error', function () { assert.ok('error received') end() }) }) test('pino.transport errors if transport worker module does not export a function', async (t) => { // TODO: add case for non-pipelined single target (needs changes in thread-stream) const plan = tspl(t, { 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) { plan.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) { plan.equal(e.message, 'exported worker is not a function') }) await plan }) test('pino.transport with esm', async (t) => { const destination = file() const transport = pino.transport({ target: join(__dirname, '..', 'fixtures', 'to-file-transport.mjs'), options: { destination } }) const instance = pino(transport) t.after(transport.end.bind(transport)) instance.info('hello') await watchFileCreated(destination) const result = JSON.parse(await readFile(destination)) delete result.time assert.deepEqual(result, { pid, hostname, level: 30, msg: 'hello' }) }) test('pino.transport with two files', async (t) => { const dest1 = file() const dest2 = file() const transport = pino.transport({ targets: [{ level: 'info', target: 'file://' + join(__dirname, '..', 'fixtures', 'to-file-transport.js'), options: { destination: dest1 } }, { level: 'info', target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'), options: { destination: dest2 } }] }) t.after(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 assert.deepEqual(result1, { pid, hostname, level: 30, msg: 'hello' }) const result2 = JSON.parse(await readFile(dest2)) delete result2.time assert.deepEqual(result2, { pid, hostname, level: 30, msg: 'hello' }) }) test('pino.transport with two files and custom levels', async (t) => { 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 } }) t.after(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 assert.deepEqual(result1, { pid, hostname, level: 30, msg: 'hello' }) const result2 = JSON.parse(await readFile(dest2)) delete result2.time assert.deepEqual(result2, { pid, hostname, level: 30, msg: 'hello' }) }) test('pino.transport without specifying default levels', async (t) => { const dest = file() const transport = pino.transport({ targets: [{ level: 'foo', target: join(__dirname, '..', 'fixtures', 'to-file-transport.js'), options: { destination: dest } }], levels: { foo: 25 } }) t.after(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 assert.deepEqual(result1, { pid, hostname, level: 30, msg: 'hello' }) }) test('pino.transport with two files and dedupe', async (t) => { 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 } }] }) t.after(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 assert.deepEqual(result1, { pid, hostname, level: 30, msg: 'hello' }) const result2 = JSON.parse(await readFile(dest2)) delete result2.time assert.deepEqual(result2, { pid, hostname, level: 50, msg: 'world' }) }) test('pino.transport with an array including a pino-pretty destination', async (t) => { 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 } }] }) t.after(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 assert.deepEqual(result1, { pid, hostname, level: 30, msg: 'hello' }) const actual = (await readFile(dest2)).toString() assert.match(strip(actual), /\[.*\] INFO.*hello/) }) test('no transport.end()', async (t) => { 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 assert.deepEqual(result, { pid, hostname, level: 30, msg: 'hello' }) }) test('autoEnd = false', async (t) => { 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 } }) t.after(transport.end.bind(transport)) await once(transport, 'ready') const instance = pino(transport) instance.info('hello') await watchFileCreated(destination) assert.equal(count, process.listenerCount('exit')) const result = JSON.parse(await readFile(destination)) delete result.time assert.deepEqual(result, { pid, hostname, level: 30, msg: 'hello' }) }) test('pino.transport with target and targets', async () => { assert.throws( () => { pino.transport({ target: '/a/file', targets: [{ target: '/a/file' }] }) }, /only one of target or targets can be specified/ ) }) test('pino.transport with target pino/file', async (t) => { const destination = file() const transport = pino.transport({ target: 'pino/file', options: { destination } }) t.after(transport.end.bind(transport)) const instance = pino(transport) instance.info('hello') await watchFileCreated(destination) const result = JSON.parse(await readFile(destination)) delete result.time assert.deepEqual(result, { pid, hostname, level: 30, msg: 'hello' }) }) test('pino.transport with target pino/file and mkdir option', async (t) => { const folder = join(tmpdir(), `pino-${process.pid}-mkdir-transport-file`) const destination = join(folder, 'log.txt') t.after(() => { try { rimraf.sync(folder) } catch (err) { // ignore } }) const transport = pino.transport({ target: 'pino/file', options: { destination, mkdir: true } }) t.after(transport.end.bind(transport)) const instance = pino(transport) instance.info('hello') await watchFileCreated(destination) const result = JSON.parse(await readFile(destination)) delete result.time assert.deepEqual(result, { pid, hostname, level: 30, msg: 'hello' }) }) test('pino.transport with target pino/file and append option', async (t) => { 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 } }) t.after(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 assert.deepEqual(result, { pid, hostname, level: 30, msg: 'goodbye' }) }) test('pino.transport should error with unknown target', async () => { assert.throws( () => { pino.transport({ target: 'origin', caller: 'unknown-file.js' }) }, /unable to determine transport target for "origin"/ ) }) test('pino.transport with target pino-pretty', async (t) => { const destination = file() const transport = pino.transport({ target: 'pino-pretty', options: { destination } }) t.after(transport.end.bind(transport)) const instance = pino(transport) instance.info('hello') await watchFileCreated(destination) const actual = await readFile(destination, 'utf8') assert.match(strip(actual), /\[.*\] INFO.*hello/) }) test('sets worker data informing the transport that pino will send its config', async (t) => { const plan = tspl(t, { plan: 1 }) const transport = pino.transport({ target: join(__dirname, '..', 'fixtures', 'transport-worker-data.js') }) t.after(transport.end.bind(transport)) const instance = pino(transport) transport.once('workerData', (workerData) => { match(workerData.workerData, { pinoWillSendConfig: true }) plan.ok('passed') }) instance.info('hello') await plan }) test('sets worker data informing the transport that pino will send its config (frozen file)', async (t) => { const plan = tspl(t, { plan: 1 }) const config = { transport: { target: join(__dirname, '..', 'fixtures', 'transport-worker-data.js'), options: {} } } Object.freeze(config) Object.freeze(config.transport) Object.freeze(config.transport.options) const instance = pino(config) const transport = instance[pino.symbols.streamSym] t.after(transport.end.bind(transport)) transport.once('workerData', (workerData) => { match(workerData.workerData, { pinoWillSendConfig: true }) plan.ok('passed') }) instance.info('hello') await plan }) test('stdout in worker', async () => { let actual = '' const child = execa(process.argv[0], [join(__dirname, '..', 'fixtures', 'transport-main.js')]) for await (const chunk of child.stdout) { actual += chunk } assert.equal(strip(actual).match(/Hello/) != null, true) }) test('log and exit on ready', async () => { 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() assert.equal(strip(actual).match(/Hello/) != null, true) }) test('log and exit before ready', async () => { 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() assert.equal(strip(actual).match(/Hello/) != null, true) }) test('log and exit before ready with async dest', async () => { 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') assert.equal(strip(actual).match(/HELLO/) != null, true) assert.equal(strip(actual).match(/WORLD/) != null, true) }) test('string integer destination', async () => { 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() assert.equal(strip(actual).match(/Hello/) != null, true) }) test('pino transport options with target', async (t) => { const destination = file() const instance = pino({ transport: { target: 'pino/file', options: { destination } } }) const transportStream = instance[pino.symbols.streamSym] t.after(transportStream.end.bind(transportStream)) instance.info('transport option test') await watchFileCreated(destination) const result = JSON.parse(await readFile(destination)) delete result.time assert.deepEqual(result, { pid, hostname, level: 30, msg: 'transport option test' }) }) test('pino transport options with targets', async (t) => { 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] t.after(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 assert.deepEqual(result1, { pid, hostname, level: 30, msg: 'transport option test' }) const result2 = JSON.parse(await readFile(dest2)) delete result2.time assert.deepEqual(result2, { pid, hostname, level: 30, msg: 'transport option test' }) }) test('transport options with target and targets', async () => { assert.throws( () => { pino({ transport: { target: {}, targets: {} } }) }, /only one of target or targets can be specified/ ) }) test('transport options with target and stream', async () => { assert.throws( () => { pino({ transport: { target: {} } }, '/log/null') }, /only one of option.transport or stream can be specified/ ) }) test('transport options with stream', async (t) => { const dest1 = file() const transportStream = pino.transport({ target: 'pino/file', options: { destination: dest1 } }) t.after(transportStream.end.bind(transportStream)) assert.throws( () => { pino({ transport: transportStream }) }, Error('option.transport do not allow stream, please pass to option directly. e.g. pino(transport)') ) }) test('pino.transport handles prototype pollution of __bundlerPathsOverrides', async (t) => { // eslint-disable-next-line no-extend-native Object.prototype.__bundlerPathsOverrides = { 'pino/file': '/malicious/path' } t.after(() => { delete Object.prototype.__bundlerPathsOverrides }) const destination = file() const transport = pino.transport({ target: 'pino/file', options: { destination } }) t.after(transport.end.bind(transport)) const instance = pino(transport) instance.info('hello') await watchFileCreated(destination) const result = JSON.parse(await readFile(destination)) delete result.time assert.deepEqual(result, { pid, hostname, level: 30, msg: 'hello' }) }) const hasThreadName = 'threadName' in require('worker_threads') test('pino.transport with single target sets worker thread name to target', { skip: !hasThreadName }, async (t) => { const plan = tspl(t, { plan: 1 }) const transport = pino.transport({ target: join(__dirname, '..', 'fixtures', 'transport-worker-name.js') }) t.after(transport.end.bind(transport)) const instance = pino(transport) transport.once('workerThreadName', (name) => { plan.equal(name, join(__dirname, '..', 'fixtures', 'transport-worker-name.js')) }) instance.info('hello') await plan }) test('pino.transport with targets sets worker thread name to pino.transport', { skip: !hasThreadName }, async (t) => { const plan = tspl(t, { plan: 1 }) const transport = pino.transport({ targets: [{ level: 'info', target: join(__dirname, '..', 'fixtures', 'transport-worker-name.js') }] }) t.after(transport.end.bind(transport)) const instance = pino(transport) transport.once('workerThreadName', (name) => { plan.equal(name, 'pino.transport') }) instance.info('hello') await plan }) test('pino.transport with pipeline sets worker thread name to pino.transport', { skip: !hasThreadName }, async (t) => { const plan = tspl(t, { plan: 1 }) const transport = pino.transport({ pipeline: [{ target: join(__dirname, '..', 'fixtures', 'transport-worker-name.js') }] }) t.after(transport.end.bind(transport)) const instance = pino(transport) transport.once('workerThreadName', (name) => { plan.equal(name, 'pino.transport') }) instance.info('hello') await plan })