UNPKG

11.5 kBJavaScriptView Raw
1'use strict'
2
3/* eslint no-prototype-builtins: 0 */
4
5const format = require('quick-format-unescaped')
6const { mapHttpRequest, mapHttpResponse } = require('pino-std-serializers')
7const SonicBoom = require('sonic-boom')
8const stringifySafe = require('fast-safe-stringify')
9const {
10 lsCacheSym,
11 chindingsSym,
12 parsedChindingsSym,
13 writeSym,
14 serializersSym,
15 formatOptsSym,
16 endSym,
17 stringifiersSym,
18 stringifySym,
19 wildcardFirstSym,
20 needsMetadataGsym,
21 redactFmtSym,
22 streamSym,
23 nestedKeySym,
24 formattersSym,
25 messageKeySym
26} = require('./symbols')
27
28function noop () {}
29
30function genLog (level, hook) {
31 if (!hook) return LOG
32
33 return function hookWrappedLog (...args) {
34 hook.call(this, args, LOG)
35 }
36
37 function LOG (o, ...n) {
38 if (typeof o === 'object') {
39 var msg = o
40 if (o !== null) {
41 if (o.method && o.headers && o.socket) {
42 o = mapHttpRequest(o)
43 } else if (typeof o.setHeader === 'function') {
44 o = mapHttpResponse(o)
45 }
46 }
47 if (this[nestedKeySym]) o = { [this[nestedKeySym]]: o }
48 var formatParams
49 if (msg === null && n.length === 0) {
50 formatParams = [null]
51 } else {
52 msg = n.shift()
53 formatParams = n
54 }
55 this[writeSym](o, format(msg, formatParams, this[formatOptsSym]), level)
56 } else {
57 this[writeSym](null, format(o, n, this[formatOptsSym]), level)
58 }
59 }
60}
61
62// magically escape strings for json
63// relying on their charCodeAt
64// everything below 32 needs JSON.stringify()
65// 34 and 92 happens all the time, so we
66// have a fast case for them
67function asString (str) {
68 var result = ''
69 var last = 0
70 var found = false
71 var point = 255
72 const l = str.length
73 if (l > 100) {
74 return JSON.stringify(str)
75 }
76 for (var i = 0; i < l && point >= 32; i++) {
77 point = str.charCodeAt(i)
78 if (point === 34 || point === 92) {
79 result += str.slice(last, i) + '\\'
80 last = i
81 found = true
82 }
83 }
84 if (!found) {
85 result = str
86 } else {
87 result += str.slice(last)
88 }
89 return point < 32 ? JSON.stringify(str) : '"' + result + '"'
90}
91
92function asJson (obj, msg, num, time) {
93 const stringify = this[stringifySym]
94 const stringifiers = this[stringifiersSym]
95 const end = this[endSym]
96 const chindings = this[chindingsSym]
97 const serializers = this[serializersSym]
98 const formatters = this[formattersSym]
99 const messageKey = this[messageKeySym]
100 var data = this[lsCacheSym][num] + time
101
102 // we need the child bindings added to the output first so instance logged
103 // objects can take precedence when JSON.parse-ing the resulting log line
104 data = data + chindings
105
106 var value
107 var notHasOwnProperty = obj.hasOwnProperty === undefined
108 if (formatters.log) {
109 obj = formatters.log(obj)
110 }
111 if (msg !== undefined) {
112 obj[messageKey] = msg
113 }
114 const wildcardStringifier = stringifiers[wildcardFirstSym]
115 for (var key in obj) {
116 value = obj[key]
117 if ((notHasOwnProperty || obj.hasOwnProperty(key)) && value !== undefined) {
118 value = serializers[key] ? serializers[key](value) : value
119
120 const stringifier = stringifiers[key] || wildcardStringifier
121
122 switch (typeof value) {
123 case 'undefined':
124 case 'function':
125 continue
126 case 'number':
127 /* eslint no-fallthrough: "off" */
128 if (Number.isFinite(value) === false) {
129 value = null
130 }
131 // this case explicity falls through to the next one
132 case 'boolean':
133 if (stringifier) value = stringifier(value)
134 break
135 case 'string':
136 value = (stringifier || asString)(value)
137 break
138 default:
139 value = (stringifier || stringify)(value)
140 }
141 if (value === undefined) continue
142 data += ',"' + key + '":' + value
143 }
144 }
145
146 return data + end
147}
148
149function asChindings (instance, bindings) {
150 var key
151 var value
152 var data = instance[chindingsSym]
153 const stringify = instance[stringifySym]
154 const stringifiers = instance[stringifiersSym]
155 const serializers = instance[serializersSym]
156 const formatter = instance[formattersSym].bindings
157 bindings = formatter(bindings)
158
159 for (key in bindings) {
160 value = bindings[key]
161 const valid = key !== 'level' &&
162 key !== 'serializers' &&
163 key !== 'formatters' &&
164 key !== 'customLevels' &&
165 bindings.hasOwnProperty(key) &&
166 value !== undefined
167 if (valid === true) {
168 value = serializers[key] ? serializers[key](value) : value
169 value = (stringifiers[key] || stringify)(value)
170 if (value === undefined) continue
171 data += ',"' + key + '":' + value
172 }
173 }
174 return data
175}
176
177function getPrettyStream (opts, prettifier, dest, instance) {
178 if (prettifier && typeof prettifier === 'function') {
179 prettifier = prettifier.bind(instance)
180 return prettifierMetaWrapper(prettifier(opts), dest)
181 }
182 try {
183 var prettyFactory = require('pino-pretty')
184 prettyFactory.asMetaWrapper = prettifierMetaWrapper
185 return prettifierMetaWrapper(prettyFactory(opts), dest)
186 } catch (e) {
187 throw Error('Missing `pino-pretty` module: `pino-pretty` must be installed separately')
188 }
189}
190
191function prettifierMetaWrapper (pretty, dest) {
192 var warned = false
193 return {
194 [needsMetadataGsym]: true,
195 lastLevel: 0,
196 lastMsg: null,
197 lastObj: null,
198 lastLogger: null,
199 flushSync () {
200 if (warned) {
201 return
202 }
203 warned = true
204 setMetadataProps(dest, this)
205 dest.write(pretty(Object.assign({
206 level: 40, // warn
207 msg: 'pino.final with prettyPrint does not support flushing',
208 time: Date.now()
209 }, this.chindings())))
210 },
211 chindings () {
212 const lastLogger = this.lastLogger
213 var chindings = null
214
215 // protection against flushSync being called before logging
216 // anything
217 if (!lastLogger) {
218 return null
219 }
220
221 if (lastLogger.hasOwnProperty(parsedChindingsSym)) {
222 chindings = lastLogger[parsedChindingsSym]
223 } else {
224 chindings = JSON.parse('{' + lastLogger[chindingsSym].substr(1) + '}')
225 lastLogger[parsedChindingsSym] = chindings
226 }
227
228 return chindings
229 },
230 write (chunk) {
231 const lastLogger = this.lastLogger
232 const chindings = this.chindings()
233
234 var time = this.lastTime
235
236 if (time.match(/^\d+/)) {
237 time = parseInt(time)
238 }
239
240 var lastObj = this.lastObj
241 var errorProps = null
242
243 const obj = Object.assign({
244 level: this.lastLevel,
245 time
246 }, lastObj, errorProps)
247
248 const serializers = lastLogger[serializersSym]
249 const keys = Object.keys(serializers)
250 var key
251
252 for (var i = 0; i < keys.length; i++) {
253 key = keys[i]
254 if (obj[key] !== undefined) {
255 obj[key] = serializers[key](obj[key])
256 }
257 }
258
259 for (key in chindings) {
260 if (!obj.hasOwnProperty(key)) {
261 obj[key] = chindings[key]
262 }
263 }
264
265 const stringifiers = lastLogger[stringifiersSym]
266 const redact = stringifiers[redactFmtSym]
267
268 const formatted = pretty(typeof redact === 'function' ? redact(obj) : obj)
269 if (formatted === undefined) return
270
271 setMetadataProps(dest, this)
272 dest.write(formatted)
273 }
274 }
275}
276
277function hasBeenTampered (stream) {
278 return stream.write !== stream.constructor.prototype.write
279}
280
281function buildSafeSonicBoom (opts) {
282 const stream = new SonicBoom(opts)
283 stream.on('error', filterBrokenPipe)
284 return stream
285
286 function filterBrokenPipe (err) {
287 // TODO verify on Windows
288 if (err.code === 'EPIPE') {
289 // If we get EPIPE, we should stop logging here
290 // however we have no control to the consumer of
291 // SonicBoom, so we just overwrite the write method
292 stream.write = noop
293 stream.end = noop
294 stream.flushSync = noop
295 stream.destroy = noop
296 return
297 }
298 stream.removeListener('error', filterBrokenPipe)
299 stream.emit('error', err)
300 }
301}
302
303function createArgsNormalizer (defaultOptions) {
304 return function normalizeArgs (instance, opts = {}, stream) {
305 // support stream as a string
306 if (typeof opts === 'string') {
307 stream = buildSafeSonicBoom({ dest: opts })
308 opts = {}
309 } else if (typeof stream === 'string') {
310 stream = buildSafeSonicBoom({ dest: stream })
311 } else if (opts instanceof SonicBoom || opts.writable || opts._writableState) {
312 stream = opts
313 opts = null
314 }
315 opts = Object.assign({}, defaultOptions, opts)
316 if ('extreme' in opts) {
317 throw Error('The extreme option has been removed, use pino.extreme instead')
318 }
319 if ('onTerminated' in opts) {
320 throw Error('The onTerminated option has been removed, use pino.final instead')
321 }
322 if ('changeLevelName' in opts) {
323 process.emitWarning(
324 'The changeLevelName option is deprecated and will be removed in v7. Use levelKey instead.',
325 { code: 'changeLevelName_deprecation' }
326 )
327 opts.levelKey = opts.changeLevelName
328 delete opts.changeLevelName
329 }
330 const { enabled, prettyPrint, prettifier, messageKey } = opts
331 if (enabled === false) opts.level = 'silent'
332 stream = stream || process.stdout
333 if (stream === process.stdout && stream.fd >= 0 && !hasBeenTampered(stream)) {
334 stream = buildSafeSonicBoom({ fd: stream.fd })
335 }
336 if (prettyPrint) {
337 const prettyOpts = Object.assign({ messageKey }, prettyPrint)
338 stream = getPrettyStream(prettyOpts, prettifier, stream, instance)
339 }
340 return { opts, stream }
341 }
342}
343
344function final (logger, handler) {
345 if (typeof logger === 'undefined' || typeof logger.child !== 'function') {
346 throw Error('expected a pino logger instance')
347 }
348 const hasHandler = (typeof handler !== 'undefined')
349 if (hasHandler && typeof handler !== 'function') {
350 throw Error('if supplied, the handler parameter should be a function')
351 }
352 const stream = logger[streamSym]
353 if (typeof stream.flushSync !== 'function') {
354 throw Error('final requires a stream that has a flushSync method, such as pino.destination and pino.extreme')
355 }
356
357 const finalLogger = new Proxy(logger, {
358 get: (logger, key) => {
359 if (key in logger.levels.values) {
360 return (...args) => {
361 logger[key](...args)
362 stream.flushSync()
363 }
364 }
365 return logger[key]
366 }
367 })
368
369 if (!hasHandler) {
370 return finalLogger
371 }
372
373 return (err = null, ...args) => {
374 try {
375 stream.flushSync()
376 } catch (e) {
377 // it's too late to wait for the stream to be ready
378 // because this is a final tick scenario.
379 // in practice there shouldn't be a situation where it isn't
380 // however, swallow the error just in case (and for easier testing)
381 }
382 return handler(err, finalLogger, ...args)
383 }
384}
385
386function stringify (obj) {
387 try {
388 return JSON.stringify(obj)
389 } catch (_) {
390 return stringifySafe(obj)
391 }
392}
393
394function buildFormatters (level, bindings, log) {
395 return {
396 level,
397 bindings,
398 log
399 }
400}
401
402function setMetadataProps (dest, that) {
403 if (dest[needsMetadataGsym] === true) {
404 dest.lastLevel = that.lastLevel
405 dest.lastMsg = that.lastMsg
406 dest.lastObj = that.lastObj
407 dest.lastTime = that.lastTime
408 dest.lastLogger = that.lastLogger
409 }
410}
411
412module.exports = {
413 noop,
414 buildSafeSonicBoom,
415 getPrettyStream,
416 asChindings,
417 asJson,
418 genLog,
419 createArgsNormalizer,
420 final,
421 stringify,
422 buildFormatters
423}