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(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(this) return method.apply(this, args) }, streamWrite (s) { expectType(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({ 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) => {} 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 const [param1, param2, param3, param4]: LogParam = [ { multiple: 'params' }, 'should', 'be', 'accepted', ] expectType(param1) expectType(param2) expectType(param3) expectType(param4) const logger = mock.fn() 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>( 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 = ( arg: Record ) => { info(arg) }