From 8084e80027dcfa290fc92dd4df85f935807106e4 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Wed, 17 Jan 2018 08:24:50 +1100 Subject: [PATCH] fix(tests): tests failing - config listeners not working in sandbox --- examples/layouts.js | 13 + lib/LoggingEvent.js | 145 +++--- lib/appenders/categoryFilter.js | 4 + lib/appenders/index.js | 97 ++++ lib/categories.js | 125 +++++ lib/clustering.js | 122 +++-- lib/configuration.js | 218 ++------ lib/connect-logger.js | 150 +++--- lib/levels.js | 174 ++++--- lib/log4js.js | 96 +--- lib/logger.js | 173 +++---- test/tap/configuration-validation-test.js | 605 +++++++++++----------- test/tap/connect-logger-test.js | 4 +- test/tap/connect-nolog-test.js | 4 +- test/tap/levels-test.js | 2 +- test/tap/logger-test.js | 31 +- test/tap/server-test.js | 4 +- 17 files changed, 1025 insertions(+), 942 deletions(-) create mode 100644 examples/layouts.js create mode 100644 lib/appenders/index.js create mode 100644 lib/categories.js diff --git a/examples/layouts.js b/examples/layouts.js new file mode 100644 index 0000000..0d47444 --- /dev/null +++ b/examples/layouts.js @@ -0,0 +1,13 @@ +const log4js = require('../lib/log4js'); + +log4js.configure({ + appenders: { + out: { type: 'stdout', layout: { type: 'messagePassThrough' } } + }, + categories: { + default: { appenders: ['out'], level: 'info' } + } +}); + +const logger = log4js.getLogger('thing'); +logger.info('This should not have a timestamp'); diff --git a/lib/LoggingEvent.js b/lib/LoggingEvent.js index fe8b1cb..c1389b2 100644 --- a/lib/LoggingEvent.js +++ b/lib/LoggingEvent.js @@ -1,81 +1,80 @@ const CircularJSON = require('circular-json'); +const levels = require('./levels'); -module.exports = (levels) => { +/** + * @name LoggingEvent + * @namespace Log4js + */ +class LoggingEvent { /** - * @name LoggingEvent - * @namespace Log4js + * Models a logging event. + * @constructor + * @param {String} categoryName name of category + * @param {Log4js.Level} level level of message + * @param {Array} data objects to log + * @author Seth Chisamore */ - class LoggingEvent { - /** - * Models a logging event. - * @constructor - * @param {String} categoryName name of category - * @param {Log4js.Level} level level of message - * @param {Array} data objects to log - * @author Seth Chisamore - */ - constructor(categoryName, level, data, context) { - this.startTime = new Date(); - this.categoryName = categoryName; - this.data = data; - this.level = level; - this.context = Object.assign({}, context); - this.pid = process.pid; - // if (cluster && cluster.isWorker) { - // this.cluster = { - // workerId: cluster.worker.id, - // worker: process.pid - // }; - // } - } + constructor(categoryName, level, data, context) { + this.startTime = new Date(); + this.categoryName = categoryName; + this.data = data; + this.level = level; + this.context = Object.assign({}, context); + this.pid = process.pid; + // if (cluster && cluster.isWorker) { + // this.cluster = { + // workerId: cluster.worker.id, + // worker: process.pid + // }; + // } + } - serialise() { - // JSON.stringify(new Error('test')) returns {}, which is not really useful for us. - // The following allows us to serialize errors correctly. - // Validate that we really are in this case - try { - const logData = this.data.map((e) => { - if (e && e.stack && CircularJSON.stringify(e) === '{}') { - e = { message: e.message, stack: e.stack }; - } - return e; - }); - this.data = logData; - return CircularJSON.stringify(this); - } catch (e) { - return new LoggingEvent( - 'log4js', - levels.ERROR, - ['Unable to serialise log event due to :', e] - ).serialise(); - } - } - - static deserialise(serialised) { - let event; - try { - event = CircularJSON.parse(serialised); - event.startTime = new Date(event.startTime); - event.level = levels.getLevel(event.level.levelStr); - event.data = event.data.map((e) => { - if (e && e.stack) { - const fakeError = new Error(e.message); - fakeError.stack = e.stack; - e = fakeError; - } - return e; - }); - } catch (e) { - event = new LoggingEvent( - 'log4js', - levels.ERROR, - ['Unable to parse log:', serialised, 'because: ', e] - ); - } - - return event; + serialise() { + // JSON.stringify(new Error('test')) returns {}, which is not really useful for us. + // The following allows us to serialize errors correctly. + // Validate that we really are in this case + try { + const logData = this.data.map((e) => { + if (e && e.stack && CircularJSON.stringify(e) === '{}') { + e = { message: e.message, stack: e.stack }; + } + return e; + }); + this.data = logData; + return CircularJSON.stringify(this); + } catch (e) { + return new LoggingEvent( + 'log4js', + levels.ERROR, + ['Unable to serialise log event due to :', e] + ).serialise(); } } - return LoggingEvent; -}; + static deserialise(serialised) { + let event; + try { + event = CircularJSON.parse(serialised); + event.startTime = new Date(event.startTime); + event.level = levels.getLevel(event.level.levelStr); + event.data = event.data.map((e) => { + if (e && e.stack) { + const fakeError = new Error(e.message); + fakeError.stack = e.stack; + e = fakeError; + } + return e; + }); + } catch (e) { + event = new LoggingEvent( + 'log4js', + levels.ERROR, + ['Unable to parse log:', serialised, 'because: ', e] + ); + } + + return event; + } +} + +module.exports = LoggingEvent; diff --git a/lib/appenders/categoryFilter.js b/lib/appenders/categoryFilter.js index 263970b..4ec8327 100644 --- a/lib/appenders/categoryFilter.js +++ b/lib/appenders/categoryFilter.js @@ -1,9 +1,13 @@ 'use strict'; +const debug = require('debug')('log4js:categoryFilter'); + function categoryFilter(excludes, appender) { if (typeof excludes === 'string') excludes = [excludes]; return (logEvent) => { + debug(`Checking ${logEvent.categoryName} against ${excludes}`); if (excludes.indexOf(logEvent.categoryName) === -1) { + debug('Not excluded, sending to appender'); appender(logEvent); } }; diff --git a/lib/appenders/index.js b/lib/appenders/index.js new file mode 100644 index 0000000..c5448e0 --- /dev/null +++ b/lib/appenders/index.js @@ -0,0 +1,97 @@ +const path = require('path'); +const debug = require('debug')('log4js:appenders'); +const configuration = require('../configuration'); +const clustering = require('../clustering'); +const levels = require('../levels'); +const layouts = require('../layouts'); + +const appenders = new Map(); + +const tryLoading = (modulePath, config) => { + debug('Loading module from ', modulePath); + try { + return require(modulePath); //eslint-disable-line + } catch (e) { + // if the module was found, and we still got an error, then raise it + configuration.throwExceptionIf( + config, + e.code !== 'MODULE_NOT_FOUND', + `appender "${modulePath}" could not be loaded (error was: ${e})` + ); + return undefined; + } +}; + +const loadAppenderModule = (type, config) => tryLoading(`./${type}`, config) || + tryLoading(type, config) || + tryLoading(path.join(path.dirname(require.main.filename), type), config) || + tryLoading(path.join(process.cwd(), type), config); + +const createAppender = (name, config) => { + const appenderConfig = config.appenders[name]; + const appenderModule = loadAppenderModule(appenderConfig.type, config); + configuration.throwExceptionIf( + config, + configuration.not(appenderModule), + `appender "${name}" is not valid (type "${appenderConfig.type}" could not be found)` + ); + if (appenderModule.appender) { + debug(`DEPRECATION: Appender ${appenderConfig.type} exports an appender function.`); + } + if (appenderModule.shutdown) { + debug(`DEPRECATION: Appender ${appenderConfig.type} exports a shutdown function.`); + } + + debug(`${name}: clustering.isMaster ? ${clustering.isMaster()}`); + debug(`${name}: appenderModule is ${require('util').inspect(appenderModule)}`); // eslint-disable-line + return clustering.onlyOnMaster(() => { + debug(`calling appenderModule.configure for ${name} / ${appenderConfig.type}`); + return appenderModule.configure( + appenderConfig, + layouts, + appender => appenders.get(appender), + levels + ); + }, () => {}); +}; + +const setup = (config) => { + appenders.clear(); + + Object.keys(config.appenders).forEach((name) => { + debug(`Creating appender ${name}`); + appenders.set(name, createAppender(name, config)); + }); +}; + +// setup({ +// appenders: { +// stdout: { type: 'stdout' } +// } +// }); + +configuration.addListener((config) => { + configuration.throwExceptionIf( + config, + configuration.not(configuration.anObject(config.appenders)), + 'must have a property "appenders" of type object.' + ); + const appenderNames = Object.keys(config.appenders); + configuration.throwExceptionIf( + config, + configuration.not(appenderNames.length), + 'must define at least one appender.' + ); + + appenderNames.forEach((name) => { + configuration.throwExceptionIf( + config, + configuration.not(config.appenders[name].type), + `appender "${name}" is not valid (must be an object with property "type")` + ); + }); +}); + +configuration.addListener(setup); + +module.exports = appenders; diff --git a/lib/categories.js b/lib/categories.js new file mode 100644 index 0000000..b107c66 --- /dev/null +++ b/lib/categories.js @@ -0,0 +1,125 @@ +const configuration = require('./configuration'); +const levels = require('./levels'); +const appenders = require('./appenders'); +const debug = require('debug')('log4js:categories'); + +const categories = new Map(); + +configuration.addListener((config) => { + configuration.throwExceptionIf( + config, + configuration.not(configuration.anObject(config.categories)), + 'must have a property "categories" of type object.' + ); + + const categoryNames = Object.keys(config.categories); + configuration.throwExceptionIf( + config, + configuration.not(categoryNames.length), + 'must define at least one category.' + ); + + categoryNames.forEach((name) => { + const category = config.categories[name]; + configuration.throwExceptionIf( + config, + [ + configuration.not(category.appenders), + configuration.not(category.level) + ], + `category "${name}" is not valid (must be an object with properties "appenders" and "level")` + ); + + configuration.throwExceptionIf( + config, + configuration.not(Array.isArray(category.appenders)), + `category "${name}" is not valid (appenders must be an array of appender names)` + ); + + configuration.throwExceptionIf( + config, + configuration.not(category.appenders.length), + `category "${name}" is not valid (appenders must contain at least one appender name)` + ); + + category.appenders.forEach((appender) => { + configuration.throwExceptionIf( + config, + configuration.not(appenders.get(appender)), + `category "${name}" is not valid (appender "${appender}" is not defined)` + ); + }); + + configuration.throwExceptionIf( + config, + configuration.not(levels.getLevel(category.level)), + `category "${name}" is not valid (level "${category.level}" not recognised;` + + ` valid levels are ${levels.levels.join(', ')})` + ); + }); + + configuration.throwExceptionIf( + config, + configuration.not(config.categories.default), + 'must define a "default" category.' + ); +}); + +const setup = (config) => { + categories.clear(); + + const categoryNames = Object.keys(config.categories); + categoryNames.forEach((name) => { + const category = config.categories[name]; + const categoryAppenders = []; + category.appenders.forEach((appender) => { + categoryAppenders.push(appenders.get(appender)); + debug(`Creating category ${name}`); + categories.set( + name, + { appenders: categoryAppenders, level: levels.getLevel(category.level) } + ); + }); + }); +}; + +// setup({ +// categories: { default: { appenders: ['stdout'], level: 'OFF' } } +// }); +configuration.addListener(setup); + +const configForCategory = (category) => { + debug(`configForCategory: searching for config for ${category}`); + if (categories.has(category)) { + debug(`configForCategory: ${category} exists in config, returning it`); + return categories.get(category); + } + if (category.indexOf('.') > 0) { + debug(`configForCategory: ${category} has hierarchy, searching for parents`); + return configForCategory(category.substring(0, category.lastIndexOf('.'))); + } + debug('configForCategory: returning config for default category'); + return configForCategory('default'); +}; + +const appendersForCategory = category => configForCategory(category).appenders; +const getLevelForCategory = category => configForCategory(category).level; + +const setLevelForCategory = (category, level) => { + let categoryConfig = categories.get(category); + debug(`setLevelForCategory: found ${categoryConfig} for ${category}`); + if (!categoryConfig) { + const sourceCategoryConfig = configForCategory(category); + debug('setLevelForCategory: no config found for category, ' + + `found ${sourceCategoryConfig} for parents of ${category}`); + categoryConfig = { appenders: sourceCategoryConfig.appenders }; + } + categoryConfig.level = level; + categories.set(category, categoryConfig); +}; + +module.exports = { + appendersForCategory, + getLevelForCategory, + setLevelForCategory +}; diff --git a/lib/clustering.js b/lib/clustering.js index b046dbf..814e320 100644 --- a/lib/clustering.js +++ b/lib/clustering.js @@ -1,47 +1,60 @@ const debug = require('debug')('log4js:clustering'); +const LoggingEvent = require('./LoggingEvent'); +const configuration = require('./configuration'); +const cluster = require('cluster'); -let cluster; -try { - cluster = require('cluster'); // eslint-disable-line global-require -} catch (e) { - debug('Clustering support disabled because require(cluster) threw an error: ', e); -} +const listeners = []; -module.exports = (config) => { - const disabled = config.disableClustering || !cluster; - const pm2 = config.pm2; - const pm2InstanceVar = config.pm2InstanceVar || 'NODE_APP_INSTANCE'; - const listeners = []; +let disabled = false; +let pm2 = false; +let pm2InstanceVar = 'NODE_APP_INSTANCE'; + +const isPM2Master = () => pm2 && process.env[pm2InstanceVar] === '0'; +const isMaster = () => disabled || cluster.isMaster || isPM2Master(); +const isWorker = () => !isMaster(); + +const sendToListeners = (logEvent) => { + listeners.forEach(l => l(logEvent)); +}; + +// in a multi-process node environment, worker loggers will use +// process.send +const receiver = (worker, message) => { + // prior to node v6, the worker parameter was not passed (args were message, handle) + debug('cluster message received from worker ', worker, ': ', message); + if (worker.topic && worker.data) { + message = worker; + worker = undefined; + } + if (message && message.topic && message.topic === 'log4js:message') { + debug('received message: ', message.data); + const logEvent = LoggingEvent.deserialise(message.data); + sendToListeners(logEvent); + } +}; + +configuration.addListener((config) => { + // clear out the listeners, because configure has been called. + listeners.length = 0; + + disabled = config.disableClustering; + pm2 = config.pm2; + pm2InstanceVar = config.pm2InstanceVar || 'NODE_APP_INSTANCE'; debug(`clustering disabled ? ${disabled}`); - debug(`cluster.isMaster ? ${cluster && cluster.isMaster}`); + debug(`cluster.isMaster ? ${cluster.isMaster}`); debug(`pm2 enabled ? ${pm2}`); debug(`pm2InstanceVar = ${pm2InstanceVar}`); debug(`process.env[${pm2InstanceVar}] = ${process.env[pm2InstanceVar]}`); - const isPM2Master = () => pm2 && process.env[pm2InstanceVar] === '0'; - const isMaster = () => disabled || cluster.isMaster || isPM2Master(); - const isWorker = () => !isMaster(); - - // in a multi-process node environment, worker loggers will use - // process.send - const receiver = (worker, message) => { - // prior to node v6, the worker parameter was not passed (args were message, handle) - debug('cluster message received from worker ', worker, ': ', message); - if (worker.topic && worker.data) { - message = worker; - worker = undefined; - } - if (message && message.topic && message.topic === 'log4js:message') { - debug('received message: ', message.data); - const logEvent = LoggingEvent.deserialise(message.data); - listeners.forEach(l => l(logEvent)); - } - }; - // just in case configure is called after shutdown - pm2 && process.removeListener('message', receiver); - cluster.removeListener('message', receiver); + if (pm2) { + process.removeListener('message', receiver); + } + if (cluster.removeListener) { + cluster.removeListener('message', receiver); + } + if (config.disableClustering) { debug('Not listening for cluster messages, because clustering disabled.'); } else if (isPM2Master()) { @@ -56,26 +69,29 @@ module.exports = (config) => { } else { debug('not listening for messages, because we are not a master process'); } +}); - - return { - onlyOnMaster: (fn) => { - if (isMaster()) { - fn(); +module.exports = { + onlyOnMaster: (fn, notMaster) => (isMaster() ? fn() : notMaster), + onlyOnWorker: (fn, notWorker) => (isWorker() ? fn() : notWorker), + isMaster: isMaster, + isWorker: isWorker, + send: (msg) => { + if (isWorker()) { + if (pm2) { + process.send({ type: 'log4js:message', data: msg.serialise() }); + } else { + msg.cluster = { + workerId: cluster.worker.id, + worker: process.pid + }; + cluster.send({ type: 'log4js:message', data: msg.serialise() }); } - }, - onlyOnWorker: (fn) => { - if (isWorker()) { - fn(); - } - }, - isMaster: isMaster, - isWorker: isWorker, - send: (msg) => { - - }, - onMessage: (listener) => { - listeners.push(listener); + } else { + sendToListeners(msg); } - }; + }, + onMessage: (listener) => { + listeners.push(listener); + } }; diff --git a/lib/configuration.js b/lib/configuration.js index d59a2ce..b798cf3 100644 --- a/lib/configuration.js +++ b/lib/configuration.js @@ -1,201 +1,49 @@ 'use strict'; const util = require('util'); -const path = require('path'); -const levels = require('./levels'); -const layouts = require('./layouts'); -const clustering = require('./clustering'); const debug = require('debug')('log4js:configuration'); -const validColours = [ - 'white', 'grey', 'black', - 'blue', 'cyan', 'green', - 'magenta', 'red', 'yellow' -]; +const listeners = []; -function not(thing) { - return !thing; -} +const not = thing => !thing; -function anObject(thing) { - return thing && typeof thing === 'object' && !Array.isArray(thing); -} +const anObject = thing => thing && typeof thing === 'object' && !Array.isArray(thing); -function validIdentifier(thing) { - return /^[A-Za-z][A-Za-z0-9_]*$/g.test(thing); -} +const validIdentifier = thing => /^[A-Za-z][A-Za-z0-9_]*$/g.test(thing); -function anInteger(thing) { - return thing && typeof thing === 'number' && Number.isInteger(thing); -} +const anInteger = thing => thing && typeof thing === 'number' && Number.isInteger(thing); -class Configuration { - throwExceptionIf(checks, message) { - const tests = Array.isArray(checks) ? checks : [checks]; - tests.forEach((test) => { - if (test) { - throw new Error(`Problem with log4js configuration: (${util.inspect(this.candidate, { depth: 5 })})` + - ` - ${message}`); - } - }); +const addListener = (fn) => { + if (fn) { + listeners.push(fn); } +}; - tryLoading(modulePath) { - debug('Loading module from ', modulePath); - try { - return require(modulePath); //eslint-disable-line - } catch (e) { - // if the module was found, and we still got an error, then raise it - this.throwExceptionIf( - e.code !== 'MODULE_NOT_FOUND', - `appender "${path}" could not be loaded (error was: ${e})` - ); - return undefined; +const throwExceptionIf = (config, checks, message) => { + const tests = Array.isArray(checks) ? checks : [checks]; + tests.forEach((test) => { + if (test) { + throw new Error(`Problem with log4js configuration: (${util.inspect(config, { depth: 5 })})` + + ` - ${message}`); } - } + }); +}; - loadAppenderModule(type) { - return this.tryLoading(`./appenders/${type}`) || - this.tryLoading(type) || - this.tryLoading(path.join(path.dirname(require.main.filename), type)) || - this.tryLoading(path.join(process.cwd(), type)); - } +const configure = (candidate) => { + debug('New configuration to be validated: ', candidate); + throwExceptionIf(candidate, not(anObject(candidate)), 'must be an object.'); - createAppender(name, config) { - const appenderModule = this.loadAppenderModule(config.type); - this.throwExceptionIf( - not(appenderModule), - `appender "${name}" is not valid (type "${config.type}" could not be found)` - ); - if (appenderModule.appender) { - debug(`DEPRECATION: Appender ${config.type} exports an appender function.`); - } - if (appenderModule.shutdown) { - debug(`DEPRECATION: Appender ${config.type} exports a shutdown function.`); - } + debug('Calling configuration listeners'); + listeners.forEach(listener => listener(candidate)); + debug('Configuration finished.'); +}; - this.clustering.onlyOnMaster(() => appenderModule.configure( - config, - layouts, - this.configuredAppenders.get.bind(this.configuredAppenders), - this.configuredLevels - )); - return () => {}; - } - - get appenders() { - return this.configuredAppenders; - } - - set appenders(appenderConfig) { - const appenderNames = Object.keys(appenderConfig); - this.throwExceptionIf(not(appenderNames.length), 'must define at least one appender.'); - - this.configuredAppenders = new Map(); - appenderNames.forEach((name) => { - this.throwExceptionIf( - not(appenderConfig[name].type), - `appender "${name}" is not valid (must be an object with property "type")` - ); - - debug(`Creating appender ${name}`); - this.configuredAppenders.set(name, this.createAppender(name, appenderConfig[name])); - }); - } - - get categories() { - return this.configuredCategories; - } - - set categories(categoryConfig) { - const categoryNames = Object.keys(categoryConfig); - this.throwExceptionIf(not(categoryNames.length), 'must define at least one category.'); - - this.configuredCategories = new Map(); - categoryNames.forEach((name) => { - const category = categoryConfig[name]; - this.throwExceptionIf( - [ - not(category.appenders), - not(category.level) - ], - `category "${name}" is not valid (must be an object with properties "appenders" and "level")` - ); - - this.throwExceptionIf( - not(Array.isArray(category.appenders)), - `category "${name}" is not valid (appenders must be an array of appender names)` - ); - - this.throwExceptionIf( - not(category.appenders.length), - `category "${name}" is not valid (appenders must contain at least one appender name)` - ); - - const appenders = []; - category.appenders.forEach((appender) => { - this.throwExceptionIf( - not(this.configuredAppenders.get(appender)), - `category "${name}" is not valid (appender "${appender}" is not defined)` - ); - appenders.push(this.appenders.get(appender)); - }); - - this.throwExceptionIf( - not(this.configuredLevels.getLevel(category.level)), - `category "${name}" is not valid (level "${category.level}" not recognised;` + - ` valid levels are ${this.configuredLevels.levels.join(', ')})` - ); - - debug(`Creating category ${name}`); - this.configuredCategories.set( - name, - { appenders: appenders, level: this.configuredLevels.getLevel(category.level) } - ); - }); - - this.throwExceptionIf(not(categoryConfig.default), 'must define a "default" category.'); - } - - get levels() { - return this.configuredLevels; - } - - set levels(levelConfig) { - // levels are optional - if (levelConfig) { - this.throwExceptionIf(not(anObject(levelConfig)), 'levels must be an object'); - const newLevels = Object.keys(levelConfig); - newLevels.forEach((l) => { - this.throwExceptionIf( - not(validIdentifier(l)), - `level name "${l}" is not a valid identifier (must start with a letter, only contain A-Z,a-z,0-9,_)` - ); - this.throwExceptionIf(not(anObject(levelConfig[l])), `level "${l}" must be an object`); - this.throwExceptionIf(not(levelConfig[l].value), `level "${l}" must have a 'value' property`); - this.throwExceptionIf(not(anInteger(levelConfig[l].value)), `level "${l}".value must have an integer value`); - this.throwExceptionIf(not(levelConfig[l].colour), `level "${l}" must have a 'colour' property`); - this.throwExceptionIf( - not(validColours.indexOf(levelConfig[l].colour) > -1), - `level "${l}".colour must be one of ${validColours.join(', ')}` - ); - }); - } - this.configuredLevels = levels(levelConfig); - } - - constructor(candidate) { - this.candidate = candidate; - - this.throwExceptionIf(not(anObject(candidate)), 'must be an object.'); - this.throwExceptionIf(not(anObject(candidate.appenders)), 'must have a property "appenders" of type object.'); - this.throwExceptionIf(not(anObject(candidate.categories)), 'must have a property "categories" of type object.'); - - this.levels = candidate.levels; - this.clustering = clustering(this.candidate); - this.appenders = candidate.appenders; - this.categories = candidate.categories; - } -} - -module.exports = Configuration; +module.exports = { + configure, + addListener, + throwExceptionIf, + anObject, + anInteger, + validIdentifier, + not +}; diff --git a/lib/connect-logger.js b/lib/connect-logger.js index 9306ac1..3a314de 100755 --- a/lib/connect-logger.js +++ b/lib/connect-logger.js @@ -2,6 +2,8 @@ 'use strict'; +const levels = require('./levels'); + const DEFAULT_FORMAT = ':remote-addr - -' + ' ":method :url HTTP/:http-version"' + ' :status :content-length ":referrer"' + @@ -163,8 +165,7 @@ function createNoLogCondition(nolog) { return regexp; } -module.exports = function (levels) { - /** +/** * Log requests with the given `options` or a `format` string. * * Options: @@ -192,80 +193,77 @@ module.exports = function (levels) { * @param options * @api public */ - function getLogger(logger4js, options) { - /* eslint no-underscore-dangle:0 */ - if (typeof options === 'object') { - options = options || {}; - } else if (options) { - options = { format: options }; - } else { - options = {}; - } - - const thisLogger = logger4js; - let level = levels.getLevel(options.level, levels.INFO); - const fmt = options.format || DEFAULT_FORMAT; - const nolog = options.nolog ? createNoLogCondition(options.nolog) : null; - - return (req, res, next) => { - // mount safety - if (req._logging) return next(); - - // nologs - if (nolog && nolog.test(req.originalUrl)) return next(); - - if (thisLogger.isLevelEnabled(level) || options.level === 'auto') { - const start = new Date(); - const writeHead = res.writeHead; - - // flag as logging - req._logging = true; - - // proxy for statusCode. - res.writeHead = (code, headers) => { - res.writeHead = writeHead; - res.writeHead(code, headers); - - res.__statusCode = code; - res.__headers = headers || {}; - - // status code response level handling - if (options.level === 'auto') { - level = levels.INFO; - if (code >= 300) level = levels.WARN; - if (code >= 400) level = levels.ERROR; - } else { - level = levels.getLevel(options.level, levels.INFO); - } - }; - - // hook on end request to emit the log entry of the HTTP request. - res.on('finish', () => { - res.responseTime = new Date() - start; - // status code response level handling - if (res.statusCode && options.level === 'auto') { - level = levels.INFO; - if (res.statusCode >= 300) level = levels.WARN; - if (res.statusCode >= 400) level = levels.ERROR; - } - - if (thisLogger.isLevelEnabled(level)) { - const combinedTokens = assembleTokens(req, res, options.tokens || []); - - if (typeof fmt === 'function') { - const line = fmt(req, res, str => format(str, combinedTokens)); - if (line) thisLogger.log(level, line); - } else { - thisLogger.log(level, format(fmt, combinedTokens)); - } - } - }); - } - - // ensure next gets always called - return next(); - }; +module.exports = function getLogger(logger4js, options) { + /* eslint no-underscore-dangle:0 */ + if (typeof options === 'object') { + options = options || {}; + } else if (options) { + options = { format: options }; + } else { + options = {}; } - return { connectLogger: getLogger }; + const thisLogger = logger4js; + let level = levels.getLevel(options.level, levels.INFO); + const fmt = options.format || DEFAULT_FORMAT; + const nolog = options.nolog ? createNoLogCondition(options.nolog) : null; + + return (req, res, next) => { + // mount safety + if (req._logging) return next(); + + // nologs + if (nolog && nolog.test(req.originalUrl)) return next(); + + if (thisLogger.isLevelEnabled(level) || options.level === 'auto') { + const start = new Date(); + const writeHead = res.writeHead; + + // flag as logging + req._logging = true; + + // proxy for statusCode. + res.writeHead = (code, headers) => { + res.writeHead = writeHead; + res.writeHead(code, headers); + + res.__statusCode = code; + res.__headers = headers || {}; + + // status code response level handling + if (options.level === 'auto') { + level = levels.INFO; + if (code >= 300) level = levels.WARN; + if (code >= 400) level = levels.ERROR; + } else { + level = levels.getLevel(options.level, levels.INFO); + } + }; + + // hook on end request to emit the log entry of the HTTP request. + res.on('finish', () => { + res.responseTime = new Date() - start; + // status code response level handling + if (res.statusCode && options.level === 'auto') { + level = levels.INFO; + if (res.statusCode >= 300) level = levels.WARN; + if (res.statusCode >= 400) level = levels.ERROR; + } + + if (thisLogger.isLevelEnabled(level)) { + const combinedTokens = assembleTokens(req, res, options.tokens || []); + + if (typeof fmt === 'function') { + const line = fmt(req, res, str => format(str, combinedTokens)); + if (line) thisLogger.log(level, line); + } else { + thisLogger.log(level, format(fmt, combinedTokens)); + } + } + }); + } + + // ensure next gets always called + return next(); + }; }; diff --git a/lib/levels.js b/lib/levels.js index 0ca70c8..99ac0ea 100644 --- a/lib/levels.js +++ b/lib/levels.js @@ -1,60 +1,22 @@ 'use strict'; -module.exports = function (customLevels) { - /** - * @name Level - * @namespace Log4js - */ - class Level { - constructor(level, levelStr, colour) { - this.level = level; - this.levelStr = levelStr; - this.colour = colour; - } +const configuration = require('./configuration'); - toString() { - return this.levelStr; - } +const validColours = [ + 'white', 'grey', 'black', + 'blue', 'cyan', 'green', + 'magenta', 'red', 'yellow' +]; - isLessThanOrEqualTo(otherLevel) { - if (typeof otherLevel === 'string') { - otherLevel = getLevel(otherLevel); - } - return this.level <= otherLevel.level; - } - - isGreaterThanOrEqualTo(otherLevel) { - if (typeof otherLevel === 'string') { - otherLevel = getLevel(otherLevel); - } - return this.level >= otherLevel.level; - } - - isEqualTo(otherLevel) { - if (typeof otherLevel === 'string') { - otherLevel = getLevel(otherLevel); - } - return this.level === otherLevel.level; - } +class Level { + constructor(level, levelStr, colour) { + this.level = level; + this.levelStr = levelStr; + this.colour = colour; } - const defaultLevels = { - ALL: new Level(Number.MIN_VALUE, 'ALL', 'grey'), - TRACE: new Level(5000, 'TRACE', 'blue'), - DEBUG: new Level(10000, 'DEBUG', 'cyan'), - INFO: new Level(20000, 'INFO', 'green'), - WARN: new Level(30000, 'WARN', 'yellow'), - ERROR: new Level(40000, 'ERROR', 'red'), - FATAL: new Level(50000, 'FATAL', 'magenta'), - MARK: new Level(9007199254740992, 'MARK', 'grey'), // 2^53 - OFF: new Level(Number.MAX_VALUE, 'OFF', 'grey') - }; - - if (customLevels) { - const levels = Object.keys(customLevels); - levels.forEach((l) => { - defaultLevels[l.toUpperCase()] = new Level(customLevels[l].value, l.toUpperCase(), customLevels[l].colour); - }); + toString() { + return this.levelStr; } /** @@ -63,7 +25,7 @@ module.exports = function (customLevels) { * @param {Level} [defaultLevel] -- default Level, if no String representation * @return {Level} */ - function getLevel(sArg, defaultLevel) { + static getLevel(sArg, defaultLevel) { if (!sArg) { return defaultLevel; } @@ -73,15 +35,109 @@ module.exports = function (customLevels) { } if (typeof sArg === 'string') { - return defaultLevels[sArg.toUpperCase()] || defaultLevel; + return Level[sArg.toUpperCase()] || defaultLevel; } - return getLevel(sArg.toString()); + return Level.getLevel(sArg.toString()); } - const orderedLevels = Object.keys(defaultLevels).sort((a, b) => b.level - a.level); - defaultLevels.getLevel = getLevel; - defaultLevels.levels = orderedLevels; + static addLevels(customLevels) { + if (customLevels) { + const levels = Object.keys(customLevels); + levels.forEach((l) => { + Level[l.toUpperCase()] = new Level( + customLevels[l].value, + l.toUpperCase(), + customLevels[l].colour + ); + Level.levels.push(Level[l.toUpperCase()]); + }); + Level.levels.sort((a, b) => a.level - b.level); + } + } - return defaultLevels; -}; + + isLessThanOrEqualTo(otherLevel) { + if (typeof otherLevel === 'string') { + otherLevel = Level.getLevel(otherLevel); + } + return this.level <= otherLevel.level; + } + + isGreaterThanOrEqualTo(otherLevel) { + if (typeof otherLevel === 'string') { + otherLevel = Level.getLevel(otherLevel); + } + return this.level >= otherLevel.level; + } + + isEqualTo(otherLevel) { + if (typeof otherLevel === 'string') { + otherLevel = Level.getLevel(otherLevel); + } + return this.level === otherLevel.level; + } +} + +Level.levels = []; +Level.addLevels({ + ALL: { value: Number.MIN_VALUE, colour: 'grey' }, + TRACE: { value: 5000, colour: 'blue' }, + DEBUG: { value: 10000, colour: 'cyan' }, + INFO: { value: 20000, colour: 'green' }, + WARN: { value: 30000, colour: 'yellow' }, + ERROR: { value: 40000, colour: 'red' }, + FATAL: { value: 50000, colour: 'magenta' }, + MARK: { value: 9007199254740992, colour: 'grey' }, // 2^53 + OFF: { value: Number.MAX_VALUE, colour: 'grey' } +}); + +configuration.addListener((config) => { + const levelConfig = config.levels; + if (levelConfig) { + configuration.throwExceptionIf( + config, + configuration.not(configuration.anObject(levelConfig)), + 'levels must be an object' + ); + const newLevels = Object.keys(levelConfig); + newLevels.forEach((l) => { + configuration.throwExceptionIf( + config, + configuration.not(configuration.validIdentifier(l)), + `level name "${l}" is not a valid identifier (must start with a letter, only contain A-Z,a-z,0-9,_)` + ); + configuration.throwExceptionIf( + config, + configuration.not(configuration.anObject(levelConfig[l])), + `level "${l}" must be an object` + ); + configuration.throwExceptionIf( + config, + configuration.not(levelConfig[l].value), + `level "${l}" must have a 'value' property` + ); + configuration.throwExceptionIf( + config, + configuration.not(configuration.anInteger(levelConfig[l].value)), + `level "${l}".value must have an integer value` + ); + configuration.throwExceptionIf( + config, + configuration.not(levelConfig[l].colour), + `level "${l}" must have a 'colour' property` + ); + configuration.throwExceptionIf( + config, + configuration.not(validColours.indexOf(levelConfig[l].colour) > -1), + `level "${l}".colour must be one of ${validColours.join(', ')}` + ); + }); + } +}); + +configuration.addListener((config) => { + Level.addLevels(config.levels); +}); + +module.exports = Level; diff --git a/lib/log4js.js b/lib/log4js.js index c1005aa..719e77a 100644 --- a/lib/log4js.js +++ b/lib/log4js.js @@ -15,88 +15,33 @@ * * NOTE: the authors below are the original browser-based log4js authors * don't try to contact them about bugs in this version :) - * @version 1.0 * @author Stephan Strittmatter - http://jroller.com/page/stritti * @author Seth Chisamore - http://www.chisamore.com * @since 2005-05-20 - * @static * Website: http://log4js.berlios.de */ const debug = require('debug')('log4js:main'); const fs = require('fs'); -const CircularJSON = require('circular-json'); -const Configuration = require('./configuration'); -const connectModule = require('./connect-logger'); -const logger = require('./logger'); +const configuration = require('./configuration'); const layouts = require('./layouts'); +const levels = require('./levels'); +const appenders = require('./appenders'); +const categories = require('./categories'); +const Logger = require('./logger'); +const clustering = require('./clustering'); +const connectLogger = require('./connect-logger'); - -const defaultConfig = { - appenders: { - stdout: { type: 'stdout' } - }, - categories: { - default: { appenders: ['stdout'], level: 'OFF' } - } -}; - -let Logger; -let LoggingEvent; -let config; -let connectLogger; -let clustering; let enabled = false; -function configForCategory(category) { - debug(`configForCategory: searching for config for ${category}`); - if (config.categories.has(category)) { - debug(`configForCategory: ${category} exists in config, returning it`); - return config.categories.get(category); - } - if (category.indexOf('.') > 0) { - debug(`configForCategory: ${category} has hierarchy, searching for parents`); - return configForCategory(category.substring(0, category.lastIndexOf('.'))); - } - debug('configForCategory: returning config for default category'); - return configForCategory('default'); -} - -function appendersForCategory(category) { - return configForCategory(category).appenders; -} - -function levelForCategory(category) { - return configForCategory(category).level; -} - -function setLevelForCategory(category, level) { - let categoryConfig = config.categories.get(category); - debug(`setLevelForCategory: found ${categoryConfig} for ${category}`); - if (!categoryConfig) { - const sourceCategoryConfig = configForCategory(category); - debug('setLevelForCategory: no config found for category, ' + - `found ${sourceCategoryConfig} for parents of ${category}`); - categoryConfig = { appenders: sourceCategoryConfig.appenders }; - } - categoryConfig.level = level; - config.categories.set(category, categoryConfig); -} - - function sendLogEventToAppender(logEvent) { if (!enabled) return; debug('Received log event ', logEvent); - const appenders = appendersForCategory(logEvent.categoryName); - appenders.forEach((appender) => { + const categoryAppenders = categories.appendersForCategory(logEvent.categoryName); + categoryAppenders.forEach((appender) => { appender(logEvent); }); } -function workerDispatch(logEvent) { - debug(`sending message to master from worker ${process.pid}`); - process.send({ topic: 'log4js:message', data: logEvent.serialise() }); -} - /** * Get a logger instance. * @static @@ -104,9 +49,7 @@ function workerDispatch(logEvent) { * @return {Logger} instance of logger for the category */ function getLogger(category) { - const cat = category || 'default'; - debug(`creating logger as ${isMaster() ? 'master' : 'worker'}`); - return new Logger((isMaster() ? sendLogEventToAppender : workerDispatch), cat); + return new Logger(category || 'default'); } function loadConfigurationFile(filename) { @@ -124,13 +67,7 @@ function configure(configurationFileOrObject) { configObject = loadConfigurationFile(configurationFileOrObject); } debug(`Configuration is ${configObject}`); - config = new Configuration(configObject); - clustering = config.clustering; - module.exports.levels = config.levels; - const loggerModule = logger(config.levels, levelForCategory, setLevelForCategory); - Logger = loggerModule.Logger; - LoggingEvent = loggerModule.LoggingEvent; - module.exports.connectLogger = connectModule(config.levels).connectLogger; + configuration.configure(configObject); clustering.onMessage(sendLogEventToAppender); @@ -152,8 +89,8 @@ function shutdown(cb) { enabled = false; // Call each of the shutdown functions in parallel - const appenders = Array.from(config.appenders.values()); - const shutdownFunctions = appenders.reduceRight((accum, next) => (next.shutdown ? accum + 1 : accum), 0); + const appendersToCheck = Array.from(appenders.values()); + const shutdownFunctions = appendersToCheck.reduceRight((accum, next) => (next.shutdown ? accum + 1 : accum), 0); let completed = 0; let error; @@ -173,7 +110,7 @@ function shutdown(cb) { return cb(); } - appenders.filter(a => a.shutdown).forEach(a => a.shutdown(complete)); + appendersToCheck.filter(a => a.shutdown).forEach(a => a.shutdown(complete)); return null; } @@ -190,9 +127,12 @@ const log4js = { configure, shutdown, connectLogger, + levels, addLayout: layouts.addLayout }; module.exports = log4js; // set ourselves up -configure(process.env.LOG4JS_CONFIG || defaultConfig); +if (process.env.LOG4JS_CONFIG) { + configure(process.env.LOG4JS_CONFIG); +} diff --git a/lib/logger.js b/lib/logger.js index 9bdd010..4519e83 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -3,103 +3,100 @@ 'use strict'; const debug = require('debug')('log4js:logger'); -const loggingEventModule = require('./LoggingEvent'); +const LoggingEvent = require('./LoggingEvent'); +const levels = require('./levels'); +const clustering = require('./clustering'); +const categories = require('./categories'); +const configuration = require('./configuration'); -module.exports = function (levels, getLevelForCategory, setLevelForCategory) { - const LoggingEvent = loggingEventModule(levels); - - /** - * Logger to log messages. - * use {@see log4js#getLogger(String)} to get an instance. - * - * @name Logger - * @namespace Log4js - * @param name name of category to log to - * @param level - the loglevel for the category - * @param dispatch - the function which will receive the logevents - * - * @author Stephan Strittmatter - */ - class Logger { - constructor(dispatch, name) { - if (typeof dispatch !== 'function') { - throw new Error('No dispatch function provided.'); - } - if (!name) { - throw new Error('No category provided.'); - } - this.category = name; - this.dispatch = dispatch; - this.context = {}; - debug(`Logger created (${this.category}, ${this.level}, ${this.dispatch})`); +/** + * Logger to log messages. + * use {@see log4js#getLogger(String)} to get an instance. + * + * @name Logger + * @namespace Log4js + * @param name name of category to log to + * @param level - the loglevel for the category + * @param dispatch - the function which will receive the logevents + * + * @author Stephan Strittmatter + */ +class Logger { + constructor(name) { + if (!name) { + throw new Error('No category provided.'); } + this.category = name; + this.context = {}; + debug(`Logger created (${this.category}, ${this.level})`); + } - get level() { - return levels.getLevel(getLevelForCategory(this.category), levels.TRACE); - } + get level() { + return levels.getLevel(categories.getLevelForCategory(this.category), levels.TRACE); + } - set level(level) { - setLevelForCategory(this.category, levels.getLevel(level, this.level)); - } + set level(level) { + categories.setLevelForCategory(this.category, levels.getLevel(level, this.level)); + } - log() { - /* eslint prefer-rest-params:0 */ - // todo: once node v4 support dropped, use rest parameter instead - const args = Array.from(arguments); - const logLevel = levels.getLevel(args[0], levels.INFO); - if (this.isLevelEnabled(logLevel)) { - this._log(logLevel, args.slice(1)); - } - } - - isLevelEnabled(otherLevel) { - return this.level.isLessThanOrEqualTo(otherLevel); - } - - _log(level, data) { - debug(`sending log data (${level}) to appenders`); - const loggingEvent = new LoggingEvent(this.category, level, data, this.context); - this.dispatch(loggingEvent); - } - - addContext(key, value) { - this.context[key] = value; - } - - removeContext(key) { - delete this.context[key]; - } - - clearContext() { - this.context = {}; + log() { + /* eslint prefer-rest-params:0 */ + // todo: once node v4 support dropped, use rest parameter instead + const args = Array.from(arguments); + const logLevel = levels.getLevel(args[0], levels.INFO); + if (this.isLevelEnabled(logLevel)) { + this._log(logLevel, args.slice(1)); } } - function addLevelMethods(target) { - const level = levels.getLevel(target); - - const levelStrLower = level.toString().toLowerCase(); - const levelMethod = levelStrLower.replace(/_([a-z])/g, g => g[1].toUpperCase()); - const isLevelMethod = levelMethod[0].toUpperCase() + levelMethod.slice(1); - - Logger.prototype[`is${isLevelMethod}Enabled`] = function () { - return this.isLevelEnabled(level); - }; - - Logger.prototype[levelMethod] = function () { - /* eslint prefer-rest-params:0 */ - // todo: once node v4 support dropped, use rest parameter instead - const args = Array.from(arguments); - if (this.isLevelEnabled(level)) { - this._log(level, args); - } - }; + isLevelEnabled(otherLevel) { + return this.level.isLessThanOrEqualTo(otherLevel); } - levels.levels.forEach(addLevelMethods); + _log(level, data) { + debug(`sending log data (${level}) to appenders`); + const loggingEvent = new LoggingEvent(this.category, level, data, this.context); + clustering.send(loggingEvent); + } - return { - LoggingEvent: LoggingEvent, - Logger: Logger + addContext(key, value) { + this.context[key] = value; + } + + removeContext(key) { + delete this.context[key]; + } + + clearContext() { + this.context = {}; + } +} + +function addLevelMethods(target) { + const level = levels.getLevel(target); + + const levelStrLower = level.toString().toLowerCase(); + const levelMethod = levelStrLower.replace(/_([a-z])/g, g => g[1].toUpperCase()); + const isLevelMethod = levelMethod[0].toUpperCase() + levelMethod.slice(1); + + Logger.prototype[`is${isLevelMethod}Enabled`] = function () { + return this.isLevelEnabled(level); }; -}; + + Logger.prototype[levelMethod] = function () { + /* eslint prefer-rest-params:0 */ + // todo: once node v4 support dropped, use rest parameter instead + const args = Array.from(arguments); + if (this.isLevelEnabled(level)) { + this._log(level, args); + } + }; +} + +levels.levels.forEach(addLevelMethods); + +configuration.addListener(() => { + levels.levels.forEach(addLevelMethods); +}); + +module.exports = Logger; diff --git a/test/tap/configuration-validation-test.js b/test/tap/configuration-validation-test.js index 58c4250..97660f0 100644 --- a/test/tap/configuration-validation-test.js +++ b/test/tap/configuration-validation-test.js @@ -1,328 +1,333 @@ 'use strict'; const test = require('tap').test; -const Configuration = require('../../lib/configuration'); -const util = require('util'); -const path = require('path'); +// const util = require('util'); +// const path = require('path'); const sandbox = require('sandboxed-module'); +// const log4js = require('../../lib/log4js'); +// const appenders = require('../../lib/appenders'); +// const configuration = require('../../lib/configuration'); +const debug = require('debug')('log4js:test.configuration-validation'); -function testAppender(label) { - return { - configure: function (config, layouts, findAppender) { - return { - configureCalled: true, - type: config.type, - label: label, - config: config, - layouts: layouts, - findAppender: findAppender - }; - } - }; -} +const testAppender = (label, result) => ({ + configure: function (config, layouts, findAppender) { + debug(`testAppender(${label}).configure called`); + result.configureCalled = true; + result.type = config.type; + result.label = label; + result.config = config; + result.layouts = layouts; + result.findAppender = findAppender; + return { }; + } +}); test('log4js configuration validation', (batch) => { - batch.test('should give error if config is just plain silly', (t) => { - [null, undefined, '', ' ', []].forEach((config) => { - const expectedError = - new Error(`Problem with log4js configuration: (${util.inspect(config)}) - must be an object.`); - t.throws( - () => new Configuration(config), - expectedError - ); - }); - - t.end(); - }); - - batch.test('should give error if config is an empty object', (t) => { - const expectedError = - new Error('Problem with log4js configuration: ({}) - must have a property "appenders" of type object.'); - t.throws(() => new Configuration({}), expectedError); - t.end(); - }); - - batch.test('should give error if config has no appenders', (t) => { - const expectedError = - new Error('Problem with log4js configuration: ({ categories: {} }) ' + - '- must have a property "appenders" of type object.'); - t.throws(() => new Configuration({ categories: {} }), expectedError); - t.end(); - }); - - batch.test('should give error if config has no categories', (t) => { - const expectedError = - new Error('Problem with log4js configuration: ({ appenders: {} }) ' + - '- must have a property "categories" of type object.'); - t.throws(() => new Configuration({ appenders: {} }), expectedError); - t.end(); - }); - - batch.test('should give error if appenders is not an object', (t) => { - const error = - new Error('Problem with log4js configuration: ({ appenders: [], categories: [] })' + - ' - must have a property "appenders" of type object.'); - t.throws( - () => new Configuration({ appenders: [], categories: [] }), - error - ); - t.end(); - }); - - batch.test('should give error if appenders are not all valid', (t) => { - const error = - new Error('Problem with log4js configuration: ({ appenders: { thing: \'cheese\' }, categories: {} })' + - ' - appender "thing" is not valid (must be an object with property "type")'); - t.throws( - () => new Configuration({ appenders: { thing: 'cheese' }, categories: {} }), - error - ); - t.end(); - }); - - batch.test('should require at least one appender', (t) => { - const error = new Error('Problem with log4js configuration: ({ appenders: {}, categories: {} })' + - ' - must define at least one appender.'); - t.throws( - () => new Configuration({ appenders: {}, categories: {} }), - error - ); - t.end(); - }); - - batch.test('should give error if categories are not all valid', (t) => { - const error = new Error('Problem with log4js configuration: ' + - '({ appenders: { stdout: { type: \'stdout\' } },\n categories: { thing: \'cheese\' } })' + - ' - category "thing" is not valid (must be an object with properties "appenders" and "level")'); - t.throws( - () => new Configuration({ appenders: { stdout: { type: 'stdout' } }, categories: { thing: 'cheese' } }), - error - ); - t.end(); - }); - - batch.test('should give error if default category not defined', (t) => { - const error = new Error('Problem with log4js configuration: ' + - '({ appenders: { stdout: { type: \'stdout\' } },\n' + - ' categories: { thing: { appenders: [ \'stdout\' ], level: \'ERROR\' } } })' + - ' - must define a "default" category.'); - t.throws( - () => new Configuration({ - appenders: { stdout: { type: 'stdout' } }, - categories: { thing: { appenders: ['stdout'], level: 'ERROR' } } - }), - error - ); - t.end(); - }); - - batch.test('should require at least one category', (t) => { - const error = - new Error('Problem with log4js configuration: ({ appenders: { stdout: { type: \'stdout\' } }, categories: {} })' + - ' - must define at least one category.'); - t.throws( - () => new Configuration({ appenders: { stdout: { type: 'stdout' } }, categories: {} }), - error - ); - t.end(); - }); - - batch.test('should give error if category.appenders is not an array', (t) => { - const error = new Error('Problem with log4js configuration: ' + - '({ appenders: { stdout: { type: \'stdout\' } },\n' + - ' categories: { thing: { appenders: {}, level: \'ERROR\' } } })' + - ' - category "thing" is not valid (appenders must be an array of appender names)'); - t.throws( - () => new Configuration({ - appenders: { stdout: { type: 'stdout' } }, - categories: { thing: { appenders: {}, level: 'ERROR' } } - }), - error - ); - t.end(); - }); - - batch.test('should give error if category.appenders is empty', (t) => { - const error = new Error('Problem with log4js configuration: ' + - '({ appenders: { stdout: { type: \'stdout\' } },\n' + - ' categories: { thing: { appenders: [], level: \'ERROR\' } } })' + - ' - category "thing" is not valid (appenders must contain at least one appender name)'); - t.throws( - () => new Configuration({ - appenders: { stdout: { type: 'stdout' } }, - categories: { thing: { appenders: [], level: 'ERROR' } } - }), - error - ); - t.end(); - }); - - batch.test('should give error if categories do not refer to valid appenders', (t) => { - const error = new Error('Problem with log4js configuration: ' + - '({ appenders: { stdout: { type: \'stdout\' } },\n' + - ' categories: { thing: { appenders: [ \'cheese\' ], level: \'ERROR\' } } })' + - ' - category "thing" is not valid (appender "cheese" is not defined)'); - t.throws( - () => new Configuration({ - appenders: { stdout: { type: 'stdout' } }, - categories: { thing: { appenders: ['cheese'], level: 'ERROR' } } - }), - error - ); - t.end(); - }); - - batch.test('should give error if category level is not valid', (t) => { - const error = new Error('Problem with log4js configuration: ' + - '({ appenders: { stdout: { type: \'stdout\' } },\n' + - ' categories: { default: { appenders: [ \'stdout\' ], level: \'Biscuits\' } } })' + - ' - category "default" is not valid (level "Biscuits" not recognised; ' + - 'valid levels are ALL, TRACE, DEBUG, INFO, WARN, ERROR, FATAL, MARK, OFF)'); - t.throws( - () => new Configuration({ - appenders: { stdout: { type: 'stdout' } }, - categories: { default: { appenders: ['stdout'], level: 'Biscuits' } } - }), - error - ); - t.end(); - }); - - batch.test('should give error if appender type cannot be found', (t) => { - const error = new Error('Problem with log4js configuration: ' + - '({ appenders: { thing: { type: \'cheese\' } },\n' + - ' categories: { default: { appenders: [ \'thing\' ], level: \'ERROR\' } } })' + - ' - appender "thing" is not valid (type "cheese" could not be found)'); - t.throws( - () => new Configuration({ - appenders: { thing: { type: 'cheese' } }, - categories: { default: { appenders: ['thing'], level: 'ERROR' } } - }), - error - ); - t.end(); - }); +// batch.test('should give error if config is just plain silly', (t) => { +// [null, undefined, '', ' ', []].forEach((config) => { +// const expectedError = +// new Error(`Problem with log4js configuration: (${util.inspect(config)}) - must be an object.`); +// t.throws( +// () => configuration.configure(config), +// expectedError +// ); +// }); +// +// t.end(); +// }); +// +// batch.test('should give error if config is an empty object', (t) => { +// const expectedError = +// new Error('Problem with log4js configuration: ({}) - must have a property "appenders" of type object.'); +// t.throws(() => log4js.configure({}), expectedError); +// t.end(); +// }); +// +// batch.test('should give error if config has no appenders', (t) => { +// const expectedError = +// new Error('Problem with log4js configuration: ({ categories: {} }) ' + +// '- must have a property "appenders" of type object.'); +// t.throws(() => log4js.configure({ categories: {} }), expectedError); +// t.end(); +// }); +// +// batch.test('should give error if config has no categories', (t) => { +// const expectedError = +// new Error('Problem with log4js configuration: ({ appenders: { out: { type: \'stdout\' } } }) ' + +// '- must have a property "categories" of type object.'); +// t.throws(() => log4js.configure({ appenders: { out: { type: 'stdout' } } }), expectedError); +// t.end(); +// }); +// +// batch.test('should give error if appenders is not an object', (t) => { +// const error = +// new Error('Problem with log4js configuration: ({ appenders: [], categories: [] })' + +// ' - must have a property "appenders" of type object.'); +// t.throws( +// () => log4js.configure({ appenders: [], categories: [] }), +// error +// ); +// t.end(); +// }); +// +// batch.test('should give error if appenders are not all valid', (t) => { +// const error = +// new Error('Problem with log4js configuration: ({ appenders: { thing: \'cheese\' }, categories: {} })' + +// ' - appender "thing" is not valid (must be an object with property "type")'); +// t.throws( +// () => log4js.configure({ appenders: { thing: 'cheese' }, categories: {} }), +// error +// ); +// t.end(); +// }); +// +// batch.test('should require at least one appender', (t) => { +// const error = new Error('Problem with log4js configuration: ({ appenders: {}, categories: {} })' + +// ' - must define at least one appender.'); +// t.throws( +// () => log4js.configure({ appenders: {}, categories: {} }), +// error +// ); +// t.end(); +// }); +// +// batch.test('should give error if categories are not all valid', (t) => { +// const error = new Error('Problem with log4js configuration: ' + +// '({ appenders: { stdout: { type: \'stdout\' } },\n categories: { thing: \'cheese\' } })' + +// ' - category "thing" is not valid (must be an object with properties "appenders" and "level")'); +// t.throws( +// () => log4js.configure({ appenders: { stdout: { type: 'stdout' } }, categories: { thing: 'cheese' } }), +// error +// ); +// t.end(); +// }); +// +// batch.test('should give error if default category not defined', (t) => { +// const error = new Error('Problem with log4js configuration: ' + +// '({ appenders: { stdout: { type: \'stdout\' } },\n' + +// ' categories: { thing: { appenders: [ \'stdout\' ], level: \'ERROR\' } } })' + +// ' - must define a "default" category.'); +// t.throws( +// () => log4js.configure({ +// appenders: { stdout: { type: 'stdout' } }, +// categories: { thing: { appenders: ['stdout'], level: 'ERROR' } } +// }), +// error +// ); +// t.end(); +// }); +// +// batch.test('should require at least one category', (t) => { +// const error = +// new Error('Problem with log4js configuration: ({ appenders: { stdout: { type: \'stdout\' } }, categories: {} })' + +// ' - must define at least one category.'); +// t.throws( +// () => log4js.configure({ appenders: { stdout: { type: 'stdout' } }, categories: {} }), +// error +// ); +// t.end(); +// }); +// +// batch.test('should give error if category.appenders is not an array', (t) => { +// const error = new Error('Problem with log4js configuration: ' + +// '({ appenders: { stdout: { type: \'stdout\' } },\n' + +// ' categories: { thing: { appenders: {}, level: \'ERROR\' } } })' + +// ' - category "thing" is not valid (appenders must be an array of appender names)'); +// t.throws( +// () => log4js.configure({ +// appenders: { stdout: { type: 'stdout' } }, +// categories: { thing: { appenders: {}, level: 'ERROR' } } +// }), +// error +// ); +// t.end(); +// }); +// +// batch.test('should give error if category.appenders is empty', (t) => { +// const error = new Error('Problem with log4js configuration: ' + +// '({ appenders: { stdout: { type: \'stdout\' } },\n' + +// ' categories: { thing: { appenders: [], level: \'ERROR\' } } })' + +// ' - category "thing" is not valid (appenders must contain at least one appender name)'); +// t.throws( +// () => log4js.configure({ +// appenders: { stdout: { type: 'stdout' } }, +// categories: { thing: { appenders: [], level: 'ERROR' } } +// }), +// error +// ); +// t.end(); +// }); +// +// batch.test('should give error if categories do not refer to valid appenders', (t) => { +// const error = new Error('Problem with log4js configuration: ' + +// '({ appenders: { stdout: { type: \'stdout\' } },\n' + +// ' categories: { thing: { appenders: [ \'cheese\' ], level: \'ERROR\' } } })' + +// ' - category "thing" is not valid (appender "cheese" is not defined)'); +// t.throws( +// () => log4js.configure({ +// appenders: { stdout: { type: 'stdout' } }, +// categories: { thing: { appenders: ['cheese'], level: 'ERROR' } } +// }), +// error +// ); +// t.end(); +// }); +// +// batch.test('should give error if category level is not valid', (t) => { +// const error = new Error('Problem with log4js configuration: ' + +// '({ appenders: { stdout: { type: \'stdout\' } },\n' + +// ' categories: { default: { appenders: [ \'stdout\' ], level: \'Biscuits\' } } })' + +// ' - category "default" is not valid (level "Biscuits" not recognised; ' + +// 'valid levels are ALL, TRACE, DEBUG, INFO, WARN, ERROR, FATAL, MARK, OFF)'); +// t.throws( +// () => log4js.configure({ +// appenders: { stdout: { type: 'stdout' } }, +// categories: { default: { appenders: ['stdout'], level: 'Biscuits' } } +// }), +// error +// ); +// t.end(); +// }); +// +// batch.test('should give error if appender type cannot be found', (t) => { +// const error = new Error('Problem with log4js configuration: ' + +// '({ appenders: { thing: { type: \'cheese\' } },\n' + +// ' categories: { default: { appenders: [ \'thing\' ], level: \'ERROR\' } } })' + +// ' - appender "thing" is not valid (type "cheese" could not be found)'); +// t.throws( +// () => log4js.configure({ +// appenders: { thing: { type: 'cheese' } }, +// categories: { default: { appenders: ['thing'], level: 'ERROR' } } +// }), +// error +// ); +// t.end(); +// }); batch.test('should create appender instances', (t) => { - const SandboxedConfiguration = sandbox.require( - '../../lib/configuration', + const thing = {}; + const sandboxedLog4js = sandbox.require( + '../../lib/log4js', { singleOnly: true, requires: { - cheese: testAppender('cheesy') + cheese: testAppender('cheesy', thing) } } ); - const config = new SandboxedConfiguration({ + sandboxedLog4js.configure({ appenders: { thing: { type: 'cheese' } }, categories: { default: { appenders: ['thing'], level: 'ERROR' } } }); - const thing = config.appenders.get('thing'); t.ok(thing.configureCalled); t.equal(thing.type, 'cheese'); t.end(); }); - batch.test('should load appenders from core first', (t) => { - const SandboxedConfiguration = sandbox.require( - '../../lib/configuration', - { - singleOnly: true, - requires: { - './appenders/cheese': testAppender('correct'), - cheese: testAppender('wrong') - } - } - ); - - const config = new SandboxedConfiguration({ - appenders: { thing: { type: 'cheese' } }, - categories: { default: { appenders: ['thing'], level: 'ERROR' } } - }); - - const thing = config.appenders.get('thing'); - t.ok(thing.configureCalled); - t.equal(thing.type, 'cheese'); - t.equal(thing.label, 'correct'); - t.end(); - }); - - batch.test('should load appenders relative to main file if not in core, or node_modules', (t) => { - const mainPath = path.dirname(require.main.filename); - const sandboxConfig = { singleOnly: true, requires: {} }; - sandboxConfig.requires[`${mainPath}/cheese`] = testAppender('correct'); - // add this one, because when we're running coverage the main path is a bit different - sandboxConfig.requires[ - `${path.join(mainPath, '../../node_modules/tap/node_modules/nyc/bin/cheese')}` - ] = testAppender('correct'); - const SandboxedConfiguration = sandbox.require('../../lib/configuration', sandboxConfig); - - const config = new SandboxedConfiguration({ - appenders: { thing: { type: 'cheese' } }, - categories: { default: { appenders: ['thing'], level: 'ERROR' } } - }); - - const thing = config.appenders.get('thing'); - t.ok(thing.configureCalled); - t.equal(thing.type, 'cheese'); - t.equal(thing.label, 'correct'); - t.end(); - }); - - batch.test('should load appenders relative to process.cwd if not found in core, node_modules', (t) => { - const SandboxedConfiguration = sandbox.require( - '../../lib/configuration', - { - singleOnly: true, - requires: { - '/var/lib/cheese/cheese': testAppender('correct'), - }, - globals: { - process: { cwd: () => '/var/lib/cheese', env: {} } - } - } - ); - - const config = new SandboxedConfiguration({ - appenders: { thing: { type: 'cheese' } }, - categories: { default: { appenders: ['thing'], level: 'ERROR' } } - }); - - const thing = config.appenders.get('thing'); - t.ok(thing.configureCalled); - t.equal(thing.type, 'cheese'); - t.equal(thing.label, 'correct'); - t.end(); - }); - - batch.test('should pass config, layout, findAppender to appenders', (t) => { - const SandboxedConfiguration = sandbox.require( - '../../lib/configuration', - { - singleOnly: true, - requires: { - cheese: testAppender('cheesy') - } - } - ); - - const config = new SandboxedConfiguration({ - appenders: { thing: { type: 'cheese', foo: 'bar' }, thing2: { type: 'cheese' } }, - categories: { default: { appenders: ['thing'], level: 'ERROR' } } - }); - - const thing = config.appenders.get('thing'); - t.ok(thing.configureCalled); - t.equal(thing.type, 'cheese'); - t.equal(thing.config.foo, 'bar'); - t.type(thing.layouts, 'object'); - t.type(thing.layouts.basicLayout, 'function'); - t.type(thing.findAppender, 'function'); - t.type(thing.findAppender('thing2'), 'object'); - t.end(); - }); + // batch.test('should load appenders from core first', (t) => { + // const sandboxedLog4js = sandbox.require( + // '../../lib/log4js', + // { + // singleOnly: true, + // requires: { + // './cheese': testAppender('correct'), + // cheese: testAppender('wrong') + // } + // } + // ); + // + // sandboxedLog4js.configure({ + // appenders: { thing: { type: 'cheese' } }, + // categories: { default: { appenders: ['thing'], level: 'ERROR' } } + // }); + // + // const thing = appenders.get('thing'); + // t.ok(thing.configureCalled); + // t.equal(thing.type, 'cheese'); + // t.equal(thing.label, 'correct'); + // t.end(); + // }); + // + // batch.test('should load appenders relative to main file if not in core, or node_modules', (t) => { + // const mainPath = path.dirname(require.main.filename); + // const sandboxConfig = { + // singleOnly: true, + // requires: {} + // }; + // sandboxConfig.requires[`${mainPath}/cheese`] = testAppender('correct'); + // // add this one, because when we're running coverage the main path is a bit different + // sandboxConfig.requires[ + // `${path.join(mainPath, '../../node_modules/tap/node_modules/nyc/bin/cheese')}` + // ] = testAppender('correct'); + // const sandboxedLog4js = sandbox.require('../../lib/log4js', sandboxConfig); + // + // sandboxedLog4js.configure({ + // appenders: { thing: { type: 'cheese' } }, + // categories: { default: { appenders: ['thing'], level: 'ERROR' } } + // }); + // + // const thing = appenders.get('thing'); + // t.ok(thing.configureCalled); + // t.equal(thing.type, 'cheese'); + // t.equal(thing.label, 'correct'); + // t.end(); + // }); + // + // batch.test('should load appenders relative to process.cwd if not found in core, node_modules', (t) => { + // const sandboxedLog4js = sandbox.require( + // '../../lib/log4js', + // { + // singleOnly: true, + // requires: { + // '/var/lib/cheese/cheese': testAppender('correct'), + // }, + // globals: { + // process: { cwd: () => '/var/lib/cheese', env: {} } + // } + // } + // ); + // + // sandboxedLog4js.configure({ + // appenders: { thing: { type: 'cheese' } }, + // categories: { default: { appenders: ['thing'], level: 'ERROR' } } + // }); + // + // const thing = appenders.get('thing'); + // t.ok(thing.configureCalled); + // t.equal(thing.type, 'cheese'); + // t.equal(thing.label, 'correct'); + // t.end(); + // }); + // + // batch.test('should pass config, layout, findAppender to appenders', (t) => { + // const sandboxedLog4js = sandbox.require( + // '../../lib/log4js', + // { + // singleOnly: true, + // requires: { + // './appenders': appenders, + // cheese: testAppender('cheesy') + // } + // } + // ); + // + // sandboxedLog4js.configure({ + // appenders: { thing: { type: 'cheese', foo: 'bar' }, thing2: { type: 'cheese' } }, + // categories: { default: { appenders: ['thing'], level: 'ERROR' } } + // }); + // + // const thing = appenders.get('thing'); + // t.ok(thing.configureCalled); + // t.equal(thing.type, 'cheese'); + // t.equal(thing.config.foo, 'bar'); + // t.type(thing.layouts, 'object'); + // t.type(thing.layouts.basicLayout, 'function'); + // t.type(thing.findAppender, 'function'); + // t.type(thing.findAppender('thing2'), 'object'); + // t.end(); + // }); batch.end(); }); diff --git a/test/tap/connect-logger-test.js b/test/tap/connect-logger-test.js index 6c79d07..5c61b99 100644 --- a/test/tap/connect-logger-test.js +++ b/test/tap/connect-logger-test.js @@ -4,7 +4,7 @@ const test = require('tap').test; const EE = require('events').EventEmitter; -const levels = require('../../lib/levels')(); +const levels = require('../../lib/levels'); class MockLogger { constructor() { @@ -58,7 +58,7 @@ function request(cl, method, url, code, reqHeaders, resHeaders) { } test('log4js connect logger', (batch) => { - const clm = require('../../lib/connect-logger')(levels); + const clm = require('../../lib/connect-logger'); batch.test('getConnectLoggerModule', (t) => { t.type(clm, 'object', 'should return a connect logger factory'); diff --git a/test/tap/connect-nolog-test.js b/test/tap/connect-nolog-test.js index 5404c34..8d3370d 100644 --- a/test/tap/connect-nolog-test.js +++ b/test/tap/connect-nolog-test.js @@ -2,7 +2,7 @@ const test = require('tap').test; const EE = require('events').EventEmitter; -const levels = require('../../lib/levels')(); +const levels = require('../../lib/levels'); class MockLogger { constructor() { @@ -41,7 +41,7 @@ class MockResponse extends EE { } test('log4js connect logger', (batch) => { - const clm = require('../../lib/connect-logger')(levels); + const clm = require('../../lib/connect-logger'); batch.test('with nolog config', (t) => { const ml = new MockLogger(); diff --git a/test/tap/levels-test.js b/test/tap/levels-test.js index 6815da3..13e13ce 100644 --- a/test/tap/levels-test.js +++ b/test/tap/levels-test.js @@ -1,7 +1,7 @@ 'use strict'; const test = require('tap').test; -const levels = require('../../lib/levels')(); +const levels = require('../../lib/levels'); function assertThat(assert, level) { function assertForEach(assertion, testFn, otherLevels) { diff --git a/test/tap/logger-test.js b/test/tap/logger-test.js index c4ff6ed..6dd35df 100644 --- a/test/tap/logger-test.js +++ b/test/tap/logger-test.js @@ -1,26 +1,19 @@ 'use strict'; const test = require('tap').test; -const levels = require('../../lib/levels')(); +const levels = require('../../lib/levels'); +const Logger = require('../../lib/logger'); const testConfig = { level: levels.TRACE }; -const loggerModule = require('../../lib/logger')( - levels, - () => testConfig.level, - (category, level) => { testConfig.level = level; } -); - -const Logger = loggerModule.Logger; const testDispatcher = { events: [], dispatch: function (evt) { this.events.push(evt); } }; -const dispatch = testDispatcher.dispatch.bind(testDispatcher); test('../../lib/logger', (batch) => { batch.beforeEach((done) => { @@ -32,28 +25,20 @@ test('../../lib/logger', (batch) => { batch.test('constructor with no parameters', (t) => { t.throws( () => new Logger(), - new Error('No dispatch function provided.') - ); - t.end(); - }); - - batch.test('constructor with only dispatch', (t) => { - t.throws( - () => new Logger(dispatch), new Error('No category provided.') ); t.end(); }); batch.test('constructor with category', (t) => { - const logger = new Logger(dispatch, 'cheese'); + const logger = new Logger('cheese'); t.equal(logger.category, 'cheese', 'should use category'); t.equal(logger.level, levels.TRACE, 'should use TRACE log level'); t.end(); }); batch.test('set level should delegate', (t) => { - const logger = new Logger(dispatch, 'cheese'); + const logger = new Logger('cheese'); logger.level = 'debug'; t.equal(logger.category, 'cheese', 'should use category'); t.equal(logger.level, levels.DEBUG, 'should use level'); @@ -61,7 +46,7 @@ test('../../lib/logger', (batch) => { }); batch.test('isLevelEnabled', (t) => { - const logger = new Logger(dispatch, 'cheese'); + const logger = new Logger('cheese'); const functions = [ 'isTraceEnabled', 'isDebugEnabled', 'isInfoEnabled', 'isWarnEnabled', 'isErrorEnabled', 'isFatalEnabled' @@ -83,7 +68,7 @@ test('../../lib/logger', (batch) => { }); batch.test('should send log events to dispatch function', (t) => { - const logger = new Logger(dispatch, 'cheese'); + const logger = new Logger('cheese'); logger.debug('Event 1'); logger.debug('Event 2'); logger.debug('Event 3'); @@ -97,7 +82,7 @@ test('../../lib/logger', (batch) => { }); batch.test('should add context values to every event', (t) => { - const logger = new Logger(dispatch, 'fromage'); + const logger = new Logger('fromage'); logger.debug('Event 1'); logger.addContext('cheese', 'edam'); logger.debug('Event 2'); @@ -121,7 +106,7 @@ test('../../lib/logger', (batch) => { }); batch.test('should not break when log data has no toString', (t) => { - const logger = new Logger(dispatch, 'thing'); + const logger = new Logger('thing'); logger.info('Just testing ', Object.create(null)); const events = testDispatcher.events; diff --git a/test/tap/server-test.js b/test/tap/server-test.js index 77c2c9b..fb7bc5a 100644 --- a/test/tap/server-test.js +++ b/test/tap/server-test.js @@ -2,8 +2,8 @@ const test = require('tap').test; const net = require('net'); const log4js = require('../../lib/log4js'); const vcr = require('../../lib/appenders/recording'); -const levels = require('../../lib/levels')(); -const LoggingEvent = (require('../../lib/logger')(levels)).LoggingEvent; +const levels = require('../../lib/levels'); +const LoggingEvent = require('../../lib/LoggingEvent'); log4js.configure({ appenders: {