pino
Version:
super fast, all natural json logger
684 lines (583 loc) • 15.4 kB
text/typescript
import { IncomingMessage, ServerResponse } from 'http'
import { mock } from 'node:test'
import { Socket } from 'net'
import { expectError, expectType } from 'tsd'
import pino, { LogFn, LoggerOptions } from '../../'
import Logger = pino.Logger
const log = pino()
const info = log.info
const error = log.error
info('hello world')
error('this is at error level')
// primitive types
info('simple string')
info(true)
info(42)
info(3.14)
info(null)
info(undefined)
// object types
info({ a: 1, b: '2' })
info(new Error())
info(new Date())
info([])
info(new Map())
info(new Set())
// placeholder messages
info('Hello %s', 'world')
info('The answer is %d', 42)
info('The object is %o', { a: 1, b: '2' })
info('The json is %j', { a: 1, b: '2' })
info('The object is %O', { a: 1, b: '2' })
info('The answer is %d and the question is %s with %o', 42, 'unknown', {
correct: 'order',
})
info('Missing placeholder is fine %s')
// %s placeholder supports all primitive types
info('Boolean %s', true)
info('Boolean %s', false)
info('Number %s', 123)
info('Number %s', 3.14)
info('BigInt %s', BigInt(123))
info('Null %s', null)
info('Undefined %s', undefined)
info('Symbol %s', Symbol('test'))
info('String %s', 'hello')
// %s placeholder with multiple primitives
info('Multiple primitives %s %s %s', true, 42, 'world')
info(
'All primitive types %s %s %s %s %s %s %s',
'string',
123,
true,
BigInt(123),
null,
undefined,
Symbol('test')
)
declare const errorOrString: string | Error
info(errorOrString)
// %o placeholder supports primitives too (except undefined)
info('Boolean %o', true)
info('Boolean %o', false)
info('Number %o', 123)
info('Number %o', 3.14)
info('BigInt %o', BigInt(123))
info('Null %o', null)
info('Symbol %o', Symbol('test'))
info('String %o', 'hello')
// placeholder messages type errors
expectError(info('The answer is %d', 'not a number'))
expectError(
info(
'The answer is %d and the question is %s with %o',
'unknown',
{ incorrect: 'order' },
42
)
)
expectError(info('Extra message %s', 'after placeholder', 'not allowed'))
// object types with messages
info({ obj: 42 }, 'hello world')
info({ obj: 42, b: 2 }, 'hello world')
info({ obj: { aa: 'bbb' } }, 'another')
info({ a: 1, b: '2' }, 'hello world with %s', 'extra data')
// Extra message after placeholder
expectError(info({ a: 1, b: '2' }, 'hello world with %d', 2, 'extra'))
// metadata with messages type passes, because of custom toString method
// We can't detect if the object has a custom toString method that returns a string
info({ a: 1, b: '2' }, 'hello world with %s', {})
// metadata after message
expectError(info('message', { a: 1, b: '2' }))
// multiple strings without placeholder
expectError(info('string1', 'string2'))
expectError(info('string1', 'string2', 'string3'))
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: pino.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({
redact: {
paths: [],
censor: (value): string => 'SECRET',
},
})
expectError(
pino({
redact: { paths: [], censor: (value: string) => value },
})
)
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({
browser: {
asObjectBindingsOnly: true,
},
})
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 withRFC3339TimeFn = pino({
timestamp: pino.stdTimeFunctions.isoTimeNano,
})
const withNestedKey = pino({
nestedKey: 'payload',
})
const withHooks = pino({
hooks: {
logMethod (args, method, level) {
expectType<pino.Logger>(this)
return method.apply(this, args)
},
streamWrite (s) {
expectType<string>(s)
return s.replaceAll('secret-key', 'xxx')
},
},
})
// 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
},
msgPrefix: 'prefix',
}
// 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,
},
},
})
// Test that we can properly extract parameters from the log fn type
type LogParam = Parameters<LogFn>
const [param1, param2, param3, param4]: LogParam = [
{ multiple: 'params' },
'should',
'be',
'accepted',
]
expectType<unknown>(param1)
expectType<string>(param2)
expectType<unknown>(param3)
expectType<unknown>(param4)
const logger = mock.fn<LogFn>()
logger.mock.calls[0].arguments[1]?.includes('I should be able to get params')
const hooks: LoggerOptions['hooks'] = {
logMethod (this, parameters, method) {
if (parameters.length >= 2) {
const [parameter1, parameter2, ...remainingParameters] = parameters
if (typeof parameter1 === 'string') {
return method.apply(this, [
parameter2,
parameter1,
...remainingParameters,
])
}
return method.apply(this, [parameter2])
}
return method.apply(this, parameters)
},
}
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)
const customLevelsOnlyOpts = {
useOnlyCustomLevels: true,
customLevels: {
customDebug: 10,
info: 20, // to make sure the default names are also available for override
customNetwork: 30,
customError: 40,
},
level: 'customDebug',
} satisfies LoggerOptions
const loggerWithCustomLevelOnly = pino(customLevelsOnlyOpts)
loggerWithCustomLevelOnly.customDebug('test3')
loggerWithCustomLevelOnly.info('test4')
loggerWithCustomLevelOnly.customError('test5')
loggerWithCustomLevelOnly.customNetwork('test6')
expectError(loggerWithCustomLevelOnly.fatal('test'))
expectError(loggerWithCustomLevelOnly.error('test'))
expectError(loggerWithCustomLevelOnly.warn('test'))
expectError(loggerWithCustomLevelOnly.debug('test'))
expectError(loggerWithCustomLevelOnly.trace('test'))
// Module extension
declare module '../../' {
interface LogFnFields {
bannedField?: never;
typeCheckedField?: string;
}
}
info({ typeCheckedField: 'bar' })
expectError(info({ bannedField: 'bar' }))
expectError(info({ typeCheckedField: 123 }))
const someGenericFunction = <T extends string | number | symbol = never>(
arg: Record<T, unknown>
) => {
info(arg)
}