diff --git a/lib/appenders/recording.js b/lib/appenders/recording.js new file mode 100644 index 0000000..da66607 --- /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 124ac97..437741a 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 0000000..915c154 --- /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 2d981ac..e5330ee 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 ae6c8ca..55a4318 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 });
-  };
+let config;
+let enabled = true;
 
-  return logger;
+function configForCategory(category) {
+  if (config.categories.has(category)) {
+    return config.categories.get(category);
+  }
+  if (category.indexOf('.') > 0) {
+    return configForCategory(category.substring(0, category.lastIndexOf('.')));
+  }
+  return configForCategory('default');
 }
 
-function normalizeCategory(category) {
-  return `${category}.`;
+function appendersForCategory(category) {
+  return configForCategory(category).appenders;
 }
 
-function doesLevelEntryContainsLogger(levelCategory, loggerCategory) {
-  const normalizedLevelCategory = normalizeCategory(levelCategory);
-  const normalizedLoggerCategory = normalizeCategory(loggerCategory);
-  return normalizedLoggerCategory.substring(0, normalizedLevelCategory.length) === normalizedLevelCategory;
+function levelForCategory(category) {
+  return configForCategory(category).level;
 }
 
-function doesAppenderContainsLogger(appenderCategory, loggerCategory) {
-  const normalizedAppenderCategory = normalizeCategory(appenderCategory);
-  const normalizedLoggerCategory = normalizeCategory(loggerCategory);
-  return normalizedLoggerCategory.substring(0, normalizedAppenderCategory.length) === normalizedAppenderCategory;
+function sendLogEventToAppender(logEvent) {
+  if (!enabled) return;
+  const appenders = appendersForCategory(logEvent.categoryName);
+  appenders.forEach((appender) => {
+    appender(logEvent);
+  });
 }
 
 /**
- * Get a logger instance. Instance is cached on categoryName level.
+ * Get a logger instance.
  * @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 */
-
-    if (appenders[ALL_CATEGORIES]) {
-      appenderList = appenders[ALL_CATEGORIES];
-      appenderList.forEach((appender) => {
-        loggers[loggerCategoryName].addListener('log', appender);
-      });
-    }
-  }
-
-  return loggers[loggerCategoryName];
+function getLogger(category) {
+  const cat = category || 'default';
+  return new Logger(cat, levelForCategory(cat), sendLogEventToAppender);
 }
 
-/**
- * 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 (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);
-        }
-      }
-    }
-  });
-}
-
-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 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 setGlobalLogLevel(level) {
-  Logger.prototype.level = levels.toLevel(level, levels.TRACE);
-}
-
-/**
- * Get the default logger instance.
- * @return {Logger} instance of default logger
- * @static
- */
-function getDefaultLogger() {
-  return getLogger(Logger.DEFAULT_CATEGORY);
-}
-
-const configState = {};
-
 function loadConfigurationFile(filename) {
   if (filename) {
     return JSON.parse(fs.readFileSync(filename, 'utf8'));
   }
-  return undefined;
+  return filename;
 }
 
-function configureOnceOff(config, options) {
-  if (config) {
-    try {
-      restoreConsole();
-      configureLevels(config.levels);
-      configureAppenders(config.appenders, options);
+function configure(configurationFileOrObject) {
+  let configObject = configurationFileOrObject;
 
-      if (config.replaceConsole) {
-        replaceConsole();
-      }
-    } catch (e) {
-      throw new Error(
-        `Problem reading log4js config ${util.inspect(config)}. Error was '${e.message}' (${e.stack})`
-      );
-    }
+  if (typeof configObject === 'string') {
+    configObject = loadConfigurationFile(configurationFileOrObject);
   }
-}
-
-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]);
-  });
-}
-
-function restoreConsole() {
-  ['log', 'debug', 'info', 'warn', 'error'].forEach((item) => {
-    console[item] = originalConsoleFunctions[item];
-  });
-}
-
-/* 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
-  }
-  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 1da0cae..4955b26 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 9c84ebf..0e44907 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 0000000..4514b14
--- /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 42321aa..f9d6122 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 7b3175f..0000000
--- 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 6ce338c..0000000
--- 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 9ae5baf..a3b0ce4 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);