diff --git a/README.MD b/README.MD index e1582fa..7b6cf99 100644 --- a/README.MD +++ b/README.MD @@ -6,6 +6,7 @@ - [Setup](#setup) - [Adding Migrations](#adding-migrations) - [Executing Migrations](#executing-migrations) + - [Logging](#logging) ## Prerequisites A node project with [mysql](https://github.com/mysqljs/mysql) used for database. @@ -216,6 +217,73 @@ node migration.js load-from-schema The above command will create all the tables and make entry in the logs table. It is helpful when setting up projects for newbies or environments without running entire migration process. +## Logging + +### Custom Logging + +By default, logging is done with `console`. You may specify your own logger with the following methods: `debug`, `log`, `info`, `warn`, `error`, and `critical`. Each one expects a string argument as a message. + +```js +var customLogger = { + debug: function (message) { + console.debug(message); + }, + log: function (message) { + console.log(message); + }, + info: function (message) { + console.info(message); + }, + warn: function (message) { + console.warn(message); + }, + error: function (message) { + console.error(message); + }, + critical: function (message) { + console.error(message); + } +}; + +migration.init( + connection, + __dirname + '/migrations', + function() {}, + [ + "--logger", + customLogger + ] +); +``` + +### Logging Threshold + +The logs can be fairly verbose. You can adjust the `--log-level ` according to the logs that you want to see. + +| Threshold | Logs | +| --- | --- | +| ALL | CRITICAL, ERROR, WARN, INFO, LOG, DEBUG | +| DEBUG | CRITICAL, ERROR, WARN, INFO, LOG, DEBUG | +| LOG | CRITICAL, ERROR, WARN, INFO, LOG | +| INFO | CRITICAL, ERROR, WARN, INFO | +| WARN | CRITICAL, ERROR, WARN | +| ERROR | CRITICAL, ERROR | +| CRITICAL | CRITICAL | +| NONE | *(nothing/silent)* | + +```js +migration.init( + connection, + __dirname + '/migrations', + function() {}, + [ + "--log-level ERROR" + ] +); +``` + +NOTE: The `--log-level` option is not evaluated before calling methods on custom loggers specified via the `--logger` option. + ## Pending >>Test cases: Will pick up when I get time. diff --git a/config.js b/config.js index 3dea0db..0ccbc2c 100644 --- a/config.js +++ b/config.js @@ -1,4 +1,6 @@ module.exports = { 'table' : 'mysql_migrations_347ertt3e', - 'migrations_types' : ['up', 'down'] + 'migrations_types': ['up', 'down'], + logger: console, + logLevel: 'ALL' }; diff --git a/core_functions.js b/core_functions.js index b724cdf..08da3df 100644 --- a/core_functions.js +++ b/core_functions.js @@ -2,9 +2,9 @@ var fs = require("fs"); var fileFunctions = require('./file'); var queryFunctions = require('./query'); -var colors = require('colors'); var exec = require('child_process').exec; -var table = require('./config')['table']; +var config = require('./config'); +var table = config.table; function add_migration(argv, path, cb) { fileFunctions.validate_file_name(argv[4]); @@ -27,7 +27,7 @@ function add_migration(argv, path, cb) { throw err; } - console.log("Added file " + file_name); + config.logger.info("Added file " + file_name); cb(); }); }); @@ -139,7 +139,7 @@ function update_schema(conn, path, cb) { exec(cmd, function(error, stdout, stderr) { fs.writeFile(filePath, stdout, function(err) { if (err) { - console.log(colors.red("Could not save schema file")); + config.logger.error("Could not save schema file"); } cb(); }); @@ -172,7 +172,7 @@ function createFromSchema(conn, path, cb) { cmd = cmd + " < " + filePath; exec(cmd, function(error, stdout, stderr) { if (error) { - console.log(colors.red("Could not load from Schema: " + error)); + config.logger.error("Could not load from Schema: " + error); cb(); } else { var file_paths = []; @@ -193,7 +193,7 @@ function createFromSchema(conn, path, cb) { } }); } else { - console.log(colors.red("Schema Missing: " + filePath)); + config.logger.error("Schema Missing: " + filePath); cb(); } } diff --git a/index.js b/index.js index 71e1195..3d7337f 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,7 @@ var queryFunctions = require('./query'); var config = require('./config'); var table = config['table']; var migrations_types = config['migrations_types']; +var logger = require('./logger'); var updateSchema = false; var migrate_all = false; @@ -32,6 +33,23 @@ function migration(conn, path, cb, options) { if (options.indexOf("--update-schema") > -1) { updateSchema = true; } + + var loggerIndex = options.findIndex(option => option === '--logger'); + if (loggerIndex !== -1) { + var customLogger = options[loggerIndex + 1]; + logger.checkLogger(customLogger); + config.logger = customLogger; + } else { + config.logger = logger; + } + + options + .filter(option => typeof option === "string" && option.startsWith('--log-level ')) + .forEach(option => { + var level = option.replace('--log-level ', '').toUpperCase(); + logger.checkLevel(level); + config.logLevel = level; + }); } queryFunctions.run_query(conn, "CREATE TABLE IF NOT EXISTS `" + table + "` (`timestamp` varchar(254) NOT NULL UNIQUE)", function (res) { @@ -94,6 +112,8 @@ function handle(argv, conn, path, cb) { else { throw new Error('command not found : ' + argv.join(" ")); } + } else { + cb(); } } diff --git a/logger.js b/logger.js new file mode 100644 index 0000000..d9dfd82 --- /dev/null +++ b/logger.js @@ -0,0 +1,55 @@ +var colors = require('colors'); +var config = require('./config'); + +var NONE = 'NONE'; +var CRITICAL = 'CRITICAL'; +var ERROR = 'ERROR'; +var WARN = 'WARN'; +var LOG = 'LOG'; +var INFO = 'INFO'; +var DEBUG = 'DEBUG'; +var ALL = 'ALL'; + +var levels = [NONE, CRITICAL, ERROR, WARN, INFO, LOG, DEBUG, ALL]; + +function canLog(level) { + var threshold = config.logLevel; + if (threshold === undefined) threshold = LOG; + var thresholdIndex = levels.indexOf(threshold); + var levelIndex = levels.indexOf(level); + return levelIndex <= thresholdIndex; +} + +function log(method, level, color, message) { + if (canLog(level)) { + console[method](color(message)); + } +} + +function checkLevel(level) { + if (!levels.includes(level)) { + throw new Error(`Invalid log level '${level}'. Acceptable values are ${levels.join(', ')}`); + } +} + +function checkLogger(customLogger) { + var keys = ['critical', 'error', 'warn', 'info', 'log', 'debug']; + keys.forEach(function (key) { + if (typeof customLogger[key] !== 'function') { + throw new Error(`Custom Logger '${key}' is not a function. Expected functions: ${keys.join(', ')}`); + } + }); +} + +var logger = { + checkLevel, + checkLogger, + critical: log.bind(null, 'error', CRITICAL, colors.red), + error: log.bind(null, 'error', ERROR, colors.red), + warn: log.bind(null, 'warn', WARN, colors.yellow), + info: log.bind(null, 'info', INFO, colors.blue), + log: log.bind(null, 'log', LOG, colors.white), + debug: log.bind(null, 'debug', DEBUG, colors.grey) +} + +module.exports = logger; \ No newline at end of file diff --git a/query.js b/query.js index 9ea29d1..fd3c2c5 100644 --- a/query.js +++ b/query.js @@ -1,6 +1,5 @@ -var table = require('./config')['table']; -var fileFunctions = require('./file'); -var colors = require('colors'); +var config = require('./config'); +var table = config.table; function run_query(conn, query, cb, run) { if (run == null) { @@ -36,7 +35,8 @@ function execute_query(conn, path, final_file_paths, type, cb, run) { var current_file_path = path + "/" + file_name; var queries = require(current_file_path); - console.info(colors.green("Run: " + run + " Type: " + type.toUpperCase() + ": " +queries[type])); + config.logger.info(`Run: ${run} Type: ${type.toUpperCase()}`); + config.logger.debug(queries[type]); var timestamp_val = file_name.split("_", 1)[0]; if (typeof(queries[type]) == 'string') { @@ -46,7 +46,7 @@ function execute_query(conn, path, final_file_paths, type, cb, run) { }); }, run); } else if (typeof(queries[type]) == 'function') { - console.info(`${type.toUpperCase()} Function: "${ queries[type].toString() }"`); + config.logger.info(`${type.toUpperCase()} Function: "${queries[type].toString()}"`); queries[type](conn, function() { updateRecords(conn, type, table, timestamp_val, function () { @@ -56,7 +56,7 @@ function execute_query(conn, path, final_file_paths, type, cb, run) { } } else { - console.info(colors.blue("No more " + type.toUpperCase() + " migrations to run")); + config.logger.info(`No more ${type.toUpperCase()} migrations to run`); cb(); } } diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..aa03a9d --- /dev/null +++ b/test/index.js @@ -0,0 +1,65 @@ +var chai = require('chai'); + +var testCommons = require('./test_commons'); +var assert = require('assert'); +var logger = require('../logger'); +var config = require('../config'); +var index = require('../index'); +var mysql = require('./mysql'); + +var should = chai.should(); +var path = __dirname + '/migrations/'; + +describe('index.js', function () { + + beforeEach(function (done) { + config.logger = logger; + config.logLevel = 'ALL'; + done(); + }); + + describe('.init', function () { + it('assigns --logger to config.logger', function (done) { + function noop() { }; + + var newLogger = { + critical: noop, + error: noop, + warn: noop, + info: noop, + log: noop, + debug: noop + }; + assert.notEqual(config.logger, newLogger); + + index.init( + mysql, + path, + function () { + assert.equal(config.logger, newLogger); + done(); + }, + [ + '--logger', + newLogger + ] + ); + }); + it('assigns --log-level to config.logLevel', function (done) { + var newLevel = 'NONE'; + assert.notEqual(config.logLevel, newLevel); + + index.init( + mysql, + path, + function () { + assert.equal(config.logLevel, newLevel); + done(); + }, + [ + `--log-level ${newLevel}` + ] + ); + }); + }); +}); diff --git a/test/logger.js b/test/logger.js new file mode 100644 index 0000000..cae155b --- /dev/null +++ b/test/logger.js @@ -0,0 +1,341 @@ +var chai = require('chai'); + +var testCommons = require('./test_commons'); +var assert = require('assert'); +var logger = require('../logger'); +var config = require('../config'); + +var should = chai.should(); + +var originalLog = console.log; +var originalError = console.error; +var originalInfo = console.info; +var originalDebug = console.debug; +var originalWarn = console.warn; +var logs = { + log: [], + error: [], + info: [], + debug: [], + warn: [] +}; + +describe('logger.js', function () { + before(function (done) { + // mock console log methods + console.log = function (message) { logs.log.push(message); }; + console.error = function (message) { logs.error.push(message); } + console.info = function (message) { logs.info.push(message); } + console.debug = function (message) { logs.debug.push(message); } + console.warn = function (message) { logs.warn.push(message); } + done(); + }); + beforeEach(function (done) { + config.logger = logger; + config.logLevel = 'all'; + logs = { + log: [], + error: [], + info: [], + debug: [], + warn: [] + }; + done(); + }); + after(function (done) { + console.log = originalLog.bind(console); + console.error = originalError.bind(console); + console.info = originalInfo.bind(console); + console.debug = originalDebug.bind(console); + console.warn = originalWarn.bind(console); + done(); + }) + + context('threshold=ALL', function () { + beforeEach(function () { + config.logLevel = 'ALL'; + }); + it('sends critical to console.error', function () { + var message = 'this is a critical message'; + logger.critical(message); + assert.ok(logs.error[0].includes(message)); + }); + it('sends error to console.error', function () { + var message = 'this is an error message'; + logger.error(message); + assert.ok(logs.error[0].includes(message)); + }); + it('sends warn to console.warn', function () { + var message = 'this is a warn message'; + logger.warn(message); + assert.ok(logs.warn[0].includes(message)); + }); + it('sends info to console.info', function () { + var message = 'this is an info message'; + logger.info(message); + assert.ok(logs.info[0].includes(message)); + }); + it('sends log to console.log', function () { + var message = 'this is a log message'; + logger.log(message); + assert.ok(logs.log[0].includes(message)); + }); + it('sends debug to console.debug', function () { + var message = 'this is a debug message'; + logger.debug(message); + assert.ok(logs.debug[0].includes(message)); + }); + }); + + context('threshold=DEBUG', function () { + beforeEach(function () { + config.logLevel = 'DEBUG'; + }); + it('sends critical to console.error', function () { + var message = 'this is a critical message'; + logger.critical(message); + assert.ok(logs.error[0].includes(message)); + }); + it('sends error to console.error', function () { + var message = 'this is an error message'; + logger.error(message); + assert.ok(logs.error[0].includes(message)); + }); + it('sends warn to console.warn', function () { + var message = 'this is a warn message'; + logger.warn(message); + assert.ok(logs.warn[0].includes(message)); + }); + it('sends info to console.info', function () { + var message = 'this is an info message'; + logger.info(message); + assert.ok(logs.info[0].includes(message)); + }); + it('sends log to console.log', function () { + var message = 'this is a log message'; + logger.log(message); + assert.ok(logs.log[0].includes(message)); + }); + it('sends debug to console.debug', function () { + var message = 'this is a debug message'; + logger.debug(message); + assert.ok(logs.debug[0].includes(message)); + }); + }); + + context('threshold=LOG', function () { + beforeEach(function () { + config.logLevel = 'LOG'; + }); + it('sends critical to console.error', function () { + var message = 'this is a critical message'; + logger.critical(message); + assert.ok(logs.error[0].includes(message)); + }); + it('sends error to console.error', function () { + var message = 'this is an error message'; + logger.error(message); + assert.ok(logs.error[0].includes(message)); + }); + it('sends warn to console.warn', function () { + var message = 'this is a warn message'; + logger.warn(message); + assert.ok(logs.warn[0].includes(message)); + }); + it('sends info to console.info', function () { + var message = 'this is an info message'; + logger.info(message); + assert.ok(logs.info[0].includes(message)); + }); + it('sends log to console.log', function () { + var message = 'this is a log message'; + logger.log(message); + assert.ok(logs.log[0].includes(message)); + }); + it('ignores debug', function () { + var message = 'this is a debug message'; + logger.debug(message); + assert.equal(logs.debug.length, 0); + }); + }); + + context('threshold=INFO', function () { + beforeEach(function () { + config.logLevel = 'INFO'; + }); + it('sends critical to console.error', function () { + var message = 'this is a critical message'; + logger.critical(message); + assert.ok(logs.error[0].includes(message)); + }); + it('sends error to console.error', function () { + var message = 'this is an error message'; + logger.error(message); + assert.ok(logs.error[0].includes(message)); + }); + it('sends warn to console.warn', function () { + var message = 'this is a warn message'; + logger.warn(message); + assert.ok(logs.warn[0].includes(message)); + }); + it('sends info to console.info', function () { + var message = 'this is an info message'; + logger.info(message); + assert.ok(logs.info[0].includes(message)); + }); + it('ignores log', function () { + var message = 'this is a log message'; + logger.log(message); + assert.equal(logs.log.length, 0); + }); + it('ignores debug', function () { + var message = 'this is a debug message'; + logger.debug(message); + assert.equal(logs.debug.length, 0); + }); + }); + + context('threshold=WARN', function () { + beforeEach(function () { + config.logLevel = 'WARN'; + }); + it('sends critical to console.error', function () { + var message = 'this is a critical message'; + logger.critical(message); + assert.ok(logs.error[0].includes(message)); + }); + it('sends error to console.error', function () { + var message = 'this is an error message'; + logger.error(message); + assert.ok(logs.error[0].includes(message)); + }); + it('sends warn to console.warn', function () { + var message = 'this is a warn message'; + logger.warn(message); + assert.ok(logs.warn[0].includes(message)); + }); + it('ignores info', function () { + var message = 'this is an info message'; + logger.info(message); + assert.equal(logs.info.length, 0); + }); + it('ignores log', function () { + var message = 'this is a log message'; + logger.log(message); + assert.equal(logs.log.length, 0); + }); + it('ignores debug', function () { + var message = 'this is a debug message'; + logger.debug(message); + assert.equal(logs.debug.length, 0); + }); + }); + + context('threshold=ERROR', function () { + beforeEach(function () { + config.logLevel = 'ERROR'; + }); + it('sends critical to console.error', function () { + var message = 'this is a critical message'; + logger.critical(message); + assert.ok(logs.error[0].includes(message)); + }); + it('sends error to console.error', function () { + var message = 'this is an error message'; + logger.error(message); + assert.ok(logs.error[0].includes(message)); + }); + it('ignores warn', function () { + var message = 'this is a warn message'; + logger.warn(message); + assert.equal(logs.warn.length, 0); + }); + it('ignores info', function () { + var message = 'this is an info message'; + logger.info(message); + assert.equal(logs.info.length, 0); + }); + it('ignores log', function () { + var message = 'this is a log message'; + logger.log(message); + assert.equal(logs.log.length, 0); + }); + it('ignores debug', function () { + var message = 'this is a debug message'; + logger.debug(message); + assert.equal(logs.debug.length, 0); + }); + }); + + context('threshold=CRITICAL', function () { + beforeEach(function () { + config.logLevel = 'CRITICAL'; + }); + it('sends critical to console.error', function () { + var message = 'this is a critical message'; + logger.critical(message); + assert.ok(logs.error[0].includes(message)); + }); + it('ignores error', function () { + var message = 'this is an error message'; + logger.error(message); + assert.equal(logs.error.length, 0); + }); + it('ignores warn', function () { + var message = 'this is a warn message'; + logger.warn(message); + assert.equal(logs.warn.length, 0); + }); + it('ignores info', function () { + var message = 'this is an info message'; + logger.info(message); + assert.equal(logs.info.length, 0); + }); + it('ignores log', function () { + var message = 'this is a log message'; + logger.log(message); + assert.equal(logs.log.length, 0); + }); + it('ignores debug', function () { + var message = 'this is a debug message'; + logger.debug(message); + assert.equal(logs.debug.length, 0); + }); + }); + + context('threshold=NONE', function () { + beforeEach(function () { + config.logLevel = 'NONE'; + }); + it('ignores critical', function () { + var message = 'this is a critical message'; + logger.critical(message); + assert.equal(logs.error.length, 0); + }); + it('ignores error', function () { + var message = 'this is an error message'; + logger.error(message); + assert.equal(logs.error.length, 0); + }); + it('ignores warn', function () { + var message = 'this is a warn message'; + logger.warn(message); + assert.equal(logs.warn.length, 0); + }); + it('ignores info', function () { + var message = 'this is an info message'; + logger.info(message); + assert.equal(logs.info.length, 0); + }); + it('ignores log', function () { + var message = 'this is a log message'; + logger.log(message); + assert.equal(logs.log.length, 0); + }); + it('ignores debug', function () { + var message = 'this is a debug message'; + logger.debug(message); + assert.equal(logs.debug.length, 0); + }); + }); +}); diff --git a/test/test_commons.js b/test/test_commons.js index 6be62c4..3248bc1 100644 --- a/test/test_commons.js +++ b/test/test_commons.js @@ -1,5 +1,7 @@ var mysql = require('./mysql'); var fs = require('fs'); +var config = require('../config'); +var logger = require('../logger'); function deleteFolderRecursive(path) { if (fs.existsSync(path)) { @@ -15,7 +17,9 @@ function deleteFolderRecursive(path) { } module.exports = function(cb) { - mysql.getConnection(function(err, connection) { + config.logLevel = 'ALL'; + config.logger = logger; + mysql.getConnection(function (err, connection) { if (err) { throw err; }