// Logger utility import winston from 'winston'; import { Loggly } from 'winston-loggly-bulk'; import nconf from 'nconf'; import _ from 'lodash'; import { CustomError, } from './errors'; const IS_PROD = nconf.get('IS_PROD'); const IS_TEST = nconf.get('IS_TEST'); const ENABLE_LOGS_IN_TEST = nconf.get('ENABLE_CONSOLE_LOGS_IN_TEST') === 'true'; const ENABLE_CONSOLE_LOGS_IN_PROD = nconf.get('ENABLE_CONSOLE_LOGS_IN_PROD') === 'true'; const LOGGLY_TOKEN = nconf.get('LOGGLY_TOKEN'); const LOGGLY_SUBDOMAIN = nconf.get('LOGGLY_SUBDOMAIN'); const logger = winston.createLogger(); const _config = { logger, loggingEnabled: true, // false if no transport has been configured }; export { _config as _loggerConfig }; // exported for use during tests const slimLogs = winston.format(info => { if (info && info.message && info.message.indexOf('BadRequest: Missing x-client headers') === 0) { info.body = undefined; info.message = 'BadRequest: Missing x-client headers'; } if (info && info.message && info.message.indexOf('NotAuthorized: Missing authentication headers.') === 0) { info.body = undefined; info.message = 'NotAuthorized: Missing authentication headers.'; } if (info && info.message && info.message.indexOf('TooManyRequests') === 0) { info.message = 'TooManyRequests'; } if (info && info.headers) { info.headers = { 'x-api-user': info.headers['x-api-user'] || 'unknown', 'x-client': info.headers['x-client'] || 'unknown', 'user-agent': info.headers['user-agent'] || 'unknown', 'x-forwarded-for': info.headers['x-forwarded-for'] || 'unknown', }; } return info; }); if (IS_PROD) { if (ENABLE_CONSOLE_LOGS_IN_PROD) { logger .add(new winston.transports.Console({ // text part format: winston.format.combine( winston.format.timestamp(), winston.format.printf( info => `${info.timestamp} - ${info.level} ${info.message}`, ), ), })) .add(new winston.transports.Console({ // json part format: winston.format.combine( winston.format.timestamp(), winston.format.json(), ), })); } if (LOGGLY_TOKEN && LOGGLY_SUBDOMAIN) { const tags = ['Winston-NodeJS']; if (nconf.get('SERVER_EMOJI')) { tags.push(nconf.get('SERVER_EMOJI')); } logger.add(new Loggly({ inputToken: LOGGLY_TOKEN, subdomain: LOGGLY_SUBDOMAIN, tags, json: true, format: winston.format.combine( slimLogs(), winston.format.timestamp(), winston.format.json(), ), })); } // Do not log anything when testing unless specified } else if (!IS_TEST || (IS_TEST && ENABLE_LOGS_IN_TEST)) { logger .add(new winston.transports.Console({ level: 'warn', // warn and errors (text part) format: winston.format.combine( winston.format.colorize(), winston.format.timestamp(), winston.format.printf( info => `${info.timestamp} - ${info.level} ${info.message}`, ), ), })) .add(new winston.transports.Console({ level: 'warn', // warn and errors (json part) format: winston.format.combine( // Remove stacktrace from json, printed separately winston.format(info => { if (info && info.message && typeof info.message.split === 'function') { const message = info.message.split('\n')[0]; info.message = message; } return info; })(), winston.format.prettyPrint({ colorize: true, }), ), })) .add(new winston.transports.Console({ level: 'info', // text part format: winston.format.combine( // Ignores warn and errors winston.format(info => { if (info.level === 'error' || info.level === 'warn') { return false; } return info; })(), winston.format.timestamp(), winston.format.colorize(), winston.format.printf(info => `${info.timestamp} - ${info.level} ${info.message}`), ), })) .add(new winston.transports.Console({ level: 'info', // json part format: winston.format.combine( // Ignores warn and errors winston.format(info => { if (info.level === 'error' || info.level === 'warn') { return false; } // If there are only two keys (message and level) it means there's nothing // to print as json if (Object.keys(info).length <= 2) return false; return info; })(), winston.format.prettyPrint({ colorize: true, }), ), })); } else { _config.loggingEnabled = false; } // exports a public interface insteaf of accessing directly the logger module const loggerInterface = { info (...args) { if (!_config.loggingEnabled) return; const [_message, _data] = args; const isMessageString = typeof _message === 'string'; const message = isMessageString ? _message : 'No message provided for log.'; let data; if (args.length === 1) { if (isMessageString) { data = {}; } else { data = { extraData: _message }; } } else if (!isMessageString || args.length > 2) { throw new Error('logger.info accepts up to two arguments: a message and an object with extra data to log.'); } else if (_.isPlainObject(_data)) { data = _data; } else { data = { extraData: _data }; } logger.info(message, data); }, // Accepts two argument, // an Error object (required) // and an object of additional data to log alongside the error // If the first argument isn't an Error, it'll call logger.error with all the arguments supplied error (...args) { if (!_config.loggingEnabled) return; const [err, _errorData] = args; if (args.length > 2) { throw new Error('logger.error accepts up to two arguments: an error and an object with extra data to log.'); } let errorData = {}; if (typeof _errorData === 'string') { errorData = { extraMessage: _errorData }; } else if (_.isPlainObject(_errorData)) { errorData = _errorData; } else if (_errorData) { errorData = { extraData: _errorData }; } if (err instanceof Error) { // pass the error stack as the first parameter to logger.error const stack = err.stack || err.message || err; if (!errorData.fullError) { // If the error object has interesting data // (not only httpCode, message and name from the CustomError class) // add it to the logs if (err instanceof CustomError) { const errWithoutCommonProps = _.omit(err, ['name', 'httpCode', 'message']); if (Object.keys(errWithoutCommonProps).length > 0) { errorData.fullError = errWithoutCommonProps; } } else { errorData.fullError = err; } } const loggerArgs = [stack, errorData]; // Treat 4xx errors that are handled as warnings, 5xx and uncaught errors as serious problems if (!errorData || !errorData.isHandledError || errorData.httpCode >= 500) { logger.error(...loggerArgs); } else { logger.warn(...loggerArgs); } } else { errorData.invalidErr = err; logger.error('logger.error expects an Error instance', errorData); } }, }; // Logs unhandled promises errors // when no catch is attached to a promise a unhandledRejection event will be triggered // reason is the error, p the promise where it originated process.on('unhandledRejection', (reason, promise) => { loggerInterface.error(reason, { message: 'unhandledPromiseRejection', promise }); }); export default loggerInterface;