diff --git a/browser.js b/browser.js index 9e98720aa..f7dd0820f 100644 --- a/browser.js +++ b/browser.js @@ -109,6 +109,7 @@ function pino (opts) { transmit, serialize, asObject: opts.browser.asObject, + formatters: opts.browser.formatters, levels, timestamp: getTimeFunction(opts) } @@ -299,8 +300,9 @@ function createWrap (self, opts, rootLogger, level) { if (opts.serialize && !opts.asObject) { applySerializers(args, this._serialize, this.serializers, this._stdErrSerialize) } - if (opts.asObject) write.call(proto, asObject(this, level, args, ts)) - else write.apply(proto, args) + if (opts.asObject || opts.formatters) { + write.call(proto, asObject(this, level, args, ts, opts.formatters)) + } else write.apply(proto, args) if (opts.transmit) { const transmitLevel = opts.transmit.level || self._level @@ -321,26 +323,33 @@ function createWrap (self, opts, rootLogger, level) { })(self[baseLogFunctionSymbol][level]) } -function asObject (logger, level, args, ts) { +function asObject (logger, level, args, ts, formatters = {}) { + const { + level: levelFormatter = () => logger.levels.values[level], + log: logObjectFormatter = (obj) => obj + } = formatters if (logger._serialize) applySerializers(args, logger._serialize, logger.serializers, logger._stdErrSerialize) const argsCloned = args.slice() let msg = argsCloned[0] - const o = {} + const logObject = {} if (ts) { - o.time = ts + logObject.time = ts } - o.level = logger.levels.values[level] + logObject.level = levelFormatter(level, logger.levels.values[level]) + let lvl = (logger._childLevel | 0) + 1 if (lvl < 1) lvl = 1 // deliberate, catching objects, arrays if (msg !== null && typeof msg === 'object') { while (lvl-- && typeof argsCloned[0] === 'object') { - Object.assign(o, argsCloned.shift()) + Object.assign(logObject, argsCloned.shift()) } msg = argsCloned.length ? format(argsCloned.shift(), argsCloned) : undefined } else if (typeof msg === 'string') msg = format(argsCloned.shift(), argsCloned) - if (msg !== undefined) o.msg = msg - return o + if (msg !== undefined) logObject.msg = msg + + const formattedLogObject = logObjectFormatter(logObject) + return formattedLogObject } function applySerializers (args, serialize, serializers, stdErrSerialize) { diff --git a/docs/api.md b/docs/api.md index 25cd588f3..9c69f86e2 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1262,7 +1262,7 @@ For more on transports, how they work, and how to create them see the [`Transpor #### Options * `target`: The transport to pass logs through. This may be an installed module name or an absolute path. -* `options`: An options object which is serialized (see [Structured Clone Algorithm][https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm]), passed to the worker thread, parsed and then passed to the exported transport function. +* `options`: An options object which is serialized (see [Structured Clone Algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm)), passed to the worker thread, parsed and then passed to the exported transport function. * `worker`: [Worker thread](https://nodejs.org/api/worker_threads.html#worker_threads_new_worker_filename_options) configuration options. Additionally, the `worker` option supports `worker.autoEnd`. If this is set to `false` logs will not be flushed on process exit. It is then up to the developer to call `transport.end()` to flush logs. * `targets`: May be specified instead of `target`. Must be an array of transport configurations. Transport configurations include the aforementioned `options` and `target` options plus a `level` option which will send only logs above a specified level to a transport. * `pipeline`: May be specified instead of `target`. Must be an array of transport configurations. Transport configurations include the aforementioned `options` and `target` options. All intermediate steps in the pipeline _must_ be `Transform` streams and not `Writable`. diff --git a/docs/browser.md b/docs/browser.md index 394de875b..deb465c06 100644 --- a/docs/browser.md +++ b/docs/browser.md @@ -27,6 +27,25 @@ pino.info('hi') // creates and logs {msg: 'hi', level: 30, time: } When `write` is set, `asObject` will always be `true`. +### `formatters` (Object) + +An object containing functions for formatting the shape of the log lines. When provided, it enables the logger to produce a pino-like log object with customized formatting. Currently, it supports formatting for the `level` object only. + +##### `level` + +Changes the shape of the log level. The default shape is `{ level: number }`. +The function takes two arguments, the label of the level (e.g. `'info'`) +and the numeric value (e.g. `30`). + +```js +const formatters = { + level (label, number) { + return { level: number } + } +} +``` + + ### `write` (Function | Object) Instead of passing log messages to `console.log` they can be passed to diff --git a/docs/transports.md b/docs/transports.md index a0ab852be..a47543f89 100644 --- a/docs/transports.md +++ b/docs/transports.md @@ -421,6 +421,7 @@ PRs to this document are welcome for any new transports! + [pino-slack-webhook](#pino-slack-webhook) + [pino-axiom](#pino-axiom) + [pino-opentelemetry-transport](#pino-opentelemetry-transport) ++ [@axiomhq/pino](#@axiomhq/pino) ### Legacy @@ -999,6 +1000,35 @@ pino(transport) Documentation on running a minimal example is available in the [README](https://github.com/Vunovati/pino-opentelemetry-transport#minimalistic-example). + +### @axiomhq/pino + +[@axiomhq/pino](https://www.npmjs.com/package/@axiomhq/pino) is the official [Axiom](https://axiom.co/) transport for Pino, using [axiom-js](https://github.com/axiomhq/axiom-js). + +```javascript +import pino from 'pino'; + +const logger = pino( + { level: 'info' }, + pino.transport({ + target: '@axiomhq/pino', + options: { + dataset: process.env.AXIOM_DATASET, + token: process.env.AXIOM_TOKEN, + }, + }), +); +``` + +then you can use the logger as usual: + +```js +logger.info('Hello from pino!'); +``` + +For further examples, head over to the [examples](https://github.com/axiomhq/axiom-js/tree/main/examples/pino) directory. + + ## Communication between Pino and Transports Here we discuss some technical details of how Pino communicates with its [worker threads](https://nodejs.org/api/worker_threads.html). diff --git a/lib/levels.js b/lib/levels.js index 5555da49c..4a3af01ea 100644 --- a/lib/levels.js +++ b/lib/levels.js @@ -84,10 +84,11 @@ function setLevel (level) { const preLevelVal = this[levelValSym] const levelVal = this[levelValSym] = values[level] const useOnlyCustomLevelsVal = this[useOnlyCustomLevelsSym] + const levelComparison = this[levelCompSym] const hook = this[hooksSym].logMethod for (const key in values) { - if (levelVal > values[key]) { + if (levelComparison(values[key], levelVal) === false) { this[key] = noop continue } diff --git a/lib/meta.js b/lib/meta.js index a808498d0..6ba09727f 100644 --- a/lib/meta.js +++ b/lib/meta.js @@ -1,3 +1,3 @@ 'use strict' -module.exports = { version: '8.18.0' } +module.exports = { version: '8.19.0' } diff --git a/package.json b/package.json index ffe4b87e2..ef8fd233b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pino", - "version": "8.18.0", + "version": "8.19.0", "description": "super fast, all natural json logger", "main": "pino.js", "type": "commonjs", diff --git a/test/browser.test.js b/test/browser.test.js index 56053219c..c29641179 100644 --- a/test/browser.test.js +++ b/test/browser.test.js @@ -166,6 +166,53 @@ test('opts.browser.asObject logs pino-like object to console', ({ end, ok, is }) 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: { diff --git a/test/levels.test.js b/test/levels.test.js index e47945dfd..1581bc134 100644 --- a/test/levels.test.js +++ b/test/levels.test.js @@ -527,6 +527,177 @@ test('changing level from info to silent and back to info in child logger', asyn 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 }