diff --git a/config/env/development.js b/config/env/development.js index e2fb300ec8..6b3232fa41 100644 --- a/config/env/development.js +++ b/config/env/development.js @@ -13,12 +13,22 @@ module.exports = { debug: process.env.MONGODB_DEBUG || false }, log: { + // logging with Morgan - https://github.com/expressjs/morgan // Can specify one of 'combined', 'common', 'dev', 'short', 'tiny' format: 'dev', - // Stream defaults to process.stdout - // Uncomment to enable logging to a log on the file system options: { - //stream: 'access.log' + // 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 + } + } } }, app: { diff --git a/config/env/production.js b/config/env/production.js index 22f71e1c88..f3094c429a 100644 --- a/config/env/production.js +++ b/config/env/production.js @@ -17,12 +17,22 @@ module.exports = { debug: process.env.MONGODB_DEBUG || false }, log: { + // logging with Morgan - https://github.com/expressjs/morgan // Can specify one of 'combined', 'common', 'dev', 'short', 'tiny' - format: 'combined', - // Stream defaults to process.stdout - // Uncomment to enable logging to a log on the file system + format: process.env.LOG_FORMAT || 'combined', options: { - stream: 'access.log' + // 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' ? true : false, // 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' ? true : false + } + } } }, facebook: { diff --git a/config/env/test.js b/config/env/test.js index 12f8ca1df6..8ea1cfc77f 100644 --- a/config/env/test.js +++ b/config/env/test.js @@ -12,6 +12,25 @@ module.exports = { // Enable mongoose debug mode debug: process.env.MONGODB_DEBUG || false }, + 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 + } + } + } + }, port: process.env.PORT || 3001, app: { title: defaultEnvConfig.app.title + ' - Test Environment' diff --git a/config/lib/express.js b/config/lib/express.js index 59f3cc4c46..4f9e764934 100644 --- a/config/lib/express.js +++ b/config/lib/express.js @@ -6,6 +6,7 @@ var config = require('../config'), express = require('express'), morgan = require('morgan'), + logger = require('./logger'), bodyParser = require('body-parser'), session = require('express-session'), MongoStore = require('connect-mongo')(session), @@ -67,11 +68,11 @@ module.exports.initMiddleware = function (app) { // Initialize favicon middleware app.use(favicon('./modules/core/client/img/brand/favicon.ico')); + // Enable logger (morgan) + app.use(morgan(logger.getFormat(), logger.getOptions())); + // Environment dependent middleware if (process.env.NODE_ENV === 'development') { - // Enable logger (morgan) - app.use(morgan('dev')); - // Disable views cache app.set('view cache', false); } else if (process.env.NODE_ENV === 'production') { diff --git a/config/lib/logger.js b/config/lib/logger.js new file mode 100644 index 0000000000..cacfb9edef --- /dev/null +++ b/config/lib/logger.js @@ -0,0 +1,109 @@ +'use strict'; + +var _ = require('lodash'), + config = require('../config'), + chalk = require('chalk'), + fileStreamRotator = require('file-stream-rotator'), + fs = require('fs'); + +// 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 +}; + +// export the logger service +module.exports = logger; + +/** + * The format to use with the logger + * + * Returns the log.format option set in the current environment configuration + */ +function getLogFormat () { + var format = config.log && config.log.format ? config.log.format.toString() : 'combined'; + + // make sure we have a valid format + if (!_.includes(validFormats, format)) { + format = 'combined'; + + if (process.env.NODE_ENV !== 'test') { + console.log(); + console.log(chalk.yellow('Warning: An invalid format was provided. The logger will use the default format of "' + format + '"')); + console.log(); + } + } + + 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) : {}; + + // 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; +} diff --git a/modules/core/tests/server/core.server.config.tests.js b/modules/core/tests/server/core.server.config.tests.js index 9635490475..8be27aac65 100644 --- a/modules/core/tests/server/core.server.config.tests.js +++ b/modules/core/tests/server/core.server.config.tests.js @@ -3,17 +3,21 @@ /** * Module dependencies. */ -var should = require('should'), +var _ = require('lodash'), + should = require('should'), mongoose = require('mongoose'), User = mongoose.model('User'), path = require('path'), + fs = require('fs'), + mock = require('mock-fs'), config = require(path.resolve('./config/config')), + logger = require(path.resolve('./config/lib/logger')), seed = require(path.resolve('./config/lib/seed')); /** * Globals */ -var user1, admin1, userFromSeedConfig, adminFromSeedConfig; +var user1, admin1, userFromSeedConfig, adminFromSeedConfig, originalLogConfig; describe('Configuration Tests:', function () { @@ -404,4 +408,184 @@ describe('Configuration Tests:', function () { config.utils.validateSessionSecret(conf, true).should.equal(true); }); }); + + describe('Testing Logger Configuration', function () { + + beforeEach(function () { + originalLogConfig = _.clone(config.log, true); + mock(); + }); + + afterEach(function () { + config.log = originalLogConfig; + mock.restore(); + }); + + it('should retrieve the log format from the logger configuration', function () { + config.log = { + format: 'tiny' + }; + + var format = logger.getFormat(); + format.should.be.equal('tiny'); + }); + + it('should retrieve the log options from the logger configuration', function () { + config.log = { + options: { + _test_log_option_: 'testing' + } + }; + + var options = logger.getOptions(); + options.should.deepEqual(config.log.options); + }); + + it('should verify that a writable stream 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 + } + } + }; + + var options = logger.getOptions(); + options.stream.writable.should.equal(true); + }); + + it('should use the default log format of "combined" when an invalid format was provided', function () { + // manually set the config log format to be invalid + config.log = { + format: '_some_invalid_format_' + }; + + var format = logger.getFormat(); + format.should.be.equal('combined'); + }); + + it('should remove the stream option when an invalid filename was supplied for the log stream option', function () { + // manually set the config stream fileName option to an empty string + config.log = { + format: 'combined', + options: { + stream: { + directoryPath: process.cwd(), + fileName: '' + } + } + }; + + var options = logger.getOptions(); + should.not.exist(options.stream); + }); + + it('should remove the stream option when an invalid directory path was supplied for the log stream option', function () { + // manually set the config stream fileName option to an empty string + config.log = { + format: 'combined', + options: { + stream: { + directoryPath: '', + fileName: 'test.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); + }); + }); }); diff --git a/package.json b/package.json index 36c54ad0eb..45f954fad5 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "crypto": "0.0.3", "express": "^4.13.1", "express-session": "^1.11.3", + "file-stream-rotator": "~0.0.6", "forever": "~0.14.2", "generate-password": "^1.1.1", "glob": "^5.0.13", @@ -112,6 +113,7 @@ "karma-ng-html2js-preprocessor": "^0.1.2", "karma-phantomjs-launcher": "~0.2.0", "load-grunt-tasks": "^3.2.0", + "mock-fs": "~3.4.0", "run-sequence": "^1.1.1", "should": "^7.0.1", "supertest": "^1.0.1"