Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add option mixinMergeStrategy (to v7) #1336

Merged
merged 1 commit into from
Feb 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ Default: `100`

Option to limit stringification of properties/elements when logging a specific object/array with circular references.

<a id="opt-mixin"></a>
#### `mixin` (Function):

Default: `undefined`
Expand All @@ -130,7 +131,8 @@ logger.info('world')
```

The result of `mixin()` is supposed to be a _new_ object. For performance reason, the object returned by `mixin()` will be mutated by pino.
In the following example, passing `mergingObject` argument to the first `info` call will mutate the global `mixin` object:
In the following example, passing `mergingObject` argument to the first `info` call will mutate the global `mixin` object by default:
(* See [`mixinMergeStrategy` option](#opt-mixin-merge-strategy)):
```js
const mixin = {
appName: 'My app'
Expand All @@ -154,6 +156,67 @@ logger.info('Message 2')
If the `mixin` feature is being used merely to add static metadata to each log message,
then a [child logger ⇗](/docs/child-loggers.md) should be used instead.

<a id="opt-mixin-merge-strategy"></a>
#### `mixinMergeStrategy` (Function):

Default: `undefined`

If provided, the `mixinMergeStrategy` function is called each time one of the active
logging methods is called. The first parameter is the value `mergeObject` or an empty object,
the second parameter is the value resulting from `mixin()` (* See [`mixin` option](#opt-mixin) or an empty object.
The function must synchronously return an object.

```js
// Default strategy, `mergeObject` has priority
const logger = pino({
mixin() {
return { tag: 'docker' }
},
// mixinMergeStrategy(mergeObject, mixinObject) {
// return Object.assign(mixinMeta, mergeObject)
// }
})

logger.info({
tag: 'local'
}, 'Message')
// {"level":30,"time":1591195061437,"pid":16012,"hostname":"x","tag":"local","msg":"Message"}
```

```js
// Custom mutable strategy, `mixin` has priority
const logger = pino({
mixin() {
return { tag: 'k8s' }
},
mixinMergeStrategy(mergeObject, mixinObject) {
return Object.assign(mergeObject, mixinObject)
}
})

logger.info({
tag: 'local'
}, 'Message')
// {"level":30,"time":1591195061437,"pid":16012,"hostname":"x","tag":"k8s","msg":"Message"}
```

```js
// Custom immutable strategy, `mixin` has priority
const logger = pino({
mixin() {
return { tag: 'k8s' }
},
mixinMergeStrategy(mergeObject, mixinObject) {
return Object.assign({}, mergeObject, mixinObject)
}
})

logger.info({
tag: 'local'
}, 'Message')
// {"level":30,"time":1591195061437,"pid":16012,"hostname":"x","tag":"k8s","msg":"Message"}
```

<a id="opt-redact"></a>
#### `redact` (Array | Object):

Expand Down
16 changes: 15 additions & 1 deletion lib/proto.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const {
mixinSym,
asJsonSym,
writeSym,
mixinMergeStrategySym,
timeSym,
timeSliceIndexSym,
streamSym,
Expand Down Expand Up @@ -158,9 +159,22 @@ function setBindings (newBindings) {
delete this[parsedChindingsSym]
}

/**
* Default strategy for creating `mergeObject` from arguments and the result from `mixin()`.
* Fields from `mergeObject` have higher priority in this strategy.
*
* @param {Object} mergeObject The object a user has supplied to the logging function.
* @param {Object} mixinObject The result of the `mixin` method.
* @return {Object}
*/
function defaultMixinMergeStrategy (mergeObject, mixinObject) {
return Object.assign(mixinObject, mergeObject)
}

function write (_obj, msg, num) {
const t = this[timeSym]()
const mixin = this[mixinSym]
const mixinMergeStrategy = this[mixinMergeStrategySym] || defaultMixinMergeStrategy
let obj

if (_obj === undefined || _obj === null) {
Expand All @@ -178,7 +192,7 @@ function write (_obj, msg, num) {
}

if (mixin) {
obj = Object.assign(mixin(obj), obj)
obj = mixinMergeStrategy(obj, mixin(obj))
}

const s = this[asJsonSym](obj, msg, num, t)
Expand Down
4 changes: 3 additions & 1 deletion lib/symbols.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const formatOptsSym = Symbol('pino.formatOpts')
const messageKeySym = Symbol('pino.messageKey')
const nestedKeySym = Symbol('pino.nestedKey')
const nestedKeyStrSym = Symbol('pino.nestedKeyStr')
const mixinMergeStrategySym = Symbol('pino.mixinMergeStrategy')

const wildcardFirstSym = Symbol('pino.wildcardFirst')

Expand Down Expand Up @@ -64,5 +65,6 @@ module.exports = {
useOnlyCustomLevelsSym,
formattersSym,
hooksSym,
nestedKeyStrSym
nestedKeyStrSym,
mixinMergeStrategySym
}
27 changes: 18 additions & 9 deletions pino.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ import type { WorkerOptions } from "worker_threads";
type ThreadStream = any

type TimeFn = () => string;
type MixinFn = () => object;
type MixinFn = (mergeObject: object) => object;
type MixinMergeStrategyFn = (mergeObject: object, mixinObject: object) => object;

type CustomLevelLogger<Options> = Options extends { customLevels: Record<string, number> } ? Record<keyof Options["customLevels"], LogFn> : Record<never, LogFn>

Expand Down Expand Up @@ -112,7 +113,7 @@ interface LoggerExtras<Options = LoggerOptions> extends EventEmitter {

declare namespace pino {
//// Exported types and interfaces

interface BaseLogger {
/**
* Set this property to the desired logging level. In order of priority, available levels are:
Expand All @@ -130,7 +131,7 @@ declare namespace pino {
* You can pass `'silent'` to disable logging.
*/
level: pino.LevelWithSilent | string;

/**
* Log at `'fatal'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line.
* If more args follows `msg`, these will be used to format `msg` using `util.format`.
Expand Down Expand Up @@ -204,14 +205,14 @@ declare namespace pino {

type SerializerFn = (value: any) => any;
type WriteFn = (o: object) => void;

type LevelChangeEventListener = (
lvl: LevelWithSilent | string,
val: number,
prevLvl: LevelWithSilent | string,
prevVal: number,
) => void;

type LogDescriptor = Record<string, any>;

type Logger<Options = LoggerOptions> = BaseLogger & LoggerExtras<Options> & CustomLevelLogger<Options>;
Expand Down Expand Up @@ -336,6 +337,14 @@ declare namespace pino {
*/
mixin?: MixinFn;

/**
* If provided, the `mixinMergeStrategy` function is called each time one of the active
* logging methods is called. The first parameter is the value `mergeObject` or an empty object,
* the second parameter is the value resulting from `mixin()` or an empty object.
* The function must synchronously return an object.
*/
mixinMergeStrategy?: MixinMergeStrategyFn

/**
* As an array, the redact option specifies paths that should have their values redacted from any log output.
*
Expand Down Expand Up @@ -671,12 +680,12 @@ declare namespace pino {
readonly formattersSym: unique symbol;
readonly hooksSym: unique symbol;
};

/**
* Exposes the Pino package version. Also available on the logger instance.
*/
export const version: string;

/**
* Provides functions for generating the timestamp property in the log output. You can set the `timestamp` option during
* initialization to one of these functions to adjust the output format. Alternatively, you can specify your own time function.
Expand All @@ -701,7 +710,7 @@ declare namespace pino {
*/
isoTime: TimeFn;
};

//// Exported functions

/**
Expand Down Expand Up @@ -755,7 +764,7 @@ declare function pino<Options extends LoggerOptions | DestinationStream>(options
* @returns a new logger instance.
*/
declare function pino<Options extends LoggerOptions>(options: Options, stream: DestinationStream): Logger<Options>;


// Pass through all the top-level exports, allows `import {version} from "pino"`
// Constants and functions
Expand Down
5 changes: 4 additions & 1 deletion pino.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ const {
useOnlyCustomLevelsSym,
formattersSym,
hooksSym,
nestedKeyStrSym
nestedKeyStrSym,
mixinMergeStrategySym
} = symbols
const { epochTime, nullTime } = time
const { pid } = process
Expand Down Expand Up @@ -94,6 +95,7 @@ function pino (...args) {
level,
customLevels,
mixin,
mixinMergeStrategy,
useOnlyCustomLevels,
formatters,
hooks,
Expand Down Expand Up @@ -166,6 +168,7 @@ function pino (...args) {
[nestedKeyStrSym]: nestedKey ? `,${JSON.stringify(nestedKey)}:{` : '',
[serializersSym]: serializers,
[mixinSym]: mixin,
[mixinMergeStrategySym]: mixinMergeStrategy,
[chindingsSym]: chindings,
[formattersSym]: allFormatters,
[hooksSym]: hooks,
Expand Down
55 changes: 55 additions & 0 deletions test/mixin-merge-strategy.test.js
Original file line number Diff line number Diff line change
@@ -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'
})
})