UNPKG

13.5 kBJavaScriptView Raw
1'use strict'
2
3const dateformat = require('dateformat')
4const stringifySafe = require('fast-safe-stringify')
5const defaultColorizer = require('./colors')()
6const {
7 DATE_FORMAT,
8 ERROR_LIKE_KEYS,
9 MESSAGE_KEY,
10 LEVEL_KEY,
11 LEVEL_LABEL,
12 TIMESTAMP_KEY,
13 LOGGER_KEYS,
14 LEVELS
15} = require('./constants')
16
17module.exports = {
18 isObject,
19 prettifyErrorLog,
20 prettifyLevel,
21 prettifyMessage,
22 prettifyMetadata,
23 prettifyObject,
24 prettifyTime
25}
26
27module.exports.internals = {
28 formatTime,
29 joinLinesWithIndentation
30}
31
32/**
33 * Converts a given `epoch` to a desired display format.
34 *
35 * @param {number|string} epoch The time to convert. May be any value that is
36 * valid for `new Date()`.
37 * @param {bool|string} [translateTime=false] When `false`, the given `epoch`
38 * will simply be returned. When `true`, the given `epoch` will be converted
39 * to a string at UTC using the `DATE_FORMAT` constant. If `translateTime` is
40 * a string, the following rules are available:
41 *
42 * - `<format string>`: The string is a literal format string. This format
43 * string will be used to interpret the `epoch` and return a display string
44 * at UTC.
45 * - `SYS:STANDARD`: The returned display string will follow the `DATE_FORMAT`
46 * constant at the system's local timezone.
47 * - `SYS:<format string>`: The returned display string will follow the given
48 * `<format string>` at the system's local timezone.
49 * - `UTC:<format string>`: The returned display string will follow the given
50 * `<format string>` at UTC.
51 *
52 * @returns {number|string} The formatted time.
53 */
54function formatTime (epoch, translateTime = false) {
55 if (translateTime === false) {
56 return epoch
57 }
58
59 const instant = new Date(epoch)
60 if (translateTime === true) {
61 return dateformat(instant, 'UTC:' + DATE_FORMAT)
62 }
63
64 const upperFormat = translateTime.toUpperCase()
65 if (upperFormat === 'SYS:STANDARD') {
66 return dateformat(instant, DATE_FORMAT)
67 }
68
69 const prefix = upperFormat.substr(0, 4)
70 if (prefix === 'SYS:' || prefix === 'UTC:') {
71 if (prefix === 'UTC:') {
72 return dateformat(instant, translateTime)
73 }
74 return dateformat(instant, translateTime.slice(4))
75 }
76
77 return dateformat(instant, `UTC:${translateTime}`)
78}
79
80function isObject (input) {
81 return Object.prototype.toString.apply(input) === '[object Object]'
82}
83
84/**
85 * Given a string with line separators, either `\r\n` or `\n`, add indentation
86 * to all lines subsequent to the first line and rejoin the lines using an
87 * end of line sequence.
88 *
89 * @param {object} input
90 * @param {string} input.input The string to split and reformat.
91 * @param {string} [input.ident] The indentation string. Default: ` ` (4 spaces).
92 * @param {string} [input.eol] The end of line sequence to use when rejoining
93 * the lines. Default: `'\n'`.
94 *
95 * @returns {string} A string with lines subsequent to the first indented
96 * with the given indentation sequence.
97 */
98function joinLinesWithIndentation ({ input, ident = ' ', eol = '\n' }) {
99 const lines = input.split(/\r?\n/)
100 for (var i = 1; i < lines.length; i += 1) {
101 lines[i] = ident + lines[i]
102 }
103 return lines.join(eol)
104}
105
106/**
107 * Given a log object that has a `type: 'Error'` key, prettify the object and
108 * return the result. In other
109 *
110 * @param {object} input
111 * @param {object} input.log The error log to prettify.
112 * @param {string} [input.messageKey] The name of the key that contains a
113 * general log message. This is not the error's message property but the logger
114 * messsage property. Default: `MESSAGE_KEY` constant.
115 * @param {string} [input.ident] The sequence to use for indentation. Default: `' '`.
116 * @param {string} [input.eol] The sequence to use for EOL. Default: `'\n'`.
117 * @param {string[]} [input.errorLikeKeys] A set of keys that should be considered
118 * to have error objects as values. Default: `ERROR_LIKE_KEYS` constant.
119 * @param {string[]} [input.errorProperties] A set of specific error object
120 * properties, that are not the value of `messageKey`, `type`, or `stack`, to
121 * include in the prettified result. The first entry in the list may be `'*'`
122 * to indicate that all sibiling properties should be prettified. Default: `[]`.
123 *
124 * @returns {string} A sring that represents the prettified error log.
125 */
126function prettifyErrorLog ({
127 log,
128 messageKey = MESSAGE_KEY,
129 ident = ' ',
130 eol = '\n',
131 errorLikeKeys = ERROR_LIKE_KEYS,
132 errorProperties = []
133}) {
134 const stack = log.stack
135 const joinedLines = joinLinesWithIndentation({ input: stack, ident, eol })
136 let result = `${ident}${joinedLines}${eol}`
137
138 if (errorProperties.length > 0) {
139 const excludeProperties = LOGGER_KEYS.concat(messageKey, 'type', 'stack')
140 let propertiesToPrint
141 if (errorProperties[0] === '*') {
142 // Print all sibling properties except for the standard exclusions.
143 propertiesToPrint = Object.keys(log).filter(k => excludeProperties.includes(k) === false)
144 } else {
145 // Print only sepcified properties unless the property is a standard exclusion.
146 propertiesToPrint = errorProperties.filter(k => excludeProperties.includes(k) === false)
147 }
148
149 for (var i = 0; i < propertiesToPrint.length; i += 1) {
150 const key = propertiesToPrint[i]
151 if (key in log === false) continue
152 if (isObject(log[key])) {
153 // The nested object may have "logger" type keys but since they are not
154 // at the root level of the object being processed, we want to print them.
155 // Thus, we invoke with `excludeLoggerKeys: false`.
156 const prettifiedObject = prettifyObject({ input: log[key], errorLikeKeys, excludeLoggerKeys: false, eol, ident })
157 result = `${result}${key}: {${eol}${prettifiedObject}}${eol}`
158 continue
159 }
160 result = `${result}${key}: ${log[key]}${eol}`
161 }
162 }
163
164 return result
165}
166
167/**
168 * Checks if the passed in log has a `level` value and returns a prettified
169 * string for that level if so.
170 *
171 * @param {object} input
172 * @param {object} input.log The log object.
173 * @param {function} [input.colorizer] A colorizer function that accepts a level
174 * value and returns a colorized string. Default: a no-op colorizer.
175 * @param {string} [levelKey='level'] The key to find the level under.
176 *
177 * @returns {undefined|string} If `log` does not have a `level` property then
178 * `undefined` will be returned. Otherwise, a string from the specified
179 * `colorizer` is returned.
180 */
181function prettifyLevel ({ log, colorizer = defaultColorizer, levelKey = LEVEL_KEY }) {
182 if (levelKey in log === false) return undefined
183 return colorizer(log[levelKey])
184}
185
186/**
187 * Prettifies a message string if the given `log` has a message property.
188 *
189 * @param {object} input
190 * @param {object} input.log The log object with the message to colorize.
191 * @param {string} [input.messageKey='msg'] The property of the `log` that is the
192 * message to be prettified.
193 * @param {string} [input.messageFormat=undefined] A format string that defines how the
194 * logged message should be formatted, e.g. `'{level} - {pid}'`.
195 * @param {function} [input.colorizer] A colorizer function that has a
196 * `.message(str)` method attached to it. This function should return a colorized
197 * string which will be the "prettified" message. Default: a no-op colorizer.
198 *
199 * @returns {undefined|string} If the message key is not found, or the message
200 * key is not a string, then `undefined` will be returned. Otherwise, a string
201 * that is the prettified message.
202 */
203function prettifyMessage ({ log, messageFormat, messageKey = MESSAGE_KEY, colorizer = defaultColorizer, levelLabel = LEVEL_LABEL }) {
204 if (messageFormat) {
205 const message = String(messageFormat).replace(/{([^{}]+)}/g, function (match, p1) {
206 // return log level as string instead of int
207 if (p1 === levelLabel && log[LEVEL_KEY]) {
208 return LEVELS[log[LEVEL_KEY]]
209 }
210 // Parse nested key access, e.g. `{keyA.subKeyB}`.
211 return p1.split('.').reduce(function (prev, curr) {
212 if (prev && prev[curr]) {
213 return prev[curr]
214 }
215 return ''
216 }, log)
217 })
218 return colorizer.message(message)
219 }
220 if (messageKey in log === false) return undefined
221 if (typeof log[messageKey] !== 'string') return undefined
222 return colorizer.message(log[messageKey])
223}
224
225/**
226 * Prettifies metadata that is usually present in a Pino log line. It looks for
227 * fields `name`, `pid`, `hostname`, and `caller` and returns a formatted string using
228 * the fields it finds.
229 *
230 * @param {object} input
231 * @param {object} input.log The log that may or may not contain metadata to
232 * be prettified.
233 *
234 * @returns {undefined|string} If no metadata is found then `undefined` is
235 * returned. Otherwise, a string of prettified metadata is returned.
236 */
237function prettifyMetadata ({ log }) {
238 let line = ''
239
240 if (log.name || log.pid || log.hostname) {
241 line += '('
242
243 if (log.name) {
244 line += log.name
245 }
246
247 if (log.name && log.pid) {
248 line += '/' + log.pid
249 } else if (log.pid) {
250 line += log.pid
251 }
252
253 if (log.hostname) {
254 // If `pid` and `name` were in the ignore keys list then we don't need
255 // the leading space.
256 line += `${line === '(' ? 'on' : ' on'} ${log.hostname}`
257 }
258
259 line += ')'
260 }
261
262 if (log.caller) {
263 line += `${line === '' ? '' : ' '}<${log.caller}>`
264 }
265
266 if (line === '') {
267 return undefined
268 } else {
269 return line
270 }
271}
272
273/**
274 * Prettifies a standard object. Special care is taken when processing the object
275 * to handle child objects that are attached to keys known to contain error
276 * objects.
277 *
278 * @param {object} input
279 * @param {object} input.input The object to prettify.
280 * @param {string} [input.ident] The identation sequence to use. Default: `' '`.
281 * @param {string} [input.eol] The EOL sequence to use. Default: `'\n'`.
282 * @param {string[]} [input.skipKeys] A set of object keys to exclude from the
283 * prettified result. Default: `[]`.
284 * @param {Object<string, function>} [input.customPrettifiers] Dictionary of
285 * custom prettifiers. Default: `{}`.
286 * @param {string[]} [input.errorLikeKeys] A set of object keys that contain
287 * error objects. Default: `ERROR_LIKE_KEYS` constant.
288 * @param {boolean} [input.excludeLoggerKeys] Indicates if known logger specific
289 * keys should be excluded from prettification. Default: `true`.
290 *
291 * @returns {string} The prettified string. This can be as little as `''` if
292 * there was nothing to prettify.
293 */
294function prettifyObject ({
295 input,
296 ident = ' ',
297 eol = '\n',
298 skipKeys = [],
299 customPrettifiers = {},
300 errorLikeKeys = ERROR_LIKE_KEYS,
301 excludeLoggerKeys = true
302}) {
303 const objectKeys = Object.keys(input)
304 const keysToIgnore = [].concat(skipKeys)
305
306 if (excludeLoggerKeys === true) Array.prototype.push.apply(keysToIgnore, LOGGER_KEYS)
307
308 let result = ''
309
310 const keysToIterate = objectKeys.filter(k => keysToIgnore.includes(k) === false)
311 for (var i = 0; i < objectKeys.length; i += 1) {
312 const keyName = keysToIterate[i]
313 const keyValue = input[keyName]
314
315 if (keyValue === undefined) continue
316
317 let lines
318 if (typeof customPrettifiers[keyName] === 'function') {
319 lines = customPrettifiers[keyName](keyValue, keyName, input)
320 } else {
321 lines = stringifySafe(keyValue, null, 2)
322 }
323
324 if (lines === undefined) continue
325 const joinedLines = joinLinesWithIndentation({ input: lines, ident, eol })
326
327 if (errorLikeKeys.includes(keyName) === true) {
328 const splitLines = `${ident}${keyName}: ${joinedLines}${eol}`.split(eol)
329 for (var j = 0; j < splitLines.length; j += 1) {
330 if (j !== 0) result += eol
331
332 const line = splitLines[j]
333 if (/^\s*"stack"/.test(line)) {
334 const matches = /^(\s*"stack":)\s*(".*"),?$/.exec(line)
335 /* istanbul ignore else */
336 if (matches && matches.length === 3) {
337 const indentSize = /^\s*/.exec(line)[0].length + 4
338 const indentation = ' '.repeat(indentSize)
339 const stackMessage = matches[2]
340 result += matches[1] + eol + indentation + JSON.parse(stackMessage).replace(/\n/g, eol + indentation)
341 }
342 } else {
343 result += line
344 }
345 }
346 } else {
347 result += `${ident}${keyName}: ${joinedLines}${eol}`
348 }
349 }
350
351 return result
352}
353
354/**
355 * Prettifies a timestamp if the given `log` has either `time`, `timestamp` or custom specified timestamp
356 * property.
357 *
358 * @param {object} input
359 * @param {object} input.log The log object with the timestamp to be prettified.
360 * @param {string} [input.timestampKey='time'] The log property that should be used to resolve timestamp value
361 * @param {bool|string} [input.translateFormat=undefined] When `true` the
362 * timestamp will be prettified into a string at UTC using the default
363 * `DATE_FORMAT`. If a string, then `translateFormat` will be used as the format
364 * string to determine the output; see the `formatTime` function for details.
365 *
366 * @returns {undefined|string} If a timestamp property cannot be found then
367 * `undefined` is returned. Otherwise, the prettified time is returned as a
368 * string.
369 */
370function prettifyTime ({ log, timestampKey = TIMESTAMP_KEY, translateFormat = undefined }) {
371 let time = null
372
373 if (timestampKey in log) {
374 time = log[timestampKey]
375 } else if ('timestamp' in log) {
376 time = log.timestamp
377 }
378
379 if (time === null) return undefined
380 if (translateFormat) {
381 return '[' + formatTime(time, translateFormat) + ']'
382 }
383
384 return `[${time}]`
385}