UNPKG

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