diff --git a/lib/winston/transports/file.js b/lib/winston/transports/file.js index 2a8749b39..cef3e0e7d 100644 --- a/lib/winston/transports/file.js +++ b/lib/winston/transports/file.js @@ -76,6 +76,7 @@ var File = exports.File = function (options) { this.label = options.label || null; this.timestamp = options.timestamp != null ? options.timestamp : true; this.eol = options.eol || '\n'; + this.tailable = options.tailable || false; this.depth = options.depth || null; this.showLevel = options.showLevel === undefined ? true : options.showLevel; @@ -505,32 +506,18 @@ File.prototype._createStream = function () { // in the case that log filesizes are being capped. // File.prototype._getFile = function (inc) { - var self = this, - ext = path.extname(this._basename), - basename = path.basename(this._basename, ext), - remaining; + var ext = path.extname(this._basename), + basename = path.basename(this._basename, ext); if (inc) { - // - // Increment the number of files created or - // checked by this instance. - // - // Check for maxFiles option and delete file - if (this.maxFiles && (this._created >= (this.maxFiles - 1))) { - remaining = this._created - (this.maxFiles - 1); - try { - if (remaining === 0) { - fs.unlinkSync(path.join(this.dirname, basename + ext)); - } - else { - fs.unlinkSync(path.join(this.dirname, basename + remaining + ext)); - } - } catch (e) { - // If the file was already removed - } + if (!this.tailable) { + this._checkMaxFilesIncrementing(ext, basename); + this._created += 1; + } + else { + this._checkMaxFilesTailable(ext, basename); + return basename + ext; } - - this._created += 1; } return this._created @@ -538,6 +525,50 @@ File.prototype._getFile = function (inc) { : basename + ext; }; +// +// ### @private function _checkMaxFilesIncrementing () +// Increment the number of files created or +// checked by this instance. +// +File.prototype._checkMaxFilesIncrementing = function (ext, basename) { + var remaining; + + // Check for maxFiles option and delete file + if (this.maxFiles && (this._created >= (this.maxFiles - 1))) { + remaining = this._created - (this.maxFiles - 1); + if (remaining === 0) { + fs.unlinkSync(path.join(this.dirname, basename + ext)); + } + else { + fs.unlinkSync(path.join(this.dirname, basename + remaining + ext)); + } + } +}; + +// +// ### @private function _checkMaxFilesTailable () +// +// Roll files forward based on integer, up to maxFiles. +// e.g. if base if file.log and it becomes oversized, roll +// to file1.log, and allow file.log to be re-used. If +// file is oversized again, roll file1.log to file2.log, +// roll file.log to file1.log, and so on. +File.prototype._checkMaxFilesTailable = function (ext, basename) { + var tmppath; + + if (!this.maxFiles) + return; + + for (var x = this.maxFiles - 1; x > 0; x--) { + tmppath = path.join(this.dirname, basename + (x - 1) + ext); + if (fs.existsSync(tmppath)) { + fs.renameSync(tmppath, path.join(this.dirname, basename + x + ext)); + } + } + + fs.renameSync(path.join(this.dirname, basename + ext), path.join(this.dirname, basename + 1 + ext)); +}; + // // ### @private function _lazyDrain () // Lazily attempts to emit the `logged` event when `this.stream` has diff --git a/test/transports/file-tailrolling-test.js b/test/transports/file-tailrolling-test.js new file mode 100644 index 000000000..199afcaf1 --- /dev/null +++ b/test/transports/file-tailrolling-test.js @@ -0,0 +1,92 @@ +var assert = require('assert'), + fs = require('fs'), + path = require('path'), + vows = require('vows'), + winston = require('../../lib/winston'), + helpers = require('../helpers'); + +var maxfilesTransport = new winston.transports.File({ + timestamp: false, + json: false, + filename: path.join(__dirname, '..', 'fixtures', 'logs', 'testtailrollingfiles.log'), + maxsize: 4096, + maxFiles: 3, + tailable: true +}); + +process.on('uncaughtException', function (err) { + console.log('caught exception'); + console.log(err); +}); + +vows.describe('winston/transports/file/tailrolling').addBatch({ + "An instance of the File Transport": { + "when delete old test files": { + topic: function () { + var logs = path.join(__dirname, '..', 'fixtures', 'logs'); + fs.readdirSync(logs).forEach(function (file) { + if (~file.indexOf('testtailrollingfiles')) { + fs.unlinkSync(path.join(logs, file)); + } + }); + + this.callback(); + }, + "and when passed more files than the maxFiles": { + topic: function () { + var that = this, + created = 0; + + function data(ch) { + return new Array(1018).join(String.fromCharCode(65 + ch)); + }; + + function logKbytes(kbytes, txt) { + // + // With no timestamp and at the info level, + // winston adds exactly 7 characters: + // [info](4)[ :](2)[\n](1) + // + for (var i = 0; i < kbytes; i++) { + maxfilesTransport.log('info', data(txt), null, function () { }); + } + } + + maxfilesTransport.on('logged', function () { + if (++created == 4) { + return that.callback(); + } + + logKbytes(4, created); + }); + + logKbytes(4, created); + }, + "should be 3 log files, base to maxFiles - 1": function () { + var file, fullpath; + for (var num = 0; num < 4; num++) { + file = !num ? 'testtailrollingfiles.log' : 'testtailrollingfiles' + num + '.log'; + fullpath = path.join(__dirname, '..', 'fixtures', 'logs', file); + + if (num == 3) { + return assert.ok(!fs.existsSync(fullpath)); + } + + assert.ok(fs.existsSync(fullpath)); + } + + return false; + }, + "should have files in correct order": function () { + var file, fullpath, content; + ['D', 'C', 'B'].forEach(function (letter, i) { + file = !i ? 'testtailrollingfiles.log' : 'testtailrollingfiles' + i + '.log'; + content = fs.readFileSync(path.join(__dirname, '..', 'fixtures', 'logs', file), 'ascii'); + + assert.lengthOf(content.match(new RegExp(letter, 'g')), 4068); + }); + } + } + } + } +}).export(module); \ No newline at end of file