log-rotator-util.js 6.78 KB
var fs = require('fs');
var util = require('util');
var zlib = require('zlib');
var events = require('events');

/**
 * Performs scheduled and on demand log rotation on files
 */
function Logrotator() {
  events.EventEmitter.call(this);
  this.timers = {};
}

util.inherits(Logrotator, events.EventEmitter);

/**
 * Schedules a file for rotation. emits a 'rotate' event whenever the file has been rotated.
 * @param file full file path to rotate
 * @param options rotation options
 *  - schedule - how often to check for file rotation conditions. possible values are '1s', '1m', '1h'. default is 5m.
 *  - size - size of the file to trigger rotation. possible values are '1k', '1m', '1g'. default is 10m.
 *  - count - number of files to keep. default is 3.
 *  - compress - whether to gzip rotated files. default is true.
 *  - format - a function to build the name of a rotated file. the function receives the index of the rotated file.
 *            default format is the index itself.
 */
Logrotator.prototype.register = function(file, options) {

  options = util._extend({schedule: '5m'}, options);

  var match = options.schedule.match(/^([0-9]+)(s|m|h)$/);
  if (!match) {
    this.emit('error', 'incorrect schedule format ' + options.schedule);
    return;
  }

  if (this.timers[file]) {
    this.unregister(file);
  }

  // calculate the schedule
  var multi = this._timeMultiplier(match[2]);
  var schedule = parseInt(match[1]) * multi;
  var scheduleMinute = parseInt(match[1]) ;
  var _this = this;

  // perform rotation
  function _doRotate() {
    _this.rotate(file, options, function(err, rotated) {
      if (err) {
        _this.emit('error', err);
        return;
      }
      if (rotated) {
        _this.emit('rotate', file);
      }
    });
  }

  // register the rotation timer
  this.timers[file] = setInterval(function() {
    var d = new Date();
    var n = d.getMinutes();
    if( (n % scheduleMinute) == 0 ){
      _doRotate();
    }
    
  }, 60 * 1000 );

  // immediately rotate
  // _doRotate(); //ignore
};

/**
 * Remove the scheduled rotation of a file
 * @param file the file to stop rotating
 */
Logrotator.prototype.unregister = function(file) {
  if (!this.timers[file]) {
    return;
  }

  clearInterval(this.timers[file]);
  delete this.timers[file];
};

/**
 * Stop all schedulers
 */
Logrotator.prototype.stop = function() {
  var _this = this;
  Object.keys(this.timers).forEach(function(name) {
    clearInterval(_this.timers[name]);
  });
  this.timers = {};
};

Logrotator.prototype._timeMultiplier = function(multi) {
  switch (multi) {
    case 's':
      return 1000;
    case 'm':
      return 60*1000;
    case 'h':
      return 60*60*1000;
  }
};

Logrotator.prototype._sizeMultiplier = function(multi) {
  switch (multi) {
    case 'k':
      return 1024;
    case 'm':
      return 1024*1024;
    case 'g':
      return 1024*1024*1024;
  }
};

/**
 * Rotate a file now if size conditions are met.
 * @param file full file path to rotate
 * @param options rotation options
 *  - size - size of the file to trigger rotation. possible values are '1k', '1m', '1g'. default is 10m.
 *  - count - number of files to keep. default is 3.
 *  - compress - gzip rotated files. default is true.
 *  - format - a function to build the name of a rotated file. the function receives the index of the rotated file.
 *            default format is the index itself.
 * @param cb - invoked on completion, receives 'err' on error
 */
Logrotator.prototype.rotate = function(file, options, cb) {

  if (!cb) {
    cb = options;
    options = null;
  }

  options = util._extend({size: '10m', count: 3, compress: true}, options);

  var match = options.size.match(/^([0-9]+)(k|m|g)$/);
  if (!match) {
    cb('incorrect size format ' + options.size);
    return;
  }

  var multi = this._sizeMultiplier(match[2]);
  var size = parseInt(match[1]) * multi;

  // check if the file reached the trigger size
  var _this = this;
  fs.stat(file, function(err, stats) {
    if (err) {
      var message = null;
      // if file does not exist, ignore
      if (err.code !== 'ENOENT') {
        // other errors
        message = file + ' stat failed: ' + err.message;
      }
      cb(message);
      return;
    }

    // this isn't a file
    if (!stats.isFile()) {
      cb(file + ' is not a file');
      return;
    }

    // check file size to see if rotation is needed
    if (stats.size >= size) {
      _this._rotate(file, options.count, options, cb);
    } else {
      cb(null, false);
    }
  });

};

/**
 * Get the correct file name based on params
 * @param file
 * @param index
 * @param options
 * @private
 */
Logrotator.prototype._filename = function(file, index, options) {
  var format = index;
  if (typeof options.format === 'function') {
    format = options.format(index);
  }

  var fileName = file + '.' + format;
  if (options.compress) {
    fileName += '.gz';
  }
  return fileName;
};

/**
 * The log rotation brains
 * @param file
 * @param index
 * @param options
 * @param cb
 * @private
 */
Logrotator.prototype._rotate = function(file, index, options, cb) {

  // rotate all existing files
  // 1. delete last file
  // 2. rename all files to with +1
  // 3. read + compress current log into 1
  // 4. truncate file to size 0
  var _this = this;
  var fileName = this._filename(file, index, options);

  // delete last file
  if (index === options.count) {
    fs.unlink(fileName, function(err) {
      if (err && err.code !== 'ENOENT') {
        cb('error deleting file ' + fileName + ': ' + err.message);
        return;
      }
      _this._rotate(file, --index, options, cb);
    });
    return;
  }

  // rename all files to with +1
  if (index > 0) {
    var renameTo = this._filename(file, index+1, options);
    fs.rename(fileName, renameTo, function(err) {
      if (err && err.code !== 'ENOENT') {
        cb('error renaming file ' + fileName + ': ' + err.message);
        return;
      }
      _this._rotate(file, --index, options, cb);
    });

    return;
  }

  // read (and compress) the file log into index 1
  var fis = fs.createReadStream(file);
  var fos = fs.createWriteStream(this._filename(file, 1, options));
  var pipe;
  if (options.compress) {
    pipe = fis.pipe(zlib.createGzip()).pipe(fos);
  } else {
    pipe = fis.pipe(fos);
  }

  var error;
  pipe.on('finish', function() {
    if (error) {
      return;
    }
    // truncate log file to size 0
    fs.truncate(file, 0, function(err) {
      if (err) {
        cb && cb('error truncating file ' + file + ': ' + err.message);
        return;
      }
      cb && cb(null, true);
    })
  });
  pipe.on('error', function(err) {
    error = true;
    cb('error compressing file ' + file + ': ' + err.message);
    cb = null;
  });
};

// create a new log rotator
module.exports.create = function() {
  return new Logrotator();
};

// global log rotator
module.exports.rotator = new Logrotator();