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
+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')