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 | TIMESTAMP_KEY,
|
11 | LOGGER_KEYS
|
12 | } = require('./constants')
|
13 |
|
14 | module.exports = {
|
15 | isObject,
|
16 | prettifyErrorLog,
|
17 | prettifyLevel,
|
18 | prettifyMessage,
|
19 | prettifyMetadata,
|
20 | prettifyObject,
|
21 | prettifyTime
|
22 | }
|
23 |
|
24 | module.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 | */
|
51 | function 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 |
|
77 | function 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 | */
|
95 | function 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 | */
|
123 | function 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 | */
|
177 | function 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 | */
|
197 | function 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 | */
|
215 | function 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 | */
|
260 | function 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 | */
|
328 | function 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 | }
|