From 5283782ba08838e48feaa5a200ca7d3121b366ad Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sun, 5 Feb 2017 10:05:49 +1100 Subject: [PATCH 01/13] refactor(all): basic logging to stdout works again - removed config reloading - removed console replacement - added recording appender - added config validation - changed config format --- lib/appenders/recording.js | 23 + lib/appenders/stdout.js | 8 +- lib/configuration.js | 135 ++++++ lib/levels.js | 12 + lib/log4js.js | 486 +++------------------ lib/logger.js | 66 ++- test/tap/configuration-test.js | 99 +---- test/tap/configuration-validation-test.js | 300 +++++++++++++ test/tap/logging-test.js | 487 ++-------------------- test/tap/reload-shutdown-test.js | 34 -- test/tap/reloadConfiguration-test.js | 350 ---------------- test/tap/stdoutAppender-test.js | 2 +- 12 files changed, 582 insertions(+), 1420 deletions(-) create mode 100644 lib/appenders/recording.js create mode 100644 lib/configuration.js create mode 100644 test/tap/configuration-validation-test.js delete mode 100644 test/tap/reload-shutdown-test.js delete mode 100644 test/tap/reloadConfiguration-test.js diff --git a/lib/appenders/recording.js b/lib/appenders/recording.js new file mode 100644 index 00000000..da666072 --- /dev/null +++ b/lib/appenders/recording.js @@ -0,0 +1,23 @@ +'use strict'; + +let recordedEvents = []; + +function configure() { + return function (logEvent) { + recordedEvents.push(logEvent); + }; +} + +function replay() { + return recordedEvents; +} + +function reset() { + recordedEvents = []; +} + +module.exports = { + configure: configure, + replay: replay, + reset: reset +}; diff --git a/lib/appenders/stdout.js b/lib/appenders/stdout.js index 124ac974..437741a5 100644 --- a/lib/appenders/stdout.js +++ b/lib/appenders/stdout.js @@ -1,21 +1,17 @@ 'use strict'; -const layouts = require('../layouts'); - function stdoutAppender(layout, timezoneOffset) { - layout = layout || layouts.colouredLayout; return function (loggingEvent) { process.stdout.write(`${layout(loggingEvent, timezoneOffset)}\n`); }; } -function configure(config) { - let layout; +function configure(config, layouts) { + let layout = layouts.colouredLayout; if (config.layout) { layout = layouts.layout(config.layout.type, config.layout); } return stdoutAppender(layout, config.timezoneOffset); } -exports.appender = stdoutAppender; exports.configure = configure; diff --git a/lib/configuration.js b/lib/configuration.js new file mode 100644 index 00000000..915c1542 --- /dev/null +++ b/lib/configuration.js @@ -0,0 +1,135 @@ +'use strict'; + +const util = require('util'); +const levels = require('./levels'); +const layouts = require('./layouts'); + +function not(thing) { + return !thing; +} + +function anObject(thing) { + return thing && typeof thing === 'object' && !Array.isArray(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}` + ); + } + }); + } + + tryLoading(path) { + try { + return require(path); //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; + } + } + + loadAppenderModule(type) { + return this.tryLoading(`./appenders/${type}`) || this.tryLoading(type); + } + + 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)` + ); + return appenderModule.configure(config, layouts, this.configuredAppenders.get.bind(this.configuredAppenders)); + } + + 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")` + ); + + 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(levels.toLevel(category.level)), + `category "${name}" is not valid (level "${category.level}" not recognised;` + + ` valid levels are ${levels.levels.join(', ')})` + ); + + this.configuredCategories.set(name, { appenders: appenders, level: levels.toLevel(category.level) }); + }); + + this.throwExceptionIf(not(categoryConfig.default), 'must define a "default" category.'); + } + + 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.appenders = candidate.appenders; + this.categories = candidate.categories; + } +} + +module.exports = Configuration; diff --git a/lib/levels.js b/lib/levels.js index 2d981acf..e5330eee 100644 --- a/lib/levels.js +++ b/lib/levels.js @@ -83,3 +83,15 @@ module.exports = { Level: Level, getLevel: getLevel }; + +module.exports.levels = [ + module.exports.ALL, + module.exports.TRACE, + module.exports.DEBUG, + module.exports.INFO, + module.exports.WARN, + module.exports.ERROR, + module.exports.FATAL, + module.exports.MARK, + module.exports.OFF +]; diff --git a/lib/log4js.js b/lib/log4js.js index ae6c8ca4..55a43183 100644 --- a/lib/log4js.js +++ b/lib/log4js.js @@ -1,23 +1,13 @@ -/* eslint no-prototype-builtins:1,no-restricted-syntax:[1, "ForInStatement"],no-plusplus:0 */ - 'use strict'; /** * @fileoverview log4js is a library to log in JavaScript in similar manner - * than in log4j for Java. The API should be nearly the same. + * than in log4j for Java (but not really). * *

Example:

*
- *  let logging = require('log4js');
- *  //add an appender that logs all messages to stdout.
- *  logging.addAppender(logging.consoleAppender());
- *  //add an appender that logs 'some-category' to a file
- *  logging.addAppender(logging.fileAppender('file.log'), 'some-category');
- *  //get a logger
- *  let log = logging.getLogger('some-category');
- *  log.setLevel(logging.levels.TRACE); //set the Level
- *
- *  ...
+ *  const logging = require('log4js');
+ *  const log = logging.getLogger('some-category');
  *
  *  //call the log
  *  log.trace('trace me' );
@@ -33,412 +23,75 @@
  * Website: http://log4js.berlios.de
  */
 const fs = require('fs');
-const util = require('util');
-const layouts = require('./layouts');
+const Configuration = require('./configuration');
 const levels = require('./levels');
-const loggerModule = require('./logger');
+const Logger = require('./logger').Logger;
 const connectLogger = require('./connect-logger').connectLogger;
 
-const Logger = loggerModule.Logger;
-
-const ALL_CATEGORIES = '[all]';
-const loggers = {};
-const appenderMakers = {};
-const appenderShutdowns = {};
 const defaultConfig = {
-  appenders: [
-    { type: 'stdout' }
-  ],
-  replaceConsole: false
-};
-
-let appenders = {};
-
-function hasLogger(logger) {
-  return loggers.hasOwnProperty(logger);
-}
-
-// todo: this method should be moved back to levels.js, but for loop require, need some refactor
-levels.forName = function (levelStr, levelVal) {
-  let level;
-  if (typeof levelStr === 'string' && typeof levelVal === 'number') {
-    const levelUpper = levelStr.toUpperCase();
-    level = new levels.Level(levelVal, levelUpper);
-    loggerModule.addLevelMethods(level);
+  appenders: {
+    STDOUT: { type: 'stdout' }
+  },
+  categories: {
+    default: { appenders: ['STDOUT'], level: 'TRACE' }
   }
-  return level;
 };
 
-function getBufferedLogger(categoryName) {
-  const baseLogger = getLogger(categoryName);
-  const logger = {};
-  logger.temp = [];
-  logger.target = baseLogger;
-  logger.flush = function () {
-    for (let i = 0; i < logger.temp.length; i++) {
-      const log = logger.temp[i];
-      logger.target[log.level](log.message);
-      delete logger.temp[i];
-    }
-  };
-  logger.trace = function (message) {
-    logger.temp.push({ level: 'trace', message: message });
-  };
-  logger.debug = function (message) {
-    logger.temp.push({ level: 'debug', message: message });
-  };
-  logger.info = function (message) {
-    logger.temp.push({ level: 'info', message: message });
-  };
-  logger.warn = function (message) {
-    logger.temp.push({ level: 'warn', message: message });
-  };
-  logger.error = function (message) {
-    logger.temp.push({ level: 'error', message: message });
-  };
-  logger.fatal = function (message) {
-    logger.temp.push({ level: 'fatal', message: message });
-  };
-
-  return logger;
-}
-
-function normalizeCategory(category) {
-  return `${category}.`;
-}
-
-function doesLevelEntryContainsLogger(levelCategory, loggerCategory) {
-  const normalizedLevelCategory = normalizeCategory(levelCategory);
-  const normalizedLoggerCategory = normalizeCategory(loggerCategory);
-  return normalizedLoggerCategory.substring(0, normalizedLevelCategory.length) === normalizedLevelCategory;
-}
-
-function doesAppenderContainsLogger(appenderCategory, loggerCategory) {
-  const normalizedAppenderCategory = normalizeCategory(appenderCategory);
-  const normalizedLoggerCategory = normalizeCategory(loggerCategory);
-  return normalizedLoggerCategory.substring(0, normalizedAppenderCategory.length) === normalizedAppenderCategory;
-}
-
-/**
- * Get a logger instance. Instance is cached on categoryName level.
- * @static
- * @param loggerCategoryName
- * @return {Logger} instance of logger for the category
- */
-function getLogger(loggerCategoryName) {
-  // Use default logger if categoryName is not specified or invalid
-  if (typeof loggerCategoryName !== 'string') {
-    loggerCategoryName = Logger.DEFAULT_CATEGORY;
-  }
-
-  if (!hasLogger(loggerCategoryName)) {
-    let level;
-
-    /* jshint -W073 */
-    // If there's a 'levels' entry in the configuration
-    if (levels.config) {
-      // Goes through the categories in the levels configuration entry,
-      // starting with the 'higher' ones.
-      const keys = Object.keys(levels.config).sort();
-      for (let idx = 0; idx < keys.length; idx++) {
-        const levelCategory = keys[idx];
-        if (doesLevelEntryContainsLogger(levelCategory, loggerCategoryName)) {
-          // level for the logger
-          level = levels.config[levelCategory];
-        }
-      }
-    }
-    /* jshint +W073 */
-
-    // Create the logger for this name if it doesn't already exist
-    loggers[loggerCategoryName] = new Logger(loggerCategoryName, level);
-
-    /* jshint -W083 */
-    let appenderList;
-    for (const appenderCategory in appenders) {
-      if (doesAppenderContainsLogger(appenderCategory, loggerCategoryName)) {
-        appenderList = appenders[appenderCategory];
-        appenderList.forEach((appender) => {
-          loggers[loggerCategoryName].addListener('log', appender);
-        });
-      }
-    }
-    /* jshint +W083 */
+let config;
+let enabled = true;
 
-    if (appenders[ALL_CATEGORIES]) {
-      appenderList = appenders[ALL_CATEGORIES];
-      appenderList.forEach((appender) => {
-        loggers[loggerCategoryName].addListener('log', appender);
-      });
-    }
+function configForCategory(category) {
+  if (config.categories.has(category)) {
+    return config.categories.get(category);
   }
-
-  return loggers[loggerCategoryName];
-}
-
-/**
- * args are appender, optional shutdown function, then zero or more categories
- */
-function addAppender() {
-  /* eslint prefer-rest-params:0 */
-  // todo: once node v4 support dropped, use rest parameter instead
-  let args = Array.from(arguments);
-  const appender = args.shift();
-  // check for a shutdown fn
-  if (args.length > 0 && typeof args[0] === 'function') {
-    appenderShutdowns[appender] = args.shift();
+  if (category.indexOf('.') > 0) {
+    return configForCategory(category.substring(0, category.lastIndexOf('.')));
   }
-
-  if (args.length === 0 || args[0] === undefined) {
-    args = [ALL_CATEGORIES];
-  }
-  // argument may already be an array
-  if (Array.isArray(args[0])) {
-    args = args[0];
-  }
-
-  args.forEach((appenderCategory) => {
-    addAppenderToCategory(appender, appenderCategory);
-
-    if (appenderCategory === ALL_CATEGORIES) {
-      addAppenderToAllLoggers(appender);
-    } else {
-      for (const loggerCategory in loggers) {
-        if (doesAppenderContainsLogger(appenderCategory, loggerCategory)) {
-          loggers[loggerCategory].addListener('log', appender);
-        }
-      }
-    }
-  });
+  return configForCategory('default');
 }
 
-function addAppenderToAllLoggers(appender) {
-  for (const logger in loggers) {
-    if (hasLogger(logger)) {
-      loggers[logger].addListener('log', appender);
-    }
-  }
-}
-
-function addAppenderToCategory(appender, category) {
-  if (!appenders[category]) {
-    appenders[category] = [];
-  }
-  appenders[category].push(appender);
-}
-
-function clearAppenders() {
-  // if we're calling clearAppenders, we're probably getting ready to write
-  // so turn log writes back on, just in case this is after a shutdown
-  loggerModule.enableAllLogWrites();
-  appenders = {};
-  for (const logger in loggers) {
-    if (hasLogger(logger)) {
-      loggers[logger].removeAllListeners('log');
-    }
-  }
-}
-
-function configureAppenders(appenderList, options) {
-  clearAppenders();
-  if (appenderList) {
-    appenderList.forEach((appenderConfig) => {
-      loadAppender(appenderConfig.type);
-      let appender;
-      appenderConfig.makers = appenderMakers;
-      try {
-        appender = appenderMakers[appenderConfig.type](appenderConfig, options);
-        addAppender(appender, appenderConfig.category);
-      } catch (e) {
-        throw new Error(`log4js configuration problem for ${util.inspect(appenderConfig)}`, e);
-      }
-    });
-  }
+function appendersForCategory(category) {
+  return configForCategory(category).appenders;
 }
 
-function configureLevels(_levels) {
-  levels.config = _levels; // Keep it so we can create loggers later using this cfg
-  if (_levels) {
-    const keys = Object.keys(levels.config).sort();
-
-    /* eslint-disable guard-for-in */
-    for (const idx in keys) {
-      const category = keys[idx];
-      if (category === ALL_CATEGORIES) {
-        setGlobalLogLevel(_levels[category]);
-      }
-
-      for (const loggerCategory in loggers) {
-        if (doesLevelEntryContainsLogger(category, loggerCategory)) {
-          loggers[loggerCategory].setLevel(_levels[category]);
-        }
-      }
-    }
-  }
+function levelForCategory(category) {
+  return configForCategory(category).level;
 }
 
-function setGlobalLogLevel(level) {
-  Logger.prototype.level = levels.toLevel(level, levels.TRACE);
+function sendLogEventToAppender(logEvent) {
+  if (!enabled) return;
+  const appenders = appendersForCategory(logEvent.categoryName);
+  appenders.forEach((appender) => {
+    appender(logEvent);
+  });
 }
 
 /**
- * Get the default logger instance.
- * @return {Logger} instance of default logger
+ * Get a logger instance.
  * @static
+ * @param loggerCategoryName
+ * @return {Logger} instance of logger for the category
  */
-function getDefaultLogger() {
-  return getLogger(Logger.DEFAULT_CATEGORY);
+function getLogger(category) {
+  const cat = category || 'default';
+  return new Logger(cat, levelForCategory(cat), sendLogEventToAppender);
 }
 
-const configState = {};
-
 function loadConfigurationFile(filename) {
   if (filename) {
     return JSON.parse(fs.readFileSync(filename, 'utf8'));
   }
-  return undefined;
-}
-
-function configureOnceOff(config, options) {
-  if (config) {
-    try {
-      restoreConsole();
-      configureLevels(config.levels);
-      configureAppenders(config.appenders, options);
-
-      if (config.replaceConsole) {
-        replaceConsole();
-      }
-    } catch (e) {
-      throw new Error(
-        `Problem reading log4js config ${util.inspect(config)}. Error was '${e.message}' (${e.stack})`
-      );
-    }
-  }
-}
-
-function reloadConfiguration(options) {
-  const mtime = getMTime(configState.filename);
-  if (!mtime) return;
-
-  if (configState.lastMTime && (mtime.getTime() > configState.lastMTime.getTime())) {
-    configureOnceOff(loadConfigurationFile(configState.filename), options);
-  }
-  configState.lastMTime = mtime;
-}
-
-function getMTime(filename) {
-  let mtime;
-  try {
-    mtime = fs.statSync(configState.filename).mtime;
-  } catch (e) {
-    getLogger('log4js').warn(`Failed to load configuration file ${filename}`);
-  }
-  return mtime;
-}
-
-function initReloadConfiguration(filename, options) {
-  if (configState.timerId) {
-    clearInterval(configState.timerId);
-    delete configState.timerId;
-  }
-  configState.filename = filename;
-  configState.lastMTime = getMTime(filename);
-  configState.timerId = setInterval(reloadConfiguration, options.reloadSecs * 1000, options);
-}
-
-function configure(configurationFileOrObject, options) {
-  let config = configurationFileOrObject;
-  config = config || process.env.LOG4JS_CONFIG;
-  options = options || {};
-
-  if (config === undefined || config === null || typeof config === 'string') {
-    if (options.reloadSecs) {
-      initReloadConfiguration(config, options);
-    }
-    config = loadConfigurationFile(config) || defaultConfig;
-  } else {
-    if (options.reloadSecs) { // eslint-disable-line
-      getLogger('log4js').warn(
-        'Ignoring configuration reload parameter for "object" configuration.'
-      );
-    }
-  }
-  configureOnceOff(config, options);
-}
-
-const originalConsoleFunctions = {
-  log: console.log,
-  debug: console.debug,
-  info: console.info,
-  warn: console.warn,
-  error: console.error
-};
-
-function replaceConsole(logger) {
-  function replaceWith(fn) {
-    return function () {
-      /* eslint prefer-rest-params:0 */
-      // todo: once node v4 support dropped, use rest parameter instead
-      fn.apply(logger, Array.from(arguments));
-    };
-  }
-
-  logger = logger || getLogger('console');
-
-  ['log', 'debug', 'info', 'warn', 'error'].forEach((item) => {
-    console[item] = replaceWith(item === 'log' ? logger.info : logger[item]);
-  });
+  return filename;
 }
 
-function restoreConsole() {
-  ['log', 'debug', 'info', 'warn', 'error'].forEach((item) => {
-    console[item] = originalConsoleFunctions[item];
-  });
-}
+function configure(configurationFileOrObject) {
+  let configObject = configurationFileOrObject;
 
-/* eslint global-require:0 */
-/**
- * Load an appenderModule based on the provided appender filepath. Will first
- * check if the appender path is a subpath of the log4js 'lib/appenders' directory.
- * If not, it will attempt to load the the appender as complete path.
- *
- * @param {string} appender The filepath for the appender.
- * @returns {Object|null} The required appender or null if appender could not be loaded.
- * @private
- */
-function requireAppender(appender) {
-  let appenderModule;
-  try {
-    appenderModule = require(`./appenders/${appender}`); // eslint-disable-line
-  } catch (e) {
-    appenderModule = require(appender); // eslint-disable-line
+  if (typeof configObject === 'string') {
+    configObject = loadConfigurationFile(configurationFileOrObject);
   }
-  return appenderModule;
-}
-
-/**
- * Load an appender. Provided the appender path to be loaded. If appenderModule is defined,
- * it will be used in place of requiring the appender module.
- *
- * @param {string} appender The path to the appender module.
- * @param {Object|void} [appenderModule] The pre-required appender module. When provided,
- * instead of requiring the appender by its path, this object will be used.
- * @returns {void}
- * @private
- */
-function loadAppender(appender, appenderModule) {
-  appenderModule = appenderModule || requireAppender(appender);
-
-  if (!appenderModule) {
-    throw new Error(`Invalid log4js appender: ${util.inspect(appender)}`);
-  }
-
-  log4js.appenders[appender] = appenderModule.appender.bind(appenderModule);
-  if (appenderModule.shutdown) {
-    appenderShutdowns[appender] = appenderModule.shutdown.bind(appenderModule);
-  }
-  appenderMakers[appender] = appenderModule.configure.bind(appenderModule);
+  config = new Configuration(configObject);
+  enabled = true;
 }
 
 /**
@@ -452,39 +105,27 @@ function loadAppender(appender, appenderModule) {
 function shutdown(cb) {
   // First, disable all writing to appenders. This prevents appenders from
   // not being able to be drained because of run-away log writes.
-  loggerModule.disableAllLogWrites();
-
-  // turn off config reloading
-  if (configState.timerId) {
-    clearInterval(configState.timerId);
-  }
+  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);
   let completed = 0;
   let error;
-  const shutdownFunctions = [];
 
   function complete(err) {
     error = error || err;
-    completed++;
-    if (completed >= shutdownFunctions.length) {
+    completed += 1;
+    if (completed >= shutdownFunctions) {
       cb(error);
     }
   }
 
-  for (const category in appenderShutdowns) {
-    if (appenderShutdowns.hasOwnProperty(category)) {
-      shutdownFunctions.push(appenderShutdowns[category]);
-    }
-  }
-
-  if (!shutdownFunctions.length) {
+  if (shutdownFunctions === 0) {
     return cb();
   }
 
-  shutdownFunctions.forEach((shutdownFct) => {
-    shutdownFct(complete);
-  });
+  appenders.forEach(a => a.shutdown(complete));
 
   return null;
 }
@@ -492,49 +133,20 @@ function shutdown(cb) {
 /**
  * @name log4js
  * @namespace Log4js
- * @property getBufferedLogger
  * @property getLogger
- * @property getDefaultLogger
- * @property hasLogger
- * @property addAppender
- * @property loadAppender
- * @property clearAppenders
  * @property configure
  * @property shutdown
- * @property replaceConsole
- * @property restoreConsole
  * @property levels
- * @property setGlobalLogLevel
- * @property layouts
- * @property appenders
- * @property appenderMakers
- * @property connectLogger
  */
 const log4js = {
-  getBufferedLogger,
   getLogger,
-  getDefaultLogger,
-  hasLogger,
-
-  addAppender,
-  loadAppender,
-  clearAppenders,
   configure,
   shutdown,
-
-  replaceConsole,
-  restoreConsole,
-
   levels,
-  setGlobalLogLevel,
-
-  layouts,
-  appenders: {},
-  appenderMakers,
   connectLogger
 };
 
 module.exports = log4js;
 
 // set ourselves up
-configure();
+configure(process.env.LOG4JS_CONFIG || defaultConfig);
diff --git a/lib/logger.js b/lib/logger.js
index 1da0cae3..4955b269 100644
--- a/lib/logger.js
+++ b/lib/logger.js
@@ -3,11 +3,8 @@
 'use strict';
 
 const levels = require('./levels');
-const EventEmitter = require('events');
 
-const DEFAULT_CATEGORY = '[default]';
-
-let logWritesEnabled = true;
+// let logWritesEnabled = true;
 
 /**
  * @name LoggingEvent
@@ -20,15 +17,13 @@ class LoggingEvent {
    * @param {String} categoryName name of category
    * @param {Log4js.Level} level level of message
    * @param {Array} data objects to log
-   * @param {Logger} logger the associated logger
    * @author Seth Chisamore
    */
-  constructor(categoryName, level, data, logger) {
+  constructor(categoryName, level, data) {
     this.startTime = new Date();
     this.categoryName = categoryName;
     this.data = data;
     this.level = level;
-    this.logger = logger;
   }
 }
 
@@ -39,38 +34,30 @@ class LoggingEvent {
  * @name Logger
  * @namespace Log4js
  * @param name name of category to log to
- * @param level
+ * @param level - the loglevel for the category
+ * @param dispatch - the function which will receive the logevents
  *
  * @author Stephan Strittmatter
  */
-class Logger extends EventEmitter {
-  constructor(name, level) {
-    super();
-
-    this.category = name || DEFAULT_CATEGORY;
-
-    if (level) {
-      this.setLevel(level);
-    }
+class Logger {
+  constructor(name, level, dispatch) {
+    this.category = name;
+    this.level = levels.toLevel(level, levels.TRACE);
+    this.dispatch = dispatch;
   }
 
   setLevel(level) {
     this.level = levels.toLevel(level, this.level || levels.TRACE);
   }
 
-  removeLevel() {
-    delete 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.toLevel(args[0], levels.INFO);
-    if (!this.isLevelEnabled(logLevel)) {
-      return;
+    if (this.isLevelEnabled(logLevel)) {
+      this._log(logLevel, args.slice(1));
     }
-    this._log(logLevel, args.slice(1));
   }
 
   isLevelEnabled(otherLevel) {
@@ -78,16 +65,11 @@ class Logger extends EventEmitter {
   }
 
   _log(level, data) {
-    const loggingEvent = new LoggingEvent(this.category, level, data, this);
-    this.emit('log', loggingEvent);
+    const loggingEvent = new LoggingEvent(this.category, level, data);
+    this.dispatch(loggingEvent);
   }
 }
 
-Logger.DEFAULT_CATEGORY = DEFAULT_CATEGORY;
-Logger.prototype.level = levels.TRACE;
-
-['Trace', 'Debug', 'Info', 'Warn', 'Error', 'Fatal', 'Mark'].forEach(addLevelMethods);
-
 function addLevelMethods(target) {
   const level = levels.toLevel(target);
 
@@ -103,30 +85,32 @@ function addLevelMethods(target) {
     /* eslint prefer-rest-params:0 */
     // todo: once node v4 support dropped, use rest parameter instead
     const args = Array.from(arguments);
-    if (logWritesEnabled && this.isLevelEnabled(level)) {
+    if (/* logWritesEnabled &&*/ this.isLevelEnabled(level)) {
       this._log(level, args);
     }
   };
 }
 
+levels.levels.forEach(addLevelMethods);
+
 /**
  * Disable all log writes.
  * @returns {void}
  */
-function disableAllLogWrites() {
-  logWritesEnabled = false;
-}
+// function disableAllLogWrites() {
+//   logWritesEnabled = false;
+// }
 
 /**
  * Enable log writes.
  * @returns {void}
  */
-function enableAllLogWrites() {
-  logWritesEnabled = true;
-}
+// function enableAllLogWrites() {
+//   logWritesEnabled = true;
+// }
 
 module.exports.LoggingEvent = LoggingEvent;
 module.exports.Logger = Logger;
-module.exports.disableAllLogWrites = disableAllLogWrites;
-module.exports.enableAllLogWrites = enableAllLogWrites;
-module.exports.addLevelMethods = addLevelMethods;
+// module.exports.disableAllLogWrites = disableAllLogWrites;
+// module.exports.enableAllLogWrites = enableAllLogWrites;
+// module.exports.addLevelMethods = addLevelMethods;
diff --git a/test/tap/configuration-test.js b/test/tap/configuration-test.js
index 9c84ebf2..0e449078 100644
--- a/test/tap/configuration-test.js
+++ b/test/tap/configuration-test.js
@@ -3,100 +3,7 @@
 const test = require('tap').test;
 const sandbox = require('sandboxed-module');
 
-function makeTestAppender() {
-  return {
-    configure: function (config, options) {
-      this.configureCalled = true;
-      this.config = config;
-      this.options = options;
-      return this.appender();
-    },
-    appender: function () {
-      const self = this;
-      return function (logEvt) {
-        self.logEvt = logEvt;
-      };
-    }
-  };
-}
-
 test('log4js configure', (batch) => {
-  batch.test('when appenders specified by type', (t) => {
-    const testAppender = makeTestAppender();
-    const log4js = sandbox.require(
-      '../../lib/log4js',
-      {
-        singleOnly: true,
-        requires: {
-          './appenders/cheese': testAppender
-        }
-      }
-    );
-
-    log4js.configure(
-      {
-        appenders: [
-          { type: 'cheese', flavour: 'gouda' }
-        ]
-      },
-      { pants: 'yes' }
-    );
-    t.ok(testAppender.configureCalled, 'should load appender');
-    t.equal(testAppender.config.flavour, 'gouda', 'should pass config to appender');
-    t.equal(testAppender.options.pants, 'yes', 'should pass log4js options to appender');
-    t.end();
-  });
-
-  batch.test('when core appender loaded via loadAppender', (t) => {
-    const testAppender = makeTestAppender();
-    const log4js = sandbox.require(
-      '../../lib/log4js',
-      {
-        singleOnly: true,
-        requires: { './appenders/cheese': testAppender }
-      }
-    );
-
-    log4js.loadAppender('cheese');
-
-    t.ok(log4js.appenders.cheese, 'should load appender from ../../lib/appenders');
-    t.type(log4js.appenderMakers.cheese, 'function', 'should add appender configure function to appenderMakers');
-    t.end();
-  });
-
-  batch.test('when appender in node_modules loaded via loadAppender', (t) => {
-    const testAppender = makeTestAppender();
-    const log4js = sandbox.require(
-      '../../lib/log4js',
-      {
-        singleOnly: true,
-        requires: { 'some/other/external': testAppender }
-      }
-    );
-
-    log4js.loadAppender('some/other/external');
-    t.ok(log4js.appenders['some/other/external'], 'should load appender via require');
-    t.type(
-      log4js.appenderMakers['some/other/external'], 'function',
-      'should add appender configure function to appenderMakers'
-    );
-    t.end();
-  });
-
-  batch.test('when appender object loaded via loadAppender', (t) => {
-    const testAppender = makeTestAppender();
-    const log4js = sandbox.require('../../lib/log4js');
-
-    log4js.loadAppender('some/other/external', testAppender);
-
-    t.ok(log4js.appenders['some/other/external'], 'should load appender with provided object');
-    t.type(
-      log4js.appenderMakers['some/other/external'], 'function',
-      'should add appender configure function to appenderMakers'
-    );
-    t.end();
-  });
-
   batch.test('when configuration file loaded via LOG4JS_CONFIG env variable', (t) => {
     process.env.LOG4JS_CONFIG = 'some/path/to/mylog4js.json';
     let fileRead = 0;
@@ -106,8 +13,10 @@ test('log4js configure', (batch) => {
 
     const fakeFS = {
       config: {
-        appenders: [{ type: 'console', layout: { type: 'messagePassThrough' } }],
-        levels: { 'a-test': 'INFO' }
+        appenders: {
+          console: { type: 'console', layout: { type: 'messagePassThrough' } }
+        },
+        categories: { default: { appenders: ['console'], level: 'INFO' } }
       },
       readdirSync: function (dir) {
         return require('fs').readdirSync(dir);
diff --git a/test/tap/configuration-validation-test.js b/test/tap/configuration-validation-test.js
new file mode 100644
index 00000000..4514b148
--- /dev/null
+++ b/test/tap/configuration-validation-test.js
@@ -0,0 +1,300 @@
+'use strict';
+
+const test = require('tap').test;
+const Configuration = require('../../lib/configuration');
+const util = require('util');
+const sandbox = require('sandboxed-module');
+
+function testAppender(label) {
+  return {
+    configure: function (config, layouts, findAppender) {
+      return {
+        configureCalled: true,
+        type: config.type,
+        label: label,
+        config: config,
+        layouts: layouts,
+        findAppender: findAppender
+      };
+    }
+  };
+}
+
+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 create appender instances', (t) => {
+    const SandboxedConfiguration = sandbox.require(
+      '../../lib/configuration',
+      {
+        singleOnly: true,
+        requires: {
+          cheese: testAppender('cheesy')
+        }
+      }
+    );
+
+    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.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 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.end();
+});
diff --git a/test/tap/logging-test.js b/test/tap/logging-test.js
index 42321aaf..f9d61227 100644
--- a/test/tap/logging-test.js
+++ b/test/tap/logging-test.js
@@ -2,82 +2,16 @@
 
 const test = require('tap').test;
 const sandbox = require('sandboxed-module');
-
-function setupConsoleTest() {
-  const fakeConsole = {};
-  const logEvents = [];
-
-  ['trace', 'debug', 'log', 'info', 'warn', 'error'].forEach((fn) => {
-    fakeConsole[fn] = function () {
-      throw new Error('this should not be called.');
-    };
-  });
-
-  const log4js = sandbox.require(
-    '../../lib/log4js',
-    {
-      globals: {
-        console: fakeConsole
-      }
-    }
-  );
-
-  log4js.clearAppenders();
-  log4js.addAppender((evt) => {
-    logEvents.push(evt);
-  });
-
-  return { log4js: log4js, logEvents: logEvents, fakeConsole: fakeConsole };
-}
+const recording = require('../../lib/appenders/recording');
 
 test('log4js', (batch) => {
-  batch.test('getBufferedLogger', (t) => {
-    const log4js = require('../../lib/log4js');
-    log4js.clearAppenders();
-    const logger = log4js.getBufferedLogger('tests');
-
-    t.test('should take a category and return a logger', (assert) => {
-      assert.equal(logger.target.category, 'tests');
-      assert.type(logger.flush, 'function');
-      assert.type(logger.trace, 'function');
-      assert.type(logger.debug, 'function');
-      assert.type(logger.info, 'function');
-      assert.type(logger.warn, 'function');
-      assert.type(logger.error, 'function');
-      assert.type(logger.fatal, 'function');
-      assert.end();
-    });
-
-    t.test('cache events', (assert) => {
-      const events = [];
-      logger.target.setLevel('TRACE');
-      logger.target.addListener('log', (logEvent) => {
-        events.push(logEvent);
-      });
-      logger.debug('Debug event');
-      logger.trace('Trace event 1');
-      logger.trace('Trace event 2');
-      logger.warn('Warning event');
-      logger.error('Aargh!', new Error('Pants are on fire!'));
-      logger.error(
-        'Simulated CouchDB problem',
-        { err: 127, cause: 'incendiary underwear' }
-      );
-
-      assert.equal(events.length, 0, 'should not emit log events if .flush() is not called.');
-      logger.flush();
-      assert.equal(events.length, 6, 'should emit log events when .flush() is called.');
-      assert.end();
-    });
-    t.end();
-  });
-
-
   batch.test('getLogger', (t) => {
     const log4js = require('../../lib/log4js');
-    log4js.clearAppenders();
+    log4js.configure({
+      appenders: { recorder: { type: 'recording' } },
+      categories: { default: { appenders: ['recorder'], level: 'DEBUG' } }
+    });
     const logger = log4js.getLogger('tests');
-    logger.setLevel('DEBUG');
 
     t.test('should take a category and return a logger', (assert) => {
       assert.equal(logger.category, 'tests');
@@ -91,10 +25,8 @@ test('log4js', (batch) => {
     });
 
     t.test('log events', (assert) => {
-      const events = [];
-      logger.addListener('log', (logEvent) => {
-        events.push(logEvent);
-      });
+      recording.reset();
+
       logger.debug('Debug event');
       logger.trace('Trace event 1');
       logger.trace('Trace event 2');
@@ -102,6 +34,8 @@ test('log4js', (batch) => {
       logger.error('Aargh!', new Error('Pants are on fire!'));
       logger.error('Simulated CouchDB problem', { err: 127, cause: 'incendiary underwear' });
 
+      const events = recording.replay();
+
       assert.equal(events[0].level.toString(), 'DEBUG');
       assert.equal(events[0].data[0], 'Debug event');
       assert.type(events[0].startTime, 'Date');
@@ -128,15 +62,16 @@ test('log4js', (batch) => {
         requires: {
           './appenders/file': {
             name: 'file',
-            appender: function () {
-            },
             configure: function () {
-              return function () {
+              function thing() {
+                return null;
+              }
+
+              thing.shutdown = function (cb) {
+                events.appenderShutdownCalled = true;
+                cb();
               };
-            },
-            shutdown: function (cb) {
-              events.appenderShutdownCalled = true;
-              cb();
+              return thing;
             }
           }
         }
@@ -144,108 +79,24 @@ test('log4js', (batch) => {
     );
 
     const config = {
-      appenders: [
-        {
+      appenders: {
+        file: {
           type: 'file',
           filename: 'cheesy-wotsits.log',
           maxLogSize: 1024,
           backups: 3
         }
-      ]
+      },
+      categories: { default: { appenders: ['file'], level: 'DEBUG' } }
     };
 
     log4js.configure(config);
     log4js.shutdown(() => {
-      // Re-enable log writing so other tests that use logger are not
-      // affected.
-      require('../../lib/logger').enableAllLogWrites();
       t.ok(events.appenderShutdownCalled, 'should invoke appender shutdowns');
       t.end();
     });
   });
 
-  // 'invalid configuration': {
-  //   'should throw an exception': function () {
-  //     assert.throws(() => {
-  //       // todo: here is weird, it's not ideal test
-  //       require('../../lib/log4js').configure({ type: 'invalid' });
-  //     });
-  //   }
-  // },
-
-  batch.test('configuration when passed as object', (t) => {
-    let appenderConfig;
-
-    const log4js = sandbox.require(
-      '../../lib/log4js',
-      {
-        requires: {
-          './appenders/file': {
-            name: 'file',
-            appender: function () {
-            },
-            configure: function (configuration) {
-              appenderConfig = configuration;
-              return function () {
-              };
-            }
-          }
-        }
-      }
-    );
-
-    const config = {
-      appenders: [
-        {
-          type: 'file',
-          filename: 'cheesy-wotsits.log',
-          maxLogSize: 1024,
-          backups: 3
-        }
-      ]
-    };
-
-    log4js.configure(config);
-    t.equal(appenderConfig.filename, 'cheesy-wotsits.log', 'should be passed to appender config');
-    t.end();
-  });
-
-  batch.test('configuration that causes an error', (t) => {
-    const log4js = sandbox.require(
-      '../../lib/log4js',
-      {
-        requires: {
-          './appenders/file': {
-            name: 'file',
-            appender: function () {
-            },
-            configure: function () {
-              throw new Error('oh noes');
-            }
-          }
-        }
-      }
-    );
-
-    const config = {
-      appenders: [
-        {
-          type: 'file',
-          filename: 'cheesy-wotsits.log',
-          maxLogSize: 1024,
-          backups: 3
-        }
-      ]
-    };
-
-    try {
-      log4js.configure(config);
-    } catch (e) {
-      t.ok(e.message.includes('log4js configuration problem for'));
-      t.end();
-    }
-  });
-
   batch.test('configuration when passed as filename', (t) => {
     let appenderConfig;
     let configFilename;
@@ -261,12 +112,13 @@ test('log4js', (batch) => {
             readFileSync: function (filename) {
               configFilename = filename;
               return JSON.stringify({
-                appenders: [
-                  {
+                appenders: {
+                  file: {
                     type: 'file',
                     filename: 'whatever.log'
                   }
-                ]
+                },
+                categories: { default: { appenders: ['file'], level: 'DEBUG' } }
               });
             },
             readdirSync: function () {
@@ -274,9 +126,6 @@ test('log4js', (batch) => {
             }
           },
           './appenders/file': {
-            name: 'file',
-            appender: function () {
-            },
             configure: function (configuration) {
               appenderConfig = configuration;
               return function () {
@@ -295,15 +144,11 @@ test('log4js', (batch) => {
 
   batch.test('with no appenders defined', (t) => {
     const fakeStdoutAppender = {
-      name: 'stdout',
-      appender: function () {
+      configure: function () {
         return function (evt) {
           t.equal(evt.data[0], 'This is a test', 'should default to the stdout appender');
           t.end();
         };
-      },
-      configure: function () {
-        return fakeStdoutAppender.appender();
       }
     };
 
@@ -321,288 +166,18 @@ test('log4js', (batch) => {
     // assert is back at the top, in the fake stdout appender
   });
 
-  batch.test('addAppender', (t) => {
-    const log4js = require('../../lib/log4js');
-    log4js.clearAppenders();
-
-    t.test('without a category', (assert) => {
-      let appenderEvent;
-
-      const appender = function (evt) {
-        appenderEvent = evt;
-      };
-
-      const logger = log4js.getLogger('tests');
-
-      log4js.addAppender(appender);
-      logger.debug('This is a test');
-
-      assert.equal(
-        appenderEvent.data[0],
-        'This is a test',
-        'should register the function as a listener for all loggers'
-      );
-      assert.equal(appenderEvent.categoryName, 'tests');
-      assert.equal(appenderEvent.level.toString(), 'DEBUG');
-      assert.end();
-    });
-
-    t.test('if an appender for a category is defined', (assert) => {
-      let otherEvent;
-      let appenderEvent;
-
-      log4js.addAppender((evt) => {
-        appenderEvent = evt;
-      });
-      log4js.addAppender((evt) => {
-        otherEvent = evt;
-      }, 'cheese');
-
-      const cheeseLogger = log4js.getLogger('cheese');
-      cheeseLogger.debug('This is a test');
-
-      assert.same(appenderEvent, otherEvent, 'should register for that category');
-      assert.equal(otherEvent.data[0], 'This is a test');
-      assert.equal(otherEvent.categoryName, 'cheese');
-
-      otherEvent = undefined;
-      appenderEvent = undefined;
-      log4js.getLogger('pants').debug('this should not be propagated to otherEvent');
-      assert.notOk(otherEvent);
-      assert.equal(appenderEvent.data[0], 'this should not be propagated to otherEvent');
-      assert.end();
-    });
-
-    t.test('with a category', (assert) => {
-      let appenderEvent;
-
-      const appender = function (evt) {
-        appenderEvent = evt;
-      };
-
-      const logger = log4js.getLogger('tests');
-
-      log4js.addAppender(appender, 'tests');
-      logger.debug('this is a category test');
-      assert.equal(
-        appenderEvent.data[0],
-        'this is a category test',
-        'should only register the function as a listener for that category'
-      );
-
-      appenderEvent = undefined;
-      log4js.getLogger('some other category').debug('Cheese');
-      assert.notOk(appenderEvent);
-      assert.end();
-    });
-
-    t.test('with multiple categories', (assert) => {
-      let appenderEvent;
-
-      const appender = function (evt) {
-        appenderEvent = evt;
-      };
-
-      const logger = log4js.getLogger('tests');
-
-      log4js.addAppender(appender, 'tests', 'biscuits');
-
-      logger.debug('this is a test');
-      assert.equal(
-        appenderEvent.data[0],
-        'this is a test',
-        'should register the function as a listener for all the categories'
-      );
-
-      appenderEvent = undefined;
-      const otherLogger = log4js.getLogger('biscuits');
-      otherLogger.debug('mmm... garibaldis');
-      assert.equal(appenderEvent.data[0], 'mmm... garibaldis');
-
-      appenderEvent = undefined;
-
-      log4js.getLogger('something else').debug('pants');
-      assert.notOk(appenderEvent);
-      assert.end();
-    });
-
-    t.test('should register the function when the list of categories is an array', (assert) => {
-      let appenderEvent;
-
-      const appender = function (evt) {
-        appenderEvent = evt;
-      };
-
-      log4js.addAppender(appender, ['tests', 'pants']);
-
-      log4js.getLogger('tests').debug('this is a test');
-      assert.equal(appenderEvent.data[0], 'this is a test');
-
-      appenderEvent = undefined;
-
-      log4js.getLogger('pants').debug('big pants');
-      assert.equal(appenderEvent.data[0], 'big pants');
-
-      appenderEvent = undefined;
-
-      log4js.getLogger('something else').debug('pants');
-      assert.notOk(appenderEvent);
-      assert.end();
-    });
-
-    t.end();
-  });
-
-  batch.test('default setup', (t) => {
-    const appenderEvents = [];
-
-    const fakeStdout = {
-      name: 'stdout',
-      appender: function () {
-        return function (evt) {
-          appenderEvents.push(evt);
-        };
-      },
-      configure: function () {
-        return fakeStdout.appender();
-      }
-    };
-
-    const globalConsole = {
-      log: function () {
-      }
-    };
-
-    const log4js = sandbox.require(
-      '../../lib/log4js',
-      {
-        requires: {
-          './appenders/stdout': fakeStdout
-        },
-        globals: {
-          console: globalConsole
-        }
-      }
-    );
-
-    const logger = log4js.getLogger('a-test');
-
-    logger.debug('this is a test');
-    globalConsole.log('this should not be logged');
-
-    t.equal(appenderEvents[0].data[0], 'this is a test', 'should configure a stdout appender');
-    t.equal(appenderEvents.length, 1, 'should not replace console.log with log4js version');
-    t.end();
-  });
-
-  batch.test('console', (t) => {
-    const setup = setupConsoleTest();
-
-    t.test('when replaceConsole called', (assert) => {
-      setup.log4js.replaceConsole();
-
-      setup.fakeConsole.log('Some debug message someone put in a module');
-      setup.fakeConsole.debug('Some debug');
-      setup.fakeConsole.error('An error');
-      setup.fakeConsole.info('some info');
-      setup.fakeConsole.warn('a warning');
-
-      setup.fakeConsole.log('cheese (%s) and biscuits (%s)', 'gouda', 'garibaldis');
-      setup.fakeConsole.log({ lumpy: 'tapioca' });
-      setup.fakeConsole.log('count %d', 123);
-      setup.fakeConsole.log('stringify %j', { lumpy: 'tapioca' });
-
-      const logEvents = setup.logEvents;
-      assert.equal(logEvents.length, 9);
-      assert.equal(logEvents[0].data[0], 'Some debug message someone put in a module');
-      assert.equal(logEvents[0].level.toString(), 'INFO');
-      assert.equal(logEvents[1].data[0], 'Some debug');
-      assert.equal(logEvents[1].level.toString(), 'DEBUG');
-      assert.equal(logEvents[2].data[0], 'An error');
-      assert.equal(logEvents[2].level.toString(), 'ERROR');
-      assert.equal(logEvents[3].data[0], 'some info');
-      assert.equal(logEvents[3].level.toString(), 'INFO');
-      assert.equal(logEvents[4].data[0], 'a warning');
-      assert.equal(logEvents[4].level.toString(), 'WARN');
-      assert.equal(logEvents[5].data[0], 'cheese (%s) and biscuits (%s)');
-      assert.equal(logEvents[5].data[1], 'gouda');
-      assert.equal(logEvents[5].data[2], 'garibaldis');
-      assert.end();
-    });
-
-    t.test('when turned off', (assert) => {
-      setup.log4js.restoreConsole();
-      try {
-        setup.fakeConsole.log('This should cause the error described in the setup');
-      } catch (e) {
-        assert.type(e, 'Error', 'should call the original console methods');
-        assert.equal(e.message, 'this should not be called.');
-        assert.end();
-      }
-    });
-    t.end();
-  });
-
-  batch.test('console configuration', (t) => {
-    const setup = setupConsoleTest();
-
-    t.test('when disabled', (assert) => {
-      setup.log4js.replaceConsole();
-      setup.log4js.configure({ replaceConsole: false });
-      try {
-        setup.fakeConsole.log('This should cause the error described in the setup');
-      } catch (e) {
-        assert.type(e, 'Error');
-        assert.equal(e.message, 'this should not be called.');
-        assert.end();
-      }
-    });
-
-    t.test('when enabled', (assert) => {
-      setup.log4js.restoreConsole();
-      setup.log4js.configure({ replaceConsole: true });
-      // log4js.configure clears all appenders
-      setup.log4js.addAppender((evt) => {
-        setup.logEvents.push(evt);
-      });
-
-      setup.fakeConsole.debug('Some debug');
-
-      const logEvents = setup.logEvents;
-      assert.equal(logEvents.length, 1);
-      assert.equal(logEvents[0].level.toString(), 'DEBUG');
-      assert.equal(logEvents[0].data[0], 'Some debug');
-      assert.end();
-    });
-
-    t.end();
-  });
-
   batch.test('configuration persistence', (t) => {
-    let logEvent;
     const firstLog4js = require('../../lib/log4js');
-
-    firstLog4js.clearAppenders();
-    firstLog4js.addAppender((evt) => {
-      logEvent = evt;
+    firstLog4js.configure({
+      appenders: { recorder: { type: 'recording' } },
+      categories: { default: { appenders: ['recorder'], level: 'DEBUG' } }
     });
+    recording.reset();
 
     const secondLog4js = require('../../lib/log4js');
     secondLog4js.getLogger().info('This should go to the appender defined in firstLog4js');
 
-    t.equal(logEvent.data[0], 'This should go to the appender defined in firstLog4js');
-    t.end();
-  });
-
-  batch.test('getDefaultLogger', (t) => {
-    const logger = require('../../lib/log4js').getDefaultLogger();
-
-    t.test('should return a logger', (assert) => {
-      assert.ok(logger.info);
-      assert.ok(logger.debug);
-      assert.ok(logger.error);
-      assert.end();
-    });
+    t.equal(recording.replay()[0].data[0], 'This should go to the appender defined in firstLog4js');
     t.end();
   });
 
diff --git a/test/tap/reload-shutdown-test.js b/test/tap/reload-shutdown-test.js
deleted file mode 100644
index 7b3175f3..00000000
--- a/test/tap/reload-shutdown-test.js
+++ /dev/null
@@ -1,34 +0,0 @@
-'use strict';
-
-const test = require('tap').test;
-const path = require('path');
-const sandbox = require('sandboxed-module');
-
-test('Reload configuration shutdown hook', (t) => {
-  let timerId;
-
-  const log4js = sandbox.require(
-    '../../lib/log4js',
-    {
-      globals: {
-        clearInterval: function (id) {
-          timerId = id;
-        },
-        setInterval: function () {
-          return '1234';
-        }
-      }
-    }
-  );
-
-  log4js.configure(
-    path.join(__dirname, 'test-config.json'),
-    { reloadSecs: 30 }
-  );
-
-  t.plan(1);
-  log4js.shutdown(() => {
-    t.equal(timerId, '1234', 'Shutdown should clear the reload timer');
-    t.end();
-  });
-});
diff --git a/test/tap/reloadConfiguration-test.js b/test/tap/reloadConfiguration-test.js
deleted file mode 100644
index 6ce338ca..00000000
--- a/test/tap/reloadConfiguration-test.js
+++ /dev/null
@@ -1,350 +0,0 @@
-'use strict';
-
-const test = require('tap').test;
-const sandbox = require('sandboxed-module');
-
-function setupConsoleTest() {
-  const fakeConsole = {};
-  const logEvents = [];
-
-  ['trace', 'debug', 'log', 'info', 'warn', 'error'].forEach((fn) => {
-    fakeConsole[fn] = function () {
-      throw new Error('this should not be called.');
-    };
-  });
-
-  const log4js = sandbox.require(
-    '../../lib/log4js',
-    {
-      globals: {
-        console: fakeConsole
-      }
-    }
-  );
-
-  log4js.clearAppenders();
-  log4js.addAppender((evt) => {
-    logEvents.push(evt);
-  });
-
-  return { log4js: log4js, logEvents: logEvents, fakeConsole: fakeConsole };
-}
-
-test('reload configuration', (batch) => {
-  batch.test('with config file changing', (t) => {
-    const pathsChecked = [];
-    const logEvents = [];
-    const modulePath = 'path/to/log4js.json';
-
-    const fakeFS = {
-      lastMtime: Date.now(),
-      config: {
-        appenders: [
-          { type: 'console', layout: { type: 'messagePassThrough' } }
-        ],
-        levels: { 'a-test': 'INFO' }
-      },
-      readFileSync: function (file, encoding) {
-        t.equal(file, modulePath);
-        t.equal(encoding, 'utf8');
-        return JSON.stringify(fakeFS.config);
-      },
-      statSync: function (path) {
-        pathsChecked.push(path);
-        if (path === modulePath) {
-          fakeFS.lastMtime += 1;
-          return { mtime: new Date(fakeFS.lastMtime) };
-        }
-        throw new Error('no such file');
-      }
-    };
-
-    const fakeConsole = {
-      name: 'console',
-      appender: function () {
-        return function (evt) {
-          logEvents.push(evt);
-        };
-      },
-      configure: function () {
-        return fakeConsole.appender();
-      }
-    };
-
-    let setIntervalCallback;
-
-    const fakeSetInterval = function (cb) {
-      setIntervalCallback = cb;
-    };
-
-    const log4js = sandbox.require(
-      '../../lib/log4js',
-      {
-        requires: {
-          fs: fakeFS,
-          './appenders/console': fakeConsole
-        },
-        globals: {
-          console: fakeConsole,
-          setInterval: fakeSetInterval,
-        }
-      }
-    );
-
-    log4js.configure('path/to/log4js.json', { reloadSecs: 30 });
-    const logger = log4js.getLogger('a-test');
-    logger.info('info1');
-    logger.debug('debug2 - should be ignored');
-    fakeFS.config.levels['a-test'] = 'DEBUG';
-    setIntervalCallback();
-    logger.info('info3');
-    logger.debug('debug4');
-
-    t.test('should configure log4js from first log4js.json found', (assert) => {
-      assert.equal(logEvents[0].data[0], 'info1');
-      assert.equal(logEvents[1].data[0], 'info3');
-      assert.equal(logEvents[2].data[0], 'debug4');
-      assert.equal(logEvents.length, 3);
-      assert.end();
-    });
-    t.end();
-  });
-
-  batch.test('with config file staying the same', (t) => {
-    const pathsChecked = [];
-    let fileRead = 0;
-    const logEvents = [];
-    const modulePath = require('path').normalize(`${__dirname}/../../lib/log4js.json`);
-    const mtime = new Date();
-
-    const fakeFS = {
-      config: {
-        appenders: [
-          { type: 'console', layout: { type: 'messagePassThrough' } }
-        ],
-        levels: { 'a-test': 'INFO' }
-      },
-      readFileSync: function (file, encoding) {
-        fileRead += 1;
-        t.type(file, 'string');
-        t.equal(file, modulePath);
-        t.equal(encoding, 'utf8');
-        return JSON.stringify(fakeFS.config);
-      },
-      statSync: function (path) {
-        pathsChecked.push(path);
-        if (path === modulePath) {
-          return { mtime: mtime };
-        }
-        throw new Error('no such file');
-      }
-    };
-
-    const fakeConsole = {
-      name: 'console',
-      appender: function () {
-        return function (evt) {
-          logEvents.push(evt);
-        };
-      },
-      configure: function () {
-        return fakeConsole.appender();
-      }
-    };
-
-    let setIntervalCallback;
-
-    const fakeSetInterval = function (cb) {
-      setIntervalCallback = cb;
-    };
-
-    const log4js = sandbox.require(
-      '../../lib/log4js',
-      {
-        requires: {
-          fs: fakeFS,
-          './appenders/console': fakeConsole
-        },
-        globals: {
-          console: fakeConsole,
-          setInterval: fakeSetInterval,
-        }
-      }
-    );
-
-    log4js.configure(modulePath, { reloadSecs: 3 });
-    const logger = log4js.getLogger('a-test');
-    logger.info('info1');
-    logger.debug('debug2 - should be ignored');
-    setIntervalCallback();
-    logger.info('info3');
-    logger.debug('debug4');
-
-    t.equal(fileRead, 1, 'should only read the configuration file once');
-    t.test('should configure log4js from first log4js.json found', (assert) => {
-      assert.equal(logEvents.length, 2);
-      assert.equal(logEvents[0].data[0], 'info1');
-      assert.equal(logEvents[1].data[0], 'info3');
-      assert.end();
-    });
-    t.end();
-  });
-
-  batch.test('when config file is removed', (t) => {
-    let fileRead = 0;
-    const logEvents = [];
-    const modulePath = require('path').normalize(`${__dirname}/../../lib/log4js.json`);
-
-    const fakeFS = {
-      config: {
-        appenders: [
-          { type: 'console', layout: { type: 'messagePassThrough' } }
-        ],
-        levels: { 'a-test': 'INFO' }
-      },
-      readFileSync: function (file, encoding) {
-        fileRead += 1;
-        t.type(file, 'string');
-        t.equal(file, modulePath);
-        t.equal(encoding, 'utf8');
-        return JSON.stringify(fakeFS.config);
-      },
-      statSync: function () {
-        this.statSync = function () {
-          throw new Error('no such file');
-        };
-        return { mtime: new Date() };
-      }
-    };
-
-    const fakeConsole = {
-      name: 'console',
-      appender: function () {
-        return function (evt) {
-          logEvents.push(evt);
-        };
-      },
-      configure: function () {
-        return fakeConsole.appender();
-      }
-    };
-
-    let setIntervalCallback;
-
-    const fakeSetInterval = function (cb) {
-      setIntervalCallback = cb;
-    };
-
-    const log4js = sandbox.require(
-      '../../lib/log4js',
-      {
-        requires: {
-          fs: fakeFS,
-          './appenders/console': fakeConsole
-        },
-        globals: {
-          console: fakeConsole,
-          setInterval: fakeSetInterval,
-        }
-      }
-    );
-
-    log4js.configure(modulePath, { reloadSecs: 3 });
-    const logger = log4js.getLogger('a-test');
-    logger.info('info1');
-    logger.debug('debug2 - should be ignored');
-    setIntervalCallback();
-    logger.info('info3');
-    logger.debug('debug4');
-
-    t.equal(fileRead, 1, 'should only read the configuration file once');
-    t.test('should not clear configuration when config file not found', (assert) => {
-      assert.equal(logEvents.length, 3);
-      assert.equal(logEvents[0].data[0], 'info1');
-      assert.equal(logEvents[1].level.toString(), 'WARN');
-      assert.include(logEvents[1].data[0], 'Failed to load configuration file');
-      assert.equal(logEvents[2].data[0], 'info3');
-      assert.end();
-    });
-    t.end();
-  });
-
-  batch.test('when passed an object', (t) => {
-    const setup = setupConsoleTest();
-    setup.log4js.configure({}, { reloadSecs: 30 });
-    const events = setup.logEvents;
-
-    t.test('should log a warning', (assert) => {
-      assert.equal(events[0].level.toString(), 'WARN');
-      assert.equal(
-        events[0].data[0],
-        'Ignoring configuration reload parameter for "object" configuration.'
-      );
-      assert.end();
-    });
-    t.end();
-  });
-
-  batch.test('when called twice with reload options', (t) => {
-    const modulePath = require('path').normalize(`${__dirname}/../../lib/log4js.json`);
-
-    const fakeFS = {
-      readFileSync: function () {
-        return JSON.stringify({});
-      },
-      statSync: function () {
-        return { mtime: new Date() };
-      }
-    };
-
-    const fakeConsole = {
-      name: 'console',
-      appender: function () {
-        return function () {
-        };
-      },
-      configure: function () {
-        return fakeConsole.appender();
-      }
-    };
-
-    let setIntervalCallback; // eslint-disable-line
-    let intervalCleared = false;
-    let clearedId;
-
-    const fakeSetInterval = function (cb) {
-      setIntervalCallback = cb;
-      return 1234;
-    };
-
-    const log4js = sandbox.require(
-      '../../lib/log4js',
-      {
-        requires: {
-          fs: fakeFS,
-          './appenders/console': fakeConsole
-        },
-        globals: {
-          console: fakeConsole,
-          setInterval: fakeSetInterval,
-          clearInterval: function (interval) {
-            intervalCleared = true;
-            clearedId = interval;
-          }
-        }
-      }
-    );
-
-    log4js.configure(modulePath, { reloadSecs: 3 });
-    log4js.configure(modulePath, { reloadSecs: 15 });
-
-    t.test('should clear the previous interval', (assert) => {
-      assert.ok(intervalCleared);
-      assert.equal(clearedId, 1234);
-      assert.end();
-    });
-    t.end();
-  });
-
-  batch.end();
-});
diff --git a/test/tap/stdoutAppender-test.js b/test/tap/stdoutAppender-test.js
index 9ae5bafd..a3b0ce4d 100644
--- a/test/tap/stdoutAppender-test.js
+++ b/test/tap/stdoutAppender-test.js
@@ -20,7 +20,7 @@ test('stdout appender', (t) => {
         }
       }
     }
-  ).appender(layouts.messagePassThroughLayout);
+  ).configure({ type: 'stdout', layout: { type: 'messagePassThrough' } }, layouts);
 
   appender({ data: ['cheese'] });
   t.plan(2);

From 1d50b82a96a7aac4358be32f8b0408dcbb571eab Mon Sep 17 00:00:00 2001
From: Gareth Jones 
Date: Tue, 7 Feb 2017 08:53:51 +1100
Subject: [PATCH 02/13] refactor: fixed a few more appenders

---
 lib/appenders/categoryFilter.js    |   8 +-
 lib/appenders/console.js           |   8 +-
 lib/appenders/dateFile.js          |  61 +++-----
 lib/appenders/file.js              |  97 ++++++-------
 lib/appenders/fileSync.js          |  11 +-
 lib/appenders/gelf.js              |  28 ++--
 lib/appenders/hipchat.js           |  29 ++--
 lib/appenders/logFacesAppender.js  |   1 -
 lib/appenders/loggly.js            |  89 ++++++------
 lib/configuration.js               |   9 ++
 lib/log4js.js                      |  10 +-
 lib/logger.js                      |  31 ++---
 test/tap/categoryFilter-test.js    |  90 +++++-------
 test/tap/configureNoLevels-test.js |  38 -----
 test/tap/consoleAppender-test.js   |  12 +-
 test/tap/dateFileAppender-test.js  |  94 ++++++-------
 test/tap/file-sighup-test.js       |   6 +-
 test/tap/fileAppender-test.js      | 216 +++++++++++------------------
 test/tap/fileSyncAppender-test.js  |  78 +++++------
 test/tap/gelfAppender-test.js      |  34 +++--
 test/tap/global-log-level-test.js  | 126 -----------------
 test/tap/hipchatAppender-test.js   |  15 +-
 test/tap/log-abspath-test.js       |  88 ------------
 test/tap/logger-test.js            |  48 ++++---
 test/tap/logglyAppender-test.js    |  30 ++--
 test/tap/with-categoryFilter.json  |  23 ---
 test/tap/with-dateFile.json        |  17 ---
 27 files changed, 444 insertions(+), 853 deletions(-)
 delete mode 100644 test/tap/configureNoLevels-test.js
 delete mode 100644 test/tap/global-log-level-test.js
 delete mode 100644 test/tap/log-abspath-test.js
 delete mode 100644 test/tap/with-categoryFilter.json
 delete mode 100644 test/tap/with-dateFile.json

diff --git a/lib/appenders/categoryFilter.js b/lib/appenders/categoryFilter.js
index c4ab9d7f..263970ba 100644
--- a/lib/appenders/categoryFilter.js
+++ b/lib/appenders/categoryFilter.js
@@ -1,7 +1,5 @@
 'use strict';
 
-const log4js = require('../log4js');
-
 function categoryFilter(excludes, appender) {
   if (typeof excludes === 'string') excludes = [excludes];
   return (logEvent) => {
@@ -11,11 +9,9 @@ function categoryFilter(excludes, appender) {
   };
 }
 
-function configure(config, options) {
-  log4js.loadAppender(config.appender.type);
-  const appender = log4js.appenderMakers[config.appender.type](config.appender, options);
+function configure(config, layouts, findAppender) {
+  const appender = findAppender(config.appender);
   return categoryFilter(config.exclude, appender);
 }
 
-module.exports.appender = categoryFilter;
 module.exports.configure = configure;
diff --git a/lib/appenders/console.js b/lib/appenders/console.js
index 6b2e6919..25211f67 100644
--- a/lib/appenders/console.js
+++ b/lib/appenders/console.js
@@ -1,23 +1,19 @@
 'use strict';
 
-const layouts = require('../layouts');
-
 const consoleLog = console.log.bind(console);
 
 function consoleAppender(layout, timezoneOffset) {
-  layout = layout || layouts.colouredLayout;
   return (loggingEvent) => {
     consoleLog(layout(loggingEvent, timezoneOffset));
   };
 }
 
-function configure(config) {
-  let layout;
+function configure(config, layouts) {
+  let layout = layouts.colouredLayout;
   if (config.layout) {
     layout = layouts.layout(config.layout.type, config.layout);
   }
   return consoleAppender(layout, config.timezoneOffset);
 }
 
-module.exports.appender = consoleAppender;
 module.exports.configure = configure;
diff --git a/lib/appenders/dateFile.js b/lib/appenders/dateFile.js
index 3b31bdc0..0357d0ee 100644
--- a/lib/appenders/dateFile.js
+++ b/lib/appenders/dateFile.js
@@ -1,18 +1,14 @@
 'use strict';
 
 const streams = require('streamroller');
-const layouts = require('../layouts');
-const path = require('path');
 const os = require('os');
 
 const eol = os.EOL || '\n';
-const openFiles = [];
+const appenders = [];
 
 // close open files on process exit.
 process.on('exit', () => {
-  openFiles.forEach((file) => {
-    file.end();
-  });
+  appenders.forEach((a) => { a.shutdown(); });
 });
 
 /**
@@ -30,21 +26,33 @@ function appender(
   options,
   timezoneOffset
 ) {
-  layout = layout || layouts.basicLayout;
   const logFile = new streams.DateRollingFileStream(
     filename,
     pattern,
     options
   );
-  openFiles.push(logFile);
 
-  return (logEvent) => {
+  const app = function (logEvent) {
     logFile.write(layout(logEvent, timezoneOffset) + eol, 'utf8');
   };
+
+  app.shutdown = function (cb) {
+    if (!logFile.write(eol, 'utf-8')) {
+      logFile.once('drain', () => {
+        logFile.end(cb);
+      });
+    } else {
+      logFile.end(cb);
+    }
+  };
+
+  appenders.push(app);
+
+  return app;
 }
 
-function configure(config, options) {
-  let layout;
+function configure(config, layouts) {
+  let layout = layouts.basicLayout;
 
   if (config.layout) {
     layout = layouts.layout(config.layout.type, config.layout);
@@ -54,10 +62,6 @@ function configure(config, options) {
     config.alwaysIncludePattern = false;
   }
 
-  if (options && options.cwd && !config.absolute) {
-    config.filename = path.join(options.cwd, config.filename);
-  }
-
   return appender(
     config.filename,
     config.pattern,
@@ -67,31 +71,4 @@ function configure(config, options) {
   );
 }
 
-function shutdown(cb) {
-  let completed = 0;
-  let error;
-  const complete = (err) => {
-    error = error || err;
-    completed++; // eslint-disable-line no-plusplus
-    if (completed >= openFiles.length) {
-      cb(error);
-    }
-  };
-  if (!openFiles.length) {
-    return cb();
-  }
-
-  return openFiles.forEach((file) => {
-    if (!file.write(eol, 'utf-8')) {
-      file.once('drain', () => {
-        file.end(complete);
-      });
-    } else {
-      file.end(complete);
-    }
-  });
-}
-
-module.exports.appender = appender;
 module.exports.configure = configure;
-module.exports.shutdown = shutdown;
diff --git a/lib/appenders/file.js b/lib/appenders/file.js
index 9284e146..32644950 100644
--- a/lib/appenders/file.js
+++ b/lib/appenders/file.js
@@ -1,20 +1,17 @@
 'use strict';
 
 const debug = require('debug')('log4js:file');
-const layouts = require('../layouts');
 const path = require('path');
 const streams = require('streamroller');
 const os = require('os');
 
 const eol = os.EOL || '\n';
-const openFiles = [];
+const appenders = [];
 
 // close open files on process exit.
 process.on('exit', () => {
   debug('Exit handler called.');
-  openFiles.forEach((file) => {
-    file.end();
-  });
+  appenders.forEach((a) => { a.shutdown(); });
 });
 
 // On SIGHUP, close and reopen all files. This allows this appender to work with
@@ -22,11 +19,25 @@ process.on('exit', () => {
 // `logSize`.
 process.on('SIGHUP', () => {
   debug('SIGHUP handler called.');
-  openFiles.forEach((writer) => {
-    writer.closeTheStream(writer.openTheStream.bind(writer));
+  appenders.forEach((a) => {
+    a.reopen();
   });
 });
 
+function openTheStream(file, fileSize, numFiles, options) {
+  const stream = new streams.RollingFileStream(
+    file,
+    fileSize,
+    numFiles,
+    options
+  );
+  stream.on('error', (err) => {
+    console.error('log4js.fileAppender - Writing to file %s, error happened ', file, err);
+  });
+  return stream;
+}
+
+
 /**
  * File Appender writing the logs to a text file. Supports rolling of logs by size.
  *
@@ -42,7 +53,6 @@ process.on('SIGHUP', () => {
  */
 function fileAppender(file, layout, logSize, numBackups, options, timezoneOffset) {
   file = path.normalize(file);
-  layout = layout || layouts.basicLayout;
   numBackups = numBackups === undefined ? 5 : numBackups;
   // there has to be at least one backup if logSize has been specified
   numBackups = numBackups === 0 ? 1 : numBackups;
@@ -54,40 +64,38 @@ function fileAppender(file, layout, logSize, numBackups, options, timezoneOffset
     options, ', ',
     timezoneOffset, ')'
   );
-  const writer = openTheStream(file, logSize, numBackups, options);
 
-  // push file to the stack of open handlers
-  openFiles.push(writer);
+  const writer = openTheStream(file, logSize, numBackups, options);
 
-  return function (loggingEvent) {
+  const app = function (loggingEvent) {
     writer.write(layout(loggingEvent, timezoneOffset) + eol, 'utf8');
   };
-}
 
-function openTheStream(file, fileSize, numFiles, options) {
-  const stream = new streams.RollingFileStream(
-    file,
-    fileSize,
-    numFiles,
-    options
-  );
-  stream.on('error', (err) => {
-    console.error('log4js.fileAppender - Writing to file %s, error happened ', file, err);
-  });
-  return stream;
-}
+  app.reopen = function () {
+    writer.closeTheStream(writer.openTheStream.bind(writer));
+  };
 
+  app.shutdown = function (complete) {
+    if (!writer.write(eol, 'utf-8')) {
+      writer.once('drain', () => {
+        writer.end(complete);
+      });
+    } else {
+      writer.end(complete);
+    }
+  };
+
+  // push file to the stack of open handlers
+  appenders.push(app);
+  return app;
+}
 
-function configure(config, options) {
-  let layout;
+function configure(config, layouts) {
+  let layout = layouts.basicLayout;
   if (config.layout) {
     layout = layouts.layout(config.layout.type, config.layout);
   }
 
-  if (options && options.cwd && !config.absolute) {
-    config.filename = path.join(options.cwd, config.filename);
-  }
-
   return fileAppender(
     config.filename,
     layout,
@@ -98,31 +106,4 @@ function configure(config, options) {
   );
 }
 
-function shutdown(cb) {
-  let completed = 0;
-  let error;
-  const complete = (err) => {
-    error = error || err;
-    completed++; // eslint-disable-line no-plusplus
-    if (completed >= openFiles.length) {
-      cb(error);
-    }
-  };
-  if (!openFiles.length) {
-    return cb();
-  }
-
-  return openFiles.forEach((file) => {
-    if (!file.write(eol, 'utf-8')) {
-      file.once('drain', () => {
-        file.end(complete);
-      });
-    } else {
-      file.end(complete);
-    }
-  });
-}
-
-module.exports.appender = fileAppender;
 module.exports.configure = configure;
-module.exports.shutdown = shutdown;
diff --git a/lib/appenders/fileSync.js b/lib/appenders/fileSync.js
index dab551c3..36254a92 100755
--- a/lib/appenders/fileSync.js
+++ b/lib/appenders/fileSync.js
@@ -1,7 +1,6 @@
 'use strict';
 
 const debug = require('debug')('log4js:fileSync');
-const layouts = require('../layouts');
 const path = require('path');
 const fs = require('fs');
 const os = require('os');
@@ -135,7 +134,6 @@ class RollingFileSync {
 function fileAppender(file, layout, logSize, numBackups, timezoneOffset) {
   debug('fileSync appender created');
   file = path.normalize(file);
-  layout = layout || layouts.basicLayout;
   numBackups = numBackups === undefined ? 5 : numBackups;
   // there has to be at least one backup if logSize has been specified
   numBackups = numBackups === 0 ? 1 : numBackups;
@@ -174,16 +172,12 @@ function fileAppender(file, layout, logSize, numBackups, timezoneOffset) {
   };
 }
 
-function configure(config, options) {
-  let layout;
+function configure(config, layouts) {
+  let layout = layouts.basicLayout;
   if (config.layout) {
     layout = layouts.layout(config.layout.type, config.layout);
   }
 
-  if (options && options.cwd && !config.absolute) {
-    config.filename = path.join(options.cwd, config.filename);
-  }
-
   return fileAppender(
     config.filename,
     layout,
@@ -193,5 +187,4 @@ function configure(config, options) {
   );
 }
 
-module.exports.appender = fileAppender;
 module.exports.configure = configure;
diff --git a/lib/appenders/gelf.js b/lib/appenders/gelf.js
index eb809edc..2d922b06 100644
--- a/lib/appenders/gelf.js
+++ b/lib/appenders/gelf.js
@@ -1,7 +1,6 @@
 'use strict';
 
 const zlib = require('zlib');
-const layouts = require('../layouts');
 const levels = require('../levels');
 const dgram = require('dgram');
 const util = require('util');
@@ -27,8 +26,6 @@ levelMapping[levels.WARN] = LOG_WARNING;
 levelMapping[levels.ERROR] = LOG_ERROR;
 levelMapping[levels.FATAL] = LOG_CRIT;
 
-let client;
-
 /**
  * GELF appender that supports sending UDP packets to a GELF compatible server such as Graylog
  *
@@ -54,7 +51,6 @@ function gelfAppender(layout, host, port, hostname, facility) {
   host = host || 'localhost';
   port = port || 12201;
   hostname = hostname || OS.hostname();
-  layout = layout || layouts.messagePassThroughLayout;
 
   const defaultCustomFields = customFields || {};
 
@@ -62,7 +58,7 @@ function gelfAppender(layout, host, port, hostname, facility) {
     defaultCustomFields._facility = facility;
   }
 
-  client = dgram.createSocket('udp4');
+  const client = dgram.createSocket('udp4');
 
   process.on('exit', () => {
     if (client) client.close();
@@ -123,7 +119,7 @@ function gelfAppender(layout, host, port, hostname, facility) {
     });
   }
 
-  return (loggingEvent) => {
+  const app = (loggingEvent) => {
     const message = preparePacket(loggingEvent);
     zlib.gzip(new Buffer(JSON.stringify(message)), (err, packet) => {
       if (err) {
@@ -137,23 +133,21 @@ function gelfAppender(layout, host, port, hostname, facility) {
       }
     });
   };
+  app.shutdown = function (cb) {
+    if (client) {
+      client.close(cb);
+    }
+  };
+
+  return app;
 }
 
-function configure(config) {
-  let layout;
+function configure(config, layouts) {
+  let layout = layouts.messagePassThroughLayout;
   if (config.layout) {
     layout = layouts.layout(config.layout.type, config.layout);
   }
   return gelfAppender(layout, config);
 }
 
-function shutdown(cb) {
-  if (client) {
-    client.close(cb);
-    client = null;
-  }
-}
-
-module.exports.appender = gelfAppender;
 module.exports.configure = configure;
-module.exports.shutdown = shutdown;
diff --git a/lib/appenders/hipchat.js b/lib/appenders/hipchat.js
index 8c3a3bea..a0713106 100644
--- a/lib/appenders/hipchat.js
+++ b/lib/appenders/hipchat.js
@@ -1,17 +1,12 @@
 'use strict';
 
 const hipchat = require('hipchat-notifier');
-const layouts = require('../layouts');
-
-module.exports.name = 'hipchat';
-module.exports.appender = hipchatAppender;
-module.exports.configure = hipchatConfigure;
 
 /**
  @invoke as
 
  log4js.configure({
-    'appenders': [
+    'appenders': { 'hipchat':
       {
         'type' : 'hipchat',
         'hipchat_token': '< User token with Notification Privileges >',
@@ -21,7 +16,8 @@ module.exports.configure = hipchatConfigure;
         'hipchat_notify': '[ notify boolean to bug people ]',
         'hipchat_host' : 'api.hipchat.com'
       }
-    ]
+    },
+    categories: { default: { appenders: ['hipchat'], level: 'debug' }}
   });
 
  var logger = log4js.getLogger('hipchat');
@@ -29,17 +25,16 @@ module.exports.configure = hipchatConfigure;
 
  @invoke
  */
-/* eslint no-unused-vars:0 */
-function hipchatNotifierResponseCallback(err, response, body) {
+
+function hipchatNotifierResponseCallback(err) {
   if (err) {
     throw err;
   }
 }
 
-function hipchatAppender(config) {
+function hipchatAppender(config, layout) {
   const notifier = hipchat.make(config.hipchat_room, config.hipchat_token);
 
-  // @lint W074 This function's cyclomatic complexity is too high. (10)
   return (loggingEvent) => {
     let notifierFn;
 
@@ -68,7 +63,7 @@ function hipchatAppender(config) {
     }
 
     // @TODO, re-work in timezoneOffset ?
-    const layoutMessage = config.layout(loggingEvent);
+    const layoutMessage = layout(loggingEvent);
 
     // dispatch hipchat api request, do not return anything
     // [overide hipchatNotifierResponseCallback]
@@ -77,12 +72,14 @@ function hipchatAppender(config) {
   };
 }
 
-function hipchatConfigure(config) {
-  let layout;
+function hipchatConfigure(config, layouts) {
+  let layout = layouts.messagePassThroughLayout;
 
-  if (!config.layout) {
-    config.layout = layouts.messagePassThroughLayout;
+  if (config.layout) {
+    layout = layouts.layout(config.layout.type, config.layout);
   }
 
   return hipchatAppender(config, layout);
 }
+
+module.exports.configure = hipchatConfigure;
diff --git a/lib/appenders/logFacesAppender.js b/lib/appenders/logFacesAppender.js
index ba7bda6a..1564b3fc 100644
--- a/lib/appenders/logFacesAppender.js
+++ b/lib/appenders/logFacesAppender.js
@@ -125,6 +125,5 @@ function wrapErrorsWithInspect(items) {
   });
 }
 
-module.exports.appender = logFacesAppender;
 module.exports.configure = configure;
 module.exports.setContext = setContext;
diff --git a/lib/appenders/loggly.js b/lib/appenders/loggly.js
index 77afd06b..84c41d13 100644
--- a/lib/appenders/loggly.js
+++ b/lib/appenders/loggly.js
@@ -2,27 +2,16 @@
 
 'use strict';
 
-const layouts = require('../layouts');
+const debug = require('debug')('log4js:loggly');
 const loggly = require('loggly');
 const os = require('os');
 
-const passThrough = layouts.messagePassThroughLayout;
-
-let openRequests = 0;
-let shutdownCB;
-
 function isAnyObject(value) {
   return value !== null && (typeof value === 'object' || typeof value === 'function');
 }
 
 function numKeys(obj) {
-  let res = 0;
-  for (const key in obj) {
-    if (obj.hasOwnProperty(key)) {
-      res++; // eslint-disable-line no-plusplus
-    }
-  }
-  return res;
+  return Object.keys(obj).length;
 }
 
 /**
@@ -64,9 +53,12 @@ function processTags(msgListArgs) {
  */
 function logglyAppender(config, layout) {
   const client = loggly.createClient(config);
-  if (!layout) layout = passThrough;
+  let openRequests = 0;
+  let shutdownCB;
 
-  return (loggingEvent) => {
+  debug('creating appender.');
+
+  function app(loggingEvent) {
     const result = processTags(loggingEvent.data);
     const deTaggedData = result.deTaggedData;
     const additionalTags = result.additionalTags;
@@ -77,45 +69,52 @@ function logglyAppender(config, layout) {
     const msg = layout(loggingEvent);
 
     openRequests += 1;
-
-    client.log({
-      msg: msg,
-      level: loggingEvent.level.levelStr,
-      category: loggingEvent.categoryName,
-      hostname: os.hostname().toString(),
-    }, additionalTags, (error) => {
-      if (error) {
-        console.error('log4js.logglyAppender - error occurred: ', error);
+    debug('sending log event to loggly');
+    client.log(
+      {
+        msg: msg,
+        level: loggingEvent.level.levelStr,
+        category: loggingEvent.categoryName,
+        hostname: os.hostname().toString(),
+      },
+      additionalTags,
+      (error) => {
+        if (error) {
+          console.error('log4js.logglyAppender - error occurred: ', error);
+        }
+
+        debug('log event received by loggly.');
+
+        openRequests -= 1;
+
+        if (shutdownCB && openRequests === 0) {
+          shutdownCB();
+
+          shutdownCB = undefined;
+        }
       }
+    );
+  }
 
-      openRequests -= 1;
-
-      if (shutdownCB && openRequests === 0) {
-        shutdownCB();
-
-        shutdownCB = undefined;
-      }
-    });
+  app.shutdown = function (cb) {
+    debug('shutdown called');
+    if (openRequests === 0) {
+      cb();
+    } else {
+      shutdownCB = cb;
+    }
   };
+
+  return app;
 }
 
-function configure(config) {
-  let layout;
+function configure(config, layouts) {
+  let layout = layouts.messagePassThroughLayout;
   if (config.layout) {
     layout = layouts.layout(config.layout.type, config.layout);
   }
+  debug('configuring new appender');
   return logglyAppender(config, layout);
 }
 
-function shutdown(cb) {
-  if (openRequests === 0) {
-    cb();
-  } else {
-    shutdownCB = cb;
-  }
-}
-
-module.exports.name = 'loggly';
-module.exports.appender = logglyAppender;
 module.exports.configure = configure;
-module.exports.shutdown = shutdown;
diff --git a/lib/configuration.js b/lib/configuration.js
index 915c1542..caebd7db 100644
--- a/lib/configuration.js
+++ b/lib/configuration.js
@@ -3,6 +3,7 @@
 const util = require('util');
 const levels = require('./levels');
 const layouts = require('./layouts');
+const debug = require('debug')('log4js:configuration');
 
 function not(thing) {
   return !thing;
@@ -48,6 +49,12 @@ class Configuration {
       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.`);
+    }
     return appenderModule.configure(config, layouts, this.configuredAppenders.get.bind(this.configuredAppenders));
   }
 
@@ -66,6 +73,7 @@ class Configuration {
         `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]));
     });
   }
@@ -114,6 +122,7 @@ class Configuration {
         ` valid levels are ${levels.levels.join(', ')})`
       );
 
+      debug(`Creating category ${name}`);
       this.configuredCategories.set(name, { appenders: appenders, level: levels.toLevel(category.level) });
     });
 
diff --git a/lib/log4js.js b/lib/log4js.js
index 55a43183..23119d2d 100644
--- a/lib/log4js.js
+++ b/lib/log4js.js
@@ -22,6 +22,7 @@
  * @static
  * Website: http://log4js.berlios.de
  */
+const debug = require('debug')('log4js:main');
 const fs = require('fs');
 const Configuration = require('./configuration');
 const levels = require('./levels');
@@ -74,11 +75,12 @@ function sendLogEventToAppender(logEvent) {
  */
 function getLogger(category) {
   const cat = category || 'default';
-  return new Logger(cat, levelForCategory(cat), sendLogEventToAppender);
+  return new Logger(sendLogEventToAppender, cat, levelForCategory(cat));
 }
 
 function loadConfigurationFile(filename) {
   if (filename) {
+    debug(`Loading configuration from ${filename}`);
     return JSON.parse(fs.readFileSync(filename, 'utf8'));
   }
   return filename;
@@ -90,6 +92,7 @@ function configure(configurationFileOrObject) {
   if (typeof configObject === 'string') {
     configObject = loadConfigurationFile(configurationFileOrObject);
   }
+  debug(`Configuration is ${configObject}`);
   config = new Configuration(configObject);
   enabled = true;
 }
@@ -103,6 +106,7 @@ function configure(configurationFileOrObject) {
  *  as the first argument.
  */
 function shutdown(cb) {
+  debug('Shutdown called. Disabling all log writing.');
   // First, disable all writing to appenders. This prevents appenders from
   // not being able to be drained because of run-away log writes.
   enabled = false;
@@ -113,15 +117,19 @@ function shutdown(cb) {
   let completed = 0;
   let error;
 
+  debug(`Found ${shutdownFunctions} appenders with shutdown functions.`);
   function complete(err) {
     error = error || err;
     completed += 1;
+    debug(`Appender shutdowns complete: ${completed} / ${shutdownFunctions}`);
     if (completed >= shutdownFunctions) {
+      debug('All shutdown functions completed.');
       cb(error);
     }
   }
 
   if (shutdownFunctions === 0) {
+    debug('No appenders with shutdown functions found.');
     return cb();
   }
 
diff --git a/lib/logger.js b/lib/logger.js
index 4955b269..cc5cc30a 100644
--- a/lib/logger.js
+++ b/lib/logger.js
@@ -2,10 +2,9 @@
 
 'use strict';
 
+const debug = require('debug')('log4js:logger');
 const levels = require('./levels');
 
-// let logWritesEnabled = true;
-
 /**
  * @name LoggingEvent
  * @namespace Log4js
@@ -40,10 +39,14 @@ class LoggingEvent {
  * @author Stephan Strittmatter
  */
 class Logger {
-  constructor(name, level, dispatch) {
+  constructor(dispatch, name, level) {
+    if (typeof dispatch !== 'function') {
+      throw new Error('No dispatch function provided.');
+    }
     this.category = name;
     this.level = levels.toLevel(level, levels.TRACE);
     this.dispatch = dispatch;
+    debug(`Logger created (${name}, ${level})`);
   }
 
   setLevel(level) {
@@ -65,6 +68,7 @@ class Logger {
   }
 
   _log(level, data) {
+    debug(`sending log data (${level}, ${data}) to appenders`);
     const loggingEvent = new LoggingEvent(this.category, level, data);
     this.dispatch(loggingEvent);
   }
@@ -85,7 +89,7 @@ function addLevelMethods(target) {
     /* eslint prefer-rest-params:0 */
     // todo: once node v4 support dropped, use rest parameter instead
     const args = Array.from(arguments);
-    if (/* logWritesEnabled &&*/ this.isLevelEnabled(level)) {
+    if (this.isLevelEnabled(level)) {
       this._log(level, args);
     }
   };
@@ -93,24 +97,5 @@ function addLevelMethods(target) {
 
 levels.levels.forEach(addLevelMethods);
 
-/**
- * Disable all log writes.
- * @returns {void}
- */
-// function disableAllLogWrites() {
-//   logWritesEnabled = false;
-// }
-
-/**
- * Enable log writes.
- * @returns {void}
- */
-// function enableAllLogWrites() {
-//   logWritesEnabled = true;
-// }
-
 module.exports.LoggingEvent = LoggingEvent;
 module.exports.Logger = Logger;
-// module.exports.disableAllLogWrites = disableAllLogWrites;
-// module.exports.enableAllLogWrites = enableAllLogWrites;
-// module.exports.addLevelMethods = addLevelMethods;
diff --git a/test/tap/categoryFilter-test.js b/test/tap/categoryFilter-test.js
index 4cd10439..cabd7f2a 100644
--- a/test/tap/categoryFilter-test.js
+++ b/test/tap/categoryFilter-test.js
@@ -1,78 +1,62 @@
 'use strict';
 
 const test = require('tap').test;
-const fs = require('fs');
-const EOL = require('os').EOL || '\n';
 const log4js = require('../../lib/log4js');
-
-function remove(filename) {
-  try {
-    fs.unlinkSync(filename);
-  } catch (e) {
-    // doesn't really matter if it failed
-  }
-}
-
-function cleanup(done) {
-  remove(`${__dirname}/categoryFilter-web.log`);
-  remove(`${__dirname}/categoryFilter-noweb.log`);
-  done();
-}
+const recording = require('../../lib/appenders/recording');
 
 test('log4js categoryFilter', (batch) => {
-  batch.beforeEach(cleanup);
+  batch.beforeEach((done) => { recording.reset(); done(); });
 
   batch.test('appender should exclude categories', (t) => {
-    const logEvents = [];
-    const appender = require(
-      '../../lib/appenders/categoryFilter'
-    ).appender(
-      ['app'],
-      (evt) => {
-        logEvents.push(evt);
-      }
-    );
-    log4js.clearAppenders();
-    log4js.addAppender(appender, ['app', 'web']);
+    log4js.configure({
+      appenders: {
+        recorder: { type: 'recording' },
+        filtered: {
+          type: 'categoryFilter',
+          exclude: 'web',
+          appender: 'recorder'
+        }
+      },
+      categories: { default: { appenders: ['filtered'], level: 'DEBUG' } }
+    });
 
     const webLogger = log4js.getLogger('web');
     const appLogger = log4js.getLogger('app');
 
-    webLogger.debug('This should get logged');
-    appLogger.debug('This should not');
+    webLogger.debug('This should not get logged');
+    appLogger.debug('This should get logged');
     webLogger.debug('Hello again');
-    log4js.getLogger('db').debug('This shouldn\'t be included by the appender anyway');
+    log4js.getLogger('db').debug('This should be included by the appender anyway');
 
+    const logEvents = recording.replay();
     t.equal(logEvents.length, 2);
     t.equal(logEvents[0].data[0], 'This should get logged');
-    t.equal(logEvents[1].data[0], 'Hello again');
+    t.equal(logEvents[1].data[0], 'This should be included by the appender anyway');
     t.end();
   });
 
-  batch.test('should work with configuration file', (t) => {
-    log4js.configure('test/tap/with-categoryFilter.json');
-    const logger = log4js.getLogger('app');
-    const weblogger = log4js.getLogger('web');
-
-    logger.info('Loading app');
-    logger.info('Initialising indexes');
-    weblogger.info('00:00:00 GET / 200');
-    weblogger.warn('00:00:00 GET / 500');
+  batch.test('should not really need a category filter any more', (t) => {
+    log4js.configure({
+      appenders: { recorder: { type: 'recording' } },
+      categories: {
+        default: { appenders: ['recorder'], level: 'DEBUG' },
+        web: { appenders: ['recorder'], level: 'OFF' }
+      }
+    });
+    const appLogger = log4js.getLogger('app');
+    const webLogger = log4js.getLogger('web');
 
-    setTimeout(() => {
-      fs.readFile(`${__dirname}/categoryFilter-noweb.log`, 'utf8', (err, contents) => {
-        const noWebMessages = contents.trim().split(EOL);
-        t.same(noWebMessages, ['Loading app', 'Initialising indexes']);
+    webLogger.debug('This should not get logged');
+    appLogger.debug('This should get logged');
+    webLogger.debug('Hello again');
+    log4js.getLogger('db').debug('This should be included by the appender anyway');
 
-        fs.readFile(`${__dirname}/categoryFilter-web.log`, 'utf8', (e, c) => {
-          const messages = c.trim().split(EOL);
-          t.same(messages, ['00:00:00 GET / 200', '00:00:00 GET / 500']);
-          t.end();
-        });
-      });
-    }, 500);
+    const logEvents = recording.replay();
+    t.equal(logEvents.length, 2);
+    t.equal(logEvents[0].data[0], 'This should get logged');
+    t.equal(logEvents[1].data[0], 'This should be included by the appender anyway');
+    t.end();
   });
 
-  batch.afterEach(cleanup);
   batch.end();
 });
diff --git a/test/tap/configureNoLevels-test.js b/test/tap/configureNoLevels-test.js
deleted file mode 100644
index 0c5988b4..00000000
--- a/test/tap/configureNoLevels-test.js
+++ /dev/null
@@ -1,38 +0,0 @@
-'use strict';
-
-// This test shows unexpected behaviour for log4js.configure() in log4js-node@0.4.3 and earlier:
-// 1) log4js.configure(), log4js.configure(null),
-// log4js.configure({}), log4js.configure()
-// all set all loggers levels to trace, even if they were previously set to something else.
-// 2) log4js.configure({levels:{}}), log4js.configure({levels: {foo:
-// bar}}) leaves previously set logger levels intact.
-//
-const test = require('tap').test;
-
-// setup the configurations we want to test
-const configs = [
-  undefined,
-  null,
-  {},
-  { foo: 'bar' },
-  { levels: null },
-  { levels: {} },
-  { levels: { foo: 'bar' } },
-  { levels: { A: 'INFO' } }
-];
-
-test('log4js dodgy config', (batch) => {
-  const log4js = require('../../lib/log4js');
-  const logger = log4js.getLogger('test-logger');
-  const error = log4js.levels.ERROR;
-  logger.setLevel('ERROR');
-
-  configs.forEach((config) => {
-    batch.test(`config of ${config} should not change logger level`, (t) => {
-      log4js.configure(config);
-      t.equal(logger.level, error);
-      t.end();
-    });
-  });
-  batch.end();
-});
diff --git a/test/tap/consoleAppender-test.js b/test/tap/consoleAppender-test.js
index 6fe32cdb..499f2d37 100644
--- a/test/tap/consoleAppender-test.js
+++ b/test/tap/consoleAppender-test.js
@@ -1,7 +1,6 @@
 'use strict';
 
 const test = require('tap').test;
-const layouts = require('../../lib/layouts');
 const sandbox = require('sandboxed-module');
 
 test('log4js console appender', (batch) => {
@@ -12,17 +11,20 @@ test('log4js console appender', (batch) => {
         messages.push(msg);
       }
     };
-    const appenderModule = sandbox.require(
-      '../../lib/appenders/console',
+    const log4js = sandbox.require(
+      '../../lib/log4js',
       {
         globals: {
           console: fakeConsole
         }
       }
     );
+    log4js.configure({
+      appenders: { console: { type: 'console', layout: { type: 'messagePassThrough' } } },
+      categories: { default: { appenders: ['console'], level: 'DEBUG' } }
+    });
 
-    const appender = appenderModule.appender(layouts.messagePassThroughLayout);
-    appender({ data: ['blah'] });
+    log4js.getLogger().info('blah');
 
     t.equal(messages[0], 'blah');
     t.end();
diff --git a/test/tap/dateFileAppender-test.js b/test/tap/dateFileAppender-test.js
index 768eb17d..d19d44ca 100644
--- a/test/tap/dateFileAppender-test.js
+++ b/test/tap/dateFileAppender-test.js
@@ -10,20 +10,25 @@ const EOL = require('os').EOL || '\n';
 function removeFile(filename) {
   try {
     fs.unlinkSync(path.join(__dirname, filename));
-  } catch (e) {}
+  } catch (e) {
+    // doesn't matter
+  }
 }
 
 test('../../lib/appenders/dateFile', (batch) => {
   batch.test('adding multiple dateFileAppenders', (t) => {
     const listenersCount = process.listeners('exit').length;
-    const dateFileAppender = require('../../lib/appenders/dateFile');
-    let count = 5;
-    let logfile;
 
-    while (count--) {
-      logfile = path.join(__dirname, `datefa-default-test${count}.log`);
-      log4js.addAppender(dateFileAppender.appender(logfile));
-    }
+    log4js.configure({
+      appenders: {
+        date0: { type: 'dateFile', filename: 'datefa-default-test0.log' },
+        date1: { type: 'dateFile', filename: 'datefa-default-test1.log' },
+        date2: { type: 'dateFile', filename: 'datefa-default-test2.log' },
+        date3: { type: 'dateFile', filename: 'datefa-default-test3.log' },
+        date4: { type: 'dateFile', filename: 'datefa-default-test4.log' }
+      },
+      categories: { default: { appenders: ['date0', 'date1', 'date2', 'date3', 'date4'], level: 'debug' } }
+    });
 
     t.teardown(() => {
       removeFile('datefa-default-test0.log');
@@ -59,6 +64,10 @@ test('../../lib/appenders/dateFile', (batch) => {
               this.end = function () {
                 openedFiles.shift();
               };
+
+              this.write = function () {
+                return true;
+              };
             }
           }
         }
@@ -66,7 +75,7 @@ test('../../lib/appenders/dateFile', (batch) => {
     );
 
     for (let i = 0; i < 5; i += 1) {
-      dateFileAppender.appender(`test${i}`);
+      dateFileAppender.configure({ filename: `test${i}` }, { basicLayout: function () {} });
     }
     t.equal(openedFiles.length, 5);
     exitListener();
@@ -76,10 +85,12 @@ test('../../lib/appenders/dateFile', (batch) => {
 
   batch.test('with default settings', (t) => {
     const testFile = path.join(__dirname, 'date-appender-default.log');
-    const appender = require('../../lib/appenders/dateFile').appender(testFile);
+    log4js.configure({
+      appenders: { date: { type: 'dateFile', filename: testFile } },
+      categories: { default: { appenders: ['date'], level: 'DEBUG' } }
+    });
+
     const logger = log4js.getLogger('default-settings');
-    log4js.clearAppenders();
-    log4js.addAppender(appender, 'default-settings');
 
     logger.info('This should be in the file.');
     t.teardown(() => { removeFile('date-appender-default.log'); });
@@ -97,9 +108,17 @@ test('../../lib/appenders/dateFile', (batch) => {
   });
 
   batch.test('configure with dateFileAppender', (t) => {
-    // this config file defines one file appender (to ./date-file-test.log)
-    // and sets the log level for "tests" to WARN
-    log4js.configure('test/tap/with-dateFile.json');
+    log4js.configure({
+      appenders: {
+        date: {
+          type: 'dateFile',
+          filename: 'test/tap/date-file-test.log',
+          pattern: '-from-MM-dd',
+          layout: { type: 'messagePassThrough' }
+        }
+      },
+      categories: { default: { appenders: ['date'], level: 'WARN' } }
+    });
     const logger = log4js.getLogger('tests');
     logger.info('this should not be written to the file');
     logger.warn('this should be written to the file');
@@ -117,8 +136,8 @@ test('../../lib/appenders/dateFile', (batch) => {
     const format = require('date-format');
 
     const options = {
-      appenders: [
-        {
+      appenders: {
+        date: {
           category: 'tests',
           type: 'dateFile',
           filename: 'test/tap/date-file-test',
@@ -128,16 +147,16 @@ test('../../lib/appenders/dateFile', (batch) => {
             type: 'messagePassThrough'
           }
         }
-      ]
+      },
+      categories: { default: { appenders: ['date'], level: 'debug' } }
     };
 
-    const thisTime = format.asString(options.appenders[0].pattern, new Date());
+    const thisTime = format.asString(options.appenders.date.pattern, new Date());
     fs.writeFileSync(
       path.join(__dirname, `date-file-test${thisTime}`),
       `this is existing data${EOL}`,
       'utf8'
     );
-    log4js.clearAppenders();
     log4js.configure(options);
     const logger = log4js.getLogger('tests');
     logger.warn('this should be written to the file with the appended date');
@@ -154,40 +173,5 @@ test('../../lib/appenders/dateFile', (batch) => {
     }, 100);
   });
 
-  batch.test('configure with cwd option', (t) => {
-    let fileOpened;
-
-    const appender = sandbox.require(
-      '../../lib/appenders/dateFile',
-      {
-        requires: {
-          streamroller: {
-            DateRollingFileStream: function (file) {
-              fileOpened = file;
-              return {
-                on: function () {
-                },
-                end: function () {
-                }
-              };
-            }
-          }
-        }
-      }
-    );
-
-    appender.configure(
-      {
-        filename: 'whatever.log',
-        maxLogSize: 10
-      },
-      { cwd: '/absolute/path/to' }
-    );
-
-    const expected = path.sep + path.join('absolute', 'path', 'to', 'whatever.log');
-    t.equal(fileOpened, expected, 'should prepend options.cwd to config.filename');
-    t.end();
-  });
-
   batch.end();
 });
diff --git a/test/tap/file-sighup-test.js b/test/tap/file-sighup-test.js
index 5ed6afa0..f8638514 100644
--- a/test/tap/file-sighup-test.js
+++ b/test/tap/file-sighup-test.js
@@ -29,11 +29,15 @@ test('file appender SIGHUP', (t) => {
 
             this.end = function () {
             };
+
+            this.write = function () {
+              return true;
+            };
           }
         }
       }
     }
-  ).appender('sighup-test-file');
+  ).configure({ type: 'file', filename: 'sighup-test-file' }, { basicLayout: function () {} });
 
   process.kill(process.pid, 'SIGHUP');
   t.plan(2);
diff --git a/test/tap/fileAppender-test.js b/test/tap/fileAppender-test.js
index 95220ece..966f7e8b 100644
--- a/test/tap/fileAppender-test.js
+++ b/test/tap/fileAppender-test.js
@@ -8,9 +8,7 @@ const log4js = require('../../lib/log4js');
 const zlib = require('zlib');
 const EOL = require('os').EOL || '\n';
 
-log4js.clearAppenders();
-
-function remove(filename) {
+function removeFile(filename) {
   try {
     fs.unlinkSync(filename);
   } catch (e) {
@@ -21,16 +19,24 @@ function remove(filename) {
 test('log4js fileAppender', (batch) => {
   batch.test('adding multiple fileAppenders', (t) => {
     const initialCount = process.listeners('exit').length;
-    let count = 5;
-    let logfile;
-
-    while (count--) {
-      logfile = path.join(__dirname, `fa-default-test${count}.log`);
-      log4js.addAppender(
-        require('../../lib/appenders/file').appender(logfile),
-        'default-settings'
-      );
-    }
+    log4js.configure({
+      appenders: {
+        file0: { type: 'file', filename: 'fa-default-test0.log' },
+        file1: { type: 'file', filename: 'fa-default-test1.log' },
+        file2: { type: 'file', filename: 'fa-default-test2.log' },
+        file3: { type: 'file', filename: 'fa-default-test3.log' },
+        file4: { type: 'file', filename: 'fa-default-test4.log' },
+      },
+      categories: { default: { appenders: ['file0', 'file1', 'file2', 'file3', 'file4'], level: 'debug' } }
+    });
+
+    t.tearDown(() => {
+      removeFile('fa-default-test0.log');
+      removeFile('fa-default-test1.log');
+      removeFile('fa-default-test2.log');
+      removeFile('fa-default-test3.log');
+      removeFile('fa-default-test4.log');
+    });
 
     t.equal(initialCount + 1, process.listeners('exit').length, 'should not add more than one exit listener');
     t.end();
@@ -62,6 +68,10 @@ test('log4js fileAppender', (batch) => {
                 openedFiles.shift();
               };
 
+              this.write = function () {
+                return true;
+              };
+
               this.on = function () {
               };
             }
@@ -71,9 +81,9 @@ test('log4js fileAppender', (batch) => {
     );
 
     for (let i = 0; i < 5; i += 1) {
-      fileAppender.appender(`test${i}`, null, 100);
+      fileAppender.configure({ filename: `test${i}` }, { basicLayout: function () {} });
     }
-    t.ok(openedFiles);
+    t.equal(openedFiles.length, 5);
     exitListener();
     t.equal(openedFiles.length, 0, 'should close all open files');
     t.end();
@@ -82,13 +92,14 @@ test('log4js fileAppender', (batch) => {
   batch.test('with default fileAppender settings', (t) => {
     const testFile = path.join(__dirname, 'fa-default-test.log');
     const logger = log4js.getLogger('default-settings');
-    remove(testFile);
+    removeFile(testFile);
 
-    log4js.clearAppenders();
-    log4js.addAppender(
-      require('../../lib/appenders/file').appender(testFile),
-      'default-settings'
-    );
+    t.tearDown(() => { removeFile(testFile); });
+
+    log4js.configure({
+      appenders: { file: { type: 'file', filename: testFile } },
+      categories: { default: { appenders: ['file'], level: 'debug' } }
+    });
 
     logger.info('This should be in the file.');
 
@@ -104,86 +115,25 @@ test('log4js fileAppender', (batch) => {
     }, 100);
   });
 
-  batch.test('fileAppender subcategories', (t) => {
-    log4js.clearAppenders();
-
-    function addAppender(cat) {
-      const testFile = path.join(
-        __dirname,
-        `fa-subcategories-test-${cat.join('-').replace(/\./g, '_')}.log`
-      );
-      remove(testFile);
-      log4js.addAppender(require('../../lib/appenders/file').appender(testFile), cat);
-      return testFile;
-    }
-
-    /* eslint-disable camelcase */
-    const file_sub1 = addAppender(['sub1']);
-    const file_sub1_sub12$sub1_sub13 = addAppender(['sub1.sub12', 'sub1.sub13']);
-    const file_sub1_sub12 = addAppender(['sub1.sub12']);
-    const logger_sub1_sub12_sub123 = log4js.getLogger('sub1.sub12.sub123');
-    const logger_sub1_sub13_sub133 = log4js.getLogger('sub1.sub13.sub133');
-    const logger_sub1_sub14 = log4js.getLogger('sub1.sub14');
-    const logger_sub2 = log4js.getLogger('sub2');
-
-    logger_sub1_sub12_sub123.info('sub1_sub12_sub123');
-    logger_sub1_sub13_sub133.info('sub1_sub13_sub133');
-    logger_sub1_sub14.info('sub1_sub14');
-    logger_sub2.info('sub2');
-
-    setTimeout(() => {
-      t.test('file contents', (assert) => {
-        const fileContents = {
-          file_sub1: fs.readFileSync(file_sub1).toString(),
-          file_sub1_sub12$sub1_sub13: fs.readFileSync(file_sub1_sub12$sub1_sub13).toString(),
-          file_sub1_sub12: fs.readFileSync(file_sub1_sub12).toString()
-        };
-        // everything but category 'sub2'
-        assert.match(
-          fileContents.file_sub1,
-          /^(\[\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\.\d{3}] \[INFO] (sub1.sub12.sub123 - sub1_sub12_sub123|sub1.sub13.sub133 - sub1_sub13_sub133|sub1.sub14 - sub1_sub14)[\s\S]){3}$/ // eslint-disable-line
-        );
-        assert.ok(
-          fileContents.file_sub1.match(/sub123/) &&
-          fileContents.file_sub1.match(/sub133/) &&
-          fileContents.file_sub1.match(/sub14/)
-        );
-        assert.ok(!fileContents.file_sub1.match(/sub2/));
-
-        // only catgories starting with 'sub1.sub12' and 'sub1.sub13'
-        assert.match(
-          fileContents.file_sub1_sub12$sub1_sub13,
-          /^(\[\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\.\d{3}] \[INFO] (sub1.sub12.sub123 - sub1_sub12_sub123|sub1.sub13.sub133 - sub1_sub13_sub133)[\s\S]){2}$/ // eslint-disable-line
-        );
-        assert.ok(
-          fileContents.file_sub1_sub12$sub1_sub13.match(/sub123/) &&
-          fileContents.file_sub1_sub12$sub1_sub13.match(/sub133/)
-        );
-        assert.ok(!fileContents.file_sub1_sub12$sub1_sub13.match(/sub14|sub2/));
-
-        // only catgories starting with 'sub1.sub12'
-        assert.match(
-          fileContents.file_sub1_sub12,
-          /^(\[\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\.\d{3}] \[INFO] (sub1.sub12.sub123 - sub1_sub12_sub123)[\s\S]){1}$/ // eslint-disable-line
-        );
-        assert.ok(!fileContents.file_sub1_sub12.match(/sub14|sub2|sub13/));
-        assert.end();
-      });
-      t.end();
-    }, 3000);
-  });
-
   batch.test('with a max file size and no backups', (t) => {
     const testFile = path.join(__dirname, 'fa-maxFileSize-test.log');
     const logger = log4js.getLogger('max-file-size');
-    remove(testFile);
-    remove(`${testFile}.1`);
+
+    t.tearDown(() => {
+      removeFile(testFile);
+      removeFile(`${testFile}.1`);
+    });
+    removeFile(testFile);
+    removeFile(`${testFile}.1`);
+
     // log file of 100 bytes maximum, no backups
-    log4js.clearAppenders();
-    log4js.addAppender(
-      require('../../lib/appenders/file').appender(testFile, log4js.layouts.basicLayout, 100, 0),
-      'max-file-size'
-    );
+    log4js.configure({
+      appenders: {
+        file: { type: 'file', filename: testFile, maxLogSize: 100, backups: 0 }
+      },
+      categories: { default: { appenders: ['file'], level: 'debug' } }
+    });
+
     logger.info('This is the first log message.');
     logger.info('This is an intermediate log message.');
     logger.info('This is the second log message.');
@@ -206,16 +156,24 @@ test('log4js fileAppender', (batch) => {
   batch.test('with a max file size and 2 backups', (t) => {
     const testFile = path.join(__dirname, 'fa-maxFileSize-with-backups-test.log');
     const logger = log4js.getLogger('max-file-size-backups');
-    remove(testFile);
-    remove(`${testFile}.1`);
-    remove(`${testFile}.2`);
+    removeFile(testFile);
+    removeFile(`${testFile}.1`);
+    removeFile(`${testFile}.2`);
+
+    t.tearDown(() => {
+      removeFile(testFile);
+      removeFile(`${testFile}.1`);
+      removeFile(`${testFile}.2`);
+    });
 
     // log file of 50 bytes maximum, 2 backups
-    log4js.clearAppenders();
-    log4js.addAppender(
-      require('../../lib/appenders/file').appender(testFile, log4js.layouts.basicLayout, 50, 2),
-      'max-file-size-backups'
-    );
+    log4js.configure({
+      appenders: {
+        file: { type: 'file', filename: testFile, maxLogSize: 50, backups: 2 }
+      },
+      categories: { default: { appenders: ['file'], level: 'debug' } }
+    });
+
     logger.info('This is the first log message.');
     logger.info('This is the second log message.');
     logger.info('This is the third log message.');
@@ -258,18 +216,23 @@ test('log4js fileAppender', (batch) => {
   batch.test('with a max file size and 2 compressed backups', (t) => {
     const testFile = path.join(__dirname, 'fa-maxFileSize-with-backups-compressed-test.log');
     const logger = log4js.getLogger('max-file-size-backups');
-    remove(testFile);
-    remove(`${testFile}.1.gz`);
-    remove(`${testFile}.2.gz`);
+    removeFile(testFile);
+    removeFile(`${testFile}.1.gz`);
+    removeFile(`${testFile}.2.gz`);
+
+    t.tearDown(() => {
+      removeFile(testFile);
+      removeFile(`${testFile}.1.gz`);
+      removeFile(`${testFile}.2.gz`);
+    });
 
     // log file of 50 bytes maximum, 2 backups
-    log4js.clearAppenders();
-    log4js.addAppender(
-      require('../../lib/appenders/file').appender(
-        testFile, log4js.layouts.basicLayout, 50, 2, { compress: true }
-      ),
-      'max-file-size-backups'
-    );
+    log4js.configure({
+      appenders: {
+        file: { type: 'file', filename: testFile, maxLogSize: 50, backups: 2, compress: true }
+      },
+      categories: { default: { appenders: ['file'], level: 'debug' } }
+    });
     logger.info('This is the first log message.');
     logger.info('This is the second log message.');
     logger.info('This is the third log message.');
@@ -309,24 +272,6 @@ test('log4js fileAppender', (batch) => {
     }, 1000);
   });
 
-  batch.test('configure with fileAppender', (t) => {
-    // this config file defines one file appender (to ./tmp-tests.log)
-    // and sets the log level for "tests" to WARN
-    log4js.configure('./test/tap/log4js.json');
-    const logger = log4js.getLogger('tests');
-    logger.info('this should not be written to the file');
-    logger.warn('this should be written to the file');
-
-    // wait for the file system to catch up
-    setTimeout(() => {
-      fs.readFile('tmp-tests.log', 'utf8', (err, contents) => {
-        t.include(contents, `this should be written to the file${EOL}`);
-        t.equal(contents.indexOf('this should not be written to the file'), -1);
-        t.end();
-      });
-    }, 100);
-  });
-
   batch.test('when underlying stream errors', (t) => {
     let consoleArgs;
     let errorHandler;
@@ -351,13 +296,16 @@ test('log4js fileAppender', (batch) => {
                   errorHandler = cb;
                 }
               };
+              this.write = function () {
+                return true;
+              };
             }
           }
         }
       }
     );
 
-    fileAppender.appender('test1.log', null, 100);
+    fileAppender.configure({ filename: 'test1.log', maxLogSize: 100 }, { basicLayout: function () {} });
     errorHandler({ error: 'aargh' });
 
     t.test('should log the error to console.error', (assert) => {
diff --git a/test/tap/fileSyncAppender-test.js b/test/tap/fileSyncAppender-test.js
index 4874862b..fc5629ae 100644
--- a/test/tap/fileSyncAppender-test.js
+++ b/test/tap/fileSyncAppender-test.js
@@ -6,8 +6,6 @@ const path = require('path');
 const log4js = require('../../lib/log4js');
 const EOL = require('os').EOL || '\n';
 
-log4js.clearAppenders();
-
 function remove(filename) {
   try {
     fs.unlinkSync(filename);
@@ -22,11 +20,14 @@ test('log4js fileSyncAppender', (batch) => {
     const logger = log4js.getLogger('default-settings');
     remove(testFile);
 
-    log4js.clearAppenders();
-    log4js.addAppender(
-      require('../../lib/appenders/fileSync').appender(testFile),
-      'default-settings'
-    );
+    t.tearDown(() => {
+      remove(testFile);
+    });
+
+    log4js.configure({
+      appenders: { sync: { type: 'fileSync', filename: testFile } },
+      categories: { default: { appenders: ['sync'], level: 'debug' } }
+    });
 
     logger.info('This should be in the file.');
 
@@ -43,21 +44,20 @@ test('log4js fileSyncAppender', (batch) => {
   batch.test('with a max file size and no backups', (t) => {
     const testFile = path.join(__dirname, '/fa-maxFileSize-sync-test.log');
     const logger = log4js.getLogger('max-file-size');
+
     remove(testFile);
     remove(`${testFile}.1`);
+
+    t.tearDown(() => {
+      remove(testFile);
+      remove(`${testFile}.1`);
+    });
+
     // log file of 100 bytes maximum, no backups
-    log4js.clearAppenders();
-    log4js.addAppender(
-      require(
-        '../../lib/appenders/fileSync'
-      ).appender(
-        testFile,
-        log4js.layouts.basicLayout,
-        100,
-        0
-      ),
-      'max-file-size'
-    );
+    log4js.configure({
+      appenders: { sync: { type: 'fileSync', filename: testFile, maxLogSize: 100, backups: 0 } },
+      categories: { default: { appenders: ['sync'], level: 'debug' } }
+    });
     logger.info('This is the first log message.');
     logger.info('This is an intermediate log message.');
     logger.info('This is the second log message.');
@@ -89,17 +89,17 @@ test('log4js fileSyncAppender', (batch) => {
     remove(`${testFile}.1`);
     remove(`${testFile}.2`);
 
+    t.tearDown(() => {
+      remove(testFile);
+      remove(`${testFile}.1`);
+      remove(`${testFile}.2`);
+    });
+
     // log file of 50 bytes maximum, 2 backups
-    log4js.clearAppenders();
-    log4js.addAppender(
-      require('../../lib/appenders/fileSync').appender(
-        testFile,
-        log4js.layouts.basicLayout,
-        50,
-        2
-      ),
-      'max-file-size-backups'
-    );
+    log4js.configure({
+      appenders: { sync: { type: 'fileSync', filename: testFile, maxLogSize: 50, backups: 2 } },
+      categories: { default: { appenders: ['sync'], level: 'debug' } }
+    });
     logger.info('This is the first log message.');
     logger.info('This is the second log message.');
     logger.info('This is the third log message.');
@@ -136,16 +136,16 @@ test('log4js fileSyncAppender', (batch) => {
     // this config defines one file appender (to ./tmp-sync-tests.log)
     // and sets the log level for "tests" to WARN
     log4js.configure({
-      appenders: [
-        {
-          category: 'tests',
-          type: 'file',
-          filename: 'tmp-sync-tests.log',
-          layout: { type: 'messagePassThrough' }
-        }
-      ],
-
-      levels: { tests: 'WARN' }
+      appenders: { sync: {
+        type: 'fileSync',
+        filename: 'tmp-sync-tests.log',
+        layout: { type: 'messagePassThrough' }
+      }
+      },
+      categories: {
+        default: { appenders: ['sync'], level: 'debug' },
+        tests: { appenders: ['sync'], level: 'warn' }
+      }
     });
     const logger = log4js.getLogger('tests');
     logger.info('this should not be written to the file');
diff --git a/test/tap/gelfAppender-test.js b/test/tap/gelfAppender-test.js
index fc822bef..12402e55 100644
--- a/test/tap/gelfAppender-test.js
+++ b/test/tap/gelfAppender-test.js
@@ -2,7 +2,6 @@
 
 const test = require('tap').test;
 const sandbox = require('sandboxed-module');
-const log4js = require('../../lib/log4js');
 const realLayouts = require('../../lib/layouts');
 
 const setupLogging = function (options, category, compressedLength) {
@@ -11,8 +10,9 @@ const setupLogging = function (options, category, compressedLength) {
     socket: {
       packetLength: 0,
       closed: false,
-      close: function () {
+      close: function (cb) {
         this.closed = true;
+        if (cb) cb();
       },
       send: function (pkt, offset, pktLength, port, host) {
         fakeDgram.sent = true;
@@ -62,12 +62,12 @@ const setupLogging = function (options, category, compressedLength) {
     messagePassThroughLayout: realLayouts.messagePassThroughLayout
   };
 
-  const appender = sandbox.require('../../lib/appenders/gelf', {
-    singleOnly: true,
+  const log4js = sandbox.require('../../lib/log4js', {
+    // singleOnly: true,
     requires: {
       dgram: fakeDgram,
       zlib: fakeZlib,
-      '../layouts': fakeLayouts
+      './layouts': fakeLayouts
     },
     globals: {
       process: {
@@ -75,21 +75,29 @@ const setupLogging = function (options, category, compressedLength) {
           if (evt === 'exit') {
             exitHandler = handler;
           }
-        }
+        },
+        env: {}
       },
       console: fakeConsole
     }
   });
 
-  log4js.clearAppenders();
-  log4js.addAppender(appender.configure(options || {}), category || 'gelf-test');
+  options = options || {};
+  options.type = 'gelf';
+
+  log4js.configure({
+    appenders: { gelf: options },
+    categories: { default: { appenders: ['gelf'], level: 'debug' } }
+  });
+
   return {
     dgram: fakeDgram,
     compress: fakeZlib,
     exitHandler: exitHandler,
     console: fakeConsole,
     layouts: fakeLayouts,
-    logger: log4js.getLogger(category || 'gelf-test')
+    logger: log4js.getLogger(category || 'gelf-test'),
+    log4js: log4js
   };
 };
 
@@ -163,6 +171,14 @@ test('log4js gelfAppender', (batch) => {
     t.end();
   });
 
+  batch.test('on shutdown should close open sockets', (t) => {
+    const setup = setupLogging();
+    setup.log4js.shutdown(() => {
+      t.ok(setup.dgram.socket.closed);
+      t.end();
+    });
+  });
+
   batch.test('on zlib error should output to console.error', (t) => {
     const setup = setupLogging();
     setup.compress.shouldError = true;
diff --git a/test/tap/global-log-level-test.js b/test/tap/global-log-level-test.js
deleted file mode 100644
index 14beb61b..00000000
--- a/test/tap/global-log-level-test.js
+++ /dev/null
@@ -1,126 +0,0 @@
-'use strict';
-
-const test = require('tap').test;
-
-test('log4js global loglevel', (batch) => {
-  batch.test('global loglevel', (t) => {
-    const log4js = require('../../lib/log4js');
-
-    t.test('set global loglevel on creation', (assert) => {
-      const log1 = log4js.getLogger('log1');
-      let level = 'OFF';
-      if (log1.level.toString() === level) {
-        level = 'TRACE';
-      }
-      assert.notEqual(log1.level.toString(), level);
-
-      log4js.setGlobalLogLevel(level);
-      assert.equal(log1.level.toString(), level);
-
-      const log2 = log4js.getLogger('log2');
-      assert.equal(log2.level.toString(), level);
-      assert.end();
-    });
-
-    t.test('global change loglevel', (assert) => {
-      const log1 = log4js.getLogger('log1');
-      const log2 = log4js.getLogger('log2');
-      let level = 'OFF';
-      if (log1.level.toString() === level) {
-        level = 'TRACE';
-      }
-      assert.notEqual(log1.level.toString(), level);
-
-      log4js.setGlobalLogLevel(level);
-      assert.equal(log1.level.toString(), level);
-      assert.equal(log2.level.toString(), level);
-      assert.end();
-    });
-
-    t.test('override loglevel', (assert) => {
-      const log1 = log4js.getLogger('log1');
-      const log2 = log4js.getLogger('log2');
-      let level = 'OFF';
-      if (log1.level.toString() === level) {
-        level = 'TRACE';
-      }
-      assert.notEqual(log1.level.toString(), level);
-
-      const oldLevel = log1.level.toString();
-      assert.equal(log2.level.toString(), oldLevel);
-
-      log2.setLevel(level);
-      assert.equal(log1.level.toString(), oldLevel);
-      assert.equal(log2.level.toString(), level);
-      assert.notEqual(oldLevel, level);
-
-      log2.removeLevel();
-      assert.equal(log1.level.toString(), oldLevel);
-      assert.equal(log2.level.toString(), oldLevel);
-      assert.end();
-    });
-
-    t.test('preload loglevel', (assert) => {
-      const log1 = log4js.getLogger('log1');
-      let level = 'OFF';
-      if (log1.level.toString() === level) {
-        level = 'TRACE';
-      }
-      assert.notEqual(log1.level.toString(), level);
-
-      const oldLevel = log1.level.toString();
-      log4js.getLogger('log2').setLevel(level);
-
-      assert.equal(log1.level.toString(), oldLevel);
-
-      // get again same logger but as different variable
-      const log2 = log4js.getLogger('log2');
-      assert.equal(log2.level.toString(), level);
-      assert.notEqual(oldLevel, level);
-
-      log2.removeLevel();
-      assert.equal(log1.level.toString(), oldLevel);
-      assert.equal(log2.level.toString(), oldLevel);
-      assert.end();
-    });
-
-    t.test('set level on all categories', (assert) => {
-      // Get 2 loggers
-      const log1 = log4js.getLogger('log1');
-      const log2 = log4js.getLogger('log2');
-
-      // First a test with 2 categories with different levels
-      const config = {
-        levels: {
-          log1: 'ERROR',
-          log2: 'WARN'
-        }
-      };
-      log4js.configure(config);
-
-      // Check if the levels are set correctly
-      assert.equal('ERROR', log1.level.toString());
-      assert.equal('WARN', log2.level.toString());
-
-      log1.removeLevel();
-      log2.removeLevel();
-
-      // Almost identical test, but now we set
-      // level on all categories
-      const config2 = {
-        levels: {
-          '[all]': 'DEBUG'
-        }
-      };
-      log4js.configure(config2);
-
-      // Check if the loggers got the DEBUG level
-      assert.equal('DEBUG', log1.level.toString());
-      assert.equal('DEBUG', log2.level.toString());
-      assert.end();
-    });
-    t.end();
-  });
-
-  batch.end();
-});
diff --git a/test/tap/hipchatAppender-test.js b/test/tap/hipchatAppender-test.js
index 032bde74..2d58687e 100644
--- a/test/tap/hipchatAppender-test.js
+++ b/test/tap/hipchatAppender-test.js
@@ -1,7 +1,6 @@
 'use strict';
 
 const test = require('tap').test;
-const log4js = require('../../lib/log4js');
 const sandbox = require('sandboxed-module');
 
 function setupLogging(category, options) {
@@ -50,13 +49,19 @@ function setupLogging(category, options) {
     }
   };
 
-  const hipchatModule = sandbox.require('../../lib/appenders/hipchat', {
+  const log4js = sandbox.require('../../lib/log4js', {
     requires: {
       'hipchat-notifier': fakeHipchatNotifier
     }
   });
-  log4js.clearAppenders();
-  log4js.addAppender(hipchatModule.configure(options), category);
+
+  options = options || {};
+  options.type = 'hipchat';
+
+  log4js.configure({
+    appenders: { hipchat: options },
+    categories: { default: { appenders: ['hipchat'], level: 'debug' } }
+  });
 
   return {
     logger: log4js.getLogger(category),
@@ -112,7 +117,7 @@ test('HipChat appender', (batch) => {
   batch.test('when basicLayout is provided', (t) => {
     const topic = setupLogging('myLogger', {
       type: 'hipchat',
-      layout: log4js.layouts.basicLayout
+      layout: { type: 'basic' }
     });
     topic.logger.debug('Log event #3');
 
diff --git a/test/tap/log-abspath-test.js b/test/tap/log-abspath-test.js
deleted file mode 100644
index aa274ac7..00000000
--- a/test/tap/log-abspath-test.js
+++ /dev/null
@@ -1,88 +0,0 @@
-'use strict';
-
-const test = require('tap').test;
-const path = require('path');
-const sandbox = require('sandboxed-module');
-
-test('log4js-abspath', (batch) => {
-  batch.test('options', (t) => {
-    let appenderOptions;
-
-    const log4js = sandbox.require(
-      '../../lib/log4js',
-      {
-        singleOnly: true,
-        requires: {
-          './appenders/fake': {
-            name: 'fake',
-            appender: function () {
-            },
-            configure: function (configuration, options) {
-              appenderOptions = options;
-              return function () {
-              };
-            }
-          }
-        }
-      }
-    );
-
-    const config = {
-      appenders: [
-        {
-          type: 'fake',
-          filename: 'cheesy-wotsits.log'
-        }
-      ]
-    };
-
-    log4js.configure(config, {
-      cwd: '/absolute/path/to'
-    });
-    t.test('should be passed to appenders during configuration', (assert) => {
-      assert.equal(appenderOptions.cwd, '/absolute/path/to');
-      assert.end();
-    });
-    t.end();
-  });
-
-  batch.test('file appender', (t) => {
-    let fileOpened;
-
-    const fileAppender = sandbox.require(
-      '../../lib/appenders/file',
-      {
-        requires: {
-          streamroller: {
-            RollingFileStream: function (file) {
-              fileOpened = file;
-              return {
-                on: function () {
-                },
-                end: function () {
-                }
-              };
-            }
-          }
-        }
-      }
-    );
-
-    fileAppender.configure(
-      {
-        filename: 'whatever.log',
-        maxLogSize: 10
-      },
-      { cwd: '/absolute/path/to' }
-    );
-
-    t.test('should prepend options.cwd to config.filename', (assert) => {
-      const expected = path.sep + path.join('absolute', 'path', 'to', 'whatever.log');
-      assert.equal(fileOpened, expected);
-      assert.end();
-    });
-    t.end();
-  });
-
-  batch.end();
-});
diff --git a/test/tap/logger-test.js b/test/tap/logger-test.js
index 6edf2295..7873c073 100644
--- a/test/tap/logger-test.js
+++ b/test/tap/logger-test.js
@@ -5,31 +5,46 @@ const levels = require('../../lib/levels');
 const loggerModule = require('../../lib/logger');
 
 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.test('constructor with no parameters', (t) => {
-    const logger = new Logger();
+    t.throws(
+      () => new Logger(),
+      new Error('No dispatch function provided.')
+    );
+    t.end();
+  });
+
+  batch.test('constructor with only dispatch', (t) => {
+    const logger = new Logger(dispatch);
     t.equal(logger.category, Logger.DEFAULT_CATEGORY, 'should use default category');
     t.equal(logger.level, levels.TRACE, 'should use TRACE log level');
     t.end();
   });
 
   batch.test('constructor with category', (t) => {
-    const logger = new Logger('cheese');
+    const logger = new Logger(dispatch, 'cheese');
     t.equal(logger.category, 'cheese', 'should use category');
     t.equal(logger.level, levels.TRACE, 'should use TRACE log level');
     t.end();
   });
 
   batch.test('constructor with category and level', (t) => {
-    const logger = new Logger('cheese', 'debug');
+    const logger = new Logger(dispatch, 'cheese', 'debug');
     t.equal(logger.category, 'cheese', 'should use category');
     t.equal(logger.level, levels.DEBUG, 'should use level');
     t.end();
   });
 
   batch.test('isLevelEnabled', (t) => {
-    const logger = new Logger('cheese', 'info');
+    const logger = new Logger(dispatch, 'cheese', 'info');
     const functions = [
       'isTraceEnabled', 'isDebugEnabled', 'isInfoEnabled',
       'isWarnEnabled', 'isErrorEnabled', 'isFatalEnabled'
@@ -52,28 +67,17 @@ test('../../lib/logger', (batch) => {
     t.end();
   });
 
-  batch.test('should emit log events', (t) => {
-    const events = [];
-    const logger = new Logger();
-    logger.addListener('log', (logEvent) => {
-      events.push(logEvent);
-    });
+  batch.test('should send log events to dispatch function', (t) => {
+    const logger = new Logger(dispatch);
     logger.debug('Event 1');
-    loggerModule.disableAllLogWrites();
     logger.debug('Event 2');
-    loggerModule.enableAllLogWrites();
     logger.debug('Event 3');
+    const events = testDispatcher.events;
 
-    t.test('when log writes are enabled', (assert) => {
-      assert.equal(events[0].data[0], 'Event 1');
-      assert.end();
-    });
-
-    t.test('but not when log writes are disabled', (assert) => {
-      assert.equal(events.length, 2);
-      assert.equal(events[1].data[0], 'Event 3');
-      assert.end();
-    });
+    t.equal(events.length, 3);
+    t.equal(events[0].data[0], 'Event 1');
+    t.equal(events[1].data[0], 'Event 2');
+    t.equal(events[2].data[0], 'Event 3');
     t.end();
   });
 
diff --git a/test/tap/logglyAppender-test.js b/test/tap/logglyAppender-test.js
index 8fb25ad2..400a4b8a 100644
--- a/test/tap/logglyAppender-test.js
+++ b/test/tap/logglyAppender-test.js
@@ -1,8 +1,8 @@
 'use strict';
 
 const test = require('tap').test;
-const log4js = require('../../lib/log4js');
 const sandbox = require('sandboxed-module');
+const layouts = require('../../lib/layouts');
 
 function setupLogging(category, options) {
   const msgs = [];
@@ -26,10 +26,10 @@ function setupLogging(category, options) {
     layout: function (type, config) {
       this.type = type;
       this.config = config;
-      return log4js.layouts.messagePassThroughLayout;
+      return layouts.messagePassThroughLayout;
     },
-    basicLayout: log4js.layouts.basicLayout,
-    messagePassThroughLayout: log4js.layouts.messagePassThroughLayout
+    basicLayout: layouts.basicLayout,
+    messagePassThroughLayout: layouts.messagePassThroughLayout
   };
 
   const fakeConsole = {
@@ -39,22 +39,26 @@ function setupLogging(category, options) {
     }
   };
 
-  const logglyModule = sandbox.require('../../lib/appenders/loggly', {
+  const log4js = sandbox.require('../../lib/log4js', {
     requires: {
       loggly: fakeLoggly,
-      '../layouts': fakeLayouts
+      './layouts': fakeLayouts
     },
     globals: {
       console: fakeConsole
     }
   });
 
-  log4js.addAppender(
-    logglyModule.configure(options),
-    logglyModule.shutdown,
-    category);
+  options = options || {};
+  options.type = 'loggly';
+
+  log4js.configure({
+    appenders: { loggly: options },
+    categories: { default: { appenders: ['loggly'], level: 'trace' } }
+  });
 
   return {
+    log4js: log4js,
     logger: log4js.getLogger(category),
     loggly: fakeLoggly,
     layouts: fakeLayouts,
@@ -63,8 +67,6 @@ function setupLogging(category, options) {
   };
 }
 
-log4js.clearAppenders();
-
 function setupTaggedLogging() {
   return setupLogging('loggly', {
     token: 'your-really-long-input-token',
@@ -105,9 +107,9 @@ test('log4js logglyAppender', (batch) => {
       tags: ['tag1', 'tag2']
     });
 
-    log4js.shutdown(() => { t.end(); });
+    setup.log4js.shutdown(() => { t.end(); });
 
-    // shutdown will until after the last message has been sent to loggly
+    // shutdown will wait until after the last message has been sent to loggly
     setup.results[0].cb();
   });
 
diff --git a/test/tap/with-categoryFilter.json b/test/tap/with-categoryFilter.json
deleted file mode 100644
index f1efa4a7..00000000
--- a/test/tap/with-categoryFilter.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
-  "appenders": [
-    {
-      "type": "categoryFilter",
-      "exclude": "web",
-      "appender": {
-        "type": "file",
-        "filename": "test/tap/categoryFilter-noweb.log",
-        "layout": {
-          "type": "messagePassThrough"
-        }
-      }
-    },
-    {
-      "category": "web",
-      "type": "file",
-      "filename": "test/tap/categoryFilter-web.log", 
-      "layout": {
-        "type": "messagePassThrough"
-      }
-    }
-  ]
-}
diff --git a/test/tap/with-dateFile.json b/test/tap/with-dateFile.json
deleted file mode 100644
index 4691278e..00000000
--- a/test/tap/with-dateFile.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
-  "appenders": [
-    {
-      "category": "tests",
-      "type": "dateFile",
-      "filename": "test/tap/date-file-test.log",
-      "pattern": "-from-MM-dd",
-      "layout": {
-        "type": "messagePassThrough"
-      }
-    }
-  ],
-
-  "levels": {
-    "tests":  "WARN"
-  }
-}

From d26b1a147bb7905ba90fb63e9f84a9243a513c60 Mon Sep 17 00:00:00 2001
From: Gareth Jones 
Date: Wed, 8 Feb 2017 09:10:14 +1100
Subject: [PATCH 03/13] refactor: fixed a few more tests

---
 lib/appenders/logLevelFilter.js        |  7 +--
 lib/appenders/logstashUDP.js           | 44 +++++++++--------
 lib/appenders/mailgun.js               | 36 +++++---------
 lib/appenders/multiprocess.js          | 46 ++++++++----------
 lib/log4js.js                          |  2 +-
 test/tap/logLevelFilter-test.js        | 67 ++++++++++++++++++++------
 test/tap/logstashUDP-test.js           | 40 ++++++++++++---
 test/tap/mailgunAppender-test.js       | 24 ++++-----
 test/tap/multiprocess-shutdown-test.js | 10 ++--
 test/tap/with-logLevelFilter.json      | 41 ----------------
 10 files changed, 163 insertions(+), 154 deletions(-)
 delete mode 100644 test/tap/with-logLevelFilter.json

diff --git a/lib/appenders/logLevelFilter.js b/lib/appenders/logLevelFilter.js
index ea0d4202..19239da1 100644
--- a/lib/appenders/logLevelFilter.js
+++ b/lib/appenders/logLevelFilter.js
@@ -1,7 +1,6 @@
 'use strict';
 
 const levels = require('../levels');
-const log4js = require('../log4js');
 
 function logLevelFilter(minLevelString, maxLevelString, appender) {
   const minLevel = levels.toLevel(minLevelString);
@@ -14,11 +13,9 @@ function logLevelFilter(minLevelString, maxLevelString, appender) {
   };
 }
 
-function configure(config, options) {
-  log4js.loadAppender(config.appender.type);
-  const appender = log4js.appenderMakers[config.appender.type](config.appender, options);
+function configure(config, layouts, findAppender) {
+  const appender = findAppender(config.appender);
   return logLevelFilter(config.level, config.maxLevel, appender);
 }
 
-module.exports.appender = logLevelFilter;
 module.exports.configure = configure;
diff --git a/lib/appenders/logstashUDP.js b/lib/appenders/logstashUDP.js
index 7805e09c..4d6ce8ae 100644
--- a/lib/appenders/logstashUDP.js
+++ b/lib/appenders/logstashUDP.js
@@ -1,19 +1,29 @@
 'use strict';
 
-const layouts = require('../layouts');
 const dgram = require('dgram');
 const util = require('util');
 
+function sendLog(udp, host, port, logObject) {
+  const buffer = new Buffer(JSON.stringify(logObject));
+
+  /* eslint no-unused-vars:0 */
+  udp.send(buffer, 0, buffer.length, port, host, (err, bytes) => {
+    if (err) {
+      console.error('log4js.logstashUDP - %s:%p Error: %s', host, port, util.inspect(err));
+    }
+  });
+}
+
+
 function logstashUDP(config, layout) {
   const udp = dgram.createSocket('udp4');
   const type = config.logType ? config.logType : config.category;
-  layout = layout || layouts.dummyLayout;
 
   if (!config.fields) {
     config.fields = {};
   }
 
-  return function log(loggingEvent) {
+  function log(loggingEvent) {
     /*
      https://gist.github.com/jordansissel/2996677
      {
@@ -30,11 +40,9 @@ function logstashUDP(config, layout) {
     /* eslint no-prototype-builtins:1,no-restricted-syntax:[1, "ForInStatement"] */
     if (loggingEvent.data.length > 1) {
       const secondEvData = loggingEvent.data[1];
-      for (const key in secondEvData) {
-        if (secondEvData.hasOwnProperty(key)) {
-          config.fields[key] = secondEvData[key];
-        }
-      }
+      Object.keys(secondEvData).forEach((key) => {
+        config.fields[key] = secondEvData[key];
+      });
     }
     config.fields.level = loggingEvent.level.levelStr;
     config.fields.category = loggingEvent.categoryName;
@@ -52,22 +60,17 @@ function logstashUDP(config, layout) {
       logObject[keys[i]] = config.fields[keys[i]];
     }
     sendLog(udp, config.host, config.port, logObject);
-  };
-}
+  }
 
-function sendLog(udp, host, port, logObject) {
-  const buffer = new Buffer(JSON.stringify(logObject));
+  log.shutdown = function (cb) {
+    udp.close(cb);
+  };
 
-  /* eslint no-unused-vars:0 */
-  udp.send(buffer, 0, buffer.length, port, host, (err, bytes) => {
-    if (err) {
-      console.error('log4js.logstashUDP - %s:%p Error: %s', host, port, util.inspect(err));
-    }
-  });
+  return log;
 }
 
-function configure(config) {
-  let layout;
+function configure(config, layouts) {
+  let layout = layouts.dummyLayout;
   if (config.layout) {
     layout = layouts.layout(config.layout.type, config.layout);
   }
@@ -75,5 +78,4 @@ function configure(config) {
   return logstashUDP(config, layout);
 }
 
-module.exports.appender = logstashUDP;
 module.exports.configure = configure;
diff --git a/lib/appenders/mailgun.js b/lib/appenders/mailgun.js
index 11341ff8..41ee19d1 100644
--- a/lib/appenders/mailgun.js
+++ b/lib/appenders/mailgun.js
@@ -1,21 +1,18 @@
 'use strict';
 
-const layouts = require('../layouts');
 const mailgunFactory = require('mailgun-js');
 
-let layout;
-let config;
-let mailgun;
-
-function mailgunAppender(_config, _layout) {
-  config = _config;
-  layout = _layout || layouts.basicLayout;
+function mailgunAppender(config, layout) {
+  const mailgun = mailgunFactory({
+    apiKey: config.apikey,
+    domain: config.domain
+  });
 
   return (loggingEvent) => {
     const data = {
-      from: _config.from,
-      to: _config.to,
-      subject: _config.subject,
+      from: config.from,
+      to: config.to,
+      subject: config.subject,
       text: layout(loggingEvent, config.timezoneOffset)
     };
 
@@ -26,20 +23,13 @@ function mailgunAppender(_config, _layout) {
   };
 }
 
-function configure(_config) {
-  config = _config;
-
-  if (_config.layout) {
-    layout = layouts.layout(_config.layout.type, _config.layout);
+function configure(config, layouts) {
+  let layout = layouts.basicLayout;
+  if (config.layout) {
+    layout = layouts.layout(config.layout.type, config.layout);
   }
 
-  mailgun = mailgunFactory({
-    apiKey: _config.apikey,
-    domain: _config.domain
-  });
-
-  return mailgunAppender(_config, layout);
+  return mailgunAppender(config, layout);
 }
 
-module.exports.appender = mailgunAppender;
 module.exports.configure = configure;
diff --git a/lib/appenders/multiprocess.js b/lib/appenders/multiprocess.js
index d56a7144..ab0d97be 100644
--- a/lib/appenders/multiprocess.js
+++ b/lib/appenders/multiprocess.js
@@ -1,10 +1,9 @@
 'use strict';
 
-const log4js = require('../log4js');
+const levels = require('../levels');
 const net = require('net');
 
 const END_MSG = '__LOG4JS__';
-const servers = [];
 
 /**
  * Creates a server, listening on config.loggerPort, config.loggerHost.
@@ -21,13 +20,13 @@ function logServer(config) {
     try {
       loggingEvent = JSON.parse(msg);
       loggingEvent.startTime = new Date(loggingEvent.startTime);
-      loggingEvent.level = log4js.levels.toLevel(loggingEvent.level.levelStr);
+      loggingEvent.level = levels.toLevel(loggingEvent.level.levelStr);
     } catch (e) {
       // JSON.parse failed, just log the contents probably a naughty.
       loggingEvent = {
         startTime: new Date(),
         categoryName: 'log4js',
-        level: log4js.levels.ERROR,
+        level: levels.ERROR,
         data: ['Unable to parse log:', msg]
       };
     }
@@ -68,12 +67,19 @@ function logServer(config) {
   });
 
   server.listen(config.loggerPort || 5000, config.loggerHost || 'localhost', function () {
-    servers.push(server);
     // allow the process to exit, if this is the only socket active
     server.unref();
   });
 
-  return actualAppender;
+  function app(event) {
+    return actualAppender(event);
+  }
+
+  app.shutdown = function (cb) {
+    server.close(cb);
+  };
+
+  return app;
 }
 
 function workerAppender(config) {
@@ -114,13 +120,18 @@ function workerAppender(config) {
 
   createSocket();
 
-  return function log(loggingEvent) {
+  function log(loggingEvent) {
     if (canWrite) {
       write(loggingEvent);
     } else {
       buffer.push(loggingEvent);
     }
+  }
+  log.shutdown = function (cb) {
+    socket.removeAllListeners('close');
+    socket.close(cb);
   };
+  return log;
 }
 
 function createAppender(config) {
@@ -131,28 +142,11 @@ function createAppender(config) {
   return workerAppender(config);
 }
 
-function configure(config, options) {
-  let actualAppender;
+function configure(config, layouts, findAppender) {
   if (config.appender && config.mode === 'master') {
-    log4js.loadAppender(config.appender.type);
-    actualAppender = log4js.appenderMakers[config.appender.type](config.appender, options);
-    config.actualAppender = actualAppender;
+    config.actualAppender = findAppender(config.appender);
   }
   return createAppender(config);
 }
 
-function shutdown(done) {
-  let toBeClosed = servers.length;
-  servers.forEach(function (server) {
-    server.close(function () {
-      toBeClosed -= 1;
-      if (toBeClosed < 1) {
-        done();
-      }
-    });
-  });
-}
-
-module.exports.appender = createAppender;
 module.exports.configure = configure;
-module.exports.shutdown = shutdown;
diff --git a/lib/log4js.js b/lib/log4js.js
index 23119d2d..c1a9887e 100644
--- a/lib/log4js.js
+++ b/lib/log4js.js
@@ -133,7 +133,7 @@ function shutdown(cb) {
     return cb();
   }
 
-  appenders.forEach(a => a.shutdown(complete));
+  appenders.filter(a => a.shutdown).forEach(a => a.shutdown(complete));
 
   return null;
 }
diff --git a/test/tap/logLevelFilter-test.js b/test/tap/logLevelFilter-test.js
index 9a09aefd..68fdab37 100644
--- a/test/tap/logLevelFilter-test.js
+++ b/test/tap/logLevelFilter-test.js
@@ -17,20 +17,17 @@ function remove(filename) {
 test('log4js logLevelFilter', (batch) => {
   batch.test('appender', (t) => {
     const log4js = require('../../lib/log4js');
-    const logEvents = [];
+    const recording = require('../../lib/appenders/recording');
 
-    log4js.clearAppenders();
-    log4js.addAppender(
-      require('../../lib/appenders/logLevelFilter')
-        .appender(
-          'ERROR',
-          undefined,
-          (evt) => {
-            logEvents.push(evt);
-          }
-        ),
-      'logLevelTest'
-    );
+    log4js.configure({
+      appenders: {
+        recorder: { type: 'recording' },
+        filtered: { type: 'logLevelFilter', appender: 'recorder', level: 'ERROR' }
+      },
+      categories: {
+        default: { appenders: ['filtered'], level: 'debug' }
+      }
+    });
 
     const logger = log4js.getLogger('logLevelTest');
     logger.debug('this should not trigger an event');
@@ -38,6 +35,8 @@ test('log4js logLevelFilter', (batch) => {
     logger.error('this should, though');
     logger.fatal('so should this');
 
+    const logEvents = recording.replay();
+
     t.test('should only pass log events greater than or equal to its own level', (assert) => {
       assert.equal(logEvents.length, 2);
       assert.equal(logEvents[0].data[0], 'this should, though');
@@ -54,7 +53,47 @@ test('log4js logLevelFilter', (batch) => {
     remove(`${__dirname}/logLevelFilter-warnings.log`);
     remove(`${__dirname}/logLevelFilter-debugs.log`);
 
-    log4js.configure('test/tap/with-logLevelFilter.json');
+    t.tearDown(() => {
+      remove(`${__dirname}/logLevelFilter.log`);
+      remove(`${__dirname}/logLevelFilter-warnings.log`);
+      remove(`${__dirname}/logLevelFilter-debugs.log`);
+    });
+
+    log4js.configure({
+      appenders: {
+        'warning-file': {
+          type: 'file',
+          filename: 'test/tap/logLevelFilter-warnings.log',
+          layout: { type: 'messagePassThrough' }
+        },
+        warnings: {
+          type: 'logLevelFilter',
+          level: 'WARN',
+          appender: 'warning-file'
+        },
+        'debug-file': {
+          type: 'file',
+          filename: 'test/tap/logLevelFilter-debugs.log',
+          layout: { type: 'messagePassThrough' }
+        },
+        debugs: {
+          type: 'logLevelFilter',
+          level: 'TRACE',
+          maxLevel: 'DEBUG',
+          appender: 'debug-file'
+        },
+        tests: {
+          type: 'file',
+          filename: 'test/tap/logLevelFilter.log',
+          layout: {
+            type: 'messagePassThrough'
+          }
+        }
+      },
+      categories: {
+        default: { appenders: ['tests', 'warnings', 'debugs'], level: 'trace' }
+      }
+    });
     const logger = log4js.getLogger('tests');
     logger.debug('debug');
     logger.info('info');
diff --git a/test/tap/logstashUDP-test.js b/test/tap/logstashUDP-test.js
index 5bacc4e5..b50b1bec 100644
--- a/test/tap/logstashUDP-test.js
+++ b/test/tap/logstashUDP-test.js
@@ -1,11 +1,11 @@
 'use strict';
 
 const test = require('tap').test;
-const log4js = require('../../lib/log4js');
 const sandbox = require('sandboxed-module');
 
 function setupLogging(category, options) {
   const udpSent = {};
+  const socket = { closed: false };
 
   const fakeDgram = {
     createSocket: function () {
@@ -18,23 +18,33 @@ function setupLogging(category, options) {
           udpSent.offset = 0;
           udpSent.buffer = buffer;
           callback(undefined, length);
+        },
+        close: function (cb) {
+          socket.closed = true;
+          cb();
         }
       };
     }
   };
 
-  const logstashModule = sandbox.require('../../lib/appenders/logstashUDP', {
-    singleOnly: true,
+  const log4js = sandbox.require('../../lib/log4js', {
     requires: {
       dgram: fakeDgram
     }
   });
-  log4js.clearAppenders();
-  log4js.addAppender(logstashModule.configure(options), category);
+
+  options = options || {};
+  options.type = 'logstashUDP';
+  log4js.configure({
+    appenders: { logstash: options },
+    categories: { default: { appenders: ['logstash'], level: 'trace' } }
+  });
 
   return {
     logger: log4js.getLogger(category),
-    results: udpSent
+    log4js: log4js,
+    results: udpSent,
+    socket: socket
   };
 }
 
@@ -72,7 +82,7 @@ test('logstashUDP appender', (batch) => {
 
     const keys = Object.keys(fields);
     for (let i = 0, length = keys.length; i < length; i += 1) {
-        t.equal(json[keys[i]], fields[keys[i]]);
+      t.equal(json[keys[i]], fields[keys[i]]);
     }
 
     t.equal(JSON.stringify(json.fields), JSON.stringify(fields));
@@ -133,5 +143,21 @@ test('logstashUDP appender', (batch) => {
     t.end();
   });
 
+  batch.test('shutdown should close sockets', (t) => {
+    const setup = setupLogging('myLogger', {
+      host: '127.0.0.1',
+      port: 10001,
+      type: 'logstashUDP',
+      category: 'myLogger',
+      layout: {
+        type: 'dummy'
+      }
+    });
+    setup.log4js.shutdown(() => {
+      t.ok(setup.socket.closed);
+      t.end();
+    });
+  });
+
   batch.end();
 });
diff --git a/test/tap/mailgunAppender-test.js b/test/tap/mailgunAppender-test.js
index 3408a385..248bd4af 100644
--- a/test/tap/mailgunAppender-test.js
+++ b/test/tap/mailgunAppender-test.js
@@ -1,7 +1,7 @@
 'use strict';
 
 const test = require('tap').test;
-const log4js = require('../../lib/log4js');
+const layouts = require('../../lib/layouts');
 const sandbox = require('sandboxed-module');
 
 function setupLogging(category, options) {
@@ -30,10 +30,10 @@ function setupLogging(category, options) {
     layout: function (type, config) {
       this.type = type;
       this.config = config;
-      return log4js.layouts.messagePassThroughLayout;
+      return layouts.messagePassThroughLayout;
     },
-    basicLayout: log4js.layouts.basicLayout,
-    messagePassThroughLayout: log4js.layouts.messagePassThroughLayout
+    basicLayout: layouts.basicLayout,
+    messagePassThroughLayout: layouts.messagePassThroughLayout
   };
 
   const fakeConsole = {
@@ -47,19 +47,21 @@ function setupLogging(category, options) {
     }
   };
 
-
-  const mailgunModule = sandbox.require('../../lib/appenders/mailgun', {
+  const log4js = sandbox.require('../../lib/log4js', {
     requires: {
       'mailgun-js': fakeMailgun,
-      '../layouts': fakeLayouts
+      './layouts': fakeLayouts
     },
     globals: {
       console: fakeConsole
     }
   });
-
-
-  log4js.addAppender(mailgunModule.configure(options), category);
+  options = options || {};
+  options.type = 'mailgun';
+  log4js.configure({
+    appenders: { mailgun: options },
+    categories: { default: { appenders: ['mailgun'], level: 'trace' } }
+  });
 
   return {
     logger: log4js.getLogger(category),
@@ -80,8 +82,6 @@ function checkMessages(assert, result) {
   }
 }
 
-log4js.clearAppenders();
-
 test('log4js mailgunAppender', (batch) => {
   batch.test('mailgun setup', (t) => {
     const result = setupLogging('mailgun setup', {
diff --git a/test/tap/multiprocess-shutdown-test.js b/test/tap/multiprocess-shutdown-test.js
index 1bde5919..660e1b41 100644
--- a/test/tap/multiprocess-shutdown-test.js
+++ b/test/tap/multiprocess-shutdown-test.js
@@ -6,14 +6,16 @@ const net = require('net');
 
 test('multiprocess appender shutdown (master)', { timeout: 2000 }, (t) => {
   log4js.configure({
-    appenders: [
-      {
+    appenders: {
+      stdout: { type: 'stdout' },
+      multi: {
         type: 'multiprocess',
         mode: 'master',
         loggerPort: 12345,
-        appender: { type: 'stdout' }
+        appender: 'stdout'
       }
-    ]
+    },
+    categories: { default: { appenders: ['multi'], level: 'debug' } }
   });
 
   setTimeout(() => {
diff --git a/test/tap/with-logLevelFilter.json b/test/tap/with-logLevelFilter.json
deleted file mode 100644
index 0995d35c..00000000
--- a/test/tap/with-logLevelFilter.json
+++ /dev/null
@@ -1,41 +0,0 @@
-{
-  "appenders": [
-    {
-      "category": "tests",
-      "type": "logLevelFilter",
-      "level": "WARN",
-      "appender": {
-        "type": "file",
-        "filename": "test/tap/logLevelFilter-warnings.log",
-        "layout": {
-          "type": "messagePassThrough"
-        }
-      }
-    },
-    {
-      "category": "tests",
-      "type": "logLevelFilter",
-      "level": "TRACE",
-      "maxLevel": "DEBUG",
-      "appender": {
-        "type": "file",
-        "filename": "test/tap/logLevelFilter-debugs.log",
-        "layout": {
-          "type": "messagePassThrough"
-          }
-        }
-    },
-    {
-      "category": "tests",
-      "type": "file",
-      "filename": "test/tap/logLevelFilter.log",
-      "layout": {
-        "type": "messagePassThrough"
-      }
-    }
-  ],
-
-  "levels": {
-    "tests":  "TRACE"
-  }
-}

From 514fb9c7716b7ea3f43d985ad4d68c6d193469ed Mon Sep 17 00:00:00 2001
From: Gareth Jones 
Date: Mon, 27 Feb 2017 08:25:11 +1100
Subject: [PATCH 04/13] fix(test): got multiprocess tests working

---
 lib/appenders/multiprocess.js |  51 ++++++--
 lib/appenders/recording.js    |   7 +-
 test/tap/multiprocess-test.js | 237 +++++++++++++++++++---------------
 3 files changed, 181 insertions(+), 114 deletions(-)

diff --git a/lib/appenders/multiprocess.js b/lib/appenders/multiprocess.js
index ab0d97be..f6db7f78 100644
--- a/lib/appenders/multiprocess.js
+++ b/lib/appenders/multiprocess.js
@@ -1,5 +1,6 @@
 'use strict';
 
+const debug = require('debug')('log4js:multiprocess');
 const levels = require('../levels');
 const net = require('net');
 
@@ -10,12 +11,13 @@ const END_MSG = '__LOG4JS__';
  * Output goes to config.actualAppender (config.appender is used to
  * set up that appender).
  */
-function logServer(config) {
+function logServer(config, actualAppender) {
   /**
    * Takes a utf-8 string, returns an object with
    * the correct log properties.
    */
   function deserializeLoggingEvent(clientSocket, msg) {
+    debug('deserialising log event');
     let loggingEvent;
     try {
       loggingEvent = JSON.parse(msg);
@@ -37,8 +39,6 @@ function logServer(config) {
     return loggingEvent;
   }
 
-  const actualAppender = config.actualAppender;
-
   /* eslint prefer-arrow-callback:0 */
   const server = net.createServer(function serverCreated(clientSocket) {
     clientSocket.setEncoding('utf8');
@@ -46,11 +46,13 @@ function logServer(config) {
 
     function logTheMessage(msg) {
       if (logMessage.length > 0) {
+        debug('deserialising log event and sending to actual appender');
         actualAppender(deserializeLoggingEvent(clientSocket, msg));
       }
     }
 
     function chunkReceived(chunk) {
+      debug('chunk of data received');
       let event;
       logMessage += chunk || '';
       if (logMessage.indexOf(END_MSG) > -1) {
@@ -67,15 +69,18 @@ function logServer(config) {
   });
 
   server.listen(config.loggerPort || 5000, config.loggerHost || 'localhost', function () {
+    debug('master server listening');
     // allow the process to exit, if this is the only socket active
     server.unref();
   });
 
   function app(event) {
+    debug('log event sent directly to actual appender (local event)');
     return actualAppender(event);
   }
 
   app.shutdown = function (cb) {
+    debug('master shutdown called, closing server');
     server.close(cb);
   };
 
@@ -88,19 +93,24 @@ function workerAppender(config) {
   let socket;
 
   function write(loggingEvent) {
+    debug('Writing log event to socket');
     // 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
-    if (loggingEvent && loggingEvent.stack && JSON.stringify(loggingEvent) === '{}') {
-      loggingEvent = { stack: loggingEvent.stack };
-    }
+    const logData = loggingEvent.data.map((e) => {
+      if (e && e.stack && JSON.stringify(e) === '{}') {
+        e = { stack: e.stack };
+      }
+      return e;
+    });
+    loggingEvent.data = logData;
     socket.write(JSON.stringify(loggingEvent), 'utf8');
     socket.write(END_MSG, 'utf8');
   }
 
   function emptyBuffer() {
     let evt;
-
+    debug('emptying worker buffer');
     /* eslint no-cond-assign:0 */
     while ((evt = buffer.shift())) {
       write(evt);
@@ -108,8 +118,10 @@ function workerAppender(config) {
   }
 
   function createSocket() {
+    debug(`worker appender creating socket to ${config.loggerHost || 'localhost'}:${config.loggerPort || 5000}`);
     socket = net.createConnection(config.loggerPort || 5000, config.loggerHost || 'localhost');
     socket.on('connect', () => {
+      debug('worker socket connected');
       emptyBuffer();
       canWrite = true;
     });
@@ -124,29 +136,44 @@ function workerAppender(config) {
     if (canWrite) {
       write(loggingEvent);
     } else {
+      debug('worker buffering log event because it cannot write at the moment');
       buffer.push(loggingEvent);
     }
   }
   log.shutdown = function (cb) {
+    debug('worker shutdown called');
     socket.removeAllListeners('close');
     socket.close(cb);
   };
   return log;
 }
 
-function createAppender(config) {
+function createAppender(config, appender) {
   if (config.mode === 'master') {
-    return logServer(config);
+    debug('Creating master appender');
+    return logServer(config, appender);
   }
 
+  debug('Creating worker appender');
   return workerAppender(config);
 }
 
 function configure(config, layouts, findAppender) {
-  if (config.appender && config.mode === 'master') {
-    config.actualAppender = findAppender(config.appender);
+  let appender;
+  debug(`configure with mode = ${config.mode}`);
+  if (config.mode === 'master') {
+    if (!config.appender) {
+      debug(`no appender found in config ${config}`);
+      throw new Error('multiprocess master must have an "appender" defined');
+    }
+    debug(`actual appender is ${config.appender}`);
+    appender = findAppender(config.appender);
+    if (!appender) {
+      debug(`actual appender "${config.appender}" not found`);
+      throw new Error(`multiprocess master appender "${config.appender}" not defined`);
+    }
   }
-  return createAppender(config);
+  return createAppender(config, appender);
 }
 
 module.exports.configure = configure;
diff --git a/lib/appenders/recording.js b/lib/appenders/recording.js
index da666072..78992a4d 100644
--- a/lib/appenders/recording.js
+++ b/lib/appenders/recording.js
@@ -1,9 +1,12 @@
 'use strict';
 
+const debug = require('debug')('log4js:recording');
+
 let recordedEvents = [];
 
 function configure() {
   return function (logEvent) {
+    debug(`received logEvent, number of events now ${recordedEvents.length + 1}`);
     recordedEvents.push(logEvent);
   };
 }
@@ -19,5 +22,7 @@ function reset() {
 module.exports = {
   configure: configure,
   replay: replay,
-  reset: reset
+  playback: replay,
+  reset: reset,
+  erase: reset
 };
diff --git a/test/tap/multiprocess-test.js b/test/tap/multiprocess-test.js
index 0b0c61c9..0d6f1ed2 100644
--- a/test/tap/multiprocess-test.js
+++ b/test/tap/multiprocess-test.js
@@ -2,16 +2,13 @@
 
 const test = require('tap').test;
 const sandbox = require('sandboxed-module');
+const recording = require('../../lib/appenders/recording');
 
 function makeFakeNet() {
   return {
-    logEvents: [],
     data: [],
     cbs: {},
     createConnectionCalled: 0,
-    fakeAppender: function (logEvent) {
-      this.logEvents.push(logEvent);
-    },
     createConnection: function (port, host) {
       const fakeNet = this;
       this.port = port;
@@ -54,27 +51,36 @@ function makeFakeNet() {
 }
 
 test('Multiprocess Appender', (batch) => {
+  batch.beforeEach((done) => {
+    recording.erase();
+    done();
+  });
+
   batch.test('worker', (t) => {
     const fakeNet = makeFakeNet();
 
-    const appender = sandbox.require(
-      '../../lib/appenders/multiprocess',
+    const log4js = sandbox.require(
+      '../../lib/log4js',
       {
         requires: {
           net: fakeNet
         }
       }
-    ).appender({ mode: 'worker', loggerPort: 1234, loggerHost: 'pants' });
+    );
+    log4js.configure({
+      appenders: { worker: { type: 'multiprocess', mode: 'worker', loggerPort: 1234, loggerHost: 'pants' } },
+      categories: { default: { appenders: ['worker'], level: 'trace' } }
+    });
 
-    // don't need a proper log event for the worker tests
-    appender('before connect');
+    const logger = log4js.getLogger();
+    logger.info('before connect');
     fakeNet.cbs.connect();
-    appender('after connect');
+    logger.info('after connect');
     fakeNet.cbs.close(true);
-    appender('after error, before connect');
+    logger.info('after error, before connect');
     fakeNet.cbs.connect();
-    appender('after error, after connect');
-    appender(new Error('Error test'));
+    logger.info('after error, after connect');
+    logger.error(new Error('Error test'));
 
     const net = fakeNet;
     t.test('should open a socket to the loggerPort and loggerHost', (assert) => {
@@ -84,23 +90,23 @@ test('Multiprocess Appender', (batch) => {
     });
 
     t.test('should buffer messages written before socket is connected', (assert) => {
-      assert.equal(net.data[0], JSON.stringify('before connect'));
+      assert.include(net.data[0], JSON.stringify('before connect'));
       assert.end();
     });
 
     t.test('should write log messages to socket as json strings with a terminator string', (assert) => {
-      assert.equal(net.data[0], JSON.stringify('before connect'));
+      assert.include(net.data[0], JSON.stringify('before connect'));
       assert.equal(net.data[1], '__LOG4JS__');
-      assert.equal(net.data[2], JSON.stringify('after connect'));
+      assert.include(net.data[2], JSON.stringify('after connect'));
       assert.equal(net.data[3], '__LOG4JS__');
       assert.equal(net.encoding, 'utf8');
       assert.end();
     });
 
     t.test('should attempt to re-open the socket on error', (assert) => {
-      assert.equal(net.data[4], JSON.stringify('after error, before connect'));
+      assert.include(net.data[4], JSON.stringify('after error, before connect'));
       assert.equal(net.data[5], '__LOG4JS__');
-      assert.equal(net.data[6], JSON.stringify('after error, after connect'));
+      assert.include(net.data[6], JSON.stringify('after error, after connect'));
       assert.equal(net.data[7], '__LOG4JS__');
       assert.equal(net.createConnectionCalled, 2);
       assert.end();
@@ -108,48 +114,53 @@ test('Multiprocess Appender', (batch) => {
 
     t.test('should serialize an Error correctly', (assert) => {
       assert.ok(
-        JSON.parse(net.data[8]).stack,
-        `Expected:\n\n${net.data[8]}\n\n to have a 'stack' property`
+        JSON.parse(net.data[8]).data[0].stack,
+        `Expected:\n\n${net.data[8]}\n\n to have a 'data[0].stack' property`
       );
-      const actual = JSON.parse(net.data[8]).stack;
+      const actual = JSON.parse(net.data[8]).data[0].stack;
       assert.match(actual, /^Error: Error test/);
       assert.end();
     });
+
     t.end();
   });
 
   batch.test('worker with timeout', (t) => {
     const fakeNet = makeFakeNet();
 
-    const appender = sandbox.require(
-      '../../lib/appenders/multiprocess',
+    const log4js = sandbox.require(
+      '../../lib/log4js',
       {
         requires: {
           net: fakeNet
         }
       }
-    ).appender({ mode: 'worker' });
+    );
+    log4js.configure({
+      appenders: { worker: { type: 'multiprocess', mode: 'worker' } },
+      categories: { default: { appenders: ['worker'], level: 'trace' } }
+    });
 
-    // don't need a proper log event for the worker tests
-    appender('before connect');
+    const logger = log4js.getLogger();
+    logger.info('before connect');
     fakeNet.cbs.connect();
-    appender('after connect');
+    logger.info('after connect');
     fakeNet.cbs.timeout();
-    appender('after timeout, before close');
+    logger.info('after timeout, before close');
     fakeNet.cbs.close();
-    appender('after close, before connect');
+    logger.info('after close, before connect');
     fakeNet.cbs.connect();
-    appender('after close, after connect');
+    logger.info('after close, after connect');
 
     const net = fakeNet;
 
     t.test('should attempt to re-open the socket', (assert) => {
       // skipping the __LOG4JS__ separators
-      assert.equal(net.data[0], JSON.stringify('before connect'));
-      assert.equal(net.data[2], JSON.stringify('after connect'));
-      assert.equal(net.data[4], JSON.stringify('after timeout, before close'));
-      assert.equal(net.data[6], JSON.stringify('after close, before connect'));
-      assert.equal(net.data[8], JSON.stringify('after close, after connect'));
+      assert.include(net.data[0], JSON.stringify('before connect'));
+      assert.include(net.data[2], JSON.stringify('after connect'));
+      assert.include(net.data[4], JSON.stringify('after timeout, before close'));
+      assert.include(net.data[6], JSON.stringify('after close, before connect'));
+      assert.include(net.data[8], JSON.stringify('after close, after connect'));
       assert.equal(net.createConnectionCalled, 2);
       assert.end();
     });
@@ -159,14 +170,18 @@ test('Multiprocess Appender', (batch) => {
   batch.test('worker defaults', (t) => {
     const fakeNet = makeFakeNet();
 
-    sandbox.require(
-      '../../lib/appenders/multiprocess',
+    const log4js = sandbox.require(
+      '../../lib/log4js',
       {
         requires: {
           net: fakeNet
         }
       }
-    ).appender({ mode: 'worker' });
+    );
+    log4js.configure({
+      appenders: { worker: { type: 'multiprocess', mode: 'worker' } },
+      categories: { default: { appenders: ['worker'], level: 'trace' } }
+    });
 
     t.test('should open a socket to localhost:5000', (assert) => {
       assert.equal(fakeNet.port, 5000);
@@ -179,22 +194,29 @@ test('Multiprocess Appender', (batch) => {
   batch.test('master', (t) => {
     const fakeNet = makeFakeNet();
 
-    const appender = sandbox.require(
-      '../../lib/appenders/multiprocess',
+    const log4js = sandbox.require(
+      '../../lib/log4js',
       {
         requires: {
-          net: fakeNet
+          net: fakeNet,
+          './appenders/recording': recording
         }
       }
-    ).appender({
-      mode: 'master',
-      loggerHost: 'server',
-      loggerPort: 1234,
-      actualAppender: fakeNet.fakeAppender.bind(fakeNet)
+    );
+    log4js.configure({
+      appenders: {
+        recorder: { type: 'recording' },
+        master: {
+          type: 'multiprocess',
+          mode: 'master',
+          loggerPort: 1234,
+          loggerHost: 'server',
+          appender: 'recorder'
+        }
+      },
+      categories: { default: { appenders: ['master'], level: 'trace' } }
     });
 
-    appender('this should be sent to the actual appender directly');
-
     const net = fakeNet;
 
     t.test('should listen for log messages on loggerPort and loggerHost', (assert) => {
@@ -204,7 +226,9 @@ test('Multiprocess Appender', (batch) => {
     });
 
     t.test('should return the underlying appender', (assert) => {
-      assert.equal(net.logEvents[0], 'this should be sent to the actual appender directly');
+      log4js.getLogger().info('this should be sent to the actual appender directly');
+
+      assert.equal(recording.replay()[0].data[0], 'this should be sent to the actual appender directly');
       assert.end();
     });
 
@@ -237,93 +261,104 @@ test('Multiprocess Appender', (batch) => {
       );
       net.cbs.data('bad message__LOG4JS__');
 
+      const logEvents = recording.replay();
       // should parse log messages into log events and send to appender
-      assert.equal(net.logEvents[1].level.toString(), 'ERROR');
-      assert.equal(net.logEvents[1].data[0], 'an error message');
-      assert.equal(net.logEvents[1].remoteAddress, '1.2.3.4');
-      assert.equal(net.logEvents[1].remotePort, '1234');
+      assert.equal(logEvents[0].level.toString(), 'ERROR');
+      assert.equal(logEvents[0].data[0], 'an error message');
+      assert.equal(logEvents[0].remoteAddress, '1.2.3.4');
+      assert.equal(logEvents[0].remotePort, '1234');
 
       // should parse log messages split into multiple chunks'
-      assert.equal(net.logEvents[2].level.toString(), 'DEBUG');
-      assert.equal(net.logEvents[2].data[0], 'some debug');
-      assert.equal(net.logEvents[2].remoteAddress, '1.2.3.4');
-      assert.equal(net.logEvents[2].remotePort, '1234');
+      assert.equal(logEvents[1].level.toString(), 'DEBUG');
+      assert.equal(logEvents[1].data[0], 'some debug');
+      assert.equal(logEvents[1].remoteAddress, '1.2.3.4');
+      assert.equal(logEvents[1].remotePort, '1234');
 
       // should parse multiple log messages in a single chunk'
-      assert.equal(net.logEvents[3].data[0], 'some debug');
-      assert.equal(net.logEvents[4].data[0], 'some debug');
-      assert.equal(net.logEvents[5].data[0], 'some debug');
+      assert.equal(logEvents[2].data[0], 'some debug');
+      assert.equal(logEvents[3].data[0], 'some debug');
+      assert.equal(logEvents[4].data[0], 'some debug');
 
       // should handle log messages sent as part of end event'
-      assert.equal(net.logEvents[6].data[0], "that's all folks");
+      assert.equal(logEvents[5].data[0], "that's all folks");
 
       // should handle unparseable log messages
-      assert.equal(net.logEvents[7].level.toString(), 'ERROR');
-      assert.equal(net.logEvents[7].categoryName, 'log4js');
-      assert.equal(net.logEvents[7].data[0], 'Unable to parse log:');
-      assert.equal(net.logEvents[7].data[1], 'bad message');
+      assert.equal(logEvents[6].level.toString(), 'ERROR');
+      assert.equal(logEvents[6].categoryName, 'log4js');
+      assert.equal(logEvents[6].data[0], 'Unable to parse log:');
+      assert.equal(logEvents[6].data[1], 'bad message');
 
       assert.end();
     });
     t.end();
   });
 
-  batch.test('master defaults', (t) => {
+  batch.test('master without actual appender throws error', (t) => {
     const fakeNet = makeFakeNet();
 
-    sandbox.require(
-      '../../lib/appenders/multiprocess',
+    const log4js = sandbox.require(
+      '../../lib/log4js',
       {
         requires: {
           net: fakeNet
         }
       }
-    ).appender({ mode: 'master' });
-
-    t.test('should listen for log messages on localhost:5000', (assert) => {
-      assert.equal(fakeNet.port, 5000);
-      assert.equal(fakeNet.host, 'localhost');
-      assert.end();
-    });
+    );
+    t.throws(() =>
+      log4js.configure({
+        appenders: { master: { type: 'multiprocess', mode: 'master' } },
+        categories: { default: { appenders: ['master'], level: 'trace' } }
+      }),
+      new Error('multiprocess master must have an "appender" defined')
+    );
     t.end();
   });
 
-  batch.test('configure', (t) => {
-    const results = {};
+  batch.test('master with unknown appender throws error', (t) => {
     const fakeNet = makeFakeNet();
 
-    sandbox.require(
-      '../../lib/appenders/multiprocess',
+    const log4js = sandbox.require(
+      '../../lib/log4js',
       {
         requires: {
-          net: fakeNet,
-          '../log4js': {
-            loadAppender: function (app) {
-              results.appenderLoaded = app;
-            },
-            appenderMakers: {
-              madeupappender: function (config, options) {
-                results.config = config;
-                results.options = options;
-              }
-            }
-          }
+          net: fakeNet
         }
       }
-    ).configure(
+    );
+    t.throws(() =>
+      log4js.configure({
+        appenders: { master: { type: 'multiprocess', mode: 'master', appender: 'cheese' } },
+        categories: { default: { appenders: ['master'], level: 'trace' } }
+      }),
+      new Error('multiprocess master appender "cheese" not defined')
+    );
+    t.end();
+  });
+
+  batch.test('master defaults', (t) => {
+    const fakeNet = makeFakeNet();
+
+    const log4js = sandbox.require(
+      '../../lib/log4js',
       {
-        mode: 'master',
-        appender: {
-          type: 'madeupappender',
-          cheese: 'gouda'
+        requires: {
+          net: fakeNet
         }
-      },
-      { crackers: 'jacobs' }
+      }
     );
+    log4js.configure({
+      appenders: {
+        stdout: { type: 'stdout' },
+        master: { type: 'multiprocess', mode: 'master', appender: 'stdout' }
+      },
+      categories: { default: { appenders: ['master'], level: 'trace' } }
+    });
 
-    t.equal(results.appenderLoaded, 'madeupappender', 'should load underlying appender for master');
-    t.equal(results.config.cheese, 'gouda', 'should pass config to underlying appender');
-    t.equal(results.options.crackers, 'jacobs', 'should pass options to underlying appender');
+    t.test('should listen for log messages on localhost:5000', (assert) => {
+      assert.equal(fakeNet.port, 5000);
+      assert.equal(fakeNet.host, 'localhost');
+      assert.end();
+    });
     t.end();
   });
 

From 46378318ae5d9a138bf2307167cff28c42550a78 Mon Sep 17 00:00:00 2001
From: Gareth Jones 
Date: Tue, 28 Feb 2017 08:22:35 +1100
Subject: [PATCH 05/13] chore(deps): bumped streamroller version

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index be0e054b..04c0b178 100644
--- a/package.json
+++ b/package.json
@@ -40,7 +40,7 @@
     "date-format": "^1.0.0",
     "debug": "^2.2.0",
     "semver": "^5.3.0",
-    "streamroller": "^0.3.0"
+    "streamroller": "^0.4.0"
   },
   "devDependencies": {
     "codecov": "^1.0.1",

From 43332617216dd5bfb11d428d8d42f6cd2231c3ac Mon Sep 17 00:00:00 2001
From: Gareth Jones 
Date: Thu, 9 Mar 2017 08:28:41 +1100
Subject: [PATCH 06/13] fix(levels): added level configuration, fixed smtp
 appender

---
 lib/appenders/clustered.js          |   2 +-
 lib/appenders/gelf.js               |  53 ++---
 lib/appenders/logLevelFilter.js     |  12 +-
 lib/appenders/multiprocess.js       |  13 +-
 lib/appenders/slack.js              |  19 +-
 lib/appenders/smtp.js               | 216 ++++++++---------
 lib/configuration.js                |  43 +++-
 lib/connect-logger.js               | 351 ++++++++++++++--------------
 lib/levels.js                       | 144 ++++++------
 lib/log4js.js                       |  11 +-
 lib/logger.js                       | 125 +++++-----
 test/tap/clusteredAppender-test.js  |   2 +-
 test/tap/connect-logger-test.js     |   4 +-
 test/tap/connect-nolog-test.js      |   4 +-
 test/tap/levels-test.js             |  30 +--
 test/tap/logger-test.js             |   4 +-
 test/tap/newLevel-test.js           | 195 ++++++++++++----
 test/tap/setLevel-asymmetry-test.js |   4 +-
 test/tap/slackAppender-test.js      |  26 ++-
 test/tap/smtpAppender-test.js       |  46 ++--
 20 files changed, 704 insertions(+), 600 deletions(-)

diff --git a/lib/appenders/clustered.js b/lib/appenders/clustered.js
index 350209fc..ef777c68 100755
--- a/lib/appenders/clustered.js
+++ b/lib/appenders/clustered.js
@@ -39,7 +39,7 @@ function deserializeLoggingEvent(loggingEventString) {
   try {
     loggingEvent = JSON.parse(loggingEventString);
     loggingEvent.startTime = new Date(loggingEvent.startTime);
-    loggingEvent.level = log4js.levels.toLevel(loggingEvent.level.levelStr);
+    loggingEvent.level = log4js.levels.getLevel(loggingEvent.level.levelStr);
     // Unwrap serialized errors
     for (let i = 0; i < loggingEvent.data.length; i++) {
       const item = loggingEvent.data[i];
diff --git a/lib/appenders/gelf.js b/lib/appenders/gelf.js
index 2d922b06..fe52502b 100644
--- a/lib/appenders/gelf.js
+++ b/lib/appenders/gelf.js
@@ -1,7 +1,7 @@
 'use strict';
 
 const zlib = require('zlib');
-const levels = require('../levels');
+// const levels = require('../levels');
 const dgram = require('dgram');
 const util = require('util');
 const OS = require('os');
@@ -17,40 +17,31 @@ const LOG_NOTICE = 5;   // normal, but significant, condition(unused)
 const LOG_INFO = 6;     // informational message
 const LOG_DEBUG = 7;    // debug-level message
 
-const levelMapping = {};
-levelMapping[levels.ALL] = LOG_DEBUG;
-levelMapping[levels.TRACE] = LOG_DEBUG;
-levelMapping[levels.DEBUG] = LOG_DEBUG;
-levelMapping[levels.INFO] = LOG_INFO;
-levelMapping[levels.WARN] = LOG_WARNING;
-levelMapping[levels.ERROR] = LOG_ERROR;
-levelMapping[levels.FATAL] = LOG_CRIT;
-
 /**
  * GELF appender that supports sending UDP packets to a GELF compatible server such as Graylog
  *
  * @param layout a function that takes a logevent and returns a string (defaults to none).
- * @param host - host to which to send logs (default:localhost)
- * @param port - port at which to send logs to (default:12201)
- * @param hostname - hostname of the current host (default:OS hostname)
- * @param facility - facility to log to (default:nodejs-server)
+ * @param config.host - host to which to send logs (default:localhost)
+ * @param config.port - port at which to send logs to (default:12201)
+ * @param config.hostname - hostname of the current host (default:OS hostname)
+ * @param config.facility - facility to log to (default:nodejs-server)
  */
 /* eslint no-underscore-dangle:0 */
-function gelfAppender(layout, host, port, hostname, facility) {
-  let config;
-  let customFields;
-  if (typeof host === 'object') {
-    config = host;
-    host = config.host;
-    port = config.port;
-    hostname = config.hostname;
-    facility = config.facility;
-    customFields = config.customFields;
-  }
-
-  host = host || 'localhost';
-  port = port || 12201;
-  hostname = hostname || OS.hostname();
+function gelfAppender(layout, config, levels) {
+  const levelMapping = {};
+  levelMapping[levels.ALL] = LOG_DEBUG;
+  levelMapping[levels.TRACE] = LOG_DEBUG;
+  levelMapping[levels.DEBUG] = LOG_DEBUG;
+  levelMapping[levels.INFO] = LOG_INFO;
+  levelMapping[levels.WARN] = LOG_WARNING;
+  levelMapping[levels.ERROR] = LOG_ERROR;
+  levelMapping[levels.FATAL] = LOG_CRIT;
+
+  const host = config.host || 'localhost';
+  const port = config.port || 12201;
+  const hostname = config.hostname || OS.hostname();
+  const facility = config.facility;
+  const customFields = config.customFields;
 
   const defaultCustomFields = customFields || {};
 
@@ -142,12 +133,12 @@ function gelfAppender(layout, host, port, hostname, facility) {
   return app;
 }
 
-function configure(config, layouts) {
+function configure(config, layouts, findAppender, levels) {
   let layout = layouts.messagePassThroughLayout;
   if (config.layout) {
     layout = layouts.layout(config.layout.type, config.layout);
   }
-  return gelfAppender(layout, config);
+  return gelfAppender(layout, config, levels);
 }
 
 module.exports.configure = configure;
diff --git a/lib/appenders/logLevelFilter.js b/lib/appenders/logLevelFilter.js
index 19239da1..f91e7584 100644
--- a/lib/appenders/logLevelFilter.js
+++ b/lib/appenders/logLevelFilter.js
@@ -1,10 +1,8 @@
 'use strict';
 
-const levels = require('../levels');
-
-function logLevelFilter(minLevelString, maxLevelString, appender) {
-  const minLevel = levels.toLevel(minLevelString);
-  const maxLevel = levels.toLevel(maxLevelString, levels.FATAL);
+function logLevelFilter(minLevelString, maxLevelString, appender, levels) {
+  const minLevel = levels.getLevel(minLevelString);
+  const maxLevel = levels.getLevel(maxLevelString, levels.FATAL);
   return (logEvent) => {
     const eventLevel = logEvent.level;
     if (eventLevel.isGreaterThanOrEqualTo(minLevel) && eventLevel.isLessThanOrEqualTo(maxLevel)) {
@@ -13,9 +11,9 @@ function logLevelFilter(minLevelString, maxLevelString, appender) {
   };
 }
 
-function configure(config, layouts, findAppender) {
+function configure(config, layouts, findAppender, levels) {
   const appender = findAppender(config.appender);
-  return logLevelFilter(config.level, config.maxLevel, appender);
+  return logLevelFilter(config.level, config.maxLevel, appender, levels);
 }
 
 module.exports.configure = configure;
diff --git a/lib/appenders/multiprocess.js b/lib/appenders/multiprocess.js
index f6db7f78..14475f07 100644
--- a/lib/appenders/multiprocess.js
+++ b/lib/appenders/multiprocess.js
@@ -1,7 +1,6 @@
 'use strict';
 
 const debug = require('debug')('log4js:multiprocess');
-const levels = require('../levels');
 const net = require('net');
 
 const END_MSG = '__LOG4JS__';
@@ -11,7 +10,7 @@ const END_MSG = '__LOG4JS__';
  * Output goes to config.actualAppender (config.appender is used to
  * set up that appender).
  */
-function logServer(config, actualAppender) {
+function logServer(config, actualAppender, levels) {
   /**
    * Takes a utf-8 string, returns an object with
    * the correct log properties.
@@ -22,7 +21,7 @@ function logServer(config, actualAppender) {
     try {
       loggingEvent = JSON.parse(msg);
       loggingEvent.startTime = new Date(loggingEvent.startTime);
-      loggingEvent.level = levels.toLevel(loggingEvent.level.levelStr);
+      loggingEvent.level = levels.getLevel(loggingEvent.level.levelStr);
     } catch (e) {
       // JSON.parse failed, just log the contents probably a naughty.
       loggingEvent = {
@@ -148,17 +147,17 @@ function workerAppender(config) {
   return log;
 }
 
-function createAppender(config, appender) {
+function createAppender(config, appender, levels) {
   if (config.mode === 'master') {
     debug('Creating master appender');
-    return logServer(config, appender);
+    return logServer(config, appender, levels);
   }
 
   debug('Creating worker appender');
   return workerAppender(config);
 }
 
-function configure(config, layouts, findAppender) {
+function configure(config, layouts, findAppender, levels) {
   let appender;
   debug(`configure with mode = ${config.mode}`);
   if (config.mode === 'master') {
@@ -173,7 +172,7 @@ function configure(config, layouts, findAppender) {
       throw new Error(`multiprocess master appender "${config.appender}" not defined`);
     }
   }
-  return createAppender(config, appender);
+  return createAppender(config, appender, levels);
 }
 
 module.exports.configure = configure;
diff --git a/lib/appenders/slack.js b/lib/appenders/slack.js
index ae366cd9..694f4f59 100644
--- a/lib/appenders/slack.js
+++ b/lib/appenders/slack.js
@@ -1,14 +1,8 @@
 'use strict';
 
 const Slack = require('slack-node');
-const layouts = require('../layouts');
-
-let layout;
-let slack;
-
-function slackAppender(_config, _layout) {
-  layout = _layout || layouts.basicLayout;
 
+function slackAppender(_config, layout, slack) {
   return (loggingEvent) => {
     const data = {
       channel_id: _config.channel_id,
@@ -31,16 +25,15 @@ function slackAppender(_config, _layout) {
   };
 }
 
-function configure(_config) {
+function configure(_config, layouts) {
+  const slack = new Slack(_config.token);
+
+  let layout = layouts.basicLayout;
   if (_config.layout) {
     layout = layouts.layout(_config.layout.type, _config.layout);
   }
 
-  slack = new Slack(_config.token);
-
-  return slackAppender(_config, layout);
+  return slackAppender(_config, layout, slack);
 }
 
-module.exports.name = 'slack';
-module.exports.appender = slackAppender;
 module.exports.configure = configure;
diff --git a/lib/appenders/smtp.js b/lib/appenders/smtp.js
index dca9a3f2..075743f2 100644
--- a/lib/appenders/smtp.js
+++ b/lib/appenders/smtp.js
@@ -1,89 +1,8 @@
 'use strict';
 
-const layouts = require('../layouts');
 const mailer = require('nodemailer');
 const os = require('os');
 
-const logEventBuffer = [];
-let subjectLayout;
-let layout;
-
-let unsentCount = 0;
-let shutdownTimeout;
-
-let sendInterval;
-let sendTimer;
-
-let config;
-
-function sendBuffer() {
-  if (logEventBuffer.length > 0) {
-    const transportOpts = getTransportOptions(config);
-    const transport = mailer.createTransport(transportOpts);
-    const firstEvent = logEventBuffer[0];
-    let body = '';
-    const count = logEventBuffer.length;
-    while (logEventBuffer.length > 0) {
-      body += `${layout(logEventBuffer.shift(), config.timezoneOffset)}\n`;
-    }
-
-    const msg = {
-      to: config.recipients,
-      subject: config.subject || subjectLayout(firstEvent),
-      headers: { Hostname: os.hostname() }
-    };
-
-    if (config.attachment.enable === true) {
-      msg[config.html ? 'html' : 'text'] = config.attachment.message;
-      msg.attachments = [
-        {
-          filename: config.attachment.filename,
-          contentType: 'text/x-log',
-          content: body
-        }
-      ];
-    } else {
-      msg[config.html ? 'html' : 'text'] = body;
-    }
-
-    if (config.sender) {
-      msg.from = config.sender;
-    }
-    transport.sendMail(msg, (error) => {
-      if (error) {
-        console.error('log4js.smtpAppender - Error happened', error);
-      }
-      transport.close();
-      unsentCount -= count;
-    });
-  }
-}
-
-function getTransportOptions() {
-  let transportOpts = null;
-  if (config.SMTP) {
-    transportOpts = config.SMTP;
-  } else if (config.transport) {
-    const plugin = config.transport.plugin || 'smtp';
-    const transportModule = `nodemailer-${plugin}-transport`;
-
-    /* eslint global-require:0 */
-    const transporter = require(transportModule); // eslint-disable-line
-    transportOpts = transporter(config.transport.options);
-  }
-
-  return transportOpts;
-}
-
-function scheduleSend() {
-  if (!sendTimer) {
-    sendTimer = setTimeout(() => {
-      sendTimer = null;
-      sendBuffer();
-    }, sendInterval);
-  }
-}
-
 /**
  * SMTP Appender. Sends logging events using SMTP protocol.
  * It can either send an email on each event or group several
@@ -95,9 +14,7 @@ function scheduleSend() {
  *    config.shutdownTimeout time to give up remaining emails (in seconds; defaults to 5).
  * @param _layout a function that takes a logevent and returns a string (defaults to basicLayout).
  */
-function smtpAppender(_config, _layout) {
-  config = _config;
-
+function smtpAppender(config, layout, subjectLayout) {
   if (!config.attachment) {
     config.attachment = {};
   }
@@ -105,13 +22,97 @@ function smtpAppender(_config, _layout) {
   config.attachment.enable = !!config.attachment.enable;
   config.attachment.message = config.attachment.message || 'See logs as attachment';
   config.attachment.filename = config.attachment.filename || 'default.log';
-  layout = _layout || layouts.basicLayout;
-  subjectLayout = layouts.messagePassThroughLayout;
-  sendInterval = config.sendInterval * 1000 || 0;
 
-  shutdownTimeout = ('shutdownTimeout' in config ? config.shutdownTimeout : 5) * 1000;
+  const sendInterval = config.sendInterval * 1000 || 0;
+  const shutdownTimeout = ('shutdownTimeout' in config ? config.shutdownTimeout : 5) * 1000;
+  const transport = mailer.createTransport(getTransportOptions());
+  const logEventBuffer = [];
+
+  let unsentCount = 0;
+  let sendTimer;
+
+  function sendBuffer() {
+    if (logEventBuffer.length > 0) {
+      const firstEvent = logEventBuffer[0];
+      let body = '';
+      const count = logEventBuffer.length;
+      while (logEventBuffer.length > 0) {
+        body += `${layout(logEventBuffer.shift(), config.timezoneOffset)}\n`;
+      }
 
-  return (loggingEvent) => {
+      const msg = {
+        to: config.recipients,
+        subject: config.subject || subjectLayout(firstEvent),
+        headers: { Hostname: os.hostname() }
+      };
+
+      if (config.attachment.enable === true) {
+        msg[config.html ? 'html' : 'text'] = config.attachment.message;
+        msg.attachments = [
+          {
+            filename: config.attachment.filename,
+            contentType: 'text/x-log',
+            content: body
+          }
+        ];
+      } else {
+        msg[config.html ? 'html' : 'text'] = body;
+      }
+
+      if (config.sender) {
+        msg.from = config.sender;
+      }
+      transport.sendMail(msg, (error) => {
+        if (error) {
+          console.error('log4js.smtpAppender - Error happened', error);
+        }
+        transport.close();
+        unsentCount -= count;
+      });
+    }
+  }
+
+  function getTransportOptions() {
+    let options = null;
+    if (config.SMTP) {
+      options = config.SMTP;
+    } else if (config.transport) {
+      options = config.transport.options || {};
+      options.transport = config.transport.plugin || 'smtp';
+    }
+    return options;
+  }
+
+  function scheduleSend() {
+    if (!sendTimer) {
+      sendTimer = setTimeout(() => {
+        sendTimer = null;
+        sendBuffer();
+      }, sendInterval);
+    }
+  }
+
+  function shutdown(cb) {
+    if (shutdownTimeout > 0) {
+      setTimeout(() => {
+        if (sendTimer) {
+          clearTimeout(sendTimer);
+        }
+
+        sendBuffer();
+      }, shutdownTimeout);
+    }
+
+    (function checkDone() {
+      if (unsentCount > 0) {
+        setTimeout(checkDone, 100);
+      } else {
+        cb();
+      }
+    }());
+  }
+
+  const appender = (loggingEvent) => {
     unsentCount++;  // eslint-disable-line no-plusplus
     logEventBuffer.push(loggingEvent);
     if (sendInterval > 0) {
@@ -120,37 +121,20 @@ function smtpAppender(_config, _layout) {
       sendBuffer();
     }
   };
-}
 
-function configure(_config) {
-  config = _config;
-  if (_config.layout) {
-    layout = layouts.layout(_config.layout.type, _config.layout);
-  }
-  return smtpAppender(_config, layout);
-}
+  appender.shutdown = shutdown;
 
-function shutdown(cb) {
-  if (shutdownTimeout > 0) {
-    setTimeout(() => {
-      if (sendTimer) {
-        clearTimeout(sendTimer);
-      }
+  return appender;
+}
 
-      sendBuffer();
-    }, shutdownTimeout);
+function configure(config, layouts) {
+  const subjectLayout = layouts.messagePassThroughLayout;
+  let layout = layouts.basicLayout;
+  if (config.layout) {
+    layout = layouts.layout(config.layout.type, config.layout);
   }
-
-  (function checkDone() {
-    if (unsentCount > 0) {
-      setTimeout(checkDone, 100);
-    } else {
-      cb();
-    }
-  }());
+  return smtpAppender(config, layout, subjectLayout);
 }
 
-module.exports.name = 'smtp';
-module.exports.appender = smtpAppender;
+
 module.exports.configure = configure;
-module.exports.shutdown = shutdown;
diff --git a/lib/configuration.js b/lib/configuration.js
index caebd7db..c348c4bf 100644
--- a/lib/configuration.js
+++ b/lib/configuration.js
@@ -13,6 +13,14 @@ function anObject(thing) {
   return thing && typeof thing === 'object' && !Array.isArray(thing);
 }
 
+function validIdentifier(thing) {
+  return /^[A-Za-z][A-Za-z0-9_]*$/g.test(thing);
+}
+
+function anInteger(thing) {
+  return thing && typeof thing === 'number' && Number.isInteger(thing);
+}
+
 class Configuration {
 
   throwExceptionIf(checks, message) {
@@ -55,7 +63,7 @@ class Configuration {
     if (appenderModule.shutdown) {
       debug(`DEPRECATION: Appender ${config.type} exports a shutdown function.`);
     }
-    return appenderModule.configure(config, layouts, this.configuredAppenders.get.bind(this.configuredAppenders));
+    return appenderModule.configure(config, layouts, this.configuredAppenders.get.bind(this.configuredAppenders), this.configuredLevels);
   }
 
   get appenders() {
@@ -117,18 +125,44 @@ class Configuration {
       });
 
       this.throwExceptionIf(
-        not(levels.toLevel(category.level)),
+        not(this.configuredLevels.getLevel(category.level)),
         `category "${name}" is not valid (level "${category.level}" not recognised;` +
-        ` valid levels are ${levels.levels.join(', ')})`
+        ` valid levels are ${this.configuredLevels.levels.join(', ')})`
       );
 
       debug(`Creating category ${name}`);
-      this.configuredCategories.set(name, { appenders: appenders, level: levels.toLevel(category.level) });
+      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(anInteger(levelConfig[l])),
+          `level "${l}" must have an integer value`
+        );
+      });
+    }
+    this.configuredLevels = levels(levelConfig);
+  }
+
   constructor(candidate) {
     this.candidate = candidate;
 
@@ -136,6 +170,7 @@ class Configuration {
     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.appenders = candidate.appenders;
     this.categories = candidate.categories;
   }
diff --git a/lib/connect-logger.js b/lib/connect-logger.js
index 2c60d3c3..433c1b52 100755
--- a/lib/connect-logger.js
+++ b/lib/connect-logger.js
@@ -2,133 +2,43 @@
 
 'use strict';
 
-const levels = require('./levels');
-
 const DEFAULT_FORMAT = ':remote-addr - -' +
   ' ":method :url HTTP/:http-version"' +
   ' :status :content-length ":referrer"' +
   ' ":user-agent"';
 
-/**
- * Log requests with the given `options` or a `format` string.
- *
- * Options:
- *
- *   - `format`        Format string, see below for tokens
- *   - `level`         A log4js levels instance. Supports also 'auto'
- *   - `nolog`         A string or RegExp to exclude target logs
- *
- * Tokens:
- *
- *   - `:req[header]` ex: `:req[Accept]`
- *   - `:res[header]` ex: `:res[Content-Length]`
- *   - `:http-version`
- *   - `:response-time`
- *   - `:remote-addr`
- *   - `:date`
- *   - `:method`
- *   - `:url`
- *   - `:referrer`
- *   - `:user-agent`
- *   - `:status`
- *
- * @return {Function}
- * @param logger4js
- * @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.toLevel(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.toLevel(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));
-          }
-        }
-      });
-    }
+  /**
+   * Return request url path,
+   * adding this function prevents the Cyclomatic Complexity,
+   * for the assemble_tokens function at low, to pass the tests.
+   *
+   * @param  {IncomingMessage} req
+   * @return {String}
+   * @api private
+   */
 
-    // ensure next gets always called
-    return next();
-  };
+function getUrl(req) {
+  return req.originalUrl || req.url;
 }
 
-/**
- * Adds custom {token, replacement} objects to defaults,
- * overwriting the defaults if any tokens clash
- *
- * @param  {IncomingMessage} req
- * @param  {ServerResponse} res
- * @param  {Array} customTokens
- *    [{ token: string-or-regexp, replacement: string-or-replace-function }]
- * @return {Array}
- */
+
+  /**
+   * Adds custom {token, replacement} objects to defaults,
+   * overwriting the defaults if any tokens clash
+   *
+   * @param  {IncomingMessage} req
+   * @param  {ServerResponse} res
+   * @param  {Array} customTokens
+   *    [{ token: string-or-regexp, replacement: string-or-replace-function }]
+   * @return {Array}
+   */
 function assembleTokens(req, res, customTokens) {
   const arrayUniqueTokens = (array) => {
     const a = array.concat();
     for (let i = 0; i < a.length; ++i) {
       for (let j = i + 1; j < a.length; ++j) {
-        // not === because token can be regexp object
-        /* eslint eqeqeq:0 */
+          // not === because token can be regexp object
+          /* eslint eqeqeq:0 */
         if (a[i].token == a[j].token) {
           a.splice(j--, 1);
         }
@@ -156,20 +66,20 @@ function assembleTokens(req, res, customTokens) {
   defaultTokens.push({
     token: ':remote-addr',
     replacement: req.headers['x-forwarded-for'] ||
-    req.ip ||
-    req._remoteAddress ||
-    (req.socket &&
-      (req.socket.remoteAddress ||
-        (req.socket.socket && req.socket.socket.remoteAddress)
+      req.ip ||
+      req._remoteAddress ||
+      (req.socket &&
+        (req.socket.remoteAddress ||
+          (req.socket.socket && req.socket.socket.remoteAddress)
+        )
       )
-    )
   });
   defaultTokens.push({ token: ':user-agent', replacement: req.headers['user-agent'] });
   defaultTokens.push({
     token: ':content-length',
     replacement: (res._headers && res._headers['content-length']) ||
-    (res.__headers && res.__headers['Content-Length']) ||
-    '-'
+      (res.__headers && res.__headers['Content-Length']) ||
+      '-'
   });
   defaultTokens.push({
     token: /:req\[([^\]]+)]/g,
@@ -181,36 +91,22 @@ function assembleTokens(req, res, customTokens) {
     token: /:res\[([^\]]+)]/g,
     replacement: function (_, field) {
       return res._headers ?
-             (res._headers[field.toLowerCase()] || res.__headers[field])
-        : (res.__headers && res.__headers[field]);
+               (res._headers[field.toLowerCase()] || res.__headers[field])
+          : (res.__headers && res.__headers[field]);
     }
   });
 
   return arrayUniqueTokens(customTokens.concat(defaultTokens));
 }
 
-/**
- * Return request url path,
- * adding this function prevents the Cyclomatic Complexity,
- * for the assemble_tokens function at low, to pass the tests.
- *
- * @param  {IncomingMessage} req
- * @return {String}
- * @api private
- */
-
-function getUrl(req) {
-  return req.originalUrl || req.url;
-}
-
-/**
- * Return formatted log line.
- *
- * @param  {String} str
- * @param {Array} tokens
- * @return {String}
- * @api private
- */
+  /**
+   * Return formatted log line.
+   *
+   * @param  {String} str
+   * @param {Array} tokens
+   * @return {String}
+   * @api private
+   */
 function format(str, tokens) {
   for (let i = 0; i < tokens.length; i++) {
     str = str.replace(tokens[i].token, tokens[i].replacement);
@@ -218,33 +114,33 @@ function format(str, tokens) {
   return str;
 }
 
-/**
- * Return RegExp Object about nolog
- *
- * @param  {String|Array} nolog
- * @return {RegExp}
- * @api private
- *
- * syntax
- *  1. String
- *   1.1 "\\.gif"
- *         NOT LOGGING http://example.com/hoge.gif and http://example.com/hoge.gif?fuga
- *         LOGGING http://example.com/hoge.agif
- *   1.2 in "\\.gif|\\.jpg$"
- *         NOT LOGGING http://example.com/hoge.gif and
- *           http://example.com/hoge.gif?fuga and http://example.com/hoge.jpg?fuga
- *         LOGGING http://example.com/hoge.agif,
- *           http://example.com/hoge.ajpg and http://example.com/hoge.jpg?hoge
- *   1.3 in "\\.(gif|jpe?g|png)$"
- *         NOT LOGGING http://example.com/hoge.gif and http://example.com/hoge.jpeg
- *         LOGGING http://example.com/hoge.gif?uid=2 and http://example.com/hoge.jpg?pid=3
- *  2. RegExp
- *   2.1 in /\.(gif|jpe?g|png)$/
- *         SAME AS 1.3
- *  3. Array
- *   3.1 ["\\.jpg$", "\\.png", "\\.gif"]
- *         SAME AS "\\.jpg|\\.png|\\.gif"
- */
+  /**
+   * Return RegExp Object about nolog
+   *
+   * @param  {String|Array} nolog
+   * @return {RegExp}
+   * @api private
+   *
+   * syntax
+   *  1. String
+   *   1.1 "\\.gif"
+   *         NOT LOGGING http://example.com/hoge.gif and http://example.com/hoge.gif?fuga
+   *         LOGGING http://example.com/hoge.agif
+   *   1.2 in "\\.gif|\\.jpg$"
+   *         NOT LOGGING http://example.com/hoge.gif and
+   *           http://example.com/hoge.gif?fuga and http://example.com/hoge.jpg?fuga
+   *         LOGGING http://example.com/hoge.agif,
+   *           http://example.com/hoge.ajpg and http://example.com/hoge.jpg?hoge
+   *   1.3 in "\\.(gif|jpe?g|png)$"
+   *         NOT LOGGING http://example.com/hoge.gif and http://example.com/hoge.jpeg
+   *         LOGGING http://example.com/hoge.gif?uid=2 and http://example.com/hoge.jpg?pid=3
+   *  2. RegExp
+   *   2.1 in /\.(gif|jpe?g|png)$/
+   *         SAME AS 1.3
+   *  3. Array
+   *   3.1 ["\\.jpg$", "\\.png", "\\.gif"]
+   *         SAME AS "\\.jpg|\\.png|\\.gif"
+   */
 function createNoLogCondition(nolog) {
   let regexp = null;
 
@@ -258,7 +154,7 @@ function createNoLogCondition(nolog) {
     }
 
     if (Array.isArray(nolog)) {
-      // convert to strings
+        // convert to strings
       const regexpsAsStrings = nolog.map(reg => (reg.source ? reg.source : reg));
       regexp = new RegExp(regexpsAsStrings.join('|'));
     }
@@ -267,4 +163,109 @@ function createNoLogCondition(nolog) {
   return regexp;
 }
 
-module.exports.connectLogger = getLogger;
+module.exports = function (levels) {
+  /**
+   * Log requests with the given `options` or a `format` string.
+   *
+   * Options:
+   *
+   *   - `format`        Format string, see below for tokens
+   *   - `level`         A log4js levels instance. Supports also 'auto'
+   *   - `nolog`         A string or RegExp to exclude target logs
+   *
+   * Tokens:
+   *
+   *   - `:req[header]` ex: `:req[Accept]`
+   *   - `:res[header]` ex: `:res[Content-Length]`
+   *   - `:http-version`
+   *   - `:response-time`
+   *   - `:remote-addr`
+   *   - `:date`
+   *   - `:method`
+   *   - `:url`
+   *   - `:referrer`
+   *   - `:user-agent`
+   *   - `:status`
+   *
+   * @return {Function}
+   * @param logger4js
+   * @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();
+    };
+  }
+
+  return { connectLogger: getLogger };
+};
diff --git a/lib/levels.js b/lib/levels.js
index e5330eee..443065ee 100644
--- a/lib/levels.js
+++ b/lib/levels.js
@@ -1,97 +1,87 @@
 'use strict';
 
-/**
- * @name Level
- * @namespace Log4js
- */
-class Level {
-  constructor(level, levelStr) {
-    this.level = level;
-    this.levelStr = levelStr;
-  }
+module.exports = function (customLevels) {
+  /**
+   * @name Level
+   * @namespace Log4js
+   */
+  class Level {
+    constructor(level, levelStr) {
+      this.level = level;
+      this.levelStr = levelStr;
+    }
 
-  toString() {
-    return this.levelStr;
-  }
+    toString() {
+      return this.levelStr;
+    }
 
-  isLessThanOrEqualTo(otherLevel) {
-    if (typeof otherLevel === 'string') {
-      otherLevel = toLevel(otherLevel);
+    isLessThanOrEqualTo(otherLevel) {
+      if (typeof otherLevel === 'string') {
+        otherLevel = getLevel(otherLevel);
+      }
+      return this.level <= otherLevel.level;
     }
-    return this.level <= otherLevel.level;
-  }
 
-  isGreaterThanOrEqualTo(otherLevel) {
-    if (typeof otherLevel === 'string') {
-      otherLevel = toLevel(otherLevel);
+    isGreaterThanOrEqualTo(otherLevel) {
+      if (typeof otherLevel === 'string') {
+        otherLevel = getLevel(otherLevel);
+      }
+      return this.level >= otherLevel.level;
     }
-    return this.level >= otherLevel.level;
-  }
 
-  isEqualTo(otherLevel) {
-    if (typeof otherLevel === 'string') {
-      otherLevel = toLevel(otherLevel);
+    isEqualTo(otherLevel) {
+      if (typeof otherLevel === 'string') {
+        otherLevel = getLevel(otherLevel);
+      }
+      return this.level === otherLevel.level;
     }
-    return this.level === otherLevel.level;
+
   }
 
-}
+  const defaultLevels = {
+    ALL: new Level(Number.MIN_VALUE, 'ALL'),
+    TRACE: new Level(5000, 'TRACE'),
+    DEBUG: new Level(10000, 'DEBUG'),
+    INFO: new Level(20000, 'INFO'),
+    WARN: new Level(30000, 'WARN'),
+    ERROR: new Level(40000, 'ERROR'),
+    FATAL: new Level(50000, 'FATAL'),
+    MARK: new Level(9007199254740992, 'MARK'), // 2^53
+    OFF: new Level(Number.MAX_VALUE, 'OFF')
+  };
 
-/**
- * converts given String to corresponding Level
- * @param {Level|String} sArg -- String value of Level OR Log4js.Level
- * @param {Level} [defaultLevel] -- default Level, if no String representation
- * @return {Level}
- */
-function toLevel(sArg, defaultLevel) {
-  if (!sArg) {
-    return defaultLevel;
+  if (customLevels) {
+    const levels = Object.keys(customLevels);
+    levels.forEach((l) => {
+      defaultLevels[l.toUpperCase()] = new Level(customLevels[l], l.toUpperCase());
+    });
   }
 
-  if (sArg instanceof Level) {
-    module.exports[sArg.toString()] = sArg;
-    return sArg;
-  }
+  /**
+   * converts given String to corresponding Level
+   * @param {Level|String} sArg -- String value of Level OR Log4js.Level
+   * @param {Level} [defaultLevel] -- default Level, if no String representation
+   * @return {Level}
+   */
+  function getLevel(sArg, defaultLevel) {
+    if (!sArg) {
+      return defaultLevel;
+    }
 
-  if (typeof sArg === 'string') {
-    return module.exports[sArg.toUpperCase()] || defaultLevel;
-  }
+    if (sArg instanceof Level) {
+      return sArg;
+    }
 
-  return toLevel(sArg.toString());
-}
+    if (typeof sArg === 'string') {
+      return defaultLevels[sArg.toUpperCase()] || defaultLevel;
+    }
 
-function getLevel(levelStr) {
-  let level;
-  if (typeof levelStr === 'string') {
-    const levelUpper = levelStr.toUpperCase();
-    level = toLevel(levelUpper);
+    return getLevel(sArg.toString());
   }
-  return level;
-}
 
-module.exports = {
-  ALL: new Level(Number.MIN_VALUE, 'ALL'),
-  TRACE: new Level(5000, 'TRACE'),
-  DEBUG: new Level(10000, 'DEBUG'),
-  INFO: new Level(20000, 'INFO'),
-  WARN: new Level(30000, 'WARN'),
-  ERROR: new Level(40000, 'ERROR'),
-  FATAL: new Level(50000, 'FATAL'),
-  MARK: new Level(9007199254740992, 'MARK'), // 2^53
-  OFF: new Level(Number.MAX_VALUE, 'OFF'),
-  toLevel: toLevel,
-  Level: Level,
-  getLevel: getLevel
-};
+  const orderedLevels = Object.keys(defaultLevels).sort((a, b) => b.level - a.level);
+  defaultLevels.getLevel = getLevel;
+  defaultLevels.levels = orderedLevels;
 
-module.exports.levels = [
-  module.exports.ALL,
-  module.exports.TRACE,
-  module.exports.DEBUG,
-  module.exports.INFO,
-  module.exports.WARN,
-  module.exports.ERROR,
-  module.exports.FATAL,
-  module.exports.MARK,
-  module.exports.OFF
-];
+  return defaultLevels;
+};
diff --git a/lib/log4js.js b/lib/log4js.js
index c1a9887e..5c812dd4 100644
--- a/lib/log4js.js
+++ b/lib/log4js.js
@@ -25,9 +25,8 @@
 const debug = require('debug')('log4js:main');
 const fs = require('fs');
 const Configuration = require('./configuration');
-const levels = require('./levels');
-const Logger = require('./logger').Logger;
-const connectLogger = require('./connect-logger').connectLogger;
+const connectModule = require('./connect-logger');
+const logger = require('./logger');
 
 const defaultConfig = {
   appenders: {
@@ -38,7 +37,9 @@ const defaultConfig = {
   }
 };
 
+let Logger;
 let config;
+let connectLogger;
 let enabled = true;
 
 function configForCategory(category) {
@@ -94,6 +95,9 @@ function configure(configurationFileOrObject) {
   }
   debug(`Configuration is ${configObject}`);
   config = new Configuration(configObject);
+  module.exports.levels = config.levels;
+  Logger = logger(config.levels).Logger;
+  connectLogger = connectModule(config.levels).connectLogger;
   enabled = true;
 }
 
@@ -150,7 +154,6 @@ const log4js = {
   getLogger,
   configure,
   shutdown,
-  levels,
   connectLogger
 };
 
diff --git a/lib/logger.js b/lib/logger.js
index cc5cc30a..e49f0bc2 100644
--- a/lib/logger.js
+++ b/lib/logger.js
@@ -3,7 +3,6 @@
 'use strict';
 
 const debug = require('debug')('log4js:logger');
-const levels = require('./levels');
 
 /**
  * @name LoggingEvent
@@ -26,76 +25,80 @@ class LoggingEvent {
   }
 }
 
-/**
- * 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, level) {
-    if (typeof dispatch !== 'function') {
-      throw new Error('No dispatch function provided.');
+module.exports = function (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, level) {
+      if (typeof dispatch !== 'function') {
+        throw new Error('No dispatch function provided.');
+      }
+      this.category = name;
+      this.level = levels.getLevel(level, levels.TRACE);
+      this.dispatch = dispatch;
+      debug(`Logger created (${name}, ${level})`);
     }
-    this.category = name;
-    this.level = levels.toLevel(level, levels.TRACE);
-    this.dispatch = dispatch;
-    debug(`Logger created (${name}, ${level})`);
-  }
 
-  setLevel(level) {
-    this.level = levels.toLevel(level, this.level || levels.TRACE);
-  }
+    setLevel(level) {
+      this.level = levels.getLevel(level, this.level || levels.TRACE);
+    }
 
-  log() {
-    /* eslint prefer-rest-params:0 */
-    // todo: once node v4 support dropped, use rest parameter instead
-    const args = Array.from(arguments);
-    const logLevel = levels.toLevel(args[0], levels.INFO);
-    if (this.isLevelEnabled(logLevel)) {
-      this._log(logLevel, args.slice(1));
+    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);
-  }
+    isLevelEnabled(otherLevel) {
+      return this.level.isLessThanOrEqualTo(otherLevel);
+    }
 
-  _log(level, data) {
-    debug(`sending log data (${level}, ${data}) to appenders`);
-    const loggingEvent = new LoggingEvent(this.category, level, data);
-    this.dispatch(loggingEvent);
+    _log(level, data) {
+      debug(`sending log data (${level}, ${data}) to appenders`);
+      const loggingEvent = new LoggingEvent(this.category, level, data);
+      this.dispatch(loggingEvent);
+    }
   }
-}
 
-function addLevelMethods(target) {
-  const level = levels.toLevel(target);
+  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);
+    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.toString());
-  };
+    Logger.prototype[`is${isLevelMethod}Enabled`] = function () {
+      return this.isLevelEnabled(level.toString());
+    };
 
-  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);
-    }
-  };
-}
+    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);
+  levels.levels.forEach(addLevelMethods);
 
-module.exports.LoggingEvent = LoggingEvent;
-module.exports.Logger = Logger;
+  return {
+    LoggingEvent: LoggingEvent,
+    Logger: Logger
+  };
+};
diff --git a/test/tap/clusteredAppender-test.js b/test/tap/clusteredAppender-test.js
index 83b1e5ee..6ebbaff8 100644
--- a/test/tap/clusteredAppender-test.js
+++ b/test/tap/clusteredAppender-test.js
@@ -2,7 +2,7 @@
 
 const test = require('tap').test;
 const sandbox = require('sandboxed-module');
-const LoggingEvent = require('../../lib/logger').LoggingEvent;
+const LoggingEvent = require('../../lib/logger')(require('../../lib/levels')()).LoggingEvent;
 
 test('log4js cluster appender', (batch) => {
   batch.test('when in master mode', (t) => {
diff --git a/test/tap/connect-logger-test.js b/test/tap/connect-logger-test.js
index 5c61b99e..6c79d074 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');
+  const clm = require('../../lib/connect-logger')(levels);
   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 8d3370da..5404c34f 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');
+  const clm = require('../../lib/connect-logger')(levels);
 
   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 54471917..aeacd1ec 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) {
@@ -74,7 +74,7 @@ test('levels', (batch) => {
           levels.OFF
         ]
       );
-      assertThat(assert, all).isEqualTo([levels.toLevel('ALL')]);
+      assertThat(assert, all).isEqualTo([levels.getLevel('ALL')]);
       assertThat(assert, all).isNotEqualTo(
         [
           levels.TRACE,
@@ -116,7 +116,7 @@ test('levels', (batch) => {
           levels.OFF
         ]
       );
-      assertThat(assert, trace).isEqualTo([levels.toLevel('TRACE')]);
+      assertThat(assert, trace).isEqualTo([levels.getLevel('TRACE')]);
       assertThat(assert, trace).isNotEqualTo(
         [
           levels.ALL,
@@ -156,7 +156,7 @@ test('levels', (batch) => {
           levels.OFF
         ]
       );
-      assertThat(assert, debug).isEqualTo([levels.toLevel('DEBUG')]);
+      assertThat(assert, debug).isEqualTo([levels.getLevel('DEBUG')]);
       assertThat(assert, debug).isNotEqualTo(
         [
           levels.ALL,
@@ -190,7 +190,7 @@ test('levels', (batch) => {
         levels.MARK,
         levels.OFF
       ]);
-      assertThat(assert, info).isEqualTo([levels.toLevel('INFO')]);
+      assertThat(assert, info).isEqualTo([levels.getLevel('INFO')]);
       assertThat(assert, info).isNotEqualTo([
         levels.ALL,
         levels.TRACE,
@@ -222,7 +222,7 @@ test('levels', (batch) => {
       assertThat(assert, warn).isNotGreaterThanOrEqualTo([
         levels.ERROR, levels.FATAL, levels.MARK, levels.OFF
       ]);
-      assertThat(assert, warn).isEqualTo([levels.toLevel('WARN')]);
+      assertThat(assert, warn).isEqualTo([levels.getLevel('WARN')]);
       assertThat(assert, warn).isNotEqualTo([
         levels.ALL,
         levels.TRACE,
@@ -253,7 +253,7 @@ test('levels', (batch) => {
         levels.WARN
       ]);
       assertThat(assert, error).isNotGreaterThanOrEqualTo([levels.FATAL, levels.MARK, levels.OFF]);
-      assertThat(assert, error).isEqualTo([levels.toLevel('ERROR')]);
+      assertThat(assert, error).isEqualTo([levels.getLevel('ERROR')]);
       assertThat(assert, error).isNotEqualTo([
         levels.ALL,
         levels.TRACE,
@@ -287,7 +287,7 @@ test('levels', (batch) => {
         levels.ERROR
       ]);
       assertThat(assert, fatal).isNotGreaterThanOrEqualTo([levels.MARK, levels.OFF]);
-      assertThat(assert, fatal).isEqualTo([levels.toLevel('FATAL')]);
+      assertThat(assert, fatal).isEqualTo([levels.getLevel('FATAL')]);
       assertThat(assert, fatal).isNotEqualTo([
         levels.ALL,
         levels.TRACE,
@@ -323,7 +323,7 @@ test('levels', (batch) => {
         levels.FATAL
       ]);
       assertThat(assert, mark).isNotGreaterThanOrEqualTo([levels.OFF]);
-      assertThat(assert, mark).isEqualTo([levels.toLevel('MARK')]);
+      assertThat(assert, mark).isEqualTo([levels.getLevel('MARK')]);
       assertThat(assert, mark).isNotEqualTo([
         levels.ALL,
         levels.TRACE,
@@ -359,7 +359,7 @@ test('levels', (batch) => {
         levels.FATAL,
         levels.MARK
       ]);
-      assertThat(assert, off).isEqualTo([levels.toLevel('OFF')]);
+      assertThat(assert, off).isEqualTo([levels.getLevel('OFF')]);
       assertThat(assert, off).isNotEqualTo([
         levels.ALL,
         levels.TRACE,
@@ -396,11 +396,11 @@ test('levels', (batch) => {
   });
 
   batch.test('toLevel', (t) => {
-    t.equal(levels.toLevel('debug'), levels.DEBUG);
-    t.equal(levels.toLevel('DEBUG'), levels.DEBUG);
-    t.equal(levels.toLevel('DeBuG'), levels.DEBUG);
-    t.notOk(levels.toLevel('cheese'));
-    t.equal(levels.toLevel('cheese', levels.DEBUG), levels.DEBUG);
+    t.equal(levels.getLevel('debug'), levels.DEBUG);
+    t.equal(levels.getLevel('DEBUG'), levels.DEBUG);
+    t.equal(levels.getLevel('DeBuG'), levels.DEBUG);
+    t.notOk(levels.getLevel('cheese'));
+    t.equal(levels.getLevel('cheese', levels.DEBUG), levels.DEBUG);
     t.end();
   });
 
diff --git a/test/tap/logger-test.js b/test/tap/logger-test.js
index 7873c073..ef2f9543 100644
--- a/test/tap/logger-test.js
+++ b/test/tap/logger-test.js
@@ -1,8 +1,8 @@
 'use strict';
 
 const test = require('tap').test;
-const levels = require('../../lib/levels');
-const loggerModule = require('../../lib/logger');
+const levels = require('../../lib/levels')();
+const loggerModule = require('../../lib/logger')(levels);
 
 const Logger = loggerModule.Logger;
 const testDispatcher = {
diff --git a/test/tap/newLevel-test.js b/test/tap/newLevel-test.js
index b817cfac..e190bb7c 100644
--- a/test/tap/newLevel-test.js
+++ b/test/tap/newLevel-test.js
@@ -1,37 +1,57 @@
 'use strict';
 
 const test = require('tap').test;
-const Level = require('../../lib/levels');
 const log4js = require('../../lib/log4js');
-const loggerModule = require('../../lib/logger');
-
-const Logger = loggerModule.Logger;
+const recording = require('../../lib/appenders/recording');
 
 test('../../lib/logger', (batch) => {
+  batch.beforeEach((done) => {
+    recording.reset();
+    done();
+  });
+
   batch.test('creating a new log level', (t) => {
-    Level.forName('DIAG', 6000);
-    const logger = new Logger();
+    log4js.configure({
+      levels: {
+        DIAG: 6000
+      },
+      appenders: {
+        stdout: { type: 'stdout' }
+      },
+      categories: {
+        default: { appenders: ['stdout'], level: 'trace' }
+      }
+    });
+
+    const logger = log4js.getLogger();
 
     t.test('should export new log level in levels module', (assert) => {
-      assert.ok(Level.DIAG);
-      assert.equal(Level.DIAG.levelStr, 'DIAG');
-      assert.equal(Level.DIAG.level, 6000);
+      assert.ok(log4js.levels.DIAG);
+      assert.equal(log4js.levels.DIAG.levelStr, 'DIAG');
+      assert.equal(log4js.levels.DIAG.level, 6000);
       assert.end();
     });
 
     t.type(logger.diag, 'function', 'should create named function on logger prototype');
     t.type(logger.isDiagEnabled, 'function', 'should create isLevelEnabled function on logger prototype');
+    t.type(logger.info, 'function', 'should retain default levels');
     t.end();
   });
 
   batch.test('creating a new log level with underscores', (t) => {
-    Level.forName('NEW_LEVEL_OTHER', 6000);
-    const logger = new Logger();
+    log4js.configure({
+      levels: {
+        NEW_LEVEL_OTHER: 6000
+      },
+      appenders: { stdout: { type: 'stdout' } },
+      categories: { default: { appenders: ['stdout'], level: 'trace' } }
+    });
+    const logger = log4js.getLogger();
 
     t.test('should export new log level to levels module', (assert) => {
-      assert.ok(Level.NEW_LEVEL_OTHER);
-      assert.equal(Level.NEW_LEVEL_OTHER.levelStr, 'NEW_LEVEL_OTHER');
-      assert.equal(Level.NEW_LEVEL_OTHER.level, 6000);
+      assert.ok(log4js.levels.NEW_LEVEL_OTHER);
+      assert.equal(log4js.levels.NEW_LEVEL_OTHER.levelStr, 'NEW_LEVEL_OTHER');
+      assert.equal(log4js.levels.NEW_LEVEL_OTHER.level, 6000);
       assert.end();
     });
 
@@ -47,19 +67,26 @@ test('../../lib/logger', (batch) => {
   });
 
   batch.test('creating log events containing newly created log level', (t) => {
-    const events = [];
-    const logger = new Logger();
-    logger.addListener('log', (logEvent) => {
-      events.push(logEvent);
+    log4js.configure({
+      levels: {
+        LVL1: 6000,
+        LVL2: 5000
+      },
+      appenders: { recorder: { type: 'recording' } },
+      categories: {
+        default: { appenders: ['recorder'], level: 'LVL1' }
+      }
     });
+    const logger = log4js.getLogger();
 
-    logger.log(Level.forName('LVL1', 6000), 'Event 1');
-    logger.log(Level.getLevel('LVL1'), 'Event 2');
+    logger.log(log4js.levels.getLevel('LVL1', log4js.levels.DEBUG), 'Event 1');
+    logger.log(log4js.levels.getLevel('LVL1'), 'Event 2');
     logger.log('LVL1', 'Event 3');
     logger.lvl1('Event 4');
 
-    logger.setLevel(Level.forName('LVL2', 7000));
-    logger.lvl1('Event 5');
+    logger.lvl2('Event 5');
+
+    const events = recording.replay();
 
     t.test('should show log events with new log level', (assert) => {
       assert.equal(events[0].level.toString(), 'LVL1');
@@ -81,44 +108,126 @@ test('../../lib/logger', (batch) => {
   });
 
   batch.test('creating a new log level with incorrect parameters', (t) => {
-    log4js.levels.forName(9000, 'FAIL_LEVEL_1');
-    log4js.levels.forName('FAIL_LEVEL_2');
+    t.throws(() => {
+      log4js.configure({
+        levels: {
+          cheese: 'biscuits'
+        },
+        appenders: { stdout: { type: 'stdout' } },
+        categories: { default: { appenders: ['stdout'], level: 'trace' } }
+      });
+    }, new Error(
+      'Problem with log4js configuration: ' +
+      "({ levels: { cheese: 'biscuits' },\n  appenders: { stdout: { type: 'stdout' } },\n" +
+      "  categories: { default: { appenders: [ 'stdout' ], level: 'trace' } } }) - " +
+      'level "cheese" must have an integer value'
+    ));
+
+    t.throws(() => {
+      log4js.configure({
+        levels: {
+          '#pants': 3
+        },
+        appenders: { stdout: { type: 'stdout' } },
+        categories: { default: { appenders: ['stdout'], level: 'trace' } }
+      });
+    }, new Error(
+      'Problem with log4js configuration: ' +
+      "({ levels: { '#pants': 3 },\n  appenders: { stdout: { type: 'stdout' } },\n" +
+      "  categories: { default: { appenders: [ 'stdout' ], level: 'trace' } } }) - " +
+      'level name "#pants" is not a valid identifier (must start with a letter, only contain A-Z,a-z,0-9,_)'
+    ));
+
+    t.throws(() => {
+      log4js.configure({
+        levels: {
+          'thing#pants': 3
+        },
+        appenders: { stdout: { type: 'stdout' } },
+        categories: { default: { appenders: ['stdout'], level: 'trace' } }
+      });
+    }, new Error(
+      'Problem with log4js configuration: ' +
+      "({ levels: { 'thing#pants': 3 },\n  appenders: { stdout: { type: 'stdout' } },\n" +
+      "  categories: { default: { appenders: [ 'stdout' ], level: 'trace' } } }) - " +
+      'level name "thing#pants" is not a valid identifier (must start with a letter, only contain A-Z,a-z,0-9,_)'
+    ));
+
+    t.throws(() => {
+      log4js.configure({
+        levels: {
+          '1pants': 3
+        },
+        appenders: { stdout: { type: 'stdout' } },
+        categories: { default: { appenders: ['stdout'], level: 'trace' } }
+      });
+    }, new Error(
+      'Problem with log4js configuration: ' +
+      "({ levels: { '1pants': 3 },\n  appenders: { stdout: { type: 'stdout' } },\n" +
+      "  categories: { default: { appenders: [ 'stdout' ], level: 'trace' } } }) - " +
+      'level name "1pants" is not a valid identifier (must start with a letter, only contain A-Z,a-z,0-9,_)'
+    ));
+
+    t.throws(() => {
+      log4js.configure({
+        levels: {
+          2: 3
+        },
+        appenders: { stdout: { type: 'stdout' } },
+        categories: { default: { appenders: ['stdout'], level: 'trace' } }
+      });
+    }, new Error(
+      'Problem with log4js configuration: ' +
+      "({ levels: { '2': 3 },\n  appenders: { stdout: { type: 'stdout' } },\n" +
+      "  categories: { default: { appenders: [ 'stdout' ], level: 'trace' } } }) - " +
+      'level name "2" is not a valid identifier (must start with a letter, only contain A-Z,a-z,0-9,_)'
+    ));
+
+    t.throws(() => {
+      log4js.configure({
+        levels: {
+          'cheese!': 3
+        },
+        appenders: { stdout: { type: 'stdout' } },
+        categories: { default: { appenders: ['stdout'], level: 'trace' } }
+      });
+    }, new Error(
+      'Problem with log4js configuration: ' +
+      "({ levels: { 'cheese!': 3 },\n  appenders: { stdout: { type: 'stdout' } },\n" +
+      "  categories: { default: { appenders: [ 'stdout' ], level: 'trace' } } }) - " +
+      'level name "cheese!" is not a valid identifier (must start with a letter, only contain A-Z,a-z,0-9,_)'
+    ));
 
-    t.test('should fail to create the level', (assert) => {
-      assert.notOk(Level.FAIL_LEVEL_1);
-      assert.notOk(Level.FAIL_LEVEL_2);
-      assert.end();
-    });
     t.end();
   });
 
   batch.test('calling log with an undefined log level', (t) => {
-    const events = [];
-    const logger = new Logger();
-    logger.addListener('log', (logEvent) => {
-      events.push(logEvent);
+    log4js.configure({
+      appenders: { recorder: { type: 'recording' } },
+      categories: { default: { appenders: ['recorder'], level: 'trace' } }
     });
 
+    const logger = log4js.getLogger();
+
     logger.log('LEVEL_DOES_NEXT_EXIST', 'Event 1');
-    logger.log(Level.forName('LEVEL_DOES_NEXT_EXIST'), 'Event 2');
+    logger.log(log4js.levels.getLevel('LEVEL_DOES_NEXT_EXIST'), 'Event 2');
 
+    const events = recording.replay();
     t.equal(events[0].level.toString(), 'INFO', 'should fall back to INFO');
     t.equal(events[1].level.toString(), 'INFO', 'should fall back to INFO');
     t.end();
   });
 
   batch.test('creating a new level with an existing level name', (t) => {
-    const events = [];
-    const logger = new Logger();
-    logger.addListener('log', (logEvent) => {
-      events.push(logEvent);
+    log4js.configure({
+      levels: {
+        info: 1234
+      },
+      appenders: { stdout: { type: 'stdout' } },
+      categories: { default: { appenders: ['stdout'], level: 'trace' } }
     });
 
-    logger.log(log4js.levels.forName('MY_LEVEL', 9000), 'Event 1');
-    logger.log(log4js.levels.forName('MY_LEVEL', 8000), 'Event 1');
-
-    t.equal(events[0].level.level, 9000, 'should override the existing log level');
-    t.equal(events[1].level.level, 8000, 'should override the existing log level');
+    t.equal(log4js.levels.INFO.level, 1234, 'should override the existing log level');
     t.end();
   });
   batch.end();
diff --git a/test/tap/setLevel-asymmetry-test.js b/test/tap/setLevel-asymmetry-test.js
index c3d52220..5b1f633b 100644
--- a/test/tap/setLevel-asymmetry-test.js
+++ b/test/tap/setLevel-asymmetry-test.js
@@ -15,12 +15,12 @@ const logger = log4js.getLogger('test-setLevel-asymmetry');
 
 // Define the array of levels as string to iterate over.
 const strLevels = ['Trace', 'Debug', 'Info', 'Warn', 'Error', 'Fatal'];
-const log4jsLevels = strLevels.map(log4js.levels.toLevel);
+const log4jsLevels = strLevels.map(log4js.levels.getLevel);
 
 test('log4js setLevel', (batch) => {
   strLevels.forEach((strLevel) => {
     batch.test(`is called with a ${strLevel} as string`, (t) => {
-      const log4jsLevel = log4js.levels.toLevel(strLevel);
+      const log4jsLevel = log4js.levels.getLevel(strLevel);
 
       t.test('should convert string to level correctly', (assert) => {
         logger.setLevel(strLevel);
diff --git a/test/tap/slackAppender-test.js b/test/tap/slackAppender-test.js
index acc1bbbc..75048d67 100644
--- a/test/tap/slackAppender-test.js
+++ b/test/tap/slackAppender-test.js
@@ -1,8 +1,8 @@
 'use strict';
 
 const test = require('tap').test;
-const log4js = require('../../lib/log4js');
 const sandbox = require('sandboxed-module');
+const realLayouts = require('../../lib/layouts');
 
 function setupLogging(category, options) {
   const msgs = [];
@@ -32,11 +32,11 @@ function setupLogging(category, options) {
     layout: function (type, config) {
       this.type = type;
       this.config = config;
-      return log4js.layouts.messagePassThroughLayout;
+      return realLayouts.messagePassThroughLayout;
     },
-    basicLayout: log4js.layouts.basicLayout,
-    coloredLayout: log4js.layouts.coloredLayout,
-    messagePassThroughLayout: log4js.layouts.messagePassThroughLayout
+    basicLayout: realLayouts.basicLayout,
+    coloredLayout: realLayouts.coloredLayout,
+    messagePassThroughLayout: realLayouts.messagePassThroughLayout
   };
 
   const fakeConsole = {
@@ -50,17 +50,25 @@ function setupLogging(category, options) {
     }
   };
 
-  const slackModule = sandbox.require('../../lib/appenders/slack', {
+  const log4js = sandbox.require('../../lib/log4js', {
     requires: {
       'slack-node': fakeSlack,
-      '../layouts': fakeLayouts
+      './layouts': fakeLayouts
     },
     globals: {
       console: fakeConsole
     }
   });
 
-  log4js.addAppender(slackModule.configure(options), category);
+  options.type = 'slack';
+  log4js.configure({
+    appenders: {
+      slack: options
+    },
+    categories: {
+      default: { appenders: ['slack'], level: 'trace' }
+    }
+  });
 
   return {
     logger: log4js.getLogger(category),
@@ -80,8 +88,6 @@ function checkMessages(assert, result) {
   }
 }
 
-log4js.clearAppenders();
-
 test('log4js slackAppender', (batch) => {
   batch.test('slack setup', (t) => {
     const result = setupLogging('slack setup', {
diff --git a/test/tap/smtpAppender-test.js b/test/tap/smtpAppender-test.js
index fef1361a..497d076f 100644
--- a/test/tap/smtpAppender-test.js
+++ b/test/tap/smtpAppender-test.js
@@ -1,10 +1,10 @@
 'use strict';
 
 const test = require('tap').test;
-const log4js = require('../../lib/log4js');
+const realLayouts = require('../../lib/layouts');
 const sandbox = require('sandboxed-module');
 
-function setupLogging(category, options) {
+function setupLogging(category, options, errorOnSend) {
   const msgs = [];
 
   const fakeMailer = {
@@ -12,6 +12,10 @@ function setupLogging(category, options) {
       return {
         config: opts,
         sendMail: function (msg, callback) {
+          if (errorOnSend) {
+            callback({ message: errorOnSend });
+            return;
+          }
           msgs.push(msg);
           callback(null, true);
         },
@@ -25,10 +29,10 @@ function setupLogging(category, options) {
     layout: function (type, config) {
       this.type = type;
       this.config = config;
-      return log4js.layouts.messagePassThroughLayout;
+      return realLayouts.messagePassThroughLayout;
     },
-    basicLayout: log4js.layouts.basicLayout,
-    messagePassThroughLayout: log4js.layouts.messagePassThroughLayout
+    basicLayout: realLayouts.basicLayout,
+    messagePassThroughLayout: realLayouts.messagePassThroughLayout
   };
 
   const fakeConsole = {
@@ -38,23 +42,23 @@ function setupLogging(category, options) {
     }
   };
 
-  const fakeTransportPlugin = function () {
-  };
-
-  const smtpModule = sandbox.require('../../lib/appenders/smtp', {
-    singleOnly: true,
+  const log4js = sandbox.require('../../lib/log4js', {
     requires: {
       nodemailer: fakeMailer,
-      'nodemailer-sendmail-transport': fakeTransportPlugin,
-      'nodemailer-smtp-transport': fakeTransportPlugin,
-      '../layouts': fakeLayouts
+      './layouts': fakeLayouts
     },
     globals: {
       console: fakeConsole
     }
   });
 
-  log4js.addAppender(smtpModule.configure(options), category);
+  options.type = 'smtp';
+  log4js.configure({
+    appenders: {
+      smtp: options
+    },
+    categories: { default: { appenders: ['smtp'], level: 'trace' } }
+  });
 
   return {
     logger: log4js.getLogger(category),
@@ -74,8 +78,6 @@ function checkMessages(assert, result, sender, subject) {
   }
 }
 
-log4js.clearAppenders();
-
 test('log4js smtpAppender', (batch) => {
   batch.test('minimal config', (t) => {
     const setup = setupLogging('minimal config', {
@@ -189,17 +191,7 @@ test('log4js smtpAppender', (batch) => {
       recipients: 'recipient@domain.com',
       sendInterval: 0,
       SMTP: { port: 25, auth: { user: 'user@domain.com' } }
-    });
-
-    setup.mailer.createTransport = function () {
-      return {
-        sendMail: function (msg, cb) {
-          cb({ message: 'oh noes' });
-        },
-        close: function () {
-        }
-      };
-    };
+    }, 'oh noes');
 
     setup.logger.info('This will break');
 

From 2de20d5d005000a4cf81d753f499b58822eb4355 Mon Sep 17 00:00:00 2001
From: Gareth Jones 
Date: Thu, 9 Mar 2017 08:51:02 +1100
Subject: [PATCH 07/13] fix(test): subcategories test now passes

---
 test/tap/subcategories-test.js | 67 ++++++++++++++++++++--------------
 1 file changed, 39 insertions(+), 28 deletions(-)

diff --git a/test/tap/subcategories-test.js b/test/tap/subcategories-test.js
index f803c69a..08295d4a 100644
--- a/test/tap/subcategories-test.js
+++ b/test/tap/subcategories-test.js
@@ -2,16 +2,17 @@
 
 const test = require('tap').test;
 const log4js = require('../../lib/log4js');
-const levels = require('../../lib/levels');
 
 test('subcategories', (batch) => {
   batch.test('loggers created after levels configuration is loaded', (t) => {
     log4js.configure({
-      levels: {
-        sub1: 'WARN',
-        'sub1.sub11': 'TRACE',
-        'sub1.sub11.sub111': 'WARN',
-        'sub1.sub12': 'INFO'
+      appenders: { stdout: { type: 'stdout' } },
+      categories: {
+        default: { appenders: ['stdout'], level: 'TRACE' },
+        sub1: { appenders: ['stdout'], level: 'WARN' },
+        'sub1.sub11': { appenders: ['stdout'], level: 'TRACE' },
+        'sub1.sub11.sub111': { appenders: ['stdout'], level: 'WARN' },
+        'sub1.sub12': { appenders: ['stdout'], level: 'INFO' }
       }
     });
 
@@ -28,15 +29,15 @@ test('subcategories', (batch) => {
     };
 
     t.test('check logger levels', (assert) => {
-      assert.equal(loggers.sub1.level, levels.WARN);
-      assert.equal(loggers.sub11.level, levels.TRACE);
-      assert.equal(loggers.sub111.level, levels.WARN);
-      assert.equal(loggers.sub12.level, levels.INFO);
+      assert.equal(loggers.sub1.level, log4js.levels.WARN);
+      assert.equal(loggers.sub11.level, log4js.levels.TRACE);
+      assert.equal(loggers.sub111.level, log4js.levels.WARN);
+      assert.equal(loggers.sub12.level, log4js.levels.INFO);
 
-      assert.equal(loggers.sub13.level, levels.WARN);
-      assert.equal(loggers.sub112.level, levels.TRACE);
-      assert.equal(loggers.sub121.level, levels.INFO);
-      assert.equal(loggers.sub0.level, levels.TRACE);
+      assert.equal(loggers.sub13.level, log4js.levels.WARN);
+      assert.equal(loggers.sub112.level, log4js.levels.TRACE);
+      assert.equal(loggers.sub121.level, log4js.levels.INFO);
+      assert.equal(loggers.sub0.level, log4js.levels.TRACE);
       assert.end();
     });
 
@@ -44,6 +45,13 @@ test('subcategories', (batch) => {
   });
 
   batch.test('loggers created before levels configuration is loaded', (t) => {
+    // reset to defaults
+    log4js.configure({
+      appenders: { stdout: { type: 'stdout' } },
+      categories: { default: { appenders: ['stdout'], level: 'info' } }
+    });
+
+    // these should all get the default log level of INFO
     const loggers = {
       sub1: log4js.getLogger('sub1'), // WARN
       sub11: log4js.getLogger('sub1.sub11'), // TRACE
@@ -57,24 +65,27 @@ test('subcategories', (batch) => {
     };
 
     log4js.configure({
-      levels: {
-        sub1: 'WARN',
-        'sub1.sub11': 'TRACE',
-        'sub1.sub11.sub111': 'WARN',
-        'sub1.sub12': 'INFO'
+      appenders: { stdout: { type: 'stdout' } },
+      categories: {
+        default: { appenders: ['stdout'], level: 'TRACE' },
+        sub1: { appenders: ['stdout'], level: 'WARN' },
+        'sub1.sub11': { appenders: ['stdout'], level: 'TRACE' },
+        'sub1.sub11.sub111': { appenders: ['stdout'], level: 'WARN' },
+        'sub1.sub12': { appenders: ['stdout'], level: 'INFO' }
       }
     });
 
-    t.test('check logger levels', (assert) => {
-      assert.equal(loggers.sub1.level, levels.WARN);
-      assert.equal(loggers.sub11.level, levels.TRACE);
-      assert.equal(loggers.sub111.level, levels.WARN);
-      assert.equal(loggers.sub12.level, levels.INFO);
+    t.test('will not get new levels', (assert) => {
+      // can't use .equal because by calling log4js.configure we create new instances
+      assert.same(loggers.sub1.level, log4js.levels.INFO);
+      assert.same(loggers.sub11.level, log4js.levels.INFO);
+      assert.same(loggers.sub111.level, log4js.levels.INFO);
+      assert.same(loggers.sub12.level, log4js.levels.INFO);
 
-      assert.equal(loggers.sub13.level, levels.WARN);
-      assert.equal(loggers.sub112.level, levels.TRACE);
-      assert.equal(loggers.sub121.level, levels.INFO);
-      assert.equal(loggers.sub0.level, levels.TRACE);
+      assert.same(loggers.sub13.level, log4js.levels.INFO);
+      assert.same(loggers.sub112.level, log4js.levels.INFO);
+      assert.same(loggers.sub121.level, log4js.levels.INFO);
+      assert.same(loggers.sub0.level, log4js.levels.INFO);
       assert.end();
     });
     t.end();

From dd208520d8e3cc34c9430f54bb09d4ef1955f0cf Mon Sep 17 00:00:00 2001
From: Gareth Jones 
Date: Wed, 15 Mar 2017 10:01:13 +1100
Subject: [PATCH 08/13] fix(logFaces): split logfaces into UDP+HTTP, added
 context to loggers

---
 .../{logFacesAppender.js => logFaces-HTTP.js} | 87 +++++------------
 lib/appenders/logFaces-UDP.js                 | 90 ++++++++++++++++++
 lib/logger.js                                 | 18 +++-
 ...Appender-test.js => logFaces-HTTP-test.js} | 52 +++++++---
 test/tap/logFaces-UDP-test.js                 | 94 +++++++++++++++++++
 test/tap/logger-test.js                       | 29 ++++++
 6 files changed, 291 insertions(+), 79 deletions(-)
 rename lib/appenders/{logFacesAppender.js => logFaces-HTTP.js} (54%)
 create mode 100644 lib/appenders/logFaces-UDP.js
 rename test/tap/{logFacesAppender-test.js => logFaces-HTTP-test.js} (68%)
 create mode 100644 test/tap/logFaces-UDP-test.js

diff --git a/lib/appenders/logFacesAppender.js b/lib/appenders/logFaces-HTTP.js
similarity index 54%
rename from lib/appenders/logFacesAppender.js
rename to lib/appenders/logFaces-HTTP.js
index 1564b3fc..5e2b6fd0 100644
--- a/lib/appenders/logFacesAppender.js
+++ b/lib/appenders/logFaces-HTTP.js
@@ -14,68 +14,24 @@
 'use strict';
 
 const util = require('util');
-
-const context = {};
-
-function datagram(config) {
-  const sock = require('dgram').createSocket('udp4');
-  const host = config.remoteHost || '127.0.0.1';
-  const port = config.port || 55201;
-
-  return function (event) {
-    const buff = new Buffer(JSON.stringify(event));
-    sock.send(buff, 0, buff.length, port, host, (err) => {
-      if (err) {
-        console.error('log4js.logFacesAppender failed to %s:%d, error: %s',
-          host, port, err);
-      }
-    });
-  };
-}
-
-function servlet(config) {
-  const axios = require('axios').create();
-  axios.defaults.baseURL = config.url;
-  axios.defaults.timeout = config.timeout || 5000;
-  axios.defaults.headers = { 'Content-Type': 'application/json' };
-  axios.defaults.withCredentials = true;
-
-  return function (lfsEvent) {
-    axios.post('', lfsEvent)
-      .then((response) => {
-        if (response.status !== 200) {
-          console.error('log4js.logFacesAppender post to %s failed: %d',
-            config.url, response.status);
-        }
-      })
-      .catch((response) => {
-        console.error('log4js.logFacesAppender post to %s excepted: %s',
-          config.url, response.status);
-      });
-  };
-}
+const axios = require('axios');
 
 /**
- * For UDP (node.js) use the following configuration params:
- *   {
-*      "type": "logFacesAppender",       // must be present for instantiation
-*      "application": "LFS-TEST",        // name of the application (domain)
-*      "remoteHost": "127.0.0.1",        // logFaces server address (hostname)
-*      "port": 55201                     // UDP receiver listening port
-*   }
  *
  * For HTTP (browsers or node.js) use the following configuration params:
  *   {
-*      "type": "logFacesAppender",       // must be present for instantiation
-*      "application": "LFS-TEST",        // name of the application (domain)
-*      "url": "http://lfs-server/logs",  // logFaces receiver servlet URL
-*   }
+ *      "type": "logFaces-HTTP",       // must be present for instantiation
+ *      "application": "LFS-TEST",        // name of the application (domain)
+ *      "url": "http://lfs-server/logs",  // logFaces receiver servlet URL
+ *   }
  */
 function logFacesAppender(config) {
-  let send = config.send;
-  if (send === undefined) {
-    send = (config.url === undefined) ? datagram(config) : servlet(config);
-  }
+  const sender = axios.create({
+    baseURL: config.url,
+    timeout: config.timeout || 5000,
+    headers: { 'Content-Type': 'application/json' },
+    withCredentials: true
+  });
 
   return function log(event) {
     // convert to logFaces compact json format
@@ -88,12 +44,22 @@ function logFacesAppender(config) {
     };
 
     // add context variables if exist
-    Object.keys(context).forEach((key) => {
-      lfsEvent[`p_${key}`] = context[key];
+    Object.keys(event.context).forEach((key) => {
+      lfsEvent[`p_${key}`] = event.context[key];
     });
 
     // send to server
-    send(lfsEvent);
+    sender.post('', lfsEvent)
+      .then((response) => {
+        if (response.status !== 200) {
+          console.error('log4js.logFacesAppender post to %s failed: %d',
+            config.url, response.status);
+        }
+      })
+      .catch((response) => {
+        console.error('log4js.logFacesAppender post to %s excepted: %s',
+          config.url, response.status);
+      });
   };
 }
 
@@ -101,10 +67,6 @@ function configure(config) {
   return logFacesAppender(config);
 }
 
-function setContext(key, value) {
-  context[key] = value;
-}
-
 function format(logData) {
   const data = Array.isArray(logData) ?
                logData : Array.prototype.slice.call(arguments);
@@ -126,4 +88,3 @@ function wrapErrorsWithInspect(items) {
 }
 
 module.exports.configure = configure;
-module.exports.setContext = setContext;
diff --git a/lib/appenders/logFaces-UDP.js b/lib/appenders/logFaces-UDP.js
new file mode 100644
index 00000000..a2d3b71f
--- /dev/null
+++ b/lib/appenders/logFaces-UDP.js
@@ -0,0 +1,90 @@
+/**
+ * logFaces appender sends JSON formatted log events to logFaces receivers.
+ * There are two types of receivers supported - raw UDP sockets (for server side apps),
+ * and HTTP (for client side apps). Depending on the usage, this appender
+ * requires either of the two:
+ *
+ * For UDP require 'dgram', see 'https://nodejs.org/api/dgram.html'
+ * For HTTP require 'axios', see 'https://www.npmjs.com/package/axios'
+ *
+ * Make sure your project have relevant dependancy installed before using this appender.
+ */
+
+'use strict';
+
+const util = require('util');
+const dgram = require('dgram');
+
+function datagram(config) {
+  const sock = dgram.createSocket('udp4');
+  const host = config.remoteHost || '127.0.0.1';
+  const port = config.port || 55201;
+
+  return function (event) {
+    const buff = new Buffer(JSON.stringify(event));
+    sock.send(buff, 0, buff.length, port, host, (err) => {
+      if (err) {
+        console.error(`log4js.logFacesUDPAppender error sending to ${host}:${port}, error: `, err);
+      }
+    });
+  };
+}
+
+/**
+ * For UDP (node.js) use the following configuration params:
+ *   {
+ *      "type": "logFaces-UDP",       // must be present for instantiation
+ *      "application": "LFS-TEST",        // name of the application (domain)
+ *      "remoteHost": "127.0.0.1",        // logFaces server address (hostname)
+ *      "port": 55201                     // UDP receiver listening port
+ *   }
+ *
+ */
+function logFacesUDPAppender(config) {
+  const send = datagram(config);
+
+  return function log(event) {
+    // convert to logFaces compact json format
+    const lfsEvent = {
+      a: config.application || '',   // application name
+      t: event.startTime.getTime(),  // time stamp
+      p: event.level.levelStr,       // level (priority)
+      g: event.categoryName,         // logger name
+      m: format(event.data)          // message text
+    };
+
+    // add context variables if exist
+    Object.keys(event.context).forEach((key) => {
+      lfsEvent[`p_${key}`] = event.context[key];
+    });
+
+    // send to server
+    send(lfsEvent);
+  };
+}
+
+function configure(config) {
+  return logFacesUDPAppender(config);
+}
+
+function wrapErrorsWithInspect(items) {
+  return items.map((item) => {
+    if ((item instanceof Error) && item.stack) {
+      return {
+        inspect: function () {
+          return `${util.format(item)}\n${item.stack}`;
+        }
+      };
+    }
+
+    return item;
+  });
+}
+
+function format(logData) {
+  const data = Array.isArray(logData) ?
+               logData : Array.prototype.slice.call(arguments);
+  return util.format.apply(util, wrapErrorsWithInspect(data));
+}
+
+module.exports.configure = configure;
diff --git a/lib/logger.js b/lib/logger.js
index e49f0bc2..20251a09 100644
--- a/lib/logger.js
+++ b/lib/logger.js
@@ -17,11 +17,12 @@ class LoggingEvent {
    * @param {Array} data objects to log
    * @author Seth Chisamore
    */
-  constructor(categoryName, level, data) {
+  constructor(categoryName, level, data, context) {
     this.startTime = new Date();
     this.categoryName = categoryName;
     this.data = data;
     this.level = level;
+    this.context = Object.assign({}, context);
   }
 }
 
@@ -46,6 +47,7 @@ module.exports = function (levels) {
       this.category = name;
       this.level = levels.getLevel(level, levels.TRACE);
       this.dispatch = dispatch;
+      this.context = {};
       debug(`Logger created (${name}, ${level})`);
     }
 
@@ -69,9 +71,21 @@ module.exports = function (levels) {
 
     _log(level, data) {
       debug(`sending log data (${level}, ${data}) to appenders`);
-      const loggingEvent = new LoggingEvent(this.category, level, data);
+      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 = {};
+    }
   }
 
   function addLevelMethods(target) {
diff --git a/test/tap/logFacesAppender-test.js b/test/tap/logFaces-HTTP-test.js
similarity index 68%
rename from test/tap/logFacesAppender-test.js
rename to test/tap/logFaces-HTTP-test.js
index fe1a6322..7988cf35 100644
--- a/test/tap/logFacesAppender-test.js
+++ b/test/tap/logFaces-HTTP-test.js
@@ -1,27 +1,51 @@
 'use strict';
 
 const test = require('tap').test;
-const log4js = require('../../lib/log4js');
+const sandbox = require('sandboxed-module');
 
 function setupLogging(category, options) {
-  const sent = {};
+  const fakeAxios = {
+    args: [],
+    create: function (config) {
+      this.config = config;
+      return {
+        post: function (emptyString, event) {
+          fakeAxios.args.push([emptyString, event]);
+          return {
+            catch: function (cb) {
+              fakeAxios.errorCb = cb;
+            }
+          };
+        }
+      };
+    }
+  };
 
-  function fake(event) {
-    Object.keys(event).forEach((key) => {
-      sent[key] = event[key];
-    });
-  }
+  const fakeConsole = {
+    error: function (msg) {
+      this.msg = msg;
+    }
+  };
 
-  const lfsModule = require('../../lib/appenders/logFacesAppender');
-  options.send = fake;
-  log4js.clearAppenders();
-  log4js.addAppender(lfsModule.configure(options), category);
-  lfsModule.setContext('foo', 'bar');
-  lfsModule.setContext('bar', 'foo');
+  const log4js = sandbox.require('../../lib/log4js', {
+    requires: {
+      axios: fakeAxios
+    },
+    globals: {
+      console: fakeConsole
+    }
+  });
+
+  options.type = 'logFaces-HTTP';
+  log4js.configure({
+    appenders: { http: options },
+    categories: { default: { appenders: ['http'], level: 'trace' } }
+  });
 
   return {
     logger: log4js.getLogger(category),
-    results: sent
+    fakeAxios: fakeAxios,
+    fakeConsole: fakeConsole
   };
 }
 
diff --git a/test/tap/logFaces-UDP-test.js b/test/tap/logFaces-UDP-test.js
new file mode 100644
index 00000000..9ddd05db
--- /dev/null
+++ b/test/tap/logFaces-UDP-test.js
@@ -0,0 +1,94 @@
+'use strict';
+
+const test = require('tap').test;
+const sandbox = require('sandboxed-module');
+
+function setupLogging(category, options) {
+  const fakeDgram = {
+    createSocket: function (type) {
+      fakeDgram.type = type;
+      return {
+        send: function (buffer, start, end, port, host, cb) {
+          fakeDgram.buffer = buffer;
+          fakeDgram.start = start;
+          fakeDgram.end = end;
+          fakeDgram.port = port;
+          fakeDgram.host = host;
+          fakeDgram.cb = cb;
+        }
+      };
+    }
+  };
+
+  const fakeConsole = {
+    error: function (msg, err) {
+      this.msg = msg;
+      this.err = err;
+    }
+  };
+
+  const log4js = sandbox.require('../../lib/log4js', {
+    requires: {
+      dgram: fakeDgram
+    },
+    globals: {
+      console: fakeConsole
+    }
+  });
+
+  options.type = 'logFaces-UDP';
+  log4js.configure({
+    appenders: {
+      udp: options
+    },
+    categories: { default: { appenders: ['udp'], level: 'trace' } }
+  });
+
+  return {
+    logger: log4js.getLogger(category),
+    dgram: fakeDgram,
+    console: fakeConsole
+  };
+}
+
+test('logFaces appender', (batch) => {
+  batch.test('when using UDP receivers', (t) => {
+    const setup = setupLogging('udpCategory', {
+      application: 'LFS-UDP',
+      remoteHost: '127.0.0.1',
+      port: 55201
+    });
+
+    setup.logger.addContext('foo', 'bar');
+    setup.logger.addCcontext('bar', 'foo');
+    setup.logger.error('Log event #2');
+
+    t.test('an event should be sent', (assert) => {
+      const event = JSON.parse(setup.dgram.buffer.toString());
+      assert.equal(event.a, 'LFS-UDP');
+      assert.equal(event.m, 'Log event #2');
+      assert.equal(event.g, 'udpCategory');
+      assert.equal(event.p, 'ERROR');
+      assert.equal(event.p_foo, 'bar');
+      assert.equal(event.p_bar, 'foo');
+
+      // Assert timestamp, up to hours resolution.
+      const date = new Date(event.t);
+      assert.equal(
+        date.toISOString().substring(0, 14),
+        new Date().toISOString().substring(0, 14)
+      );
+      assert.end();
+    });
+
+    t.test('dgram errors should be sent to console.error', (assert) => {
+      setup.dgram.cb('something went wrong');
+      assert.equal(setup.console.msg, 'log4js.logFacesUDPAppender error sending to 127.0.0.1:55201, error: ');
+      assert.equal(setup.console.err, 'something went wrong');
+      assert.end();
+    });
+    t.end();
+  });
+
+  batch.end();
+});
diff --git a/test/tap/logger-test.js b/test/tap/logger-test.js
index ef2f9543..1e72a598 100644
--- a/test/tap/logger-test.js
+++ b/test/tap/logger-test.js
@@ -14,6 +14,11 @@ const testDispatcher = {
 const dispatch = testDispatcher.dispatch.bind(testDispatcher);
 
 test('../../lib/logger', (batch) => {
+  batch.beforeEach((done) => {
+    testDispatcher.events = [];
+    done();
+  });
+
   batch.test('constructor with no parameters', (t) => {
     t.throws(
       () => new Logger(),
@@ -81,5 +86,29 @@ test('../../lib/logger', (batch) => {
     t.end();
   });
 
+  batch.test('should add context values to every event', (t) => {
+    const logger = new Logger(dispatch);
+    logger.debug('Event 1');
+    logger.addContext('cheese', 'edam');
+    logger.debug('Event 2');
+    logger.debug('Event 3');
+    logger.addContext('biscuits', 'timtam');
+    logger.debug('Event 4');
+    logger.removeContext('cheese');
+    logger.debug('Event 5');
+    logger.clearContext();
+    logger.debug('Event 6');
+    const events = testDispatcher.events;
+
+    t.equal(events.length, 6);
+    t.same(events[0].context, {});
+    t.same(events[1].context, { cheese: 'edam' });
+    t.same(events[2].context, { cheese: 'edam' });
+    t.same(events[3].context, { cheese: 'edam', biscuits: 'timtam' });
+    t.same(events[4].context, { biscuits: 'timtam' });
+    t.same(events[5].context, {});
+    t.end();
+  });
+
   batch.end();
 });

From ea14c9a995986c8d359a21ab30cc29d47b2b0c89 Mon Sep 17 00:00:00 2001
From: Gareth Jones 
Date: Fri, 17 Mar 2017 07:48:26 +1100
Subject: [PATCH 09/13] fix(logFaces): got logFaces appenders working

---
 lib/appenders/logFaces-HTTP.js | 15 +++++------
 test/tap/logFaces-HTTP-test.js | 47 +++++++++++++---------------------
 test/tap/logFaces-UDP-test.js  |  2 +-
 3 files changed, 26 insertions(+), 38 deletions(-)

diff --git a/lib/appenders/logFaces-HTTP.js b/lib/appenders/logFaces-HTTP.js
index 5e2b6fd0..41dbac50 100644
--- a/lib/appenders/logFaces-HTTP.js
+++ b/lib/appenders/logFaces-HTTP.js
@@ -50,15 +50,14 @@ function logFacesAppender(config) {
 
     // send to server
     sender.post('', lfsEvent)
-      .then((response) => {
-        if (response.status !== 200) {
-          console.error('log4js.logFacesAppender post to %s failed: %d',
-            config.url, response.status);
+      .catch((error) => {
+        if (error.response) {
+          console.error(
+            `log4js.logFaces-HTTP Appender error posting to ${config.url}: ${error.response.status} - ${error.response.data}`
+          );
+          return;
         }
-      })
-      .catch((response) => {
-        console.error('log4js.logFacesAppender post to %s excepted: %s',
-          config.url, response.status);
+        console.error(`log4js.logFaces-HTTP Appender error: ${error.message}`);
       });
   };
 }
diff --git a/test/tap/logFaces-HTTP-test.js b/test/tap/logFaces-HTTP-test.js
index 7988cf35..05df419e 100644
--- a/test/tap/logFaces-HTTP-test.js
+++ b/test/tap/logFaces-HTTP-test.js
@@ -5,12 +5,11 @@ const sandbox = require('sandboxed-module');
 
 function setupLogging(category, options) {
   const fakeAxios = {
-    args: [],
     create: function (config) {
       this.config = config;
       return {
         post: function (emptyString, event) {
-          fakeAxios.args.push([emptyString, event]);
+          fakeAxios.args = [emptyString, event];
           return {
             catch: function (cb) {
               fakeAxios.errorCb = cb;
@@ -52,15 +51,24 @@ function setupLogging(category, options) {
 test('logFaces appender', (batch) => {
   batch.test('when using HTTP receivers', (t) => {
     const setup = setupLogging('myCategory', {
-      type: 'logFacesAppender',
       application: 'LFS-HTTP',
       url: 'http://localhost/receivers/rx1'
     });
 
+    t.test('axios should be configured', (assert) => {
+      assert.equal(setup.fakeAxios.config.baseURL, 'http://localhost/receivers/rx1');
+      assert.equal(setup.fakeAxios.config.timeout, 5000);
+      assert.equal(setup.fakeAxios.config.withCredentials, true);
+      assert.same(setup.fakeAxios.config.headers, { 'Content-Type': 'application/json' });
+      assert.end();
+    });
+
+    setup.logger.addContext('foo', 'bar');
+    setup.logger.addContext('bar', 'foo');
     setup.logger.warn('Log event #1');
 
     t.test('an event should be sent', (assert) => {
-      const event = setup.results;
+      const event = setup.fakeAxios.args[1];
       assert.equal(event.a, 'LFS-HTTP');
       assert.equal(event.m, 'Log event #1');
       assert.equal(event.g, 'myCategory');
@@ -76,34 +84,15 @@ test('logFaces appender', (batch) => {
       );
       assert.end();
     });
-    t.end();
-  });
-
-  batch.test('when using UDP receivers', (t) => {
-    const setup = setupLogging('udpCategory', {
-      type: 'logFacesAppender',
-      application: 'LFS-UDP',
-      remoteHost: '127.0.0.1',
-      port: 55201
-    });
-
-    setup.logger.error('Log event #2');
 
-    t.test('an event should be sent', (assert) => {
-      const event = setup.results;
-      assert.equal(event.a, 'LFS-UDP');
-      assert.equal(event.m, 'Log event #2');
-      assert.equal(event.g, 'udpCategory');
-      assert.equal(event.p, 'ERROR');
-      assert.equal(event.p_foo, 'bar');
-      assert.equal(event.p_bar, 'foo');
-
-      // Assert timestamp, up to hours resolution.
-      const date = new Date(event.t);
+    t.test('errors should be sent to console.error', (assert) => {
+      setup.fakeAxios.errorCb({ response: { status: 500, data: 'oh no' } });
       assert.equal(
-        date.toISOString().substring(0, 14),
-        new Date().toISOString().substring(0, 14)
+        setup.fakeConsole.msg,
+        'log4js.logFaces-HTTP Appender error posting to http://localhost/receivers/rx1: 500 - oh no'
       );
+      setup.fakeAxios.errorCb(new Error('oh dear'));
+      assert.equal(setup.fakeConsole.msg, 'log4js.logFaces-HTTP Appender error: oh dear');
       assert.end();
     });
     t.end();
diff --git a/test/tap/logFaces-UDP-test.js b/test/tap/logFaces-UDP-test.js
index 9ddd05db..a5f0fa81 100644
--- a/test/tap/logFaces-UDP-test.js
+++ b/test/tap/logFaces-UDP-test.js
@@ -60,7 +60,7 @@ test('logFaces appender', (batch) => {
     });
 
     setup.logger.addContext('foo', 'bar');
-    setup.logger.addCcontext('bar', 'foo');
+    setup.logger.addContext('bar', 'foo');
     setup.logger.error('Log event #2');
 
     t.test('an event should be sent', (assert) => {

From 9631b620c449a5c00c0f44c3900b8e11250ab864 Mon Sep 17 00:00:00 2001
From: Gareth Jones 
Date: Mon, 20 Mar 2017 08:46:44 +1100
Subject: [PATCH 10/13] fix(stderr): updated stderr to new appender format

---
 lib/appenders/stderr.js         | 8 ++------
 lib/appenders/stdout.js         | 2 +-
 test/tap/stderrAppender-test.js | 2 +-
 3 files changed, 4 insertions(+), 8 deletions(-)

diff --git a/lib/appenders/stderr.js b/lib/appenders/stderr.js
index 8944468e..2c5a6892 100644
--- a/lib/appenders/stderr.js
+++ b/lib/appenders/stderr.js
@@ -1,21 +1,17 @@
 'use strict';
 
-const layouts = require('../layouts');
-
 function stderrAppender(layout, timezoneOffset) {
-  layout = layout || layouts.colouredLayout;
   return (loggingEvent) => {
     process.stderr.write(`${layout(loggingEvent, timezoneOffset)}\n`);
   };
 }
 
-function configure(config) {
-  let layout;
+function configure(config, layouts) {
+  let layout = layouts.colouredLayout;
   if (config.layout) {
     layout = layouts.layout(config.layout.type, config.layout);
   }
   return stderrAppender(layout, config.timezoneOffset);
 }
 
-module.exports.appender = stderrAppender;
 module.exports.configure = configure;
diff --git a/lib/appenders/stdout.js b/lib/appenders/stdout.js
index 437741a5..80b96050 100644
--- a/lib/appenders/stdout.js
+++ b/lib/appenders/stdout.js
@@ -1,7 +1,7 @@
 'use strict';
 
 function stdoutAppender(layout, timezoneOffset) {
-  return function (loggingEvent) {
+  return (loggingEvent) => {
     process.stdout.write(`${layout(loggingEvent, timezoneOffset)}\n`);
   };
 }
diff --git a/test/tap/stderrAppender-test.js b/test/tap/stderrAppender-test.js
index 9fd4871c..d311aa98 100644
--- a/test/tap/stderrAppender-test.js
+++ b/test/tap/stderrAppender-test.js
@@ -20,7 +20,7 @@ test('stderr appender', (t) => {
         }
       }
     }
-  ).appender(layouts.messagePassThroughLayout);
+  ).configure({ type: 'stderr', layout: { type: 'messagePassThrough' } }, layouts);
 
   appender({ data: ['biscuits'] });
   t.plan(2);

From c4a6efaf0fc8a5a9c03bd55416efd386ed6ceea9 Mon Sep 17 00:00:00 2001
From: Gareth Jones 
Date: Mon, 20 Mar 2017 08:47:48 +1100
Subject: [PATCH 11/13] fix(test): exit listener test fix for new shutdown
 functions

---
 test/tap/dateFileAppender-test.js | 4 ++--
 test/tap/fileAppender-test.js     | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/test/tap/dateFileAppender-test.js b/test/tap/dateFileAppender-test.js
index 0c77c584..3a0024c9 100644
--- a/test/tap/dateFileAppender-test.js
+++ b/test/tap/dateFileAppender-test.js
@@ -65,8 +65,8 @@ test('../../lib/appenders/dateFile', (batch) => {
                 openedFiles.shift();
               };
 
-              this.write = function () {
-                return true;
+              this.write = function (data, encoding, cb) {
+                return cb();
               };
             }
           }
diff --git a/test/tap/fileAppender-test.js b/test/tap/fileAppender-test.js
index 1a968929..1d5b817e 100644
--- a/test/tap/fileAppender-test.js
+++ b/test/tap/fileAppender-test.js
@@ -68,8 +68,8 @@ test('log4js fileAppender', (batch) => {
                 openedFiles.shift();
               };
 
-              this.write = function () {
-                return true;
+              this.write = function (data, encoding, cb) {
+                return cb();
               };
 
               this.on = function () {

From 2e035286d15e6a8429add922fadeb0c1ea7c4d40 Mon Sep 17 00:00:00 2001
From: Gareth Jones 
Date: Mon, 20 Mar 2017 09:04:36 +1100
Subject: [PATCH 12/13] fix(exit): made exit listeners responsibility of caller

---
 lib/appenders/dateFile.js         |  9 ----
 lib/appenders/file.js             | 28 ++++--------
 test/tap/dateFileAppender-test.js | 68 -----------------------------
 test/tap/fileAppender-test.js     | 72 -------------------------------
 v2-changes.md                     | 11 +++++
 5 files changed, 19 insertions(+), 169 deletions(-)
 create mode 100644 v2-changes.md

diff --git a/lib/appenders/dateFile.js b/lib/appenders/dateFile.js
index d0b2622c..9d2c3855 100644
--- a/lib/appenders/dateFile.js
+++ b/lib/appenders/dateFile.js
@@ -4,12 +4,6 @@ const streams = require('streamroller');
 const os = require('os');
 
 const eol = os.EOL || '\n';
-const appenders = [];
-
-// close open files on process exit.
-process.on('exit', () => {
-  appenders.forEach((a) => { a.shutdown(); });
-});
 
 /**
  * File appender that rolls files according to a date pattern.
@@ -42,8 +36,6 @@ function appender(
     });
   };
 
-  appenders.push(app);
-
   return app;
 }
 
@@ -67,5 +59,4 @@ function configure(config, layouts) {
   );
 }
 
-module.exports.appender = appender;
 module.exports.configure = configure;
diff --git a/lib/appenders/file.js b/lib/appenders/file.js
index 85a11fda..d414ae67 100644
--- a/lib/appenders/file.js
+++ b/lib/appenders/file.js
@@ -6,23 +6,6 @@ const streams = require('streamroller');
 const os = require('os');
 
 const eol = os.EOL || '\n';
-const appenders = [];
-
-// close open files on process exit.
-process.on('exit', () => {
-  debug('Exit handler called.');
-  appenders.forEach((a) => { a.shutdown(); });
-});
-
-// On SIGHUP, close and reopen all files. This allows this appender to work with
-// logrotate. Note that if you are using logrotate, you should not set
-// `logSize`.
-process.on('SIGHUP', () => {
-  debug('SIGHUP handler called.');
-  appenders.forEach((a) => {
-    a.reopen();
-  });
-});
 
 function openTheStream(file, fileSize, numFiles, options) {
   const stream = new streams.RollingFileStream(
@@ -81,8 +64,14 @@ function fileAppender(file, layout, logSize, numBackups, options, timezoneOffset
     });
   };
 
-  // push file to the stack of open handlers
-  appenders.push(app);
+  // On SIGHUP, close and reopen all files. This allows this appender to work with
+  // logrotate. Note that if you are using logrotate, you should not set
+  // `logSize`.
+  process.on('SIGHUP', () => {
+    debug('SIGHUP handler called.');
+    app.reopen();
+  });
+
   return app;
 }
 
@@ -102,5 +91,4 @@ function configure(config, layouts) {
   );
 }
 
-module.exports.appender = fileAppender;
 module.exports.configure = configure;
diff --git a/test/tap/dateFileAppender-test.js b/test/tap/dateFileAppender-test.js
index 3a0024c9..8ebfb109 100644
--- a/test/tap/dateFileAppender-test.js
+++ b/test/tap/dateFileAppender-test.js
@@ -3,7 +3,6 @@
 const test = require('tap').test;
 const path = require('path');
 const fs = require('fs');
-const sandbox = require('sandboxed-module');
 const log4js = require('../../lib/log4js');
 const EOL = require('os').EOL || '\n';
 
@@ -16,73 +15,6 @@ function removeFile(filename) {
 }
 
 test('../../lib/appenders/dateFile', (batch) => {
-  batch.test('adding multiple dateFileAppenders', (t) => {
-    const listenersCount = process.listeners('exit').length;
-
-    log4js.configure({
-      appenders: {
-        date0: { type: 'dateFile', filename: 'datefa-default-test0.log' },
-        date1: { type: 'dateFile', filename: 'datefa-default-test1.log' },
-        date2: { type: 'dateFile', filename: 'datefa-default-test2.log' },
-        date3: { type: 'dateFile', filename: 'datefa-default-test3.log' },
-        date4: { type: 'dateFile', filename: 'datefa-default-test4.log' }
-      },
-      categories: { default: { appenders: ['date0', 'date1', 'date2', 'date3', 'date4'], level: 'debug' } }
-    });
-
-    t.teardown(() => {
-      removeFile('datefa-default-test0.log');
-      removeFile('datefa-default-test1.log');
-      removeFile('datefa-default-test2.log');
-      removeFile('datefa-default-test3.log');
-      removeFile('datefa-default-test4.log');
-    });
-
-    t.equal(process.listeners('exit').length, listenersCount + 1, 'should only add one exit listener');
-    t.end();
-  });
-
-  batch.test('exit listener', (t) => {
-    let exitListener;
-    const openedFiles = [];
-
-    const dateFileAppender = sandbox.require(
-      '../../lib/appenders/dateFile',
-      {
-        globals: {
-          process: {
-            on: function (evt, listener) {
-              exitListener = listener;
-            }
-          }
-        },
-        requires: {
-          streamroller: {
-            DateRollingFileStream: function (filename) {
-              openedFiles.push(filename);
-
-              this.end = function () {
-                openedFiles.shift();
-              };
-
-              this.write = function (data, encoding, cb) {
-                return cb();
-              };
-            }
-          }
-        }
-      }
-    );
-
-    for (let i = 0; i < 5; i += 1) {
-      dateFileAppender.configure({ filename: `test${i}` }, { basicLayout: function () {} });
-    }
-    t.equal(openedFiles.length, 5);
-    exitListener();
-    t.equal(openedFiles.length, 0, 'should close all opened files');
-    t.end();
-  });
-
   batch.test('with default settings', (t) => {
     const testFile = path.join(__dirname, 'date-appender-default.log');
     log4js.configure({
diff --git a/test/tap/fileAppender-test.js b/test/tap/fileAppender-test.js
index 1d5b817e..b7347245 100644
--- a/test/tap/fileAppender-test.js
+++ b/test/tap/fileAppender-test.js
@@ -17,78 +17,6 @@ function removeFile(filename) {
 }
 
 test('log4js fileAppender', (batch) => {
-  batch.test('adding multiple fileAppenders', (t) => {
-    const initialCount = process.listeners('exit').length;
-    log4js.configure({
-      appenders: {
-        file0: { type: 'file', filename: 'fa-default-test0.log' },
-        file1: { type: 'file', filename: 'fa-default-test1.log' },
-        file2: { type: 'file', filename: 'fa-default-test2.log' },
-        file3: { type: 'file', filename: 'fa-default-test3.log' },
-        file4: { type: 'file', filename: 'fa-default-test4.log' },
-      },
-      categories: { default: { appenders: ['file0', 'file1', 'file2', 'file3', 'file4'], level: 'debug' } }
-    });
-
-    t.tearDown(() => {
-      removeFile('fa-default-test0.log');
-      removeFile('fa-default-test1.log');
-      removeFile('fa-default-test2.log');
-      removeFile('fa-default-test3.log');
-      removeFile('fa-default-test4.log');
-    });
-
-    t.equal(initialCount + 1, process.listeners('exit').length, 'should not add more than one exit listener');
-    t.end();
-  });
-
-  batch.test('exit listener', (t) => {
-    let exitListener;
-    const openedFiles = [];
-
-    const fileAppender = sandbox.require(
-      '../../lib/appenders/file',
-      {
-        globals: {
-          process: {
-            on: function (evt, listener) {
-              if (evt === 'exit') {
-                exitListener = listener;
-              }
-            }
-          }
-        },
-        singleOnly: true,
-        requires: {
-          streamroller: {
-            RollingFileStream: function (filename) {
-              openedFiles.push(filename);
-
-              this.end = function () {
-                openedFiles.shift();
-              };
-
-              this.write = function (data, encoding, cb) {
-                return cb();
-              };
-
-              this.on = function () {
-              };
-            }
-          }
-        }
-      }
-    );
-
-    for (let i = 0; i < 5; i += 1) {
-      fileAppender.configure({ filename: `test${i}` }, { basicLayout: function () {} });
-    }
-    t.equal(openedFiles.length, 5);
-    exitListener();
-    t.equal(openedFiles.length, 0, 'should close all open files');
-    t.end();
-  });
-
   batch.test('with default fileAppender settings', (t) => {
     const testFile = path.join(__dirname, 'fa-default-test.log');
     const logger = log4js.getLogger('default-settings');
diff --git a/v2-changes.md b/v2-changes.md
new file mode 100644
index 00000000..97593698
--- /dev/null
+++ b/v2-changes.md
@@ -0,0 +1,11 @@
+CHANGES
+=======
+
+- no exit listeners defined for appenders by default. users should call log4js.shutdown in their exit listeners.
+- context added to loggers (only logstash uses it so far)
+- logstash split into two appenders (udp and http)
+- no cwd, reload options in config
+- configure only by calling configure, no manual adding of appenders, etc
+- config format changed a lot, now need to define named appenders and at least one category
+- appender format changed, will break any non-core appenders (maybe create adapter?)
+- no replacement of console functions

From fd9f1389318cd3c21430fcd908ee1422ce831256 Mon Sep 17 00:00:00 2001
From: Gareth Jones 
Date: Fri, 24 Mar 2017 08:45:16 +1100
Subject: [PATCH 13/13] chore(merge): redis appender merged into version-2

---
 lib/appenders/redis.js         |  18 ++--
 test/tap/redisAppender-test.js | 175 +++++++++++----------------------
 2 files changed, 69 insertions(+), 124 deletions(-)

diff --git a/lib/appenders/redis.js b/lib/appenders/redis.js
index 33a8d66d..66036ef5 100644
--- a/lib/appenders/redis.js
+++ b/lib/appenders/redis.js
@@ -1,29 +1,32 @@
 'use strict';
 
-const layouts = require('../layouts');
 const redis = require('redis');
 const util = require('util');
 
 function redisAppender(config, layout) {
-  layout = layout || layouts.messagePassThroughLayout;
-  const redisClient = redis.createClient(config.port, config.host, { auth_pass: config.pass });
+  const host = config.host || '127.0.0.1';
+  const port = config.port || 6379;
+  const auth = config.pass ? { auth_pass: config.pass } : {};
+  const redisClient = redis.createClient(port, host, auth);
+
   redisClient.on('error', (err) => {
     if (err) {
-      console.error('log4js.redisAppender - %s:%p Error: %s', config.host, config.port, util.inspect(err));
+      console.error(`log4js.redisAppender - ${host}:${port} Error: ${util.inspect(err)}`);
     }
   });
+
   return function (loggingEvent) {
     const message = layout(loggingEvent);
     redisClient.publish(config.channel, message, (err) => {
       if (err) {
-        console.error('log4js.redisAppender - %s:%p Error: %s', config.host, config.port, util.inspect(err));
+        console.error(`log4js.redisAppender - ${host}:${port} Error: ${util.inspect(err)}`);
       }
     });
   };
 }
 
-function configure(config) {
-  let layout;
+function configure(config, layouts) {
+  let layout = layouts.messagePassThroughLayout;
   if (config.layout) {
     layout = layouts.layout(config.layout.type, config.layout);
   }
@@ -31,5 +34,4 @@ function configure(config) {
   return redisAppender(config, layout);
 }
 
-module.exports.appender = redisAppender;
 module.exports.configure = configure;
diff --git a/test/tap/redisAppender-test.js b/test/tap/redisAppender-test.js
index 788a6138..7f67d7a6 100644
--- a/test/tap/redisAppender-test.js
+++ b/test/tap/redisAppender-test.js
@@ -1,183 +1,126 @@
 'use strict';
 
 const test = require('tap').test;
-const log4js = require('../../lib/log4js');
+// const log4js = require('../../lib/log4js');
 const sandbox = require('sandboxed-module');
 
 function setupLogging(category, options) {
-  const msgs = [];
-
-  const redisCredentials = {
-    type: options.type,
-    host: options.host,
-    port: options.port,
-    pass: options.pass,
-    channel: options.channel,
-    layout: options.layout
-  };
-
   const fakeRedis = {
+    msgs: [],
     createClient: function (port, host, optionR) {
       this.port = port;
       this.host = host;
-      this.optionR = {};
-      this.optionR.auth_pass = optionR.pass;
+      this.optionR = optionR;
 
       return {
         on: function (event, callback) {
-          callback('throw redis error #1');
+          fakeRedis.errorCb = callback;
         },
         publish: function (channel, message, callback) {
-          msgs.push(message);
-          callback(null, {status: 'sent'});
+          fakeRedis.msgs.push({ channel: channel, message: message });
+          fakeRedis.publishCb = callback;
         }
       };
     }
   };
 
-  const fakeLayouts = {
-    layout: function (type, config) {
-      this.type = type;
-      this.config = config;
-      return log4js.layouts.messagePassThroughLayout;
-    },
-    basicLayout: log4js.layouts.basicLayout,
-    coloredLayout: log4js.layouts.coloredLayout,
-    messagePassThroughLayout: log4js.layouts.messagePassThroughLayout
-  };
-
-  const fakeUtil = {
-    inspect: function (item) {
-      return JSON.stringify(item);
-    }
-  };
-
   const fakeConsole = {
     errors: [],
-    logs: [],
-    error: function (msg, value) {
-      this.errors.push({ msg: msg, value: value });
-    },
-    log: function (msg, value) {
-      this.logs.push({ msg: msg, value: value });
+    error: function (msg) {
+      this.errors.push(msg);
     }
   };
 
-  const redisModule = sandbox.require('../../lib/appenders/redis', {
+  const log4js = sandbox.require('../../lib/log4js', {
     requires: {
-      'redis': fakeRedis,
-      '../layouts': fakeLayouts,
-      'util': fakeUtil
+      redis: fakeRedis
     },
     globals: {
       console: fakeConsole
     }
   });
-
-  log4js.addAppender(redisModule.configure(options), category);
+  log4js.configure({
+    appenders: { redis: options },
+    categories: { default: { appenders: ['redis'], level: 'trace' } }
+  });
 
   return {
     logger: log4js.getLogger(category),
-    redis: fakeRedis,
-    layouts: fakeLayouts,
-    console: fakeConsole,
-    messages: msgs,
-    credentials: redisCredentials
+    fakeRedis: fakeRedis,
+    fakeConsole: fakeConsole
   };
 }
 
-function checkMessages(assert, result) {
-  for (let i = 0; i < result.messages.length; i++) {
-    assert.ok(new RegExp(`Log event #${i + 1}`).test(result.messages[i]));
-  }
-}
-
-log4js.clearAppenders();
-
 test('log4js redisAppender', (batch) => {
   batch.test('redis setup', (t) => {
     const result = setupLogging('redis setup', {
-      host: '127.0.0.1',
-      port: 6739,
+      host: '123.123.123.123',
+      port: 1234,
       pass: '123456',
       channel: 'log',
       type: 'redis',
       layout: {
         type: 'pattern',
-        pattern: '%d{yyyy-MM-dd hh:mm:ss:SSS}#%p#%m'
+        pattern: 'cheese %m'
       }
     });
+
+    result.logger.info('Log event #1');
+    result.fakeRedis.publishCb();
+
     t.test('redis credentials should match', (assert) => {
-      assert.equal(result.credentials.host, '127.0.0.1');
-      assert.equal(result.credentials.port, 6739);
-      assert.equal(result.credentials.pass, '123456');
-      assert.equal(result.credentials.channel, 'log');
-      assert.equal(result.credentials.type, 'redis');
-      assert.equal(result.credentials.layout.type, 'pattern');
-      assert.equal(result.credentials.layout.pattern, '%d{yyyy-MM-dd hh:mm:ss:SSS}#%p#%m');
+      assert.equal(result.fakeRedis.host, '123.123.123.123');
+      assert.equal(result.fakeRedis.port, 1234);
+      assert.equal(result.fakeRedis.optionR.auth_pass, '123456');
+      assert.equal(result.fakeRedis.msgs.length, 1, 'should be one message only');
+      assert.equal(result.fakeRedis.msgs[0].channel, 'log');
+      assert.equal(result.fakeRedis.msgs[0].message, 'cheese Log event #1');
       assert.end();
     });
 
     t.end();
   });
 
-  batch.test('basic usage', (t) => {
-    const setup = setupLogging('basic usage', {
-      host: '127.0.0.1',
-      port: 6739,
-      pass: '',
-      channel: 'log',
+  batch.test('default values', (t) => {
+    const setup = setupLogging('defaults', {
       type: 'redis',
-      layout: {
-        type: 'pattern',
-        pattern: '%d{yyyy-MM-dd hh:mm:ss:SSS}#%p#%m'
-      }
+      channel: 'thing'
     });
 
-    setup.logger.info('Log event #1');
-
-    t.equal(setup.messages.length, 1, 'should be one message only');
-    checkMessages(t, setup);
-    t.end();
-  });
+    setup.logger.info('just testing');
+    setup.fakeRedis.publishCb();
 
+    t.test('should use localhost', (assert) => {
+      assert.equal(setup.fakeRedis.host, '127.0.0.1');
+      assert.equal(setup.fakeRedis.port, 6379);
+      assert.same(setup.fakeRedis.optionR, {});
+      assert.end();
+    });
 
-  batch.test('config with layout', (t) => {
-    const result = setupLogging('config with layout', {
-      layout: {
-        type: 'redis'
-      }
+    t.test('should use message pass through layout', (assert) => {
+      assert.equal(setup.fakeRedis.msgs.length, 1);
+      assert.equal(setup.fakeRedis.msgs[0].channel, 'thing');
+      assert.equal(setup.fakeRedis.msgs[0].message, 'just testing');
+      assert.end();
     });
-    t.equal(result.layouts.type, 'redis', 'should configure layout');
+
     t.end();
   });
 
-  batch.test('separate notification for each event', (t) => {
-    const setup = setupLogging('separate notification for each event', {
-      host: '127.0.0.1',
-      port: 6739,
-      pass: '',
-      channel: 'log',
-      type: 'redis',
-      layout: {
-        type: 'pattern',
-        pattern: '%d{yyyy-MM-dd hh:mm:ss:SSS}#%p#%m'
-      }
+  batch.test('redis errors', (t) => {
+    const setup = setupLogging('errors', { type: 'redis', channel: 'testing' });
+
+    setup.fakeRedis.errorCb('oh no, error on connect');
+    setup.logger.info('something something');
+    setup.fakeRedis.publishCb('oh no, error on publish');
+
+    t.test('should go to the console', (assert) => {
+      assert.equal(setup.fakeConsole.errors.length, 2);
+      assert.equal(setup.fakeConsole.errors[0], 'log4js.redisAppender - 127.0.0.1:6379 Error: \'oh no, error on connect\'');
+      assert.equal(setup.fakeConsole.errors[1], 'log4js.redisAppender - 127.0.0.1:6379 Error: \'oh no, error on publish\'');
+      assert.end();
     });
-    setTimeout(() => {
-      setup.logger.info('Log event #1');
-    }, 0);
-    setTimeout(() => {
-      setup.logger.info('Log event #2');
-    }, 500);
-    setTimeout(() => {
-      setup.logger.info('Log event #3');
-    }, 1100);
-    setTimeout(() => {
-      t.equal(setup.messages.length, 3, 'should be three messages');
-      checkMessages(t, setup);
-      t.end();
-    }, 3000);
+    t.end();
   });
 
   batch.end();