feat(logs): replacing unmaintained and vulnerable file-stream-rotator package with winston log facility (#1334)

* replacing file-stream-rotator with a better logging mechanism using winston which can be extended later for other use cases and integrations

* refactoring logger mechanism, accomodating for tests and environment variable configurations

* only enabling morgan logger if config.log.format option was defined, and disabling the app.log file transport option for the test environment

* disabling all kind of logging when in test enviroment
This commit is contained in:
Liran Tal
2016-05-22 01:03:18 +03:00
parent c61640bfdb
commit c8cbcd326f
7 changed files with 169 additions and 234 deletions

View File

@@ -16,19 +16,12 @@ module.exports = {
// logging with Morgan - https://github.com/expressjs/morgan
// Can specify one of 'combined', 'common', 'dev', 'short', 'tiny'
format: 'dev',
options: {
// Stream defaults to process.stdout
// Uncomment/comment to toggle the logging to a log on the file system
// stream: {
// directoryPath: process.cwd(),
// fileName: 'access.log',
// rotatingLogs: { // for more info on rotating logs - https://github.com/holidayextras/file-stream-rotator#usage
// active: false, // activate to use rotating logs
// fileName: 'access-%DATE%.log', // if rotating logs are active, this fileName setting will be used
// frequency: 'daily',
// verbose: false
// }
// }
fileLogger: {
directoryPath: process.cwd(),
fileName: 'app.log',
maxsize: 10485760,
maxFiles: 2,
json: false
}
},
app: {

View File

@@ -22,19 +22,12 @@ module.exports = {
// logging with Morgan - https://github.com/expressjs/morgan
// Can specify one of 'combined', 'common', 'dev', 'short', 'tiny'
format: process.env.LOG_FORMAT || 'combined',
options: {
// Stream defaults to process.stdout
// Uncomment/comment to toggle the logging to a log on the file system
stream: {
directoryPath: process.env.LOG_DIR_PATH || process.cwd(),
fileName: process.env.LOG_FILE || 'access.log',
rotatingLogs: { // for more info on rotating logs - https://github.com/holidayextras/file-stream-rotator#usage
active: process.env.LOG_ROTATING_ACTIVE === 'true', // activate to use rotating logs
fileName: process.env.LOG_ROTATING_FILE || 'access-%DATE%.log', // if rotating logs are active, this fileName setting will be used
frequency: process.env.LOG_ROTATING_FREQUENCY || 'daily',
verbose: process.env.LOG_ROTATING_VERBOSE === 'true'
}
}
fileLogger: {
directoryPath: process.env.LOG_DIR_PATH || process.cwd(),
fileName: process.env.LOG_FILE || 'app.log',
maxsize: 10485760,
maxFiles: 2,
json: false
}
},
facebook: {

23
config/env/test.js vendored
View File

@@ -15,21 +15,14 @@ module.exports = {
log: {
// logging with Morgan - https://github.com/expressjs/morgan
// Can specify one of 'combined', 'common', 'dev', 'short', 'tiny'
format: process.env.LOG_FORMAT || 'combined',
options: {
// Stream defaults to process.stdout
// Uncomment/comment to toggle the logging to a log on the file system
stream: {
directoryPath: process.cwd(),
fileName: 'access.log',
rotatingLogs: { // for more info on rotating logs - https://github.com/holidayextras/file-stream-rotator#usage
active: false, // activate to use rotating logs
fileName: 'access-%DATE%.log', // if rotating logs are active, this fileName setting will be used
frequency: 'daily',
verbose: false
}
}
}
// format: 'dev'
// fileLogger: {
// directoryPath: process.cwd(),
// fileName: 'app.log',
// maxsize: 10485760,
// maxFiles: 2,
// json: false
// }
},
port: process.env.PORT || 3001,
app: {

View File

@@ -18,6 +18,7 @@ var config = require('../config'),
flash = require('connect-flash'),
consolidate = require('consolidate'),
path = require('path'),
_ = require('lodash'),
lusca = require('lusca');
/**
@@ -68,8 +69,10 @@ module.exports.initMiddleware = function (app) {
// Initialize favicon middleware
app.use(favicon(app.locals.favicon));
// Enable logger (morgan)
app.use(morgan(logger.getFormat(), logger.getOptions()));
// Enable logger (morgan) if enabled in the configuration file
if (_.has(config, 'log.format')) {
app.use(morgan(logger.getLogFormat(), logger.getMorganOptions()));
}
// Environment dependent middleware
if (process.env.NODE_ENV === 'development') {

View File

@@ -3,27 +3,128 @@
var _ = require('lodash'),
config = require('../config'),
chalk = require('chalk'),
fileStreamRotator = require('file-stream-rotator'),
fs = require('fs');
fs = require('fs'),
winston = require('winston');
// list of valid formats for the logging
var validFormats = ['combined', 'common', 'dev', 'short', 'tiny'];
// build logger service
var logger = {
getFormat: getLogFormat, // log format to use
getOptions: getLogOptions // log options to use
// Instantiating the default winston application logger with the Console
// transport
var logger = new winston.Logger({
transports: [
new winston.transports.Console({
level: 'info',
colorize: true,
showLevel: true,
handleExceptions: true,
humanReadableUnhandledException: true
})
],
exitOnError: false
});
// A stream object with a write function that will call the built-in winston
// logger.info() function.
// Useful for integrating with stream-related mechanism like Morgan's stream
// option to log all HTTP requests to a file
logger.stream = {
write: function(msg) {
logger.info(msg);
}
};
// export the logger service
module.exports = logger;
/**
* Instantiate a winston's File transport for disk file logging
*
* @param logger a valid winston logger object
*/
logger.setupFileLogger = function setupFileLogger(options) {
var fileLoggerTransport = this.getLogOptions();
if (!fileLoggerTransport) {
return false;
}
try {
// Check first if the configured path is writable and only then
// instantiate the file logging transport
if (fs.openSync(fileLoggerTransport.filename, 'a+')) {
logger.add(winston.transports.File, fileLoggerTransport);
}
return true;
} catch (err) {
if (process.env.NODE_ENV !== 'test') {
console.log();
console.log(chalk.red('An error has occured during the creation of the File transport logger.'));
console.log(chalk.red(err));
console.log();
}
return false;
}
};
/**
* The options to use with winston logger
*
* Returns a Winston object for logging with the File transport
*/
logger.getLogOptions = function getLogOptions(configOptions) {
var _config = _.clone(config, true);
if (configOptions) {
_config = configOptions;
}
var configFileLogger = _config.log.fileLogger;
if (!_.has(_config, 'log.fileLogger.directoryPath') || !_.has(_config, 'log.fileLogger.fileName')) {
console.log('unable to find logging file configuration');
return false;
}
var logPath = configFileLogger.directoryPath + '/' + configFileLogger.fileName;
return {
level: 'debug',
colorize: false,
filename: logPath,
timestamp: true,
maxsize: configFileLogger.maxsize ? configFileLogger.maxsize : 10485760,
maxFiles: configFileLogger.maxFiles ? configFileLogger.maxFiles : 2,
json: (_.has(configFileLogger, 'json')) ? configFileLogger.json : false,
eol: '\n',
tailable: true,
showLevel: true,
handleExceptions: true,
humanReadableUnhandledException: true
};
};
/**
* The options to use with morgan logger
*
* Returns a log.options object with a writable stream based on winston
* file logging transport (if available)
*/
logger.getMorganOptions = function getMorganOptions() {
return {
stream: logger.stream
};
};
/**
* The format to use with the logger
*
* Returns the log.format option set in the current environment configuration
*/
function getLogFormat () {
logger.getLogFormat = function getLogFormat() {
var format = config.log && config.log.format ? config.log.format.toString() : 'combined';
// make sure we have a valid format
@@ -38,72 +139,8 @@ function getLogFormat () {
}
return format;
}
};
/**
* The options to use with the logger
*
* Returns the log.options object set in the current environment configuration.
* NOTE: Any options, requiring special handling (e.g. 'stream'), that encounter an error will be removed from the options.
*/
function getLogOptions () {
var options = config.log && config.log.options ? _.clone(config.log.options, true) : {};
logger.setupFileLogger({});
// check if the current environment config has the log stream option set
if (_.has(options, 'stream')) {
try {
// check if we need to use rotating logs
if (_.has(options, 'stream.rotatingLogs') && options.stream.rotatingLogs.active) {
if (options.stream.rotatingLogs.fileName.length && options.stream.directoryPath.length) {
// ensure the log directory exists
if (!fs.existsSync(options.stream.directoryPath)) {
fs.mkdirSync(options.stream.directoryPath);
}
options.stream = fileStreamRotator.getStream({
filename: options.stream.directoryPath + '/' + options.stream.rotatingLogs.fileName,
frequency: options.stream.rotatingLogs.frequency,
verbose: options.stream.rotatingLogs.verbose
});
} else {
// throw a new error so we can catch and handle it gracefully
throw new Error('An invalid fileName or directoryPath was provided for the rotating logs option.');
}
} else {
// create the WriteStream to use for the logs
if (options.stream.fileName.length && options.stream.directoryPath.length) {
// ensure the log directory exists
if (!fs.existsSync(options.stream.directoryPath)) {
fs.mkdirSync(options.stream.directoryPath);
}
options.stream = fs.createWriteStream(options.stream.directoryPath + '/' + config.log.options.stream.fileName, { flags: 'a' });
} else {
// throw a new error so we can catch and handle it gracefully
throw new Error('An invalid fileName or directoryPath was provided for stream option.');
}
}
} catch (err) {
// remove the stream option
delete options.stream;
if (process.env.NODE_ENV !== 'test') {
console.log();
console.log(chalk.red('An error has occured during the creation of the WriteStream. The stream option has been omitted.'));
console.log(chalk.red(err));
console.log();
}
}
}
return options;
}
module.exports = logger;

View File

@@ -426,53 +426,55 @@ describe('Configuration Tests:', function () {
});
it('should retrieve the log format from the logger configuration', function () {
config.log = {
format: 'tiny'
};
var format = logger.getFormat();
var format = logger.getLogFormat();
format.should.be.equal('tiny');
});
it('should retrieve the log options from the logger configuration', function () {
config.log = {
options: {
_test_log_option_: 'testing'
}
};
it('should retrieve the log options from the logger configuration for a valid stream object', function () {
var options = logger.getMorganOptions();
options.should.be.instanceof(Object);
options.should.have.property('stream');
var options = logger.getOptions();
should.deepEqual(options, config.log.options);
});
it('should verify that a writable stream was created using the logger configuration', function () {
it('should verify that a file logger object was created using the logger configuration', function () {
var _dir = process.cwd();
var _filename = 'unit-test-access.log';
config.log = {
options: {
stream: {
directoryPath: _dir,
fileName: _filename
}
fileLogger: {
directoryPath: _dir,
fileName: _filename
}
};
var options = logger.getOptions();
options.stream.writable.should.equal(true);
var fileTransport = logger.getLogOptions(config);
fileTransport.should.be.instanceof(Object);
fileTransport.filename.should.equal(_dir + '/' + _filename);
});
it('should use the default log format of "combined" when an invalid format was provided', function () {
var _logger = require(path.resolve('./config/lib/logger'));
// manually set the config log format to be invalid
config.log = {
format: '_some_invalid_format_'
};
var format = logger.getFormat();
var format = _logger.getLogFormat();
format.should.be.equal('combined');
});
it('should remove the stream option when an invalid filename was supplied for the log stream option', function () {
it('should not create a file transport object if critical options are missing: filename', function () {
// manually set the config stream fileName option to an empty string
config.log = {
format: 'combined',
@@ -484,112 +486,25 @@ describe('Configuration Tests:', function () {
}
};
var options = logger.getOptions();
should.not.exist(options.stream);
var fileTransport = logger.getLogOptions(config);
fileTransport.should.be.false();
});
it('should remove the stream option when an invalid directory path was supplied for the log stream option', function () {
it('should not create a file transport object if critical options are missing: directory', function () {
// manually set the config stream fileName option to an empty string
config.log = {
format: 'combined',
options: {
stream: {
directoryPath: '',
fileName: 'test.log'
fileName: 'app.log'
}
}
};
var options = logger.getOptions();
should.not.exist(options.stream);
});
it('should confirm that the log directory is created if it does not already exist', function () {
var _dir = process.cwd() + '/temp-logs';
var _filename = 'unit-test-access.log';
// manually set the config stream fileName option to an empty string
config.log = {
format: 'combined',
options: {
stream: {
directoryPath: _dir,
fileName: _filename
}
}
};
var options = logger.getOptions();
options.stream.writable.should.equal(true);
});
it('should remove the stream option when an invalid filename was supplied for the rotating log stream option', function () {
// enable rotating logs
config.log = {
format: 'combined',
options: {
stream: {
directoryPath: process.cwd(),
rotatingLogs: {
active: true,
fileName: '',
frequency: 'daily',
verbose: false
}
}
}
};
var options = logger.getOptions();
should.not.exist(options.stream);
});
it('should confirm that the rotating log is created using the logger configuration', function () {
var _dir = process.cwd();
var _filename = 'unit-test-rotating-access-%DATE%.log';
// enable rotating logs
config.log = {
format: 'combined',
options: {
stream: {
directoryPath: _dir,
rotatingLogs: {
active: true,
fileName: _filename,
frequency: 'daily',
verbose: false
}
}
}
};
var options = logger.getOptions();
should.exist(options.stream.write);
});
it('should confirm that the rotating log directory is created if it does not already exist', function () {
var _dir = process.cwd() + '/temp-rotating-logs';
var _filename = 'unit-test-rotating-access-%DATE%.log';
// enable rotating logs
config.log = {
format: 'combined',
options: {
stream: {
directoryPath: _dir,
rotatingLogs: {
active: true,
fileName: _filename,
frequency: 'daily',
verbose: false
}
}
}
};
var options = logger.getOptions();
should.exist(options.stream.write);
var fileTransport = logger.getLogOptions(config);
fileTransport.should.be.false();
});
});
});

View File

@@ -40,12 +40,12 @@
"glob": "~7.0.0",
"grunt": "0.4.5",
"grunt-cli": "~0.1.13",
"gulp-wiredep": "~0.0.0",
"helmet": "~1.3.0",
"jasmine-core": "~2.4.1",
"lodash": "~4.6.1",
"lusca": "~1.3.0",
"method-override": "~2.3.5",
"gulp-wiredep": "~0.0.0",
"mocha": "~2.4.5",
"mongoose": "~4.4.8",
"morgan": "~1.7.0",
@@ -64,7 +64,8 @@
"serve-favicon": "~2.3.0",
"socket.io": "~1.4.5",
"swig": "~1.4.2",
"validator": "~5.1.0"
"validator": "~5.1.0",
"winston": "^2.2.0"
},
"devDependencies": {
"coveralls": "~2.11.6",