daily-rotate-file.js 10.4 KB
'use strict';

var fs = require('fs');
var os = require('os');
var path = require('path');
var util = require('util');
var zlib = require('zlib');
var hash = require('object-hash');
var MESSAGE = require('triple-beam').MESSAGE;
var PassThrough = require('stream').PassThrough;
var Transport = require('winston-transport');

var loggerDefaults = {
    json: false,
    colorize: false,
    eol: os.EOL,
    logstash: null,
    prettyPrint: false,
    label: null,
    stringify: false,
    depth: null,
    showLevel: true,
    timestamp: function () {
        return new Date().toISOString();
    }
};

var DailyRotateFile = function (options) {
    options = options || {};
    Transport.call(this, options);

    function throwIf(target /* , illegal... */) {
        Array.prototype.slice.call(arguments, 1).forEach(function (name) {
            if (options[name]) {
                throw new Error('Cannot set ' + name + ' and ' + target + ' together');
            }
        });
    }

    function getMaxSize(size) {
        if (size && typeof size === 'string') {
            var _s = size.toLowerCase().match(/^((?:0\.)?\d+)([k|m|g])$/);
            if (_s) {
                return size;
            }
        } else if (size && Number.isInteger(size)) {
            var sizeK = Math.round(size / 1024);
            return sizeK === 0 ? '1k' : sizeK + 'k';
        }

        return null;
    }

    function isValidFileName(filename) {
        // eslint-disable-next-line no-control-regex
        return !/["<>|:*?\\/\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f]/g.test(filename);
    }

    function isValidDirName(dirname) {
        // eslint-disable-next-line no-control-regex
        return !/["<>|\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f]/g.test(dirname);
    }

    this.options = Object.assign({}, loggerDefaults, options);

    if (options.stream) {
        throwIf('stream', 'filename', 'maxsize');
        this.logStream = new PassThrough();
        this.logStream.pipe(options.stream);
    } else {
        this.filename = options.filename ? path.basename(options.filename) : 'winston.log';
        this.dirname = options.dirname || path.dirname(options.filename);

        if (!isValidFileName(this.filename) || !isValidDirName(this.dirname)) {
            throw new Error('Your path or filename contain an invalid character.');
        }

        var self = this;

        this.logStream = require('file-stream-rotator').getStream({
            filename: path.join(this.dirname, this.filename),
            frequency: options.frequency ? options.frequency : 'custom',
            date_format: options.datePattern ? options.datePattern : 'YYYY-MM-DD',
            verbose: false,
            size: getMaxSize(options.maxSize),
            max_logs: options.maxFiles,
            end_stream: true,
            audit_file: options.auditFile ? options.auditFile : path.join(self.dirname, '.' + hash(options) + '-audit.json'),
            file_options: options.options ? options.options : {flags: 'a'},
            utc: options.utc ? options.utc : false,
            extension: options.extension ? options.extension : '',
            create_symlink: options.createSymlink ? options.createSymlink : false,
            symlink_name: options.symlinkName ? options.symlinkName : 'current.log',
            watch_log: options.watchLog ? options.watchLog : false,
            audit_hash_type: options.auditHashType ? options.auditHashType  : 'sha256'
        });

        this.logStream.on('new', function (newFile) {
            self.emit('new', newFile);
        });

        this.logStream.on('rotate', function (oldFile, newFile) {
            self.emit('rotate', oldFile, newFile);
        });

        this.logStream.on('logRemoved', function (params) {
            if (options.zippedArchive) {
                var gzName = params.name + '.gz';
                if (fs.existsSync(gzName)) {
                    try {
                        fs.unlinkSync(gzName);
                    }
                    catch (_err) {
                        // file is there but we got an error when trying to delete,
                        // so permissions problem or concurrency issue and another
                        // process already deleted it we could detect the concurrency
                        // issue by checking err.type === ENOENT or EACCESS for
                        // permissions ... but then?
                    }
                    self.emit('logRemoved', gzName);
                    return;
                }
            }
            self.emit('logRemoved', params.name);
        });

        if (options.zippedArchive) {
            this.logStream.on('rotate', function (oldFile) {
                var oldFileExist = fs.existsSync(oldFile);
                var gzExist = fs.existsSync(oldFile + '.gz');
                if (!oldFileExist || gzExist) {
                    return;
                }

                var gzip = zlib.createGzip();
                var inp = fs.createReadStream(oldFile);
                var out = fs.createWriteStream(oldFile + '.gz');
                inp.pipe(gzip).pipe(out).on('finish', function () {
                    if (fs.existsSync(oldFile)) {
                        fs.unlinkSync(oldFile);
                    }
                    self.emit('archive', oldFile + '.gz');
                });
            });
        }

        if (options.watchLog) {
            this.logStream.on('addWatcher', (newFile) => {
                self.emit('addWatcher', newFile);
            })
        }
    }
};

module.exports = DailyRotateFile;

util.inherits(DailyRotateFile, Transport);

DailyRotateFile.prototype.name = 'dailyRotateFile';

var noop = function () {};
DailyRotateFile.prototype.log = function (info, callback) {
    callback = callback || noop;

    this.logStream.write(info[MESSAGE] + this.options.eol);
    this.emit('logged', info);
    callback(null, true);
};

DailyRotateFile.prototype.close = function () {
    var self = this;
    if (this.logStream) {
        this.logStream.end(function () {
            self.emit('finish');
        });
    }
};

DailyRotateFile.prototype.query = function (options, callback) {
    if (typeof options === 'function') {
        callback = options;
        options = {};
    }

    if (!this.options.json) {
        throw new Error('query() may not be used without the json option being set to true');
    }

    if (!this.filename) {
        throw new Error('query() may not be used when initializing with a stream');
    }

    var self = this;
    var results = [];
    options = options || {};

    // limit
    options.rows = options.rows || options.limit || 10;

    // starting row offset
    options.start = options.start || 0;

    // now
    options.until = options.until || new Date;
    if (typeof options.until !== 'object') {
        options.until = new Date(options.until);
    }

    // now - 24
    options.from = options.from || (options.until - (24 * 60 * 60 * 1000));
    if (typeof options.from !== 'object') {
        options.from = new Date(options.from);
    }

    // 'asc' or 'desc'
    options.order = options.order || 'desc';

    var logFiles = (function () {
        var fileRegex = new RegExp(self.filename.replace('%DATE%', '.*'), 'i');
        return fs.readdirSync(self.dirname).filter(function (file) {
            return path.basename(file).match(fileRegex);
        });
    })();

    if (logFiles.length === 0 && callback) {
        callback(null, results);
    }

    (function processLogFile(file) {
        if (!file) {
            return;
        }

        var logFile = path.join(self.dirname, file);
        var buff = '';

        var stream;

        if (file.endsWith('.gz')) {
            stream = new PassThrough();
            fs.createReadStream(logFile).pipe(zlib.createGunzip()).pipe(stream);
        } else {
            stream = fs.createReadStream(logFile, {
                encoding: 'utf8'
            });
        }

        stream.on('error', function (err) {
            if (stream.readable) {
                stream.destroy();
            }

            if (!callback) {
                return;
            }

            return err.code === 'ENOENT' ? callback(null, results) : callback(err);
        });

        stream.on('data', function (data) {
            data = (buff + data).split(/\n+/);
            var l = data.length - 1;

            for (var i = 0; i < l; i++) {
                add(data[i]);
            }

            buff = data[l];
        });

        stream.on('end', function () {
            if (buff) {
                add(buff, true);
            }

            if (logFiles.length) {
                processLogFile(logFiles.shift());
            } else if (callback) {
                results.sort(function (a, b) {
                    var d1 = new Date(a.timestamp).getTime();
                    var d2 = new Date(b.timestamp).getTime();

                    return d1 > d2 ? 1 : d1 < d2 ? -1 : 0;
                });

                if (options.order === 'desc') {
                    results = results.reverse();
                }

                var start = options.start || 0;
                var limit = options.limit || results.length;

                results = results.slice(start, start + limit);

                if (options.fields) {
                    results = results.map(function (log) {
                        var obj = {};
                        options.fields.forEach(function (key) {
                            obj[key] = log[key];
                        });
                        return obj;
                    });
                }

                callback(null, results);
            }
        });

        function add(buff, attempt) {
            try {
                var log = JSON.parse(buff);
                if (!log || typeof log !== 'object') {
                    return;
                }

                var time = new Date(log.timestamp);
                if ((options.from && time < options.from) ||
                    (options.until && time > options.until) ||
                    (options.level && options.level !== log.level)) {
                    return;
                }

                results.push(log);
            } catch (e) {
                if (!attempt) {
                    stream.emit('error', e);
                }
            }
        }
    })(logFiles.shift());
};