Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transport refactor #107

Merged
merged 22 commits into from
Mar 16, 2018
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = exports = {
extends: 'xo',
env: {
node: true,
mocha: true
},
rules: {
indent: ['error', 4],
camelcase: ['error', {properties: 'never'}]
}
};
5 changes: 2 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Logs
logs
*.log
*.log.*
*log*
test.js

# Runtime data
pids
Expand Down
14 changes: 11 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
sudo: false
language: node_js
node_js:
- "0.10"
- "0.12"
- "4.1"
- "4"
- "6"
- "8"

env:
- WINSTON_VER=winston@^2
- WINSTON_VER=winston@next

before_install:
- npm install $WINSTON_VER

59 changes: 29 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,41 @@

[![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-image]][daviddm-url]

> A transport for winston which logs to a rotating file each day.
A transport for [winston](https://github.com/winstonjs/winston) which logs to a rotating file. Logs can be rotated based on a date, size limit, and old logs can be removed based on count or elapsed days.

## Usage
Starting with version 2.0.0, the transport has been refactored to leverage the the [file-stream-rotator](https://github.com/rogerc/file-stream-rotator/) module. _Some of the options in the 1.x versions of the transport have changed._ Please review the options below to identify any changes needed.

## Install
```
npm install winston-daily-rotate-file
```

## Options
The DailyRotateFile transport can rotate files by minute, hour, day, month, year or weekday. In addition to the options accepted by the logger, `winston-daily-rotate-file` also accepts the following options:

* **datePattern:** A string representing the [moment.js date format](http://momentjs.com/docs/#/displaying/format/) to be used for rotating. The meta characters used in this string will dictate the frequency of the file rotation. For example, if your datePattern is simply 'HH' you will end up with 24 log files that are picked up and appended to every day. (default 'YYYY-MM-DD')
* **zippedArchive:** A boolean to define whether or not to gzip archived log files. (default 'false')
* **filename:** Filename to be used to log to. This filename can include the `%DATE%` placeholder which will include the formatted datePattern at that point in the filename. (default: 'winston.log.%DATE%)
* **dirname:** The directory name to save log files to. (default: '.')
* **stream:** Write directly to a custom stream and bypass the rotation capabilities. (default: null)
* **maxSize:** Maximum size of the file after which it will rotate. This can be a number of bytes, or units of kb, mb, and gb. If using the units, add 'k', 'm', or 'g' as the suffix. The units need to directly follow the number. (default: null)
* **maxFiles:** Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null)

## Usage
``` js
var winston = require('winston');
require('winston-daily-rotate-file');

var transport = new (winston.transports.DailyRotateFile)({
filename: './log',
datePattern: 'yyyy-MM-dd.',
prepend: true,
level: process.env.ENV === 'development' ? 'debug' : 'info'
filename: 'application-%DATE%.log',
datePattern: 'YYYY-MM-DD-HH',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d'
});

transport.on('rotate', function(oldFilename, newFilename) {
// do something fun
});

var logger = new (winston.Logger)({
Expand All @@ -26,30 +48,7 @@
logger.info('Hello World!');
```

The DailyRotateFile transport can rotate files by minute, hour, day, month, year or weekday. In addition to the options accepted by the File transport, the Daily Rotate File Transport also accepts the following options:

* __datePattern:__ A string representing the pattern to be used when appending the date to the filename (default 'yyyy-MM-dd'). The meta characters used in this string will dictate the frequency of the file rotation. For example, if your datePattern is simply 'HH' you will end up with 24 log files that are picked up and appended to every day.
* __prepend:__ Defines if the rolling time of the log file should be prepended at the beginning of the filename (default 'false').
* __localTime:__ A boolean to define whether time stamps should be local (default 'false' means that UTC time will be used).
* __zippedArchive:__ A boolean to define whether or not to gzip archived log files (default 'false').
* __maxDays:__ A number representing the maximum number of days a log file will be saved. Any log file older than this specified number of days will be removed. If not value or a 0, no log files will be removed.
* __createTree:__ When combined with a `datePattern` that includes path delimiters, the transport will create the entire folder tree to the log file. Example: `datePattern: '/yyyy/MM/dd.log', createTree: true` will create the entire path to the log file prior to writing an entry.

Valid meta characters in the datePattern are:

* __yy:__ Last two digits of the year.
* __yyyy:__ Full year.
* __M:__ The month.
* __MM:__ The zero padded month.
* __d:__ The day.
* __dd:__ The zero padded day.
* __H:__ The hour.
* __HH:__ The zero padded hour.
* __m:__ The minute.
* __mm:__ The zero padded minute.
* __ddd:__ The weekday (Mon, Tue, ..., Sun).

*Metadata:* Logged via util.inspect(meta);
You can listen for the *rotate* custom event. The rotate event will pass two parameters to the callback (*oldFilename*, *newFilename*).

## LICENSE
MIT
Expand Down
260 changes: 260 additions & 0 deletions daily-rotate-file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
'use strict';

var fs = require('fs');
var os = require('os');
var path = require('path');
var util = require('util');
var semver = require('semver');
var zlib = require('zlib');
var winston = require('winston');
var compat = require('winston-compat');
var MESSAGE = require('triple-beam').MESSAGE;
var PassThrough = require('stream').PassThrough;
var Transport = semver.major(winston.version) === 2 ? compat.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;
}

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);

var self = this;

this.logStream = require('file-stream-rotator').getStream({
filename: path.join(this.dirname, this.filename),
frequency: 'custom',
date_format: options.datePattern ? options.datePattern : 'YYYY-MM-DD',
verbose: false,
size: getMaxSize(options.maxSize),
max_logs: options.maxFiles
});

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

if (options.zippedArchive) {
this.logStream.on('rotate', function (oldFile) {
var gzip = zlib.createGzip();
var inp = fs.createReadStream(oldFile);
var out = fs.createWriteStream(oldFile + '.gz');
inp.pipe(gzip).pipe(out).on('finish', function () {
fs.unlinkSync(oldFile);
});
});
}
}
};

module.exports = DailyRotateFile;

util.inherits(DailyRotateFile, Transport);

DailyRotateFile.prototype.name = 'dailyRotateFile';

var noop = function () {};
if (semver.major(winston.version) === 2) {
DailyRotateFile.prototype.log = function (level, msg, meta, callback) {
callback = callback || noop;
var options = Object.assign({}, this.options, {
level: level,
message: msg,
meta: meta
});

var output = compat.log(options) + options.eol;
this.logStream.write(output);
callback(null, true);
};
} else {
DailyRotateFile.prototype.normalizeQuery = compat.Transport.prototype.normalizeQuery;
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 () {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little concerned about this function name. Streams already expose a close event and this feels ripe for confusion. For my part (not a stream expert by any stretch), I had to go and look at the core NodeJS stream and winston-transport source code to verify that this wasn't overwriting a pre-existing close() function.

If this is specifically for winston-daily-rotate-rotate (WDRF) would it be possible to sidestep any such confusion and call it something WDRF-specific? Something like endRotation() or endLogging() or closeTransport()?

@indexzero This raises an interesting question. Is there a set of functions (hooks?) defined in winston-transport that cover setup and teardown? Or at least a convention by which Transports can name basic functionality like this?

Copy link

@crussell52 crussell52 Mar 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ericdrobinson

For my part (not a stream expert by any stretch), I had to go and look at the core NodeJS stream and winston-transport source code to verify that this wasn't overwriting a pre-existing close() function.

FWIW, I did the same thing.

However, I think it does have meaning to winston. In the area I explored with the rotation bug -- In winston-transport -- the unpipe handler calls close on the Transport if it is defined: (https://github.com/winstonjs/winston-transport/blob/master/index.js#L49-L62)

Perhaps this just needs some clarity in the documentation -- maybe this is also the "most correct" place for transports to call this.end()" and do additional cleanup if necessary since _final()` is not an option in node versions < 8.0. I'll add more thoughts about that to the thread about _final() main PR conversation

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.filename) {
throw new Error('query() may not be used when initializing with a stream');
}

var self = this;
var results = [];
var row = 0;
options = self.normalizeQuery(options);

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 = 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++) {
if (!options.start || row >= options.start) {
add(data[i]);
}
row++;
}

buff = data[l];
});

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

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

if (logFiles.length) {
processLogFile(logFiles.shift());
} else if (callback) {
callback(null, results);
}
});

function add(buff, attempt) {
try {
var log = JSON.parse(buff);
if (check(log)) {
push(log);
}
} catch (e) {
if (!attempt) {
stream.emit('error', e);
}
}
}

function check(log) {
if (!log || typeof log !== 'object') {
return;
}

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

return true;
}

function push(log) {
if (options.rows && results.length >= options.rows && options.order !== 'desc') {
if (stream.readable) {
stream.destroy();
}
return;
}

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

if (options.order === 'desc') {
if (results.length >= options.rows) {
results.shift();
}
}
results.push(log);
}
})(logFiles.shift());
};
Loading