pino icon indicating copy to clipboard operation
pino copied to clipboard

Provide solution for creating custom loggers easily

Open kibertoad opened this issue 4 years ago • 12 comments
trafficstars

Judging by https://github.com/fastify/fastify/blob/main/test/types/logger.test-d.ts, the following used to be OK in pino 6:

class CustomLoggerImpl implements CustomLogger {
  customMethod (msg: string, ...args: unknown[]) { console.log(msg, args) }

  // Implementation signature must be compatible with all overloads of FastifyLogFn
  info (arg1: unknown, arg2?: unknown, ...args: unknown[]): void {
    console.log(arg1, arg2, ...args)
  }

  warn (...args: unknown[]) { console.log(args) }
  error (...args: unknown[]) { console.log(args) }
  fatal (...args: unknown[]) { console.log(args) }
  trace (...args: unknown[]) { console.log(args) }
  debug (...args: unknown[]) { console.log(args) }
  child () { return new CustomLoggerImpl() }
}

const customLogger = new CustomLoggerImpl()

However, our Logger interface relies on having such things as

        level: LevelWithSilent | string;
        /**
         * Returns the integer value for the logger instance's logging level.
         */
        levelVal: number;

which rely on internal getters. We probably don't want our users to implement all that from a scratch, so I wonder if we should expose some kind of a class they could extend for their custom loggers, or at least a prototype they could inherit from.

Alternatively we can officially embrace minimal subset of logger fields for custom loggers, as originally expected by fastify and split within Pino in https://github.com/pinojs/pino/pull/1099

kibertoad avatar Aug 26 '21 17:08 kibertoad

pino relies on prototypical inheritance for implementing the child loggers, so we can't provide a prototype to extend.

Maybe an interface?

mcollina avatar Aug 26 '21 22:08 mcollina

Interface we already have, but it cannot provide any logic by itself, only types. And what I'm trying to say is that only having interface forces users to implement way too much internal stuff on their own. If BaseLogger as defined in #1099 is ok, though, this might be a non-issue, as that provides a more sensible surface for users to cover.

kibertoad avatar Aug 27 '21 04:08 kibertoad

Maybe add a test for this use case?

mcollina avatar Aug 27 '21 06:08 mcollina

Wrapping a Pino instance is really not that difficult. There shouldn't be any need to mutate the prototype.

jsumners avatar Aug 28 '21 14:08 jsumners

I ditched prettyprint and wrote this minimal transformer to output colorised terminal output, with a short timestamp. It could be extended easiy with the options object.

logger.js

import pino from 'pino'

const log = pino({
	transport: {
		pipeline: [
			{
				// logger output for terminal, colorised by level for easy reading
				// https://gist.github.com/replete/3220dd280c60279cee5396c6f65b02dd
				target: './logger.colorisedTerminalTransform.mjs',
				options: {},
			},
		],
	},
})

export default log

logger.colorisedTerminalTransform.mjs (source in this gist)

import { Writable } from 'stream'

export default (options) => {
	const levels = {
		// https://github.com/pinojs/pino/blob/master/docs/api.md#logger-level
		10: `\x1b[2m[>]\x1b[0m`, //trace
		20: `\x1b[35m[DEBUG]\x1b[0m`, //debug
		30: `\x1b[36m`, //info
		40: `\x1b[33m[?]`, //warn
		50: `\x1b[31m[!]`, //error
		60: `\x1b[41m[!!!]`, //fatal
		Infinity: ``,
	}

	const colorizedLoggerTransportStream = new Writable({
		write(chunk, enc, cb) {
			const logEvents = chunk.toString().match(/[^\r\n]+/g) // split into lines
			logEvents.forEach((logEvent) => {
				const { level, time, msg } = JSON.parse(logEvent)
				const timestamp = new Date(time).toISOString().slice(11).substring(8, 0)
				console.log(`\x1b[2m${timestamp}\x1b[0m ${levels[level] || ''} ${msg}\x1b[0m`)
			})
			cb()
		},
	})
	return colorizedLoggerTransportStream
}

replete avatar Oct 30 '21 01:10 replete

Don't use console.log, use pino.destination() instead (otherwise you loose all the benefit of worker_threads).

mcollina avatar Oct 30 '21 09:10 mcollina

Don't use console.log, use pino.destination() instead (otherwise you loose all the benefit of worker_threads). Screen Shot 2021-10-30 at 16 07 24 The documentation gave an example using console.log

The 'transport' implementation I wrote above sometimes misses errors, which has been pretty frustrating given I only wrote this transformer because I got a 'pino-pretty is deprecated' type warnings.

What I want to do seems like a typical use-case to me, could you please recommend how I approach this?

This is what I want to do:

  • In 'development environment:
    • Write transformed (human-readable, colors, short timestamp, json expansion etc) output to stdout
  • In 'production' environment:
    • write raw json to stdout
    • write raw json to a logfile
    • maybe pipe raw json to some third-party logging service
  • In 'test' environment:
    • Write transformed (human-readable, colors, short timestamp, json expansion etc) output to stdout
    • write raw json to a logfile
  • Anywhere:
    • write logfile contents to stdout with the same human-readable transformer used in 'development'

replete avatar Oct 30 '21 15:10 replete

pino-pretty is not deprecated, only the prettyPrint option.

The following would do what you want:

const pino = require('pino')
const transport = pino.transport({
  target: 'pino-pretty',
  options: { destination: 1 } // use 2 for stderr
})
pino(transport)

mcollina avatar Oct 30 '21 17:10 mcollina

Don't use console.log, use pino.destination() instead (otherwise you loose all the benefit of worker_threads).

But how I could use pino.destination() to show the log in the stdout of the application?

jlenon7 avatar Sep 09 '22 00:09 jlenon7

I don't understand the question, could you clarify?

mcollina avatar Sep 09 '22 06:09 mcollina

Please excuse my ignorance @mcollina

I was trying to create a custom transporter which would format the log similarly to pino-pretty. Then this formatted log would be transport to the stdout of the application and then to a Telegram ChatId.

The problem I was facing was: How to send the log to stdout but without using the console.log()?

It wasn't so clear to me that to send the log to stdout or stderr I should pass 1 or 2 as destination to SonicBoom destination. I thought SonicBoom should only be used to transport logs to files. But now I got it. 😅

jlenon7 avatar Sep 13 '22 03:09 jlenon7

fd: 1 is standard output. fd: 2 is standard error.

mcollina avatar Sep 13 '22 13:09 mcollina

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

github-actions[bot] avatar Mar 20 '23 00:03 github-actions[bot]