Skip to content

Commit 32b759d

Browse files
authored
fix: browser respect child logger log level (#1725)
* test: add browser child level tests * feat: child level is independent of parent logger
1 parent 3d78a80 commit 32b759d

File tree

2 files changed

+212
-43
lines changed

2 files changed

+212
-43
lines changed

browser.js

Lines changed: 106 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,38 @@ const stdSerializers = {
1616
err: asErrValue,
1717
errWithCause: asErrValue
1818
}
19+
function levelToValue (level, logger) {
20+
return level === 'silent'
21+
? Infinity
22+
: logger.levels.values[level]
23+
}
24+
const baseLogFunctionSymbol = Symbol('pino.logFuncs')
25+
const hierarchySymbol = Symbol('pino.hierarchy')
26+
27+
const logFallbackMap = {
28+
error: 'log',
29+
fatal: 'error',
30+
warn: 'error',
31+
info: 'log',
32+
debug: 'log',
33+
trace: 'log'
34+
}
35+
36+
function appendChildLogger (parentLogger, childLogger) {
37+
const newEntry = {
38+
logger: childLogger,
39+
parent: parentLogger[hierarchySymbol]
40+
}
41+
childLogger[hierarchySymbol] = newEntry
42+
}
43+
44+
function setupBaseLogFunctions (logger, levels, proto) {
45+
const logFunctions = {}
46+
levels.forEach(level => {
47+
logFunctions[level] = proto[level] ? proto[level] : (_console[level] || _console[logFallbackMap[level] ?? 'log'] || noop)
48+
})
49+
logger[baseLogFunctionSymbol] = logFunctions
50+
}
1951

2052
function shouldSerialize (serialize, serializers) {
2153
if (Array.isArray(serialize)) {
@@ -61,6 +93,10 @@ function pino (opts) {
6193
const logger = Object.create(proto)
6294
if (!logger.log) logger.log = noop
6395

96+
setupBaseLogFunctions(logger, levels, proto)
97+
// setup root hierarchy entry
98+
appendChildLogger({}, logger)
99+
64100
Object.defineProperty(logger, 'levelVal', {
65101
get: getLevelVal
66102
})
@@ -94,9 +130,7 @@ function pino (opts) {
94130
if (transmit) logger._logEvent = createLogEventShape()
95131

96132
function getLevelVal () {
97-
return this.level === 'silent'
98-
? Infinity
99-
: this.levels.values[this.level]
133+
return levelToValue(this.level, this)
100134
}
101135

102136
function getLevel () {
@@ -108,15 +142,15 @@ function pino (opts) {
108142
}
109143
this._level = level
110144

111-
set(setOpts, logger, 'error', 'log') // <-- must stay first
112-
set(setOpts, logger, 'fatal', 'error')
113-
set(setOpts, logger, 'warn', 'error')
114-
set(setOpts, logger, 'info', 'log')
115-
set(setOpts, logger, 'debug', 'log')
116-
set(setOpts, logger, 'trace', 'log')
145+
set(this, setOpts, logger, 'error') // <-- must stay first
146+
set(this, setOpts, logger, 'fatal')
147+
set(this, setOpts, logger, 'warn')
148+
set(this, setOpts, logger, 'info')
149+
set(this, setOpts, logger, 'debug')
150+
set(this, setOpts, logger, 'trace')
117151

118-
customLevels.forEach(function (level) {
119-
set(setOpts, logger, level, 'log')
152+
customLevels.forEach((level) => {
153+
set(this, setOpts, logger, level)
120154
})
121155
}
122156

@@ -139,12 +173,10 @@ function pino (opts) {
139173
}
140174
function Child (parent) {
141175
this._childLevel = (parent._childLevel | 0) + 1
142-
this.error = bind(parent, bindings, 'error')
143-
this.fatal = bind(parent, bindings, 'fatal')
144-
this.warn = bind(parent, bindings, 'warn')
145-
this.info = bind(parent, bindings, 'info')
146-
this.debug = bind(parent, bindings, 'debug')
147-
this.trace = bind(parent, bindings, 'trace')
176+
177+
// make sure bindings are available in the `set` function
178+
this.bindings = bindings
179+
148180
if (childSerializers) {
149181
this.serializers = childSerializers
150182
this._serialize = childSerialize
@@ -156,7 +188,14 @@ function pino (opts) {
156188
}
157189
}
158190
Child.prototype = this
159-
return new Child(this)
191+
const newLogger = new Child(this)
192+
193+
// must happen before the level is assigned
194+
appendChildLogger(this, newLogger)
195+
// required to actually initialize the logger functions for any given child
196+
newLogger.level = this.level
197+
198+
return newLogger
160199
}
161200
return logger
162201
}
@@ -203,19 +242,54 @@ pino.levels = {
203242
pino.stdSerializers = stdSerializers
204243
pino.stdTimeFunctions = Object.assign({}, { nullTime, epochTime, unixTime, isoTime })
205244

206-
function set (opts, logger, level, fallback) {
207-
const proto = Object.getPrototypeOf(logger)
208-
logger[level] = logger.levelVal > logger.levels.values[level]
245+
function getBindingChain (logger) {
246+
const bindings = []
247+
if (logger.bindings) {
248+
bindings.push(logger.bindings)
249+
}
250+
251+
// traverse up the tree to get all bindings
252+
let hierarchy = logger[hierarchySymbol]
253+
while (hierarchy.parent) {
254+
hierarchy = hierarchy.parent
255+
if (hierarchy.logger.bindings) {
256+
bindings.push(hierarchy.logger.bindings)
257+
}
258+
}
259+
260+
return bindings.reverse()
261+
}
262+
263+
function set (self, opts, rootLogger, level) {
264+
// override the current log functions with either `noop` or the base log function
265+
self[level] = levelToValue(self.level, rootLogger) > levelToValue(level, rootLogger)
209266
? noop
210-
: (proto[level] ? proto[level] : (_console[level] || _console[fallback] || noop))
267+
: rootLogger[baseLogFunctionSymbol][level]
211268

212-
wrap(opts, logger, level)
269+
if (!opts.transmit && self[level] === noop) {
270+
return
271+
}
272+
273+
// make sure the log format is correct
274+
self[level] = createWrap(self, opts, rootLogger, level)
275+
276+
// prepend bindings if it is not the root logger
277+
const bindings = getBindingChain(self)
278+
if (bindings.length === 0) {
279+
// early exit in case for rootLogger
280+
return
281+
}
282+
self[level] = prependBindingsInArguments(bindings, self[level])
213283
}
214284

215-
function wrap (opts, logger, level) {
216-
if (!opts.transmit && logger[level] === noop) return
285+
function prependBindingsInArguments (bindings, logFunc) {
286+
return function () {
287+
return logFunc.apply(this, [...bindings, ...arguments])
288+
}
289+
}
217290

218-
logger[level] = (function (write) {
291+
function createWrap (self, opts, rootLogger, level) {
292+
return (function (write) {
219293
return function LOG () {
220294
const ts = opts.timestamp()
221295
const args = new Array(arguments.length)
@@ -229,22 +303,22 @@ function wrap (opts, logger, level) {
229303
else write.apply(proto, args)
230304

231305
if (opts.transmit) {
232-
const transmitLevel = opts.transmit.level || logger.level
233-
const transmitValue = logger.levels.values[transmitLevel]
234-
const methodValue = logger.levels.values[level]
306+
const transmitLevel = opts.transmit.level || self._level
307+
const transmitValue = rootLogger.levels.values[transmitLevel]
308+
const methodValue = rootLogger.levels.values[level]
235309
if (methodValue < transmitValue) return
236310
transmit(this, {
237311
ts,
238312
methodLevel: level,
239313
methodValue,
240314
transmitLevel,
241-
transmitValue: logger.levels.values[opts.transmit.level || logger.level],
315+
transmitValue: rootLogger.levels.values[opts.transmit.level || self._level],
242316
send: opts.transmit.send,
243-
val: logger.levelVal
317+
val: levelToValue(self._level, rootLogger)
244318
}, args)
245319
}
246320
}
247-
})(logger[level])
321+
})(self[baseLogFunctionSymbol][level])
248322
}
249323

250324
function asObject (logger, level, args, ts) {
@@ -283,17 +357,6 @@ function applySerializers (args, serialize, serializers, stdErrSerialize) {
283357
}
284358
}
285359

286-
function bind (parent, bindings, level) {
287-
return function () {
288-
const args = new Array(1 + arguments.length)
289-
args[0] = bindings
290-
for (var i = 1; i < args.length; i++) {
291-
args[i] = arguments[i - 1]
292-
}
293-
return parent[level].apply(this, args)
294-
}
295-
}
296-
297360
function transmit (logger, opts, args) {
298361
const send = opts.send
299362
const ts = opts.ts

test/browser-child.test.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
'use strict'
2+
const test = require('tape')
3+
const pino = require('../browser')
4+
5+
test('child has parent level', ({ end, same, is }) => {
6+
const instance = pino({
7+
level: 'error',
8+
browser: {}
9+
})
10+
11+
const child = instance.child({})
12+
13+
same(child.level, instance.level)
14+
end()
15+
})
16+
17+
test('changing child level does not affect parent', ({ end, same, is }) => {
18+
const instance = pino({
19+
level: 'error',
20+
browser: {}
21+
})
22+
23+
const child = instance.child({})
24+
child.level = 'info'
25+
26+
same(instance.level, 'error')
27+
end()
28+
})
29+
30+
test('child should log, if its own level allows it', ({ end, same, is }) => {
31+
const expected = [
32+
{
33+
level: 30,
34+
msg: 'this is info'
35+
},
36+
{
37+
level: 40,
38+
msg: 'this is warn'
39+
},
40+
{
41+
level: 50,
42+
msg: 'this is an error'
43+
}
44+
]
45+
const instance = pino({
46+
level: 'error',
47+
browser: {
48+
write (actual) {
49+
checkLogObjects(is, same, actual, expected.shift())
50+
}
51+
}
52+
})
53+
54+
const child = instance.child({})
55+
child.level = 'info'
56+
57+
child.debug('this is debug')
58+
child.info('this is info')
59+
child.warn('this is warn')
60+
child.error('this is an error')
61+
62+
same(expected.length, 0, 'not all messages were read')
63+
end()
64+
})
65+
66+
test('changing child log level should not affect parent log behavior', ({ end, same, is }) => {
67+
const expected = [
68+
{
69+
level: 50,
70+
msg: 'this is an error'
71+
},
72+
{
73+
level: 60,
74+
msg: 'this is fatal'
75+
}
76+
]
77+
const instance = pino({
78+
level: 'error',
79+
browser: {
80+
write (actual) {
81+
checkLogObjects(is, same, actual, expected.shift())
82+
}
83+
}
84+
})
85+
86+
const child = instance.child({})
87+
child.level = 'info'
88+
89+
instance.warn('this is warn')
90+
instance.error('this is an error')
91+
instance.fatal('this is fatal')
92+
93+
same(expected.length, 0, 'not all messages were read')
94+
end()
95+
})
96+
97+
function checkLogObjects (is, same, actual, expected) {
98+
is(actual.time <= Date.now(), true, 'time is greater than Date.now()')
99+
100+
const actualCopy = Object.assign({}, actual)
101+
const expectedCopy = Object.assign({}, expected)
102+
delete actualCopy.time
103+
delete expectedCopy.time
104+
105+
same(actualCopy, expectedCopy)
106+
}

0 commit comments

Comments
 (0)