diff --git a/lib/appenders/categoryFilter.js b/lib/appenders/categoryFilter.js index c4ab9d7..263970b 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/clustered.js b/lib/appenders/clustered.js index 350209f..ef777c6 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/console.js b/lib/appenders/console.js index 6b2e691..25211f6 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 3c69ae5..9d2c385 100644 --- a/lib/appenders/dateFile.js +++ b/lib/appenders/dateFile.js @@ -1,19 +1,9 @@ '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 = []; - -// close open files on process exit. -process.on('exit', () => { - openFiles.forEach((file) => { - file.end(); - }); -}); /** * File appender that rolls files according to a date pattern. @@ -30,21 +20,27 @@ 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 (complete) { + logFile.write('', 'utf-8', () => { + logFile.end(complete); + }); + }; + + 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 +50,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,27 +59,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) => { - file.write('', 'utf-8', () => { - 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 9c2f2bb..d414ae6 100644 --- a/lib/appenders/file.js +++ b/lib/appenders/file.js @@ -1,31 +1,25 @@ '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 = []; -// close open files on process exit. -process.on('exit', () => { - debug('Exit handler called.'); - openFiles.forEach((file) => { - file.end(); +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); //eslint-disable-line }); -}); + return stream; +} -// 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.'); - openFiles.forEach((writer) => { - writer.closeTheStream(writer.openTheStream.bind(writer)); - }); -}); /** * File Appender writing the logs to a text file. Supports rolling of logs by size. @@ -42,7 +36,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 +47,40 @@ 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); - - 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); + app.reopen = function () { + writer.closeTheStream(writer.openTheStream.bind(writer)); + }; + + app.shutdown = function (complete) { + writer.write('', 'utf-8', () => { + writer.end(complete); + }); + }; + + // 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 stream; + + 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,27 +91,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) => { - file.write('', 'utf-8', () => { - 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 dab551c..36254a9 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 eb809ed..fe52502 100644 --- a/lib/appenders/gelf.js +++ b/lib/appenders/gelf.js @@ -1,8 +1,7 @@ 'use strict'; const zlib = require('zlib'); -const layouts = require('../layouts'); -const levels = require('../levels'); +// const levels = require('../levels'); const dgram = require('dgram'); const util = require('util'); const OS = require('os'); @@ -18,43 +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; - -let client; - /** * 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; - } +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; - host = host || 'localhost'; - port = port || 12201; - hostname = hostname || OS.hostname(); - layout = layout || layouts.messagePassThroughLayout; + 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 || {}; @@ -62,7 +49,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 +110,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 +124,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, 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); } -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 8c3a3be..a071310 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/logFaces-HTTP.js b/lib/appenders/logFaces-HTTP.js new file mode 100644 index 0000000..41dbac5 --- /dev/null +++ b/lib/appenders/logFaces-HTTP.js @@ -0,0 +1,89 @@ +/** + * 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. + */ +/* eslint global-require:0 */ + +'use strict'; + +const util = require('util'); +const axios = require('axios'); + +/** + * + * For HTTP (browsers or node.js) use the following configuration params: + * { + * "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) { + 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 + 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 + sender.post('', lfsEvent) + .catch((error) => { + if (error.response) { + console.error( + `log4js.logFaces-HTTP Appender error posting to ${config.url}: ${error.response.status} - ${error.response.data}` + ); + return; + } + console.error(`log4js.logFaces-HTTP Appender error: ${error.message}`); + }); + }; +} + +function configure(config) { + return logFacesAppender(config); +} + +function format(logData) { + const data = Array.isArray(logData) ? + logData : Array.prototype.slice.call(arguments); + return util.format.apply(util, wrapErrorsWithInspect(data)); +} + +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; + }); +} + +module.exports.configure = configure; diff --git a/lib/appenders/logFaces-UDP.js b/lib/appenders/logFaces-UDP.js new file mode 100644 index 0000000..a2d3b71 --- /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/appenders/logFacesAppender.js b/lib/appenders/logFacesAppender.js deleted file mode 100644 index ba7bda6..0000000 --- a/lib/appenders/logFacesAppender.js +++ /dev/null @@ -1,130 +0,0 @@ -/** - * 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. - */ -/* eslint global-require:0 */ - -'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); - }); - }; -} - -/** - * 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 -* } - */ -function logFacesAppender(config) { - let send = config.send; - if (send === undefined) { - send = (config.url === undefined) ? datagram(config) : servlet(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(context).forEach((key) => { - lfsEvent[`p_${key}`] = context[key]; - }); - - // send to server - send(lfsEvent); - }; -} - -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); - return util.format.apply(util, wrapErrorsWithInspect(data)); -} - -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; - }); -} - -module.exports.appender = logFacesAppender; -module.exports.configure = configure; -module.exports.setContext = setContext; diff --git a/lib/appenders/logLevelFilter.js b/lib/appenders/logLevelFilter.js index ea0d420..f91e758 100644 --- a/lib/appenders/logLevelFilter.js +++ b/lib/appenders/logLevelFilter.js @@ -1,11 +1,8 @@ 'use strict'; -const levels = require('../levels'); -const log4js = require('../log4js'); - -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)) { @@ -14,11 +11,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); - return logLevelFilter(config.level, config.maxLevel, appender); +function configure(config, layouts, findAppender, levels) { + const appender = findAppender(config.appender); + return logLevelFilter(config.level, config.maxLevel, appender, levels); } -module.exports.appender = logLevelFilter; module.exports.configure = configure; diff --git a/lib/appenders/loggly.js b/lib/appenders/loggly.js index 77afd06..84c41d1 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; + 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); + } - 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/appenders/logstashUDP.js b/lib/appenders/logstashUDP.js index 7805e09..4d6ce8a 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); + } + + log.shutdown = function (cb) { + udp.close(cb); }; + + return log; } -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 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 11341ff..41ee19d 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 d56a714..14475f0 100644 --- a/lib/appenders/multiprocess.js +++ b/lib/appenders/multiprocess.js @@ -1,33 +1,33 @@ 'use strict'; -const log4js = require('../log4js'); +const debug = require('debug')('log4js:multiprocess'); const net = require('net'); const END_MSG = '__LOG4JS__'; -const servers = []; /** * Creates a server, listening on config.loggerPort, config.loggerHost. * Output goes to config.actualAppender (config.appender is used to * set up that appender). */ -function logServer(config) { +function logServer(config, actualAppender, levels) { /** * 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); loggingEvent.startTime = new Date(loggingEvent.startTime); - loggingEvent.level = log4js.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 = { startTime: new Date(), categoryName: 'log4js', - level: log4js.levels.ERROR, + level: levels.ERROR, data: ['Unable to parse log:', msg] }; } @@ -38,8 +38,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'); @@ -47,11 +45,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) { @@ -68,12 +68,22 @@ function logServer(config) { }); server.listen(config.loggerPort || 5000, config.loggerHost || 'localhost', function () { - servers.push(server); + debug('master server listening'); // allow the process to exit, if this is the only socket active server.unref(); }); - return actualAppender; + 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); + }; + + return app; } function workerAppender(config) { @@ -82,19 +92,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); @@ -102,8 +117,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; }); @@ -114,45 +131,48 @@ function workerAppender(config) { createSocket(); - return function log(loggingEvent) { + function log(loggingEvent) { 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, levels) { if (config.mode === 'master') { - return logServer(config); + debug('Creating master appender'); + return logServer(config, appender, levels); } + debug('Creating worker appender'); return workerAppender(config); } -function configure(config, options) { - let actualAppender; - if (config.appender && config.mode === 'master') { - log4js.loadAppender(config.appender.type); - actualAppender = log4js.appenderMakers[config.appender.type](config.appender, options); - config.actualAppender = actualAppender; +function configure(config, layouts, findAppender, levels) { + 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, levels); } -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/appenders/recording.js b/lib/appenders/recording.js new file mode 100644 index 0000000..78992a4 --- /dev/null +++ b/lib/appenders/recording.js @@ -0,0 +1,28 @@ +'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); + }; +} + +function replay() { + return recordedEvents; +} + +function reset() { + recordedEvents = []; +} + +module.exports = { + configure: configure, + replay: replay, + playback: replay, + reset: reset, + erase: reset +}; diff --git a/lib/appenders/redis.js b/lib/appenders/redis.js index 33a8d66..66036ef 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/lib/appenders/slack.js b/lib/appenders/slack.js index ae366cd..694f4f5 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 dca9a3f..075743f 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 = []; - return (loggingEvent) => { + 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`; + } + + 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(); } }; + + appender.shutdown = shutdown; + + return appender; } -function configure(_config) { - config = _config; - if (_config.layout) { - layout = layouts.layout(_config.layout.type, _config.layout); +function configure(config, layouts) { + const subjectLayout = layouts.messagePassThroughLayout; + let layout = layouts.basicLayout; + if (config.layout) { + layout = layouts.layout(config.layout.type, config.layout); } - return smtpAppender(_config, layout); + return smtpAppender(config, layout, subjectLayout); } -function shutdown(cb) { - if (shutdownTimeout > 0) { - setTimeout(() => { - if (sendTimer) { - clearTimeout(sendTimer); - } - sendBuffer(); - }, shutdownTimeout); - } - - (function checkDone() { - if (unsentCount > 0) { - setTimeout(checkDone, 100); - } else { - cb(); - } - }()); -} - -module.exports.name = 'smtp'; -module.exports.appender = smtpAppender; module.exports.configure = configure; -module.exports.shutdown = shutdown; diff --git a/lib/appenders/stderr.js b/lib/appenders/stderr.js index 8944468..2c5a689 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 124ac97..80b9605 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) { + return (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..c348c4b --- /dev/null +++ b/lib/configuration.js @@ -0,0 +1,179 @@ +'use strict'; + +const util = require('util'); +const levels = require('./levels'); +const layouts = require('./layouts'); +const debug = require('debug')('log4js:configuration'); + +function not(thing) { + return !thing; +} + +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) { + 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)` + ); + 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), this.configuredLevels); + } + + get appenders() { + return this.configuredAppenders; + } + + set appenders(appenderConfig) { + const appenderNames = Object.keys(appenderConfig); + this.throwExceptionIf(not(appenderNames.length), 'must define at least one appender.'); + + this.configuredAppenders = new Map(); + appenderNames.forEach((name) => { + this.throwExceptionIf( + not(appenderConfig[name].type), + `appender "${name}" is not valid (must be an object with property "type")` + ); + + debug(`Creating appender ${name}`); + this.configuredAppenders.set(name, this.createAppender(name, appenderConfig[name])); + }); + } + + get categories() { + return this.configuredCategories; + } + + set categories(categoryConfig) { + const categoryNames = Object.keys(categoryConfig); + this.throwExceptionIf(not(categoryNames.length), 'must define at least one category.'); + + this.configuredCategories = new Map(); + categoryNames.forEach((name) => { + const category = categoryConfig[name]; + this.throwExceptionIf( + [ + not(category.appenders), + not(category.level) + ], + `category "${name}" is not valid (must be an object with properties "appenders" and "level")` + ); + + this.throwExceptionIf( + not(Array.isArray(category.appenders)), + `category "${name}" is not valid (appenders must be an array of appender names)` + ); + + this.throwExceptionIf( + not(category.appenders.length), + `category "${name}" is not valid (appenders must contain at least one appender name)` + ); + + const appenders = []; + category.appenders.forEach((appender) => { + this.throwExceptionIf( + not(this.configuredAppenders.get(appender)), + `category "${name}" is not valid (appender "${appender}" is not defined)` + ); + appenders.push(this.appenders.get(appender)); + }); + + this.throwExceptionIf( + not(this.configuredLevels.getLevel(category.level)), + `category "${name}" is not valid (level "${category.level}" not recognised;` + + ` valid levels are ${this.configuredLevels.levels.join(', ')})` + ); + + debug(`Creating category ${name}`); + this.configuredCategories.set( + name, + { appenders: appenders, level: this.configuredLevels.getLevel(category.level) } + ); + }); + + this.throwExceptionIf(not(categoryConfig.default), 'must define a "default" category.'); + } + + get levels() { + return this.configuredLevels; + } + + set levels(levelConfig) { + // levels are optional + if (levelConfig) { + this.throwExceptionIf(not(anObject(levelConfig)), 'levels must be an object'); + const newLevels = Object.keys(levelConfig); + newLevels.forEach((l) => { + this.throwExceptionIf( + not(validIdentifier(l)), + `level name "${l}" is not a valid identifier (must start with a letter, only contain A-Z,a-z,0-9,_)` + ); + this.throwExceptionIf( + not(anInteger(levelConfig[l])), + `level "${l}" must have an integer value` + ); + }); + } + this.configuredLevels = levels(levelConfig); + } + + constructor(candidate) { + this.candidate = candidate; + + this.throwExceptionIf(not(anObject(candidate)), 'must be an object.'); + this.throwExceptionIf(not(anObject(candidate.appenders)), 'must have a property "appenders" of type object.'); + this.throwExceptionIf(not(anObject(candidate.categories)), 'must have a property "categories" of type object.'); + + this.levels = candidate.levels; + this.appenders = candidate.appenders; + this.categories = candidate.categories; + } +} + +module.exports = Configuration; diff --git a/lib/connect-logger.js b/lib/connect-logger.js index 2c60d3c..433c1b5 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 = {}; - } + /** + * 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 + */ - 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)); - } - } - }); - } - - // 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 2d981ac..443065e 100644 --- a/lib/levels.js +++ b/lib/levels.js @@ -1,85 +1,87 @@ 'use strict'; -/** - * @name Level - * @namespace Log4js - */ -class Level { - constructor(level, levelStr) { - this.level = level; - this.levelStr = levelStr; - } - - toString() { - return this.levelStr; - } - - isLessThanOrEqualTo(otherLevel) { - if (typeof otherLevel === 'string') { - otherLevel = toLevel(otherLevel); +module.exports = function (customLevels) { + /** + * @name Level + * @namespace Log4js + */ + class Level { + constructor(level, levelStr) { + this.level = level; + this.levelStr = levelStr; } - return this.level <= otherLevel.level; - } - isGreaterThanOrEqualTo(otherLevel) { - if (typeof otherLevel === 'string') { - otherLevel = toLevel(otherLevel); + toString() { + return this.levelStr; } - return this.level >= otherLevel.level; - } - isEqualTo(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 = getLevel(otherLevel); + } + return this.level >= otherLevel.level; + } + + isEqualTo(otherLevel) { + if (typeof otherLevel === 'string') { + otherLevel = getLevel(otherLevel); + } + 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 (sArg instanceof Level) { + return sArg; + } + + if (typeof sArg === 'string') { + return defaultLevels[sArg.toUpperCase()] || defaultLevel; + } + + return getLevel(sArg.toString()); } - if (typeof sArg === 'string') { - return module.exports[sArg.toUpperCase()] || defaultLevel; - } + const orderedLevels = Object.keys(defaultLevels).sort((a, b) => b.level - a.level); + defaultLevels.getLevel = getLevel; + defaultLevels.levels = orderedLevels; - return toLevel(sArg.toString()); -} - -function getLevel(levelStr) { - let level; - if (typeof levelStr === 'string') { - const levelUpper = levelStr.toUpperCase(); - level = toLevel(levelUpper); - } - 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 + return defaultLevels; }; diff --git a/lib/log4js.js b/lib/log4js.js index ae6c8ca..5c812dd 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' );
@@ -32,413 +22,83 @@
  * @static
  * Website: http://log4js.berlios.de
  */
+const debug = require('debug')('log4js:main');
 const fs = require('fs');
-const util = require('util');
-const layouts = require('./layouts');
-const levels = require('./levels');
-const loggerModule = require('./logger');
-const connectLogger = require('./connect-logger').connectLogger;
+const Configuration = require('./configuration');
+const connectModule = require('./connect-logger');
+const logger = require('./logger');
 
-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 Logger;
+let config;
+let connectLogger;
+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(sendLogEventToAppender, cat, levelForCategory(cat));
 }
 
-/**
- * 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) {
+    debug(`Loading configuration from ${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);
+  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;
 }
 
 /**
@@ -450,41 +110,34 @@ function loadAppender(appender, appenderModule) {
  *  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.
-  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 = [];
 
+  debug(`Found ${shutdownFunctions} appenders with shutdown functions.`);
   function complete(err) {
     error = error || err;
-    completed++;
-    if (completed >= shutdownFunctions.length) {
+    completed += 1;
+    debug(`Appender shutdowns complete: ${completed} / ${shutdownFunctions}`);
+    if (completed >= shutdownFunctions) {
+      debug('All shutdown functions completed.');
       cb(error);
     }
   }
 
-  for (const category in appenderShutdowns) {
-    if (appenderShutdowns.hasOwnProperty(category)) {
-      shutdownFunctions.push(appenderShutdowns[category]);
-    }
-  }
-
-  if (!shutdownFunctions.length) {
+  if (shutdownFunctions === 0) {
+    debug('No appenders with shutdown functions found.');
     return cb();
   }
 
-  shutdownFunctions.forEach((shutdownFct) => {
-    shutdownFct(complete);
-  });
+  appenders.filter(a => a.shutdown).forEach(a => a.shutdown(complete));
 
   return null;
 }
@@ -492,49 +145,19 @@ 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..20251a0 100644
--- a/lib/logger.js
+++ b/lib/logger.js
@@ -2,12 +2,7 @@
 
 'use strict';
 
-const levels = require('./levels');
-const EventEmitter = require('events');
-
-const DEFAULT_CATEGORY = '[default]';
-
-let logWritesEnabled = true;
+const debug = require('debug')('log4js:logger');
 
 /**
  * @name LoggingEvent
@@ -20,113 +15,104 @@ 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, context) {
     this.startTime = new Date();
     this.categoryName = categoryName;
     this.data = data;
     this.level = level;
-    this.logger = logger;
+    this.context = Object.assign({}, context);
   }
 }
 
-/**
- * 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
- *
- * @author Stephan Strittmatter
- */
-class Logger extends EventEmitter {
-  constructor(name, level) {
-    super();
+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;
+      this.context = {};
+      debug(`Logger created (${name}, ${level})`);
+    }
 
-    this.category = name || DEFAULT_CATEGORY;
+    setLevel(level) {
+      this.level = levels.getLevel(level, this.level || levels.TRACE);
+    }
 
-    if (level) {
-      this.setLevel(level);
+    log() {
+      /* eslint prefer-rest-params:0 */
+      // todo: once node v4 support dropped, use rest parameter instead
+      const args = Array.from(arguments);
+      const logLevel = levels.getLevel(args[0], levels.INFO);
+      if (this.isLevelEnabled(logLevel)) {
+        this._log(logLevel, args.slice(1));
+      }
+    }
+
+    isLevelEnabled(otherLevel) {
+      return this.level.isLessThanOrEqualTo(otherLevel);
+    }
+
+    _log(level, data) {
+      debug(`sending log data (${level}, ${data}) to appenders`);
+      const loggingEvent = new LoggingEvent(this.category, level, data, this.context);
+      this.dispatch(loggingEvent);
+    }
+
+    addContext(key, value) {
+      this.context[key] = value;
+    }
+
+    removeContext(key) {
+      delete this.context[key];
+    }
+
+    clearContext() {
+      this.context = {};
     }
   }
 
-  setLevel(level) {
-    this.level = levels.toLevel(level, this.level || levels.TRACE);
+  function addLevelMethods(target) {
+    const level = levels.getLevel(target);
+
+    const levelStrLower = level.toString().toLowerCase();
+    const levelMethod = levelStrLower.replace(/_([a-z])/g, g => g[1].toUpperCase());
+    const isLevelMethod = levelMethod[0].toUpperCase() + levelMethod.slice(1);
+
+    Logger.prototype[`is${isLevelMethod}Enabled`] = function () {
+      return this.isLevelEnabled(level.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);
+      }
+    };
   }
 
-  removeLevel() {
-    delete this.level;
-  }
+  levels.levels.forEach(addLevelMethods);
 
-  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;
-    }
-    this._log(logLevel, args.slice(1));
-  }
-
-  isLevelEnabled(otherLevel) {
-    return this.level.isLessThanOrEqualTo(otherLevel);
-  }
-
-  _log(level, data) {
-    const loggingEvent = new LoggingEvent(this.category, level, data, this);
-    this.emit('log', 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);
-
-  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());
+  return {
+    LoggingEvent: LoggingEvent,
+    Logger: Logger
   };
-
-  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 (logWritesEnabled && this.isLevelEnabled(level)) {
-      this._log(level, args);
-    }
-  };
-}
-
-/**
- * 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/package.json b/package.json
index 2a88636..124cd5d 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",
diff --git a/test/tap/categoryFilter-test.js b/test/tap/categoryFilter-test.js
index 4cd1043..cabd7f2 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');
+  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');
 
-    logger.info('Loading app');
-    logger.info('Initialising indexes');
-    weblogger.info('00:00:00 GET / 200');
-    weblogger.warn('00:00:00 GET / 500');
+    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');
 
-    setTimeout(() => {
-      fs.readFile(`${__dirname}/categoryFilter-noweb.log`, 'utf8', (err, contents) => {
-        const noWebMessages = contents.trim().split(EOL);
-        t.same(noWebMessages, ['Loading app', 'Initialising indexes']);
-
-        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/clusteredAppender-test.js b/test/tap/clusteredAppender-test.js
index 83b1e5e..6ebbaff 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/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/configureNoLevels-test.js b/test/tap/configureNoLevels-test.js
deleted file mode 100644
index 0c5988b..0000000
--- 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/connect-logger-test.js b/test/tap/connect-logger-test.js
index 5c61b99..6c79d07 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 8d3370d..5404c34 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/consoleAppender-test.js b/test/tap/consoleAppender-test.js
index 6fe32cd..499f2d3 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 1dfb268..8ebfb10 100644
--- a/test/tap/dateFileAppender-test.js
+++ b/test/tap/dateFileAppender-test.js
@@ -3,83 +3,26 @@
 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';
 
 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));
-    }
-
-    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();
-              };
-            }
-          }
-        }
-      }
-    );
-
-    for (let i = 0; i < 5; i += 1) {
-      dateFileAppender.appender(`test${i}`);
-    }
-    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');
-    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 +40,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 +68,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 +79,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,49 +105,14 @@ 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.test('should flush logs on shutdown', (t) => {
     const testFile = path.join(__dirname, 'date-appender-default.log');
-    const appender = require('../../lib/appenders/dateFile').appender(testFile);
+    log4js.configure({
+      appenders: { test: { type: 'dateFile', filename: testFile } },
+      categories: { default: { appenders: ['test'], level: 'trace' } }
+    });
     const logger = log4js.getLogger('default-settings');
 
-    log4js.clearAppenders();
-    log4js.addAppender(appender, 'default-settings');
-
     logger.info('1');
     logger.info('2');
     logger.info('3');
diff --git a/test/tap/file-sighup-test.js b/test/tap/file-sighup-test.js
index 5ed6afa..f863851 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 afad844..b734724 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) {
@@ -19,76 +17,17 @@ 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'
-      );
-    }
-
-    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.on = function () {
-              };
-            }
-          }
-        }
-      }
-    );
-
-    for (let i = 0; i < 5; i += 1) {
-      fileAppender.appender(`test${i}`, null, 100);
-    }
-    t.ok(openedFiles);
-    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');
-    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.');
 
@@ -106,16 +45,13 @@ test('log4js fileAppender', (batch) => {
 
   batch.test('should flush logs on shutdown', (t) => {
     const testFile = path.join(__dirname, 'fa-default-test.log');
-    const logger = log4js.getLogger('default-settings');
-    remove(testFile);
+    removeFile(testFile);
 
-    log4js.clearAppenders();
-    const fileAppender = require('../../lib/appenders/file');
-    log4js.addAppender(
-      fileAppender.appender(testFile),
-      fileAppender.shutdown,
-      'default-settings'
-    );
+    log4js.configure({
+      appenders: { test: { type: 'file', filename: testFile } },
+      categories: { default: { appenders: ['test'], level: 'trace' } }
+    });
+    const logger = log4js.getLogger('default-settings');
 
     logger.info('1');
     logger.info('2');
@@ -134,86 +70,25 @@ test('log4js fileAppender', (batch) => {
     });
   });
 
-  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.');
@@ -236,16 +111,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.');
@@ -288,18 +171,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.');
@@ -339,24 +227,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;
@@ -381,13 +251,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 4874862..fc5629a 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 fc822be..12402e5 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 14beb61..0000000
--- 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 032bde7..2d58687 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/levels-test.js b/test/tap/levels-test.js
index 5447191..aeacd1e 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/log-abspath-test.js b/test/tap/log-abspath-test.js
deleted file mode 100644
index aa274ac..0000000
--- 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/logFaces-HTTP-test.js b/test/tap/logFaces-HTTP-test.js
new file mode 100644
index 0000000..05df419
--- /dev/null
+++ b/test/tap/logFaces-HTTP-test.js
@@ -0,0 +1,102 @@
+'use strict';
+
+const test = require('tap').test;
+const sandbox = require('sandboxed-module');
+
+function setupLogging(category, options) {
+  const fakeAxios = {
+    create: function (config) {
+      this.config = config;
+      return {
+        post: function (emptyString, event) {
+          fakeAxios.args = [emptyString, event];
+          return {
+            catch: function (cb) {
+              fakeAxios.errorCb = cb;
+            }
+          };
+        }
+      };
+    }
+  };
+
+  const fakeConsole = {
+    error: function (msg) {
+      this.msg = msg;
+    }
+  };
+
+  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),
+    fakeAxios: fakeAxios,
+    fakeConsole: fakeConsole
+  };
+}
+
+test('logFaces appender', (batch) => {
+  batch.test('when using HTTP receivers', (t) => {
+    const setup = setupLogging('myCategory', {
+      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.fakeAxios.args[1];
+      assert.equal(event.a, 'LFS-HTTP');
+      assert.equal(event.m, 'Log event #1');
+      assert.equal(event.g, 'myCategory');
+      assert.equal(event.p, 'WARN');
+      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('errors should be sent to console.error', (assert) => {
+      setup.fakeAxios.errorCb({ response: { status: 500, data: 'oh no' } });
+      assert.equal(
+        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();
+  });
+
+  batch.end();
+});
diff --git a/test/tap/logFaces-UDP-test.js b/test/tap/logFaces-UDP-test.js
new file mode 100644
index 0000000..a5f0fa8
--- /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.addContext('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/logFacesAppender-test.js b/test/tap/logFacesAppender-test.js
deleted file mode 100644
index fe1a632..0000000
--- a/test/tap/logFacesAppender-test.js
+++ /dev/null
@@ -1,89 +0,0 @@
-'use strict';
-
-const test = require('tap').test;
-const log4js = require('../../lib/log4js');
-
-function setupLogging(category, options) {
-  const sent = {};
-
-  function fake(event) {
-    Object.keys(event).forEach((key) => {
-      sent[key] = event[key];
-    });
-  }
-
-  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');
-
-  return {
-    logger: log4js.getLogger(category),
-    results: sent
-  };
-}
-
-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'
-    });
-
-    setup.logger.warn('Log event #1');
-
-    t.test('an event should be sent', (assert) => {
-      const event = setup.results;
-      assert.equal(event.a, 'LFS-HTTP');
-      assert.equal(event.m, 'Log event #1');
-      assert.equal(event.g, 'myCategory');
-      assert.equal(event.p, 'WARN');
-      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.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);
-      assert.equal(
-        date.toISOString().substring(0, 14),
-        new Date().toISOString().substring(0, 14)
-      );
-      assert.end();
-    });
-    t.end();
-  });
-
-  batch.end();
-});
diff --git a/test/tap/logLevelFilter-test.js b/test/tap/logLevelFilter-test.js
index 9a09aef..68fdab3 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/logger-test.js b/test/tap/logger-test.js
index 6edf229..1e72a59 100644
--- a/test/tap/logger-test.js
+++ b/test/tap/logger-test.js
@@ -1,35 +1,55 @@
 '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 = {
+  events: [],
+  dispatch: function (evt) {
+    this.events.push(evt);
+  }
+};
+const dispatch = testDispatcher.dispatch.bind(testDispatcher);
 
 test('../../lib/logger', (batch) => {
+  batch.beforeEach((done) => {
+    testDispatcher.events = [];
+    done();
+  });
+
   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 +72,41 @@ 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.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();
+  });
 
-    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();
-    });
+  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();
   });
 
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/logglyAppender-test.js b/test/tap/logglyAppender-test.js
index 8fb25ad..400a4b8 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/logstashUDP-test.js b/test/tap/logstashUDP-test.js
index 5bacc4e..b50b1be 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 3408a38..248bd4a 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 1bde591..660e1b4 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/multiprocess-test.js b/test/tap/multiprocess-test.js
index 0b0c61c..0d6f1ed 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,48 +261,98 @@ 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.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('master with unknown appender throws error', (t) => {
+    const fakeNet = makeFakeNet();
+
+    const log4js = sandbox.require(
+      '../../lib/log4js',
+      {
+        requires: {
+          net: fakeNet
+        }
+      }
+    );
+    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',
+      {
+        requires: {
+          net: fakeNet
+        }
+      }
+    );
+    log4js.configure({
+      appenders: {
+        stdout: { type: 'stdout' },
+        master: { type: 'multiprocess', mode: 'master', appender: 'stdout' }
+      },
+      categories: { default: { appenders: ['master'], level: 'trace' } }
+    });
 
     t.test('should listen for log messages on localhost:5000', (assert) => {
       assert.equal(fakeNet.port, 5000);
@@ -288,44 +362,5 @@ test('Multiprocess Appender', (batch) => {
     t.end();
   });
 
-  batch.test('configure', (t) => {
-    const results = {};
-    const fakeNet = makeFakeNet();
-
-    sandbox.require(
-      '../../lib/appenders/multiprocess',
-      {
-        requires: {
-          net: fakeNet,
-          '../log4js': {
-            loadAppender: function (app) {
-              results.appenderLoaded = app;
-            },
-            appenderMakers: {
-              madeupappender: function (config, options) {
-                results.config = config;
-                results.options = options;
-              }
-            }
-          }
-        }
-      }
-    ).configure(
-      {
-        mode: 'master',
-        appender: {
-          type: 'madeupappender',
-          cheese: 'gouda'
-        }
-      },
-      { crackers: 'jacobs' }
-    );
-
-    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.end();
-  });
-
   batch.end();
 });
diff --git a/test/tap/newLevel-test.js b/test/tap/newLevel-test.js
index b817cfa..e190bb7 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' } }
     });
 
-    logger.log('LEVEL_DOES_NEXT_EXIST', 'Event 1');
-    logger.log(Level.forName('LEVEL_DOES_NEXT_EXIST'), 'Event 2');
+    const logger = log4js.getLogger();
 
+    logger.log('LEVEL_DOES_NEXT_EXIST', 'Event 1');
+    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/redisAppender-test.js b/test/tap/redisAppender-test.js
index 788a613..7f67d7a 100644
--- a/test/tap/redisAppender-test.js
+++ b/test/tap/redisAppender-test.js
@@ -1,184 +1,127 @@
 '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');
+    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();
+    });
+
+    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(setup.messages.length, 1, 'should be one message only');
-    checkMessages(t, setup);
     t.end();
   });
 
+  batch.test('redis errors', (t) => {
+    const setup = setupLogging('errors', { type: 'redis', channel: 'testing' });
 
-  batch.test('config with layout', (t) => {
-    const result = setupLogging('config with layout', {
-      layout: {
-        type: 'redis'
-      }
+    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();
     });
-    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'
-      }
-    });
-    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);
-  });
-
   batch.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/setLevel-asymmetry-test.js b/test/tap/setLevel-asymmetry-test.js
index c3d5222..5b1f633 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 acc1bbb..75048d6 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 fef1361..497d076 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');
 
diff --git a/test/tap/stderrAppender-test.js b/test/tap/stderrAppender-test.js
index 9fd4871..d311aa9 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);
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);
diff --git a/test/tap/subcategories-test.js b/test/tap/subcategories-test.js
index f803c69..08295d4 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();
diff --git a/test/tap/with-categoryFilter.json b/test/tap/with-categoryFilter.json
deleted file mode 100644
index f1efa4a..0000000
--- 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 4691278..0000000
--- 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"
-  }
-}
diff --git a/test/tap/with-logLevelFilter.json b/test/tap/with-logLevelFilter.json
deleted file mode 100644
index 0995d35..0000000
--- 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"
-  }
-}
diff --git a/v2-changes.md b/v2-changes.md
new file mode 100644
index 0000000..9759369
--- /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