import pino from 'pino' import envPaths from 'env-paths' import { sync as mkdir } from 'make-dir' import { dirname, basename } from 'path' import pinoPretty from 'pino-pretty' import uniqid from 'uniqid' // TODO add custom levels // is pro log to file working??? function child (bindings = {}, options = {}) { const pinoOpts = Object.assign({}, options.pino) const opts = Object.assign({}, options) delete opts.pino const enabled = !!process.env.UCI_ENV let pretty; let filter; let LOG_PATH; let DATE_TIME; let destination; let file let hidden = Array.isArray(opts.hide) ? opts.hide : [opts.hide] const PRETTY_DEFAULTS = { translateTime: true, colorize: true, levelFirst: true } const DEFAULT_FILTER_PROPS = ['level', 'time', 'pid', 'msg'] const WHERE_FILTER_PROPS = ['package', 'file', 'class', 'method', 'function', 'line'] if (enabled) { if (opts.env) (opts.envForce) ? (process.env.UCI_ENV = opts.env) : (process.env.UCI_ENV = process.env.UCI_ENV || opts.env) if (process.env.UCI_ENV === 'node') process.env.UCI_ENV = process.env.NODE_ENV if ((process.env.UCI_ENV.indexOf('dev') > -1 || process.env.UCI_LOG_PRETTY) && process.env.UCI_LOG_JSON !== 'true') { // pretty is on // process UCI_LOG_PRETTY try { pretty = JSON.parse(process.env.UCI_LOG_PRETTY) } catch (error) { // console.log('LOGGER: could not JSON parse',process.env.UCI_LOG_PRETTY ) } if (process.env.UCI_LOG_PRETTY === 'verbose') pretty = { include: 'all' } pretty = pretty || opts.pretty || {} pretty = Object.assign({}, PRETTY_DEFAULTS, pretty) pretty.search = process.env.UCI_LOG_SEARCH pretty.include = pretty.include === 'all' ? 'all' : `level${pretty.include ? ',' + pretty.include : ''}` pretty.ignore = pretty.include === 'all' ? null : Object.keys(opts).filter(key => pretty.include.indexOf(key) === -1).join() if (process.env.UCI_LOG_PRETTY === 'terse' || process.env.UCI_LOG_PRETTY === 'where' || pretty.filter) { filter = filterPretty // only call prefilter if some filter is supplied, otherwise pretty.filter = [...DEFAULT_FILTER_PROPS, ...(process.env.UCI_LOG_PRETTY === 'where' ? WHERE_FILTER_PROPS : []), ...(pretty.filter || [])] pretty.ignore = null } } DATE_TIME = new Date().toString() LOG_PATH = (process.env.UCI_ENV.indexOf('pro') > -1 || process.env.UCI_ENV === 'logfile') ? (process.env.UCI_LOG_PATH || `${envPaths(opts.appName || opts.name || 'default').log}/${opts.logFileName || DATE_TIME}.log`) : undefined if (LOG_PATH) { mkdir(dirname(LOG_PATH)) // makes recursively for any missing parent directories destination = LOG_PATH } } // end enabled // default bindings bindings.appName = process.env.UCI_LOG_APP || bindings.appName || bindings.name bindings.library = bindings.library || typeof bindings.package === 'string' ? (bindings.package.includes('@') ? (bindings.package.replace(/[@]+/g, '')).split('/')[0] : undefined) : undefined bindings.repo = bindings.repo || ((typeof bindings.package === 'string') ? bindings.package.replace(/[@]+/g, '').replace(/[/]+/g, '-') : undefined) bindings.file = bindings.file || ((typeof bindings.package === 'string') ? `src/${basename(bindings.package)}.js` : undefined) bindings.class = bindings.class || (bindings.class !== false && typeof bindings.package === 'string') ? capitalize(basename(bindings.package)) : undefined bindings.instanceCreatedHR = DATE_TIME bindings.instanceCreated = Date.now() // console.dir(pretty) // console.dir(logOpts) if (pinoOpts.destination) { destination = pinoOpts.destination delete pinoOpts.destination } const defaultLevel = pinoOpts.level || opts.level || process.env.UCI_LOG_LEVEL || 'info' const defaultOpts = { enabled: enabled, safe: true, level: defaultLevel, formatters: { bindings (obj) { return { machine: obj } } }, timestamp: pino.stdTimeFunctions.epochTime, serializers: { req: pino.stdSerializers.req, res: pino.stdSerializers.res }, prettyPrint: pretty, prettifier: filter // only call prefilter if some filter is supplied, otherwise see line 53 } const logger = pino( Object.assign({}, defaultOpts, pinoOpts), // if production not enabled then LOG_PATH is empty and logs go to stdout/stderr and can be piped from there destination ) const child = logger.child({ meta: bindings }, { formatters: { log (obj) { // console.log('formatter', obj.level, child.level) obj.dateTime = new Date().toString() if (!pretty) { obj.label = child.level if (!opts.noID) obj[opts.keyID || '_id'] = uniqid() } else { obj.file = obj.file || file hidden.forEach(prop => { // console.log('hiding', prop, obj[prop]) obj[prop] = undefined }) } return obj } } }) child.default = defaultLevel child.clear = () => { if (enabled) { if (process.env.UCI_ENV.indexOf('pro') > -1) return null } // enable feature here } child.div = value => { if (enabled) { if (process.env.UCI_ENV.indexOf('dev') > -1) console.log(`===== ${value} ========`) } } child.lvlset = (level) => { if (enabled) child.level = level || child.default } child.hide = (props) => { hidden = arraysMerge(props, hidden) } child.unhide = (props) => { hidden = arraysFilter(props, hidden) } child.locate = locate return child } export default child const levels = pino.levels export { child as logger, levels, locate, arraysMerge } function filterPretty (options) { // console.log('filter options',options) return function prettifier (inputData) { // console.log('calling filter',inputData) let log // let parsedData if (typeof inputData === 'string') { try { log = JSON.Parse(inputData) } catch (err) { return inputData } } else log = inputData const filteredLog = {} options.filter.forEach(prop => { filteredLog[prop] = log[prop] }) // console.log('filtered',options.filter, filteredLog) return pinoPretty(options)(filteredLog) } } // TODO move these to @uci-utils/helpers function capitalize (s) { return s.charAt(0).toUpperCase() + s.slice(1) } function locate (frame = 2, ret) { if (typeof frame === 'string') { ret = frame; frame = 2 } const e = new Error() const stack = e.stack.toString().split(/\r\n|\n/) const RE = /file:\/\/(.*):(\d+):(?:\d+)[^\d]*$/ const reg = RE.exec(stack[frame]) const str = ret ? (ret === 'n' ? reg[2] : reg[1]) : `${reg[2]}:${reg[1]}` return str // return line#:path } function arraysMerge (els, arr) { els = Array.isArray(els) ? els : [els] return [...new Set([...arr, ...els])].filter(el => el != null) } function arraysFilter (els, arr) { els = Array.isArray(els) ? els : [els] return arr.filter(function (item) { return !els.includes(item) }) }