UNPKG

resume-client-socket.io

Version:

Resume Client API for Socket.IO and Node.JS - Medical Speech to Summarized Text

571 lines (510 loc) 19.1 kB
'use strict' const clone = require('rfdc')({ circles: true }) const dateformat = require('dateformat') const stringifySafe = require('fast-safe-stringify') const defaultColorizer = require('./colors')() const { DATE_FORMAT, ERROR_LIKE_KEYS, MESSAGE_KEY, LEVEL_KEY, LEVEL_LABEL, TIMESTAMP_KEY, LOGGER_KEYS, LEVELS } = require('./constants') module.exports = { isObject, prettifyErrorLog, prettifyLevel, prettifyMessage, prettifyMetadata, prettifyObject, prettifyTime, filterLog } module.exports.internals = { formatTime, joinLinesWithIndentation, prettifyError, deleteLogProperty, splitIgnoreKey, createDate, isValidDate } /** * Converts a given `epoch` to a desired display format. * * @param {number|string} epoch The time to convert. May be any value that is * valid for `new Date()`. * @param {boolean|string} [translateTime=false] When `false`, the given `epoch` * will simply be returned. When `true`, the given `epoch` will be converted * to a string at UTC using the `DATE_FORMAT` constant. If `translateTime` is * a string, the following rules are available: * * - `<format string>`: The string is a literal format string. This format * string will be used to interpret the `epoch` and return a display string * at UTC. * - `SYS:STANDARD`: The returned display string will follow the `DATE_FORMAT` * constant at the system's local timezone. * - `SYS:<format string>`: The returned display string will follow the given * `<format string>` at the system's local timezone. * - `UTC:<format string>`: The returned display string will follow the given * `<format string>` at UTC. * * @returns {number|string} The formatted time. */ function formatTime (epoch, translateTime = false) { if (translateTime === false) { return epoch } const instant = createDate(epoch) // If the Date is invalid, do not attempt to format if (!isValidDate(instant)) { return epoch } if (translateTime === true) { return dateformat(instant, 'UTC:' + DATE_FORMAT) } const upperFormat = translateTime.toUpperCase() if (upperFormat === 'SYS:STANDARD') { return dateformat(instant, DATE_FORMAT) } const prefix = upperFormat.substr(0, 4) if (prefix === 'SYS:' || prefix === 'UTC:') { if (prefix === 'UTC:') { return dateformat(instant, translateTime) } return dateformat(instant, translateTime.slice(4)) } return dateformat(instant, `UTC:${translateTime}`) } /** * Constructs a JS Date from a number or string. Accepts any single number * or single string argument that is valid for the Date() constructor, * or an epoch as a string. * * @param {string|number} epoch The representation of the Date. * * @returns {Date} The constructed Date. */ function createDate (epoch) { // If epoch is already a valid argument, return the valid Date let date = new Date(epoch) if (isValidDate(date)) { return date } // Convert to a number to permit epoch as a string date = new Date(+epoch) return date } /** * Checks if the argument is a JS Date and not 'Invalid Date'. * * @param {Date} date The date to check. * * @returns {boolean} true if the argument is a JS Date and not 'Invalid Date'. */ function isValidDate (date) { return date instanceof Date && !Number.isNaN(date.getTime()) } function isObject (input) { return Object.prototype.toString.apply(input) === '[object Object]' } /** * Given a string with line separators, either `\r\n` or `\n`, add indentation * to all lines subsequent to the first line and rejoin the lines using an * end of line sequence. * * @param {object} input * @param {string} input.input The string to split and reformat. * @param {string} [input.ident] The indentation string. Default: ` ` (4 spaces). * @param {string} [input.eol] The end of line sequence to use when rejoining * the lines. Default: `'\n'`. * * @returns {string} A string with lines subsequent to the first indented * with the given indentation sequence. */ function joinLinesWithIndentation ({ input, ident = ' ', eol = '\n' }) { const lines = input.split(/\r?\n/) for (let i = 1; i < lines.length; i += 1) { lines[i] = ident + lines[i] } return lines.join(eol) } /** * Given a log object that has a `type: 'Error'` key, prettify the object and * return the result. In other * * @param {object} input * @param {object} input.log The error log to prettify. * @param {string} [input.messageKey] The name of the key that contains a * general log message. This is not the error's message property but the logger * messsage property. Default: `MESSAGE_KEY` constant. * @param {string} [input.ident] The sequence to use for indentation. Default: `' '`. * @param {string} [input.eol] The sequence to use for EOL. Default: `'\n'`. * @param {string[]} [input.errorLikeKeys] A set of keys that should be considered * to have error objects as values. Default: `ERROR_LIKE_KEYS` constant. * @param {string[]} [input.errorProperties] A set of specific error object * properties, that are not the value of `messageKey`, `type`, or `stack`, to * include in the prettified result. The first entry in the list may be `'*'` * to indicate that all sibiling properties should be prettified. Default: `[]`. * * @returns {string} A sring that represents the prettified error log. */ function prettifyErrorLog ({ log, messageKey = MESSAGE_KEY, ident = ' ', eol = '\n', errorLikeKeys = ERROR_LIKE_KEYS, errorProperties = [] }) { const stack = log.stack const joinedLines = joinLinesWithIndentation({ input: stack, ident, eol }) let result = `${ident}${joinedLines}${eol}` if (errorProperties.length > 0) { const excludeProperties = LOGGER_KEYS.concat(messageKey, 'type', 'stack') let propertiesToPrint if (errorProperties[0] === '*') { // Print all sibling properties except for the standard exclusions. propertiesToPrint = Object.keys(log).filter(k => excludeProperties.includes(k) === false) } else { // Print only specified properties unless the property is a standard exclusion. propertiesToPrint = errorProperties.filter(k => excludeProperties.includes(k) === false) } for (let i = 0; i < propertiesToPrint.length; i += 1) { const key = propertiesToPrint[i] if (key in log === false) continue if (isObject(log[key])) { // The nested object may have "logger" type keys but since they are not // at the root level of the object being processed, we want to print them. // Thus, we invoke with `excludeLoggerKeys: false`. const prettifiedObject = prettifyObject({ input: log[key], errorLikeKeys, excludeLoggerKeys: false, eol, ident: ident + ident }) result = `${result}${ident}${key}: {${eol}${prettifiedObject}${ident}}${eol}` continue } result = `${result}${ident}${key}: ${log[key]}${eol}` } } return result } /** * Checks if the passed in log has a `level` value and returns a prettified * string for that level if so. * * @param {object} input * @param {object} input.log The log object. * @param {function} [input.colorizer] A colorizer function that accepts a level * value and returns a colorized string. Default: a no-op colorizer. * @param {string} [levelKey='level'] The key to find the level under. * @param {function} [input.prettifier] A user-supplied formatter to be called instead of colorizer. * * @returns {undefined|string} If `log` does not have a `level` property then * `undefined` will be returned. Otherwise, a string from the specified * `colorizer` is returned. */ function prettifyLevel ({ log, colorizer = defaultColorizer, levelKey = LEVEL_KEY, prettifier }) { if (levelKey in log === false) return undefined const output = log[levelKey] return prettifier ? prettifier(output) : colorizer(output) } /** * Prettifies a message string if the given `log` has a message property. * * @param {object} input * @param {object} input.log The log object with the message to colorize. * @param {string} [input.messageKey='msg'] The property of the `log` that is the * message to be prettified. * @param {string|function} [input.messageFormat=undefined] A format string or function that defines how the * logged message should be formatted, e.g. `'{level} - {pid}'`. * @param {function} [input.colorizer] A colorizer function that has a * `.message(str)` method attached to it. This function should return a colorized * string which will be the "prettified" message. Default: a no-op colorizer. * * @returns {undefined|string} If the message key is not found, or the message * key is not a string, then `undefined` will be returned. Otherwise, a string * that is the prettified message. */ function prettifyMessage ({ log, messageFormat, messageKey = MESSAGE_KEY, colorizer = defaultColorizer, levelLabel = LEVEL_LABEL }) { if (messageFormat && typeof messageFormat === 'string') { const message = String(messageFormat).replace(/{([^{}]+)}/g, function (match, p1) { // return log level as string instead of int if (p1 === levelLabel && log[LEVEL_KEY]) { return LEVELS[log[LEVEL_KEY]] } // Parse nested key access, e.g. `{keyA.subKeyB}`. return p1.split('.').reduce(function (prev, curr) { if (prev && prev[curr]) { return prev[curr] } return '' }, log) }) return colorizer.message(message) } if (messageFormat && typeof messageFormat === 'function') { const msg = messageFormat(log, messageKey, levelLabel) return colorizer.message(msg) } if (messageKey in log === false) return undefined if (typeof log[messageKey] !== 'string') return undefined return colorizer.message(log[messageKey]) } /** * Prettifies metadata that is usually present in a Pino log line. It looks for * fields `name`, `pid`, `hostname`, and `caller` and returns a formatted string using * the fields it finds. * * @param {object} input * @param {object} input.log The log that may or may not contain metadata to * be prettified. * * @returns {undefined|string} If no metadata is found then `undefined` is * returned. Otherwise, a string of prettified metadata is returned. */ function prettifyMetadata ({ log }) { let line = '' if (log.name || log.pid || log.hostname) { line += '(' if (log.name) { line += log.name } if (log.name && log.pid) { line += '/' + log.pid } else if (log.pid) { line += log.pid } if (log.hostname) { // If `pid` and `name` were in the ignore keys list then we don't need // the leading space. line += `${line === '(' ? 'on' : ' on'} ${log.hostname}` } line += ')' } if (log.caller) { line += `${line === '' ? '' : ' '}<${log.caller}>` } if (line === '') { return undefined } else { return line } } /** * Prettifies a standard object. Special care is taken when processing the object * to handle child objects that are attached to keys known to contain error * objects. * * @param {object} input * @param {object} input.input The object to prettify. * @param {string} [input.ident] The identation sequence to use. Default: `' '`. * @param {string} [input.eol] The EOL sequence to use. Default: `'\n'`. * @param {string[]} [input.skipKeys] A set of object keys to exclude from the * prettified result. Default: `[]`. * @param {Object<string, function>} [input.customPrettifiers] Dictionary of * custom prettifiers. Default: `{}`. * @param {string[]} [input.errorLikeKeys] A set of object keys that contain * error objects. Default: `ERROR_LIKE_KEYS` constant. * @param {boolean} [input.excludeLoggerKeys] Indicates if known logger specific * keys should be excluded from prettification. Default: `true`. * @param {boolean} [input.singleLine] Should non-error keys all be formatted * on a single line? This does NOT apply to errors, which will still be * multi-line. Default: `false` * * @returns {string} The prettified string. This can be as little as `''` if * there was nothing to prettify. */ function prettifyObject ({ input, ident = ' ', eol = '\n', skipKeys = [], customPrettifiers = {}, errorLikeKeys = ERROR_LIKE_KEYS, excludeLoggerKeys = true, singleLine = false, colorizer = defaultColorizer }) { const keysToIgnore = [].concat(skipKeys) if (excludeLoggerKeys === true) Array.prototype.push.apply(keysToIgnore, LOGGER_KEYS) let result = '' // Split object keys into two categories: error and non-error const { plain, errors } = Object.entries(input).reduce(({ plain, errors }, [k, v]) => { if (keysToIgnore.includes(k) === false) { // Pre-apply custom prettifiers, because all 3 cases below will need this const pretty = typeof customPrettifiers[k] === 'function' ? customPrettifiers[k](v, k, input) : v if (errorLikeKeys.includes(k)) { errors[k] = pretty } else { plain[k] = pretty } } return { plain, errors } }, { plain: {}, errors: {} }) if (singleLine) { // Stringify the entire object as a single JSON line if (Object.keys(plain).length > 0) { result += colorizer.greyMessage(stringifySafe(plain)) } result += eol } else { // Put each object entry on its own line Object.entries(plain).forEach(([keyName, keyValue]) => { // custom prettifiers are already applied above, so we can skip it now const lines = typeof customPrettifiers[keyName] === 'function' ? keyValue : stringifySafe(keyValue, null, 2) if (lines === undefined) return const joinedLines = joinLinesWithIndentation({ input: lines, ident, eol }) result += `${ident}${keyName}:${joinedLines.startsWith(eol) ? '' : ' '}${joinedLines}${eol}` }) } // Errors Object.entries(errors).forEach(([keyName, keyValue]) => { // custom prettifiers are already applied above, so we can skip it now const lines = typeof customPrettifiers[keyName] === 'function' ? keyValue : stringifySafe(keyValue, null, 2) if (lines === undefined) return result += prettifyError({ keyName, lines, eol, ident }) }) return result } /** * Prettifies a timestamp if the given `log` has either `time`, `timestamp` or custom specified timestamp * property. * * @param {object} input * @param {object} input.log The log object with the timestamp to be prettified. * @param {string} [input.timestampKey='time'] The log property that should be used to resolve timestamp value * @param {boolean|string} [input.translateFormat=undefined] When `true` the * timestamp will be prettified into a string at UTC using the default * `DATE_FORMAT`. If a string, then `translateFormat` will be used as the format * string to determine the output; see the `formatTime` function for details. * @param {function} [input.prettifier] A user-supplied formatter for altering output. * * @returns {undefined|string} If a timestamp property cannot be found then * `undefined` is returned. Otherwise, the prettified time is returned as a * string. */ function prettifyTime ({ log, timestampKey = TIMESTAMP_KEY, translateFormat = undefined, prettifier }) { let time = null if (timestampKey in log) { time = log[timestampKey] } else if ('timestamp' in log) { time = log.timestamp } if (time === null) return undefined const output = translateFormat ? formatTime(time, translateFormat) : time return prettifier ? prettifier(output) : `[${output}]` } /** * Prettifies an error string into a multi-line format. * @param {object} input * @param {string} input.keyName The key assigned to this error in the log object * @param {string} input.lines The STRINGIFIED error. If the error field has a * custom prettifier, that should be pre-applied as well * @param {string} input.ident The indentation sequence to use * @param {string} input.eol The EOL sequence to use */ function prettifyError ({ keyName, lines, eol, ident }) { let result = '' const joinedLines = joinLinesWithIndentation({ input: lines, ident, eol }) const splitLines = `${ident}${keyName}: ${joinedLines}${eol}`.split(eol) for (let j = 0; j < splitLines.length; j += 1) { if (j !== 0) result += eol const line = splitLines[j] if (/^\s*"stack"/.test(line)) { const matches = /^(\s*"stack":)\s*(".*"),?$/.exec(line) /* istanbul ignore else */ if (matches && matches.length === 3) { const indentSize = /^\s*/.exec(line)[0].length + 4 const indentation = ' '.repeat(indentSize) const stackMessage = matches[2] result += matches[1] + eol + indentation + JSON.parse(stackMessage).replace(/\n/g, eol + indentation) } else { result += line } } else { result += line } } return result } /** * Splits the input key delimited by a dot character but not when it is preceded * by a backslash. * * @param {string} key A string identifying the property. * * @returns {string[]} Returns a list of string containing each delimited property. * e.g. `'prop2\.domain\.corp.prop2'` should return [ 'prop2.domain.com', 'prop2' ] */ function splitIgnoreKey (key) { const result = [] let backslash = false let segment = '' for (let i = 0; i < key.length; i++) { const c = key.charAt(i) if (c === '\\') { backslash = true continue } if (backslash) { backslash = false segment += c continue } /* Non-escaped dot, push to result */ if (c === '.') { result.push(segment) segment = '' continue } segment += c } /* Push last entry to result */ if (segment.length) { result.push(segment) } return result } /** * Deletes a specified property from a log object if it exists. * This function mutates the passed in `log` object. * * @param {object} log The log object to be modified. * @param {string} property A string identifying the property to be deleted from * the log object. Accepts nested properties delimited by a `.` * Delimiter can be escaped to preserve property names that contain the delimiter. * e.g. `'prop1.prop2'` or `'prop2\.domain\.corp.prop2'` */ function deleteLogProperty (log, property) { const props = splitIgnoreKey(property) const propToDelete = props.pop() props.forEach((prop) => { if (!Object.prototype.hasOwnProperty.call(log, prop)) { return } log = log[prop] }) delete log[propToDelete] } /** * Filter a log object by removing any ignored keys. * * @param {object} log The log object to be modified. * @param {string} ignoreKeys An array of strings identifying the properties to be removed. * * @returns {object} A new `log` object instance that does not include the ignored keys. */ function filterLog (log, ignoreKeys) { const logCopy = clone(log) ignoreKeys.forEach((ignoreKey) => { deleteLogProperty(logCopy, ignoreKey) }) return logCopy }