diff --git a/README.md b/README.md index 981d52c..82ad54b 100644 --- a/README.md +++ b/README.md @@ -2,22 +2,30 @@ This is a conversion of the [log4js](http://log4js.berlios.de/index.html) framework to work with [node](http://nodejs.org). I've mainly stripped out the browser-specific code -and tidied up some of the javascript. +and tidied up some of the javascript. It includes a basic file logger, with log rolling based on file size. + +NOTE: since v0.2.0 require('log4js') returns a function, so you need to call that function in your code before you can use it. I've done this to make testing easier (allows dependency injection). ## installation npm install log4js - ## tests -Run the tests with `node tests.js`. They use the awesome [jspec](http://visionmedia.github.com/jspec) - 3.1.3 +Tests now use [vows](http://vowsjs.org), run with `vows test/logging.js`. I am slowly porting the previous tests from jspec (run those with `node tests.js`), since jspec is no longer maintained. ## usage +Minimalist version: + var log4js = require('log4js')(); + var logger = log4js.getLogger(); + logger.debug("Some debug messages"); +By default, log4js outputs to stdout with the coloured layout (thanks to [masylum](http://github.com/masylum)), so for the above you would see: + [2010-01-17 11:43:37.987] [DEBUG] [default] - Some debug messages + See example.js: - var log4js = require('log4js'); + var log4js = require('log4js')(); //note the need to call the function log4js.addAppender(log4js.consoleAppender()); log4js.addAppender(log4js.fileAppender('logs/cheese.log'), 'cheese'); @@ -39,13 +47,11 @@ Output ## configuration You can either configure the appenders and log levels manually (as above), or provide a -configuration file (`log4js.configure('path/to/file.json')`). An example file can be found -in spec/fixtures/log4js.json +configuration file (`log4js.configure('path/to/file.json')`) explicitly, or just let log4js look for a file called `log4js.json` (it looks in the current directory first, then the require paths, and finally looks for the default config included in the same directory as the `log4js.js` file). +An example file can be found in `test/log4js.json`. An example config file with log rolling is in `test/with-log-rolling.json` ## todo -I need to make a RollingFileAppender, which will do log rotation. - patternLayout has no tests. This is mainly because I haven't found a use for it yet, and am not entirely sure what it was supposed to do. It is more-or-less intact from the original log4js. diff --git a/example.js b/example.js index b4eab22..66d9ce4 100644 --- a/example.js +++ b/example.js @@ -1,4 +1,4 @@ -var log4js = require('./lib/log4js'); +var log4js = require('./lib/log4js')(); log4js.addAppender(log4js.consoleAppender()); log4js.addAppender(log4js.fileAppender('cheese.log'), 'cheese'); diff --git a/lib/log4js.js b/lib/log4js.js index e5d73d4..5ef7f4f 100644 --- a/lib/log4js.js +++ b/lib/log4js.js @@ -1,7 +1,3 @@ -var fs = require('fs'), sys = require('sys'); -var DEFAULT_CATEGORY = '[default]'; -var ALL_CATEGORIES = '[all]'; - /* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +22,7 @@ var ALL_CATEGORIES = '[all]'; * *

Example:

*
- *  var logging = require('log4js-node');
+ *  var logging = require('log4js-node')();
  *  //add an appender that logs all messages to stdout.
  *  logging.addAppender(logging.consoleAppender());
  *  //add an appender that logs "some-category" to a file
@@ -48,514 +44,601 @@ var ALL_CATEGORIES = '[all]';
  * @static
  * Website: http://log4js.berlios.de
  */
-var log4js = {
-	
-	/** 
-	 * Current version of log4js-node. 
-	 * @static
-	 * @final
-	 */
-  	version: "0.1.1",
-
-	/**  
-	 * Date of logger initialized.
-	 * @static
-	 * @final
-	 */
-	applicationStartDate: new Date(),
-		
-	/**  
-	 * Hashtable of loggers.
-	 * @static
-	 * @final
-	 * @private  
-	 */
-	loggers: {},
-  
-  appenders: {}
-};
+module.exports = function (fileSystem, standardOutput, configPaths) {
+    var fs = fileSystem || require('fs'),
+    standardOutput = standardOutput || console.log,
+    configPaths = configPaths || require.paths,
+    sys = require('sys'),
+    events = require('events'),
+    path = require('path'),
+    DEFAULT_CATEGORY = '[default]',
+    ALL_CATEGORIES = '[all]',
+    loggers = {},
+    appenders = {},
+    levels = {
+	ALL: new Level(Number.MIN_VALUE, "ALL", "grey"),
+	TRACE: new Level(5000, "TRACE", "blue"),
+	DEBUG: new Level(10000, "DEBUG", "cyan"),
+	INFO: new Level(20000, "INFO", "green"),
+	WARN: new Level(30000, "WARN", "yellow"),
+	ERROR: new Level(40000, "ERROR", "red"),
+	FATAL: new Level(50000, "FATAL", "magenta"),
+	OFF: new Level(Number.MAX_VALUE, "OFF", "grey")  
+    },
+    appenderMakers = {
+	"file": function(config) {
+	    var layout;
+	    if (config.layout) {
+		layout = layoutMakers[config.layout.type](config.layout);
+	    }
+	    return fileAppender(config.filename, layout, config.maxLogSize, config.backups, config.pollInterval);
+	},
+	"console": function(config) {
+	    var layout;
+	    if (config.layout) {
+		layout = layoutMakers[config.layout.type](config.layout);
+	    }
+	    return consoleAppender(layout);
+	},
+	"logLevelFilter": function(config) {
+	    var appender = appenderMakers[config.appender.type](config.appender);
+	    return logLevelFilter(config.level, appender);
+	}
+    },
+    layoutMakers = {
+	"messagePassThrough": function() { return messagePassThroughLayout; },
+	"basic": function() { return basicLayout; },
+	"pattern": function (config) {
+	    var pattern = config.pattern || undefined;
+	    return patternLayout(pattern);
+	}
+    };
 
-  
-	/**
-	 * Get a logger instance. Instance is cached on categoryName level.
-	 * @param  {String} categoryName name of category to log to.
-	 * @return {Logger} instance of logger for the category
-	 * @static
-	 */
-exports.getLogger = log4js.getLogger = function(categoryName) {
+    /**
+     * Get a logger instance. Instance is cached on categoryName level.
+     * @param  {String} categoryName name of category to log to.
+     * @return {Logger} instance of logger for the category
+     * @static
+     */
+    function getLogger (categoryName) {
 		
-		// Use default logger if categoryName is not specified or invalid
-		if (!(typeof categoryName == "string")) {
-			categoryName = DEFAULT_CATEGORY;
-		}
+	// Use default logger if categoryName is not specified or invalid
+	if (!(typeof categoryName == "string")) {
+	    categoryName = DEFAULT_CATEGORY;
+	}
 
-    var appenderList;
-		if (!log4js.loggers[categoryName]) {
-			// Create the logger for this name if it doesn't already exist
-			log4js.loggers[categoryName] = new Logger(categoryName);
-      if (log4js.appenders[categoryName]) {
-        appenderList = log4js.appenders[categoryName];
-        appenderList.forEach(function(appender) {
-          log4js.loggers[categoryName].addListener("log", appender);
-        });
-      }
-      if (log4js.appenders[ALL_CATEGORIES]) {
-        appenderList = log4js.appenders[ALL_CATEGORIES];
-        appenderList.forEach(function(appender) {
-          log4js.loggers[categoryName].addListener("log", appender);
-        });
-      }
-		}
+	var appenderList;
+	if (!loggers[categoryName]) {
+	    // Create the logger for this name if it doesn't already exist
+	    loggers[categoryName] = new Logger(categoryName);
+	    if (appenders[categoryName]) {
+		appenderList = appenders[categoryName];
+		appenderList.forEach(function(appender) {
+		    loggers[categoryName].addListener("log", appender);
+		});
+	    }
+	    if (appenders[ALL_CATEGORIES]) {
+		appenderList = appenders[ALL_CATEGORIES];
+		appenderList.forEach(function(appender) {
+		    loggers[categoryName].addListener("log", appender);
+		});
+	    }
+	}
 		
-		return log4js.loggers[categoryName];
-};
-	
-	/**
-	 * Get the default logger instance.
-	 * @return {Logger} instance of default logger
-	 * @static
-	 */
-exports.getDefaultLogger = log4js.getDefaultLogger = function() {
-	return log4js.getLogger(DEFAULT_CATEGORY); 
-};
-  
-/**
- * args are appender, then zero or more categories
- */
-exports.addAppender = log4js.addAppender = function () {
-  var args = Array.prototype.slice.call(arguments);
-  var appender = args.shift();
-  if (args.length == 0) {
-    args = [ ALL_CATEGORIES ];
-  }
-  //argument may already be an array
-  if (args[0].forEach) {
-    args = args[0];
-  }
-  
-  args.forEach(function(category) {
-    if (!log4js.appenders[category]) {
-      log4js.appenders[category] = [];
-    }
-    log4js.appenders[category].push(appender);
-    
-    if (category === ALL_CATEGORIES) {
-      for (var logger in log4js.loggers) {
-        if (log4js.loggers.hasOwnProperty(logger)) {
-          log4js.loggers[logger].addListener("log", appender);
-        }
-      }
-    } else if (log4js.loggers[category]) {
-      log4js.loggers[category].addListener("log", appender);
+	return loggers[categoryName];
     }
-  });
-};
-  
-exports.clearAppenders = log4js.clearAppenders = function() {
-    log4js.appenders = [];
-    for (var logger in log4js.loggers) {
-      if (log4js.loggers.hasOwnProperty(logger)) {
-        log4js.loggers[logger].removeAllListeners("log");
-      }
-    }
-};
-  
-exports.configure = log4js.configure = function(configurationFile) {
-    var config = JSON.parse(fs.readFileSync(configurationFile));
-    configureAppenders(config.appenders);
-    configureLevels(config.levels);
-};
-  
-exports.levels = log4js.levels = {
-    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"),
-		OFF: new Level(Number.MAX_VALUE, "OFF")  
-};
 
-var appenderMakers = {
-  "file": function(config) {
-    var layout;
-    if (config.layout) {
-      layout = layoutMakers[config.layout.type](config.layout);
+    /**
+     * args are appender, then zero or more categories
+     */
+    function addAppender () {
+	var args = Array.prototype.slice.call(arguments);
+	var appender = args.shift();
+	if (args.length == 0 || args[0] === undefined) {
+	    args = [ ALL_CATEGORIES ];
+	}
+	//argument may already be an array
+	if (args[0].forEach) {
+	    args = args[0];
+	}
+	
+	args.forEach(function(category) {
+	    if (!appenders[category]) {
+		appenders[category] = [];
+	    }
+	    appenders[category].push(appender);
+	    
+	    if (category === ALL_CATEGORIES) {
+		for (var logger in loggers) {
+		    if (loggers.hasOwnProperty(logger)) {
+			loggers[logger].addListener("log", appender);
+		    }
+		}
+	    } else if (loggers[category]) {
+		loggers[category].addListener("log", appender);
+	    }
+	});
     }
-    return fileAppender(config.filename, layout);
-  },
-  "console": function(config) {
-    var layout;
-    if (config.layout) {
-      layout = layoutMakers[config.layout.type](config.layout);
+
+    function clearAppenders () {
+	appenders = [];
+	for (var logger in loggers) {
+	    if (loggers.hasOwnProperty(logger)) {
+		loggers[logger].removeAllListeners("log");
+	    }
+	}
     }
-    return consoleAppender(layout);
-  },
-  "logLevelFilter": function(config) {
-    var appender = appenderMakers[config.appender.type](config.appender);
-    return logLevelFilter(config.level, appender);
-  }
-};
 
-var layoutMakers = {
-  "messagePassThrough": function() { return messagePassThroughLayout; },
-  "basic": function() { return basicLayout; },
-  "pattern": function (config) {
-    var pattern = config.pattern || undefined;
-    return patternLayout(pattern);
-  }
-};
+    function configure (configurationFile) {
+        if (configurationFile) {
+            try {
+	        var config = JSON.parse(fs.readFileSync(configurationFile, "utf8"));
+	        configureAppenders(config.appenders);
+	        configureLevels(config.levels);
+            } catch (e) {
+                throw new Error("Problem reading log4js config file " + configurationFile + ". Error was " + e.message);
+            }
+        }
+    }
 
-function configureAppenders(appenderList) {
-  log4js.clearAppenders();
-  if (appenderList) {
-    appenderList.forEach(function(appenderConfig) {
-      var appender = appenderMakers[appenderConfig.type](appenderConfig);
-      if (appender) {
-        log4js.addAppender(appender, appenderConfig.category);    
-      } else {
-        throw new Error("log4js configuration problem for "+sys.inspect(appenderConfig));
-      }
-    });
-  } else {
-    log4js.addAppender(consoleAppender);
-  }
-}
+    function findConfiguration() {
+        //add current directory onto the list of configPaths
+        var paths = ['.'].concat(configPaths);
+        //add this module's directory to the end of the list, so that we pick up the default config
+        paths.push(__dirname);
+        var pathsWithConfig = paths.filter( function (pathToCheck) {
+            try {
+                fs.statSync(path.join(pathToCheck, "log4js.json"));
+                return true;
+            } catch (e) {
+                return false;
+            }
+        });
+        if (pathsWithConfig.length > 0) {
+            return path.join(pathsWithConfig[0], 'log4js.json');
+        }
+        return undefined;
+    }
 
-function configureLevels(levels) {
-  if (levels) {
-    for (var category in levels) {
-      if (levels.hasOwnProperty(category)) {
-        log4js.getLogger(category).setLevel(levels[category]);
-      }
+    function configureAppenders(appenderList) {
+	clearAppenders();
+	if (appenderList) {
+	    appenderList.forEach(function(appenderConfig) {
+		var appender = appenderMakers[appenderConfig.type](appenderConfig);
+		if (appender) {
+		    addAppender(appender, appenderConfig.category);    
+		} else {
+		    throw new Error("log4js configuration problem for "+sys.inspect(appenderConfig));
+		}
+	    });
+	} else {
+	    addAppender(consoleAppender);
+	}
     }
-  }
-} 
 
+    function configureLevels(levels) {
+	if (levels) {
+	    for (var category in levels) {
+		if (levels.hasOwnProperty(category)) {
+		    getLogger(category).setLevel(levels[category]);
+		}
+	    }
+	}
+    } 
 
-/**
- * Log4js.Level Enumeration. Do not use directly. Use static objects instead.
- * @constructor
- * @param {Number} level number of level
- * @param {String} levelString String representation of level
- * @private
- */
-function Level(level, levelStr) {
+    function Level(level, levelStr, colour) {
 	this.level = level;
 	this.levelStr = levelStr;
-};
-
-/** 
- * converts given String to corresponding Level
- * @param {String} sArg String value of Level
- * @param {Log4js.Level} defaultLevel default Level, if no String representation
- * @return Level object
- * @type Log4js.Level
- */
-Level.toLevel = function(sArg, defaultLevel) {                  
-      
-  if (sArg === null) {
-    return defaultLevel;
-  }
-  
-  if (typeof sArg == "string") { 
-    var s = sArg.toUpperCase();
-    if (log4js.levels[s]) {
-      return log4js.levels[s];
+        this.colour = colour;
     }
-  }
-  return defaultLevel;
-};
 
-Level.prototype.toString = function() {
-  return this.levelStr;	
-};
-  
-Level.prototype.isLessThanOrEqualTo = function(otherLevel) {
-  return this.level <= otherLevel.level;
-};
+    /** 
+     * converts given String to corresponding Level
+     * @param {String} sArg String value of Level
+     * @param {Log4js.Level} defaultLevel default Level, if no String representation
+     * @return Level object
+     * @type Log4js.Level
+     */
+    Level.toLevel = function(sArg, defaultLevel) {                  
+	
+	if (sArg === null) {
+	    return defaultLevel;
+	}
+	
+	if (typeof sArg == "string") { 
+	    var s = sArg.toUpperCase();
+	    if (levels[s]) {
+		return levels[s];
+	    }
+	}
+	return defaultLevel;
+    };
 
-Level.prototype.isGreaterThanOrEqualTo = function(otherLevel) {
-    return this.level >= otherLevel.level;
-};
+    Level.prototype.toString = function() {
+	return this.levelStr;	
+    };
+    
+    Level.prototype.isLessThanOrEqualTo = function(otherLevel) {
+	return this.level <= otherLevel.level;
+    };
 
-/**
- * Models a logging event.
- * @constructor
- * @param {String} categoryName name of category
- * @param {Log4js.Level} level level of message
- * @param {String} message message to log
- * @param {Log4js.Logger} logger the associated logger
- * @author Seth Chisamore
- */
-LoggingEvent = function(categoryName, level, message, exception, logger) {
-	/**
-	 * the timestamp of the Logging Event
-	 * @type Date
-	 * @private
-	 */
+    Level.prototype.isGreaterThanOrEqualTo = function(otherLevel) {
+	return this.level >= otherLevel.level;
+    };
+
+    /**
+     * Models a logging event.
+     * @constructor
+     * @param {String} categoryName name of category
+     * @param {Log4js.Level} level level of message
+     * @param {String} message message to log
+     * @param {Log4js.Logger} logger the associated logger
+     * @author Seth Chisamore
+     */
+    function LoggingEvent (categoryName, level, message, exception, logger) {
 	this.startTime = new Date();
-	/**
-	 * category of event
-	 * @type String
-	 * @private
-	 */
 	this.categoryName = categoryName;
-	/**
-	 * the logging message
-	 * @type String
-	 * @private
-	 */
 	this.message = message;
-	/**
-	 * the logging exception
-	 * @type Exception
-	 * @private
-	 */
 	this.exception = exception;
-	/**
-	 * level of log
-	 * @type Log4js.Level
-	 * @private
-	 */
 	this.level = level;
-	/**
-	 * reference to logger
-	 * @type Log4js.Logger
-	 * @private
-	 */
 	this.logger = logger;
-};
+    }
 
-/**
- * Logger to log messages.
- * use {@see Log4js#getLogger(String)} to get an instance.
- * @constructor
- * @param name name of category to log to
- * @author Stephan Strittmatter
- */
-Logger = function(name, level) {
+    /**
+     * Logger to log messages.
+     * use {@see Log4js#getLogger(String)} to get an instance.
+     * @constructor
+     * @param name name of category to log to
+     * @author Stephan Strittmatter
+     */
+    function Logger (name, level) {
 	this.category = name || DEFAULT_CATEGORY;
-	this.level = Level.toLevel(level, log4js.levels.TRACE);
-};
-
-sys.inherits(Logger, process.EventEmitter);
-
-Logger.prototype.setLevel = function(level) {
-  this.level = Level.toLevel(level, log4js.levels.TRACE);
-};
-	
-Logger.prototype.log = function(logLevel, message, exception) {
-  var loggingEvent = new LoggingEvent(this.category, logLevel, message, exception, this);
-  this.emit("log", loggingEvent);
-};
- 
-Logger.prototype.isLevelEnabled = function(otherLevel) {
-  return this.level.isLessThanOrEqualTo(otherLevel);
-};
+	this.level = Level.toLevel(level, levels.TRACE);
+    }
+    sys.inherits(Logger, events.EventEmitter);
 
-['Trace','Debug','Info','Warn','Error','Fatal'].forEach(
-  function(levelString) {
-    var level = Level.toLevel(levelString);
-    Logger.prototype['is'+levelString+'Enabled'] = function() {
-      return this.isLevelEnabled(level);
+    Logger.prototype.setLevel = function(level) {
+	this.level = Level.toLevel(level, levels.TRACE);
     };
     
-    Logger.prototype[levelString.toLowerCase()] = function (message, exception) {
-      if (this.isLevelEnabled(level)) {
-        this.log(level, message, exception);
-      }
+    Logger.prototype.log = function(logLevel, message, exception) {
+	var loggingEvent = new LoggingEvent(this.category, logLevel, message, exception, this);
+	this.emit("log", loggingEvent);
+    };
+    
+    Logger.prototype.isLevelEnabled = function(otherLevel) {
+	return this.level.isLessThanOrEqualTo(otherLevel);
     };
-  }
-);
-	
-var consoleAppender = function (layout) {
-  layout = layout || basicLayout;
-  return function(loggingEvent) {
-    sys.puts(layout(loggingEvent));
-  };  
-};
 
-/**
- * File Appender writing the logs to a text file.
- * 
- * @param file file log messages will be written to
- * @param layout a function that takes a logevent and returns a string (defaults to basicLayout).
- */
-var fileAppender = function(file, layout) {
+    ['Trace','Debug','Info','Warn','Error','Fatal'].forEach(
+	function(levelString) {
+	    var level = Level.toLevel(levelString);
+	    Logger.prototype['is'+levelString+'Enabled'] = function() {
+		return this.isLevelEnabled(level);
+	    };
+	    
+	    Logger.prototype[levelString.toLowerCase()] = function (message, exception) {
+		if (this.isLevelEnabled(level)) {
+		    this.log(level, message, exception);
+		}
+	    };
+	}
+    );
+
+    /**
+     * Get the default logger instance.
+     * @return {Logger} instance of default logger
+     * @static
+     */
+    function getDefaultLogger () {
+	return getLogger(DEFAULT_CATEGORY); 
+    }
+
+    function consoleAppender (layout) {
+	layout = layout || colouredLayout;
+	return function(loggingEvent) {
+	    standardOutput(layout(loggingEvent));
+	};  
+    }
+
+    /**
+     * File Appender writing the logs to a text file. Supports rolling of logs by size.
+     * 
+     * @param file file log messages will be written to
+     * @param layout a function that takes a logevent and returns a string (defaults to basicLayout).
+     * @param logSize - the maximum size (in bytes) for a log file, if not provided then logs won't be rotated.
+     * @param numBackups - the number of log files to keep after logSize has been reached (default 5)
+     * @param filePollInterval - the time in seconds between file size checks (default 30s)
+     */
+    function fileAppender (file, layout, logSize, numBackups, filePollInterval) {
 	layout = layout || basicLayout;	
-	file = file || "log4js.log";	
-  //syncs are generally bad, but we need 
-  //the file to be open before we start doing any writing.
-  var logFile = fs.openSync(file, process.O_APPEND | process.O_WRONLY | process.O_CREAT, 0644);    
-  //register ourselves as listeners for shutdown
-  //so that we can close the file.
-  //not entirely sure this is necessary, but still.
-  process.addListener("exit", function() { fs.close(logFile); });
-  
-  return function(loggingEvent) {
-    fs.write(logFile, layout(loggingEvent)+'\n', null, "utf-8");
-  };
-};
+	//syncs are generally bad, but we need 
+	//the file to be open before we start doing any writing.
+	var logFile = fs.openSync(file, 'a', 0644);
 
-var logLevelFilter = function(levelString, appender) {
-  var level = Level.toLevel(levelString);
-  return function(logEvent) {
-    if (logEvent.level.isGreaterThanOrEqualTo(level)) {
-      appender(logEvent);
+        if (logSize > 0) {
+            setupLogRolling(logFile, file, logSize, numBackups || 5, (filePollInterval * 1000) || 30000);
+        }
+	
+	return function(loggingEvent) {
+	    fs.write(logFile, layout(loggingEvent)+'\n', null, "utf8");
+	};
     }
-  }
-};
 
-/**
- * BasicLayout is a simple layout for storing the logs. The logs are stored
- * in following format:
- * 
- * [startTime] [logLevel] categoryName - message\n
- * 
- * - * @author Stephan Strittmatter - */ -var basicLayout = function(loggingEvent) { - var timestampLevelAndCategory = '[' + loggingEvent.startTime.toFormattedString() + '] '; - timestampLevelAndCategory += '[' + loggingEvent.level.toString() + '] '; - timestampLevelAndCategory += loggingEvent.categoryName + ' - '; - - var output = timestampLevelAndCategory + loggingEvent.message; - - if (loggingEvent.exception) { - output += '\n' - output += timestampLevelAndCategory; - if (loggingEvent.exception.stack) { - output += loggingEvent.exception.stack; - } else { - output += loggingEvent.exception.name + ': '+loggingEvent.exception.message; + function setupLogRolling (logFile, filename, logSize, numBackups, filePollInterval) { + fs.watchFile(filename, + { + persistent: false, + interval: filePollInterval + }, + function (curr, prev) { + if (curr.size >= logSize) { + rollThatLog(logFile, filename, numBackups); + } + } + ); } - } - return output; -}; -var messagePassThroughLayout = function(loggingEvent) { - return loggingEvent.message; -}; + function rollThatLog (logFile, filename, numBackups) { + //doing all of this fs stuff sync, because I don't want to lose any log events. + //first close the current one. + fs.closeSync(logFile); + //roll the backups (rename file.n-1 to file.n, where n <= numBackups) + for (var i=numBackups; i > 0; i--) { + if (i > 1) { + if (fileExists(filename + '.' + (i-1))) { + fs.renameSync(filename+'.'+(i-1), filename+'.'+i); + } + } else { + fs.renameSync(filename, filename+'.1'); + } + } + //open it up again + logFile = fs.openSync(filename, 'a', 0644); + } -/** - * PatternLayout - * Takes a pattern string and returns a layout function. - * @author Stephan Strittmatter - */ -var patternLayout = function(pattern) { - pattern = pattern || patternLayout.DEFAULT_CONVERSION_PATTERN; + function fileExists (filename) { + try { + fs.statSync(filename); + return true; + } catch (e) { + return false; + } + } + + function logLevelFilter (levelString, appender) { + var level = Level.toLevel(levelString); + return function(logEvent) { + if (logEvent.level.isGreaterThanOrEqualTo(level)) { + appender(logEvent); + } + } + } + + /** + * BasicLayout is a simple layout for storing the logs. The logs are stored + * in following format: + *
+     * [startTime] [logLevel] categoryName - message\n
+     * 
+ * + * @author Stephan Strittmatter + */ + function basicLayout (loggingEvent) { + var timestampLevelAndCategory = '[' + loggingEvent.startTime.toFormattedString() + '] '; + timestampLevelAndCategory += '[' + loggingEvent.level.toString() + '] '; + timestampLevelAndCategory += loggingEvent.categoryName + ' - '; + + var output = timestampLevelAndCategory + loggingEvent.message; + + if (loggingEvent.exception) { + output += '\n' + output += timestampLevelAndCategory; + if (loggingEvent.exception.stack) { + output += loggingEvent.exception.stack; + } else { + output += loggingEvent.exception.name + ': '+loggingEvent.exception.message; + } + } + return output; + } + + /** + * Taken from masylum's fork (https://github.com/masylum/log4js-node) + */ + function colorize (str, style) { + var styles = { + //styles + 'bold' : [1, 22], + 'italic' : [3, 23], + 'underline' : [4, 24], + 'inverse' : [7, 27], + //grayscale + 'white' : [37, 39], + 'grey' : [90, 39], + 'black' : [90, 39], + //colors + 'blue' : [34, 39], + 'cyan' : [36, 39], + 'green' : [32, 39], + 'magenta' : [35, 39], + 'red' : [31, 39], + 'yellow' : [33, 39] + }; + return '\033[' + styles[style][0] + 'm' + str + + '\033[' + styles[style][1] + 'm'; + } + + /** + * colouredLayout - taken from masylum's fork. + * same as basicLayout, but with colours. + */ + function colouredLayout (loggingEvent) { + var timestampLevelAndCategory = colorize('[' + loggingEvent.startTime.toFormattedString() + '] ', 'grey'); + timestampLevelAndCategory += colorize( + '[' + loggingEvent.level.toString() + '] ', loggingEvent.level.colour + ); + timestampLevelAndCategory += colorize(loggingEvent.categoryName + ' - ', 'grey'); + + var output = timestampLevelAndCategory + loggingEvent.message; + + if (loggingEvent.exception) { + output += '\n' + output += timestampLevelAndCategory; + if (loggingEvent.exception.stack) { + output += loggingEvent.exception.stack; + } else { + output += loggingEvent.exception.name + ': '+loggingEvent.exception.message; + } + } + return output; + } + + function messagePassThroughLayout (loggingEvent) { + return loggingEvent.message; + } + + /** + * PatternLayout + * Takes a pattern string and returns a layout function. + * @author Stephan Strittmatter + */ + function patternLayout (pattern) { + var TTCC_CONVERSION_PATTERN = "%r %p %c - %m%n"; var regex = /%(-?[0-9]+)?(\.?[0-9]+)?([cdmnpr%])(\{([^\}]+)\})?|([^%]+)/; - - return function(loggingEvent) { - var formattedString = ""; - var result; - var searchString = this.pattern; - - while ((result = regex.exec(searchString))) { - var matchedString = result[0]; - var padding = result[1]; - var truncation = result[2]; - var conversionCharacter = result[3]; - var specifier = result[5]; - var text = result[6]; - - // Check if the pattern matched was just normal text - if (text) { - formattedString += "" + text; + + pattern = pattern || patternLayout.TTCC_CONVERSION_PATTERN; + + return function(loggingEvent) { + var formattedString = ""; + var result; + var searchString = this.pattern; + + while ((result = regex.exec(searchString))) { + var matchedString = result[0]; + var padding = result[1]; + var truncation = result[2]; + var conversionCharacter = result[3]; + var specifier = result[5]; + var text = result[6]; + + // Check if the pattern matched was just normal text + if (text) { + formattedString += "" + text; + } else { + // Create a raw replacement string based on the conversion + // character and specifier + var replacement = ""; + switch(conversionCharacter) { + case "c": + var loggerName = loggingEvent.categoryName; + if (specifier) { + var precision = parseInt(specifier, 10); + var loggerNameBits = loggingEvent.categoryName.split("."); + if (precision >= loggerNameBits.length) { + replacement = loggerName; + } else { + replacement = loggerNameBits.slice(loggerNameBits.length - precision).join("."); + } } else { - // Create a raw replacement string based on the conversion - // character and specifier - var replacement = ""; - switch(conversionCharacter) { - case "c": - var loggerName = loggingEvent.categoryName; - if (specifier) { - var precision = parseInt(specifier, 10); - var loggerNameBits = loggingEvent.categoryName.split("."); - if (precision >= loggerNameBits.length) { - replacement = loggerName; - } else { - replacement = loggerNameBits.slice(loggerNameBits.length - precision).join("."); - } - } else { - replacement = loggerName; - } - break; - case "d": - var dateFormat = Date.ISO8601_FORMAT; - if (specifier) { - dateFormat = specifier; - // Pick up special cases - if (dateFormat == "ISO8601") { - dateFormat = Date.ISO8601_FORMAT; - } else if (dateFormat == "ABSOLUTE") { - dateFormat = Date.ABSOLUTETIME_FORMAT; - } else if (dateFormat == "DATE") { - dateFormat = Date.DATETIME_FORMAT; - } - } - // Format the date - replacement = loggingEvent.startTime.toFormattedString(dateFormat); - break; - case "m": - replacement = loggingEvent.message; - break; - case "n": - replacement = "\n"; - break; - case "p": - replacement = loggingEvent.level.toString(); - break; - case "r": - replacement = "" + loggingEvent.startTime.toLocaleTimeString(); //TODO: .getDifference(Log4js.applicationStartDate); - break; - case "%": - replacement = "%"; - break; - default: - replacement = matchedString; - break; - } - // Format the replacement according to any padding or - // truncation specified - - var len; - - // First, truncation - if (truncation) { - len = parseInt(truncation.substr(1), 10); - replacement = replacement.substring(0, len); - } - // Next, padding - if (padding) { - if (padding.charAt(0) == "-") { - len = parseInt(padding.substr(1), 10); - // Right pad with spaces - while (replacement.length < len) { - replacement += " "; - } - } else { - len = parseInt(padding, 10); - // Left pad with spaces - while (replacement.length < len) { - replacement = " " + replacement; - } - } - } - formattedString += replacement; + replacement = loggerName; } - searchString = searchString.substr(result.index + result[0].length); + break; + case "d": + var dateFormat = Date.ISO8601_FORMAT; + if (specifier) { + dateFormat = specifier; + // Pick up special cases + if (dateFormat == "ISO8601") { + dateFormat = Date.ISO8601_FORMAT; + } else if (dateFormat == "ABSOLUTE") { + dateFormat = Date.ABSOLUTETIME_FORMAT; + } else if (dateFormat == "DATE") { + dateFormat = Date.DATETIME_FORMAT; + } + } + // Format the date + replacement = loggingEvent.startTime.toFormattedString(dateFormat); + break; + case "m": + replacement = loggingEvent.message; + break; + case "n": + replacement = "\n"; + break; + case "p": + replacement = loggingEvent.level.toString(); + break; + case "r": + replacement = "" + loggingEvent.startTime.toLocaleTimeString(); + break; + case "%": + replacement = "%"; + break; + default: + replacement = matchedString; + break; + } + // Format the replacement according to any padding or + // truncation specified + + var len; + + // First, truncation + if (truncation) { + len = parseInt(truncation.substr(1), 10); + replacement = replacement.substring(0, len); + } + // Next, padding + if (padding) { + if (padding.charAt(0) == "-") { + len = parseInt(padding.substr(1), 10); + // Right pad with spaces + while (replacement.length < len) { + replacement += " "; + } + } else { + len = parseInt(padding, 10); + // Left pad with spaces + while (replacement.length < len) { + replacement = " " + replacement; + } + } + } + formattedString += replacement; } - return formattedString; + searchString = searchString.substr(result.index + result[0].length); + } + return formattedString; }; -}; + }; + + //set ourselves up if we can find a default log4js.json + configure(findConfiguration()); + + return { + getLogger: getLogger, + getDefaultLogger: getDefaultLogger, + + addAppender: addAppender, + clearAppenders: clearAppenders, + configure: configure, + + levels: levels, + + consoleAppender: consoleAppender, + fileAppender: fileAppender, + logLevelFilter: logLevelFilter, + + basicLayout: basicLayout, + messagePassThroughLayout: messagePassThroughLayout, + patternLayout: patternLayout, + colouredLayout: colouredLayout, + coloredLayout: colouredLayout + }; +} -patternLayout.TTCC_CONVERSION_PATTERN = "%r %p %c - %m%n"; -patternLayout.DEFAULT_CONVERSION_PATTERN = "%m%n"; Date.ISO8601_FORMAT = "yyyy-MM-dd hh:mm:ss.SSS"; Date.ISO8601_WITH_TZ_OFFSET_FORMAT = "yyyy-MM-ddThh:mm:ssO"; @@ -563,19 +646,19 @@ Date.DATETIME_FORMAT = "dd MMM YYYY hh:mm:ss.SSS"; Date.ABSOLUTETIME_FORMAT = "hh:mm:ss.SSS"; Date.prototype.toFormattedString = function(format) { - format = format || Date.ISO8601_FORMAT; - - var vDay = addZero(this.getDate()); - var vMonth = addZero(this.getMonth()+1); - var vYearLong = addZero(this.getFullYear()); - var vYearShort = addZero(this.getFullYear().toString().substring(3,4)); - var vYear = (format.indexOf("yyyy") > -1 ? vYearLong : vYearShort); - var vHour = addZero(this.getHours()); - var vMinute = addZero(this.getMinutes()); - var vSecond = addZero(this.getSeconds()); - var vMillisecond = padWithZeros(this.getMilliseconds(), 3); - var vTimeZone = offset(this); - var formatted = format + format = format || Date.ISO8601_FORMAT; + + var vDay = addZero(this.getDate()); + var vMonth = addZero(this.getMonth()+1); + var vYearLong = addZero(this.getFullYear()); + var vYearShort = addZero(this.getFullYear().toString().substring(3,4)); + var vYear = (format.indexOf("yyyy") > -1 ? vYearLong : vYearShort); + var vHour = addZero(this.getHours()); + var vMinute = addZero(this.getMinutes()); + var vSecond = addZero(this.getSeconds()); + var vMillisecond = padWithZeros(this.getMilliseconds(), 3); + var vTimeZone = offset(this); + var formatted = format .replace(/dd/g, vDay) .replace(/MM/g, vMonth) .replace(/y{1,4}/g, vYear) @@ -584,40 +667,33 @@ Date.prototype.toFormattedString = function(format) { .replace(/ss/g, vSecond) .replace(/SSS/g, vMillisecond) .replace(/O/g, vTimeZone); - return formatted; + return formatted; - function padWithZeros(vNumber, width) { - var numAsString = vNumber + ""; - while (numAsString.length < width) { - numAsString = "0" + numAsString; + function padWithZeros(vNumber, width) { + var numAsString = vNumber + ""; + while (numAsString.length < width) { + numAsString = "0" + numAsString; + } + return numAsString; } - return numAsString; - } - function addZero(vNumber) { - return padWithZeros(vNumber, 2); - } + function addZero(vNumber) { + return padWithZeros(vNumber, 2); + } - /** - * Formats the TimeOffest - * Thanks to http://www.svendtofte.com/code/date_format/ - * @private - */ - function offset(date) { - // Difference to Greenwich time (GMT) in hours - var os = Math.abs(date.getTimezoneOffset()); - var h = String(Math.floor(os/60)); - var m = String(os%60); - h.length == 1? h = "0"+h:1; - m.length == 1? m = "0"+m:1; - return date.getTimezoneOffset() < 0 ? "+"+h+m : "-"+h+m; - } + /** + * Formats the TimeOffest + * Thanks to http://www.svendtofte.com/code/date_format/ + * @private + */ + function offset(date) { + // Difference to Greenwich time (GMT) in hours + var os = Math.abs(date.getTimezoneOffset()); + var h = String(Math.floor(os/60)); + var m = String(os%60); + h.length == 1? h = "0"+h:1; + m.length == 1? m = "0"+m:1; + return date.getTimezoneOffset() < 0 ? "+"+h+m : "-"+h+m; + } }; -exports.consoleAppender = log4js.consoleAppender = consoleAppender; -exports.fileAppender = log4js.fileAppender = fileAppender; -exports.logLevelFilter = log4js.logLevelFilter = logLevelFilter; -exports.basicLayout = log4js.basicLayout = basicLayout; -exports.patternLayout = log4js.patternLayout = patternLayout; -exports.messagePassThroughLayout = log4js.messagePassThroughLayout = messagePassThroughLayout; - diff --git a/lib/log4js.json b/lib/log4js.json new file mode 100644 index 0000000..7b6d3e7 --- /dev/null +++ b/lib/log4js.json @@ -0,0 +1,7 @@ +{ + "appenders": [ + { + "type": "console" + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 16f1429..e5d10f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "log4js", - "version": "0.1.1", + "version": "0.2.0", "description": "Port of Log4js to work with node.", "keywords": [ "logging", @@ -15,9 +15,10 @@ }, "engines": [ "node >=0.1.100" ], "scripts": { - "test": "test.js" + "test": "vows test/logging.js" }, "directories": { - "test": "spec" + "test": "test", + "lib": "lib" } } diff --git a/spec/spec.logging.js b/spec/spec.logging.js index fbaea45..0b91e5a 100644 --- a/spec/spec.logging.js +++ b/spec/spec.logging.js @@ -1,12 +1,7 @@ describe 'log4js' before extend(context, { - log4js : require("log4js"), - fs: require("fs"), - waitForWriteAndThenReadFile : function (filename) { - process.loop(); - return fs.readFileSync(filename, "utf8"); - } + log4js : require("log4js")() }); end @@ -17,35 +12,7 @@ describe 'log4js' logger.setLevel("TRACE"); logger.addListener("log", function (logEvent) { event = logEvent; }); end - - describe 'getLogger' - - it 'should take a category and return a Logger' - logger.category.should.be 'tests' - logger.level.should.be log4js.levels.TRACE - logger.should.respond_to 'debug' - logger.should.respond_to 'info' - logger.should.respond_to 'warn' - logger.should.respond_to 'error' - logger.should.respond_to 'fatal' - end - - it 'should emit log events' - logger.trace("Trace event"); - - event.level.toString().should.be 'TRACE' - event.message.should.be 'Trace event' - event.startTime.should.not.be undefined - end - it 'should not emit events of a lower level than the minimum' - logger.setLevel("DEBUG"); - event = undefined; - logger.trace("This should not generate a log message"); - event.should.be undefined - end - end - describe 'addAppender' before_each appenderEvent = undefined; @@ -170,112 +137,8 @@ describe 'log4js' end end - describe 'messagePassThroughLayout' - it 'should take a logevent and output only the message' - logger.debug('this is a test'); - log4js.messagePassThroughLayout(event).should.be 'this is a test' - end - end - describe 'fileAppender' - before - log4js.clearAppenders(); - try { - fs.unlinkSync('./tmp-tests.log'); - } catch(e) { - //print('Could not delete tmp-tests.log: '+e.message); - } - end - - it 'should write log events to a file' - log4js.addAppender(log4js.fileAppender('./tmp-tests.log', log4js.messagePassThroughLayout), 'tests'); - logger.debug('this is a test'); - - waitForWriteAndThenReadFile('./tmp-tests.log').should.be 'this is a test\n' - end - end - describe 'logLevelFilter' - - it 'should only pass log events greater than or equal to its own level' - var logEvent; - log4js.addAppender(log4js.logLevelFilter('ERROR', function(evt) { logEvent = evt; })); - logger.debug('this should not trigger an event'); - logEvent.should.be undefined - - logger.warn('neither should this'); - logEvent.should.be undefined - - logger.error('this should, though'); - logEvent.should.not.be undefined - logEvent.message.should.be 'this should, though' - - logger.fatal('so should this') - logEvent.message.should.be 'so should this' - end - - end - - describe 'configure' - before_each - log4js.clearAppenders(); - try { - fs.unlinkSync('./tmp-tests.log'); - } catch(e) { - //print('Could not delete tmp-tests.log: '+e.message); - } - try { - fs.unlinkSync('./tmp-tests-warnings.log'); - } catch (e) { - //print('Could not delete tmp-tests-warnings.log: '+e.message); - } - end - - it 'should load appender configuration from a json file' - //this config file defines one file appender (to ./tmp-tests.log) - //and sets the log level for "tests" to WARN - log4js.configure('spec/fixtures/log4js.json'); - event = undefined; - logger = log4js.getLogger("tests"); - logger.addListener("log", function(evt) { event = evt }); - - logger.info('this should not fire an event'); - event.should.be undefined - - logger.warn('this should fire an event'); - event.message.should.be 'this should fire an event' - waitForWriteAndThenReadFile('./tmp-tests.log').should.be 'this should fire an event\n' - end - - it 'should handle logLevelFilter configuration' - log4js.configure('spec/fixtures/with-logLevelFilter.json'); - - logger.info('main'); - logger.error('both'); - logger.warn('both'); - logger.debug('main'); - - waitForWriteAndThenReadFile('./tmp-tests.log').should.be 'main\nboth\nboth\nmain\n' - waitForWriteAndThenReadFile('./tmp-tests-warnings.log').should.be 'both\nboth\n' - end - end end -describe 'Date' - before - require("log4js"); - end - - describe 'toFormattedString' - it 'should add a toFormattedString method to Date' - var date = new Date(); - date.should.respond_to 'toFormattedString' - end - - it 'should default to a format' - var date = new Date(2010, 0, 11, 14, 31, 30, 5); - date.toFormattedString().should.be '2010-01-11 14:31:30.005' - end - end -end diff --git a/spec/fixtures/log4js.json b/test/log4js.json similarity index 100% rename from spec/fixtures/log4js.json rename to test/log4js.json diff --git a/test/logging.js b/test/logging.js new file mode 100644 index 0000000..8fd9d8a --- /dev/null +++ b/test/logging.js @@ -0,0 +1,362 @@ +var vows = require('vows'), +assert = require('assert'); + +vows.describe('log4js').addBatch({ + 'getLogger': { + topic: function() { + var log4js = require('../lib/log4js')(); + log4js.clearAppenders(); + var logger = log4js.getLogger('tests'); + logger.setLevel("DEBUG"); + return logger; + }, + + 'should take a category and return a logger': function(logger) { + assert.equal(logger.category, 'tests'); + assert.equal(logger.level.toString(), "DEBUG"); + assert.isFunction(logger.debug); + assert.isFunction(logger.info); + assert.isFunction(logger.warn); + assert.isFunction(logger.error); + assert.isFunction(logger.fatal); + }, + + 'log events' : { + topic: function(logger) { + var events = []; + logger.addListener("log", function (logEvent) { events.push(logEvent); }); + logger.debug("Debug event"); + logger.trace("Trace event 1"); + logger.trace("Trace event 2"); + logger.warn("Warning event"); + return events; + }, + + 'should emit log events': function(events) { + assert.equal(events[0].level.toString(), 'DEBUG'); + assert.equal(events[0].message, 'Debug event'); + assert.instanceOf(events[0].startTime, Date); + }, + + 'should not emit events of a lower level': function(events) { + assert.length(events, 2); + assert.equal(events[1].level.toString(), 'WARN'); + } + }, + + }, + + 'fileAppender': { + topic: function() { + var appender, logmessages = [], thing = "thing", fakeFS = { + openSync: function() { + assert.equal(arguments[0], './tmp-tests.log'); + assert.equal(arguments[1], 'a'); + assert.equal(arguments[2], 0644); + return thing; + }, + write: function() { + assert.equal(arguments[0], thing); + assert.isString(arguments[1]); + assert.isNull(arguments[2]); + assert.equal(arguments[3], "utf8"); + logmessages.push(arguments[1]); + }, + watchFile: function() { + throw new Error("watchFile should not be called if logSize is not defined"); + } + }, + log4js = require('../lib/log4js')(fakeFS); + log4js.clearAppenders(); + + appender = log4js.fileAppender('./tmp-tests.log', log4js.messagePassThroughLayout); + log4js.addAppender(appender, 'file-test'); + + var logger = log4js.getLogger('file-test'); + logger.debug("this is a test"); + + return logmessages; + }, + 'should write log messages to file': function(logmessages) { + assert.length(logmessages, 1); + assert.equal(logmessages, "this is a test\n"); + } + }, + + 'fileAppender - with rolling based on size and number of files to keep': { + topic: function() { + var watchCb, + filesOpened = [], + filesClosed = [], + filesRenamed = [], + newFilenames = [], + existingFiles = ['tests.log'], + log4js = require('../lib/log4js')({ + watchFile: function(file, options, callback) { + assert.equal(file, 'tests.log'); + assert.equal(options.persistent, false); + assert.equal(options.interval, 30000); + assert.isFunction(callback); + watchCb = callback; + }, + openSync: function(file) { + assert.equal(file, 'tests.log'); + filesOpened.push(file); + return file; + }, + statSync: function(file) { + if (existingFiles.indexOf(file) < 0) { + throw new Error("this file doesn't exist"); + } else { + return true; + } + }, + renameSync: function(oldFile, newFile) { + filesRenamed.push(oldFile); + existingFiles.push(newFile); + }, + closeSync: function(file) { + //it should always be closing tests.log + assert.equal(file, 'tests.log'); + filesClosed.push(file); + } + }); + var appender = log4js.fileAppender('tests.log', log4js.messagePassThroughLayout, 1024, 2, 30); + return [watchCb, filesOpened, filesClosed, filesRenamed, existingFiles]; + }, + + 'should close current log file, rename all old ones, open new one on rollover': function(args) { + var watchCb = args[0], filesOpened = args[1], filesClosed = args[2], filesRenamed = args[3], existingFiles = args[4]; + assert.isFunction(watchCb); + //tell the watchCb that the file is below the threshold + watchCb({ size: 891 }, { size: 0 }); + //filesOpened should still be the first one. + assert.length(filesOpened, 1); + //tell the watchCb that the file is now over the threshold + watchCb({ size: 1053 }, { size: 891 }); + //it should have closed the first log file. + assert.length(filesClosed, 1); + //it should have renamed the previous log file + assert.length(filesRenamed, 1); + //and we should have two files now + assert.length(existingFiles, 2); + assert.deepEqual(existingFiles, ['tests.log', 'tests.log.1']); + //and opened a new log file. + assert.length(filesOpened, 2); + + //now tell the watchCb that we've flipped over the threshold again + watchCb({ size: 1025 }, { size: 123 }); + //it should have closed the old file + assert.length(filesClosed, 2); + //it should have renamed both the old log file, and the previous '.1' file + assert.length(filesRenamed, 3); + assert.deepEqual(filesRenamed, ['tests.log', 'tests.log.1', 'tests.log' ]); + //it should have renamed 2 more file + assert.length(existingFiles, 4); + assert.deepEqual(existingFiles, ['tests.log', 'tests.log.1', 'tests.log.2', 'tests.log.1']); + //and opened a new log file + assert.length(filesOpened, 3); + + //tell the watchCb we've flipped again. + watchCb({ size: 1024 }, { size: 234 }); + //close the old one again. + assert.length(filesClosed, 3); + //it should have renamed the old log file and the 2 backups, with the last one being overwritten. + assert.length(filesRenamed, 5); + assert.deepEqual(filesRenamed, ['tests.log', 'tests.log.1', 'tests.log', 'tests.log.1', 'tests.log' ]); + //it should have renamed 2 more files + assert.length(existingFiles, 6); + assert.deepEqual(existingFiles, ['tests.log', 'tests.log.1', 'tests.log.2', 'tests.log.1', 'tests.log.2', 'tests.log.1']); + //and opened a new log file + assert.length(filesOpened, 4); + } + }, + + 'configure' : { + topic: function() { + var messages = {}, fakeFS = { + openSync: function(file) { + return file; + }, + write: function(file, message) { + if (!messages.hasOwnProperty(file)) { + messages[file] = []; + } + messages[file].push(message); + }, + readFileSync: function(file, encoding) { + return require('fs').readFileSync(file, encoding); + }, + watchFile: function(file) { + messages.watchedFile = file; + } + }, + log4js = require('../lib/log4js')(fakeFS); + return [ log4js, messages ]; + }, + 'should load appender configuration from a json file': function(args) { + var log4js = args[0], messages = args[1]; + delete messages['tmp-tests.log']; + log4js.clearAppenders(); + //this config file defines one file appender (to ./tmp-tests.log) + //and sets the log level for "tests" to WARN + log4js.configure('test/log4js.json'); + var logger = log4js.getLogger("tests"); + logger.info('this should not be written to the file'); + logger.warn('this should be written to the file'); + assert.length(messages['tmp-tests.log'], 1); + assert.equal(messages['tmp-tests.log'][0], 'this should be written to the file\n'); + }, + 'should handle logLevelFilter configuration': function(args) { + var log4js = args[0], messages = args[1]; + delete messages['tmp-tests.log']; + delete messages['tmp-tests-warnings.log']; + log4js.clearAppenders(); + log4js.configure('test/with-logLevelFilter.json'); + var logger = log4js.getLogger("tests"); + logger.info('main'); + logger.error('both'); + logger.warn('both'); + logger.debug('main'); + + assert.length(messages['tmp-tests.log'], 4); + assert.length(messages['tmp-tests-warnings.log'], 2); + assert.deepEqual(messages['tmp-tests.log'], ['main\n','both\n','both\n','main\n']); + assert.deepEqual(messages['tmp-tests-warnings.log'], ['both\n','both\n']); + }, + 'should handle fileAppender with log rolling' : function(args) { + var log4js = args[0], messages = args[1]; + delete messages['tmp-test.log']; + log4js.configure('test/with-log-rolling.json'); + assert.equal(messages.watchedFile, 'tmp-test.log'); + } + }, + + 'with no appenders defined' : { + topic: function() { + var logger, message, log4js = require('../lib/log4js')(null, function (msg) { message = msg; } ); + logger = log4js.getLogger("some-logger"); + logger.debug("This is a test"); + return message; + }, + 'should default to the console appender': function(message) { + assert.isTrue(/This is a test$/.test(message)); + } + }, + + 'default setup': { + topic: function() { + var pathsChecked = [], + message, + logger, + fakeFS = { + readFileSync: function (file, encoding) { + assert.equal(file, '/path/to/config/log4js.json'); + assert.equal(encoding, 'utf8'); + return '{ "appenders" : [ { "type": "console", "layout": { "type": "messagePassThrough" }} ] }'; + }, + statSync: function (path) { + pathsChecked.push(path); + if (path === '/path/to/config/log4js.json') { + return true; + } else { + throw new Error("no such file"); + } + } + }, + fakeConsoleLog = function (msg) { message = msg; }, + fakeRequirePath = [ '/a/b/c', '/some/other/path', '/path/to/config', '/some/later/directory' ], + log4js = require('../lib/log4js')(fakeFS, fakeConsoleLog, fakeRequirePath), + logger = log4js.getLogger('a-test'); + logger.debug("this is a test"); + + return [ pathsChecked, message ]; + }, + + 'should check current directory, require paths, and finally the module dir for log4js.json': function(args) { + var pathsChecked = args[0]; + assert.deepEqual(pathsChecked, [ + 'log4js.json', + '/a/b/c/log4js.json', + '/some/other/path/log4js.json', + '/path/to/config/log4js.json', + '/some/later/directory/log4js.json', + require('path').normalize(__dirname + '/../lib/log4js.json') + ]); + }, + + 'should configure log4js from first log4js.json found': function(args) { + var message = args[1]; + assert.equal(message, 'this is a test'); + } + }, + + 'colouredLayout': { + topic: function() { + return require('../lib/log4js')().colouredLayout; + }, + + 'should apply level colour codes to output': function(layout) { + var output = layout({ + message: "nonsense", + startTime: new Date(2010, 11, 5, 14, 18, 30, 45), + categoryName: "cheese", + level: { + colour: "green", + toString: function() { return "ERROR"; } + } + }); + assert.equal(output, '\033[90m[2010-12-05 14:18:30.045] \033[39m\033[32m[ERROR] \033[39m\033[90mcheese - \033[39mnonsense'); + } + }, + + 'messagePassThroughLayout': { + topic: function() { + return require('../lib/log4js')().messagePassThroughLayout; + }, + 'should take a logevent and output only the message' : function(layout) { + assert.equal(layout({ + message: "nonsense", + startTime: new Date(2010, 11, 5, 14, 18, 30, 45), + categoryName: "cheese", + level: { + colour: "green", + toString: function() { return "ERROR"; } + } + }), "nonsense"); + } + }, + + 'logLevelFilter': { + topic: function() { + var log4js = require('../lib/log4js')(), logEvents = [], logger; + log4js.clearAppenders(); + log4js.addAppender(log4js.logLevelFilter('ERROR', function(evt) { logEvents.push(evt); })); + logger = log4js.getLogger(); + logger.debug('this should not trigger an event'); + logger.warn('neither should this'); + logger.error('this should, though'); + logger.fatal('so should this'); + return logEvents; + }, + 'should only pass log events greater than or equal to its own level' : function(logEvents) { + assert.length(logEvents, 2); + assert.equal(logEvents[0].message, 'this should, though'); + assert.equal(logEvents[1].message, 'so should this'); + } + }, + + 'Date extensions': { + topic: function() { + require('../lib/log4js'); + return new Date(2010, 0, 11, 14, 31, 30, 5); + }, + 'should add a toFormattedString method to Date': function(date) { + assert.isFunction(date.toFormattedString); + }, + 'should default to a format': function(date) { + assert.equal(date.toFormattedString(), '2010-01-11 14:31:30.005'); + } + } + +}).export(module); diff --git a/test/with-log-rolling.json b/test/with-log-rolling.json new file mode 100644 index 0000000..1d745ca --- /dev/null +++ b/test/with-log-rolling.json @@ -0,0 +1,11 @@ +{ + "appenders": [ + { + "type": "file", + "filename": "tmp-test.log", + "maxLogSize": 1024, + "backups": 3, + "pollInterval": 15 + } + ] +} \ No newline at end of file diff --git a/spec/fixtures/with-logLevelFilter.json b/test/with-logLevelFilter.json similarity index 100% rename from spec/fixtures/with-logLevelFilter.json rename to test/with-logLevelFilter.json