1 |
|
2 |
|
3 | const dateformat = require('dateformat')
|
4 | const stringifySafe = require('fast-safe-stringify')
|
5 | const defaultColorizer = require('./colors')()
|
6 | const {
|
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 |
|
17 | module.exports = {
|
18 | isObject,
|
19 | prettifyErrorLog,
|
20 | prettifyLevel,
|
21 | prettifyMessage,
|
22 | prettifyMetadata,
|
23 | prettifyObject,
|
24 | prettifyTime
|
25 | }
|
26 |
|
27 | module.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 | */
|
54 | function 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 |
|
80 | function 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 | */
|
98 | function 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 | */
|
126 | function 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 | */
|
181 | function 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 | */
|
203 | function 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 | */
|
237 | function 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 | */
|
294 | function 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 | */
|
370 | function 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 | }
|