backend logging

This commit is contained in:
root
2020-03-31 01:53:06 +02:00
parent 13822fcaba
commit 0313b1350c
3324 changed files with 23773 additions and 473711 deletions
+110
View File
@@ -0,0 +1,110 @@
- 2019-10-20 - v1.4.6
- tests fix
- 2019-10-18 - v1.4.5
- Fixed a [bug](https://github.com/iccicci/rotating-file-stream/issues/39) when **immutable** and **history** are both set.
- devDependencies update
- 2019-10-01 - v1.4.4
- Fixed a [bug](https://github.com/iccicci/rotating-file-stream/issues/42) occurring adding properties to **Array.prototype**
- devDependencies update
- 2019-07-23 - v1.4.3
- Exported the options interface
- devDependencies update
- 2019-06-27 - v1.4.2
- Fixed a [bug causing a ERR_MULTIPLE_CALLBACK error](https://github.com/iccicci/rotating-file-stream/issues/36) (thanks to [rooftopsparrow](https://github.com/rooftopsparrow))
- devDependencies update
- 2019-04-22 - v1.4.1
- From [istanbul](https://www.npmjs.com/package/istanbul) to [nyc](https://www.npmjs.com/package/nyc) for tests coverage
- Several typos fixed in [README.md](https://github.com/iccicci/rotating-file-stream/blob/master/README.md); thanks to [dhurlburtusa](https://github.com/dhurlburtusa)
- devDependencies update
- 2019-01-09 - v1.4.0
- Fixed the [TimeoutOverflowWarning bug](https://github.com/iccicci/rotating-file-stream/issues/34)
- Added **experimental** monthly rotation
- devDependencies update
- 2019-01-04 - v1.3.10
- Fixed a [bug occurring when two calls to _makePath_ are concurrently done](https://github.com/iccicci/rotating-file-stream/pull/33) (thanks to [cchare](https://github.com/cchare))
- devDependencies update
- 2018-09-26 - v1.3.9
- Fixed TypeScript Definition file (thanks to [rakshith-ravi](https://www.npmjs.com/~rakshith-ravi) and [kbirger](https://www.npmjs.com/~kbirger))
- Added TOC and **TypeScript** import documentation
- devDependencies update
- 2018-09-18 - v1.3.8
- Added TypeScript Definition file (thanks to [rakshith-ravi](https://www.npmjs.com/~rakshith-ravi))
- 2018-07-19 - v1.3.7
- devDependencies update
- 2018-04-05 - v1.3.6
- Discovered and solved: ["write after end" error with immutable option](https://github.com/iccicci/rotating-file-stream/issues/23) (thanks to [JcBernack](https://github.com/JcBernack))
- Added a test case to cover that bug
- devDependencies update
- 2018-03-15 - v1.3.5
- Using slightly faster timestamp generator function (thanks to [jorgemsrs](https://github.com/jorgemsrs))
- devDependencies update
- 2017-11-13 - v1.3.4
- **immutable** option review
- 2017-11-13 - v1.3.3
- Solved: [problem with TypeScript](https://github.com/iccicci/rotating-file-stream/issues/19)
- **immutable** option added
- 2017-09-23 - v1.3.2
- devDependencies updated
- 2017-09-21 - v1.3.1
- devDependencies updated
- 2017-09-17 - v1.3.0
- **initialRotation** option added
- 2017-04-26 - v1.2.2
- Fixed bug: [Handle does not close](https://github.com/iccicci/rotating-file-stream/issues/11)
- 2017-03-22 - v1.2.1
- fixed removed event
- 2017-03-20 - v1.2.0
- **maxFiles** and **maxSize** options added
- 2017-02-14 - v1.1.9
- fixed warning events order in case of external compression errors
- 2017-02-13 - v1.1.8
- removed tmp dependecy due it was causing a strange instability now disappeared
- 2017-02-07 - v1.1.7
- fixed tmp.file call
- 2017-02-03 - v1.1.6
- eslint
- 2017-01-23 - v1.1.5
- README fix
- 2017-01-23 - v1.1.4
- Changed dependencies badges
- 2016-12-27 - v1.1.3
- Fixed bug: [end method wrong implementation](https://github.com/iccicci/rotating-file-stream/issues/9)
- 2016-12-19 - v1.1.2
- Fixed bug: [unable to reuse configuration object](https://github.com/iccicci/rotating-file-stream/issues/10)
- Fixed bug: [Events cross over: rotate and rotated](https://github.com/iccicci/rotating-file-stream/issues/6)
- 2016-12-05 - v1.1.1
- Dependencies update
- 2016-10-18 - v1.1.0
- Added classical **UNIX logrotate** tool behaviour.
- Dependencies update
- 2016-04-29 - v1.0.5
- Tested on node v6.0
- Fixed a bug on rotation with interval and compression
- 2015-11-09 - v1.0.4
- Tested on node v5.0
- Fixed bug on [initial rotation with interval](https://github.com/iccicci/rotating-file-stream/issues/2)
- 2015-10-25 - v1.0.3
- Tested on node v4.2
- Dependencies update
- 2015-10-09 - v1.0.2
- README update
- 2015-10-08 - v1.0.1
- README fix
- 2015-10-08 - v1.0.0
- Async error reporting refactory
- 2015-10-07 - v0.1.0
- Internal gzip compression
- 2015-10-06 - v0.0.5
- External compression
- 2015-09-30 - v0.0.4
- Added _path_ option
- Missing path creation
- 2015-09-29 - v0.0.3
- Rotation by interval
- **Buffer** optimization (thanks to [allevo](https://www.npmjs.com/~allevo))
- 2015-09-17 - v0.0.2
- Rotation by size
- 2015-09-14 - v0.0.1
- README.md
- 2015-09-10 - v0.0.0
- Embryonal stage
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015-2017 Daniele Ricci
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+427
View File
@@ -0,0 +1,427 @@
# rotating-file-stream
[![Build Status](https://travis-ci.org/iccicci/rotating-file-stream.png?branch=master)](https://travis-ci.org/iccicci/rotating-file-stream?branch=master)
[![Code Climate](https://codeclimate.com/github/iccicci/rotating-file-stream/badges/gpa.svg)](https://codeclimate.com/github/iccicci/rotating-file-stream)
[![Test Coverage](https://codeclimate.com/github/iccicci/rotating-file-stream/badges/coverage.svg)](https://codeclimate.com/github/iccicci/rotating-file-stream/coverage)
[![Donate](https://img.shields.io/badge/donate-bitcoin-blue.svg)](https://blockchain.info/address/12p1p5q7sK75tPyuesZmssiMYr4TKzpSCN)
[![NPM version](https://badge.fury.io/js/rotating-file-stream.svg)](https://www.npmjs.com/package/rotating-file-stream)
[![Dependencies](https://david-dm.org/iccicci/rotating-file-stream.svg)](https://david-dm.org/iccicci/rotating-file-stream)
[![Dev Dependencies](https://david-dm.org/iccicci/rotating-file-stream/dev-status.svg)](https://david-dm.org/iccicci/rotating-file-stream?type=dev)
[![NPM](https://nodei.co/npm/rotating-file-stream.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/rotating-file-stream/)
### Description
Creates a [stream.Writable](https://nodejs.org/api/stream.html#stream_class_stream_writable) to a file which is rotated.
Rotation behaviour can be deeply customized; optionally, classical UNIX **logrotate** behaviour can be used.
### Usage
```javascript
var rfs = require("rotating-file-stream");
var stream = rfs("file.log", {
size: "10M", // rotate every 10 MegaBytes written
interval: "1d", // rotate daily
compress: "gzip" // compress rotated files
});
```
### Installation
With [npm](https://www.npmjs.com/package/rotating-file-stream):
```sh
$ npm install --save rotating-file-stream
```
### Table of contents
- [API](#api)
- [Class: RotatingFileStream](#class-rotatingfilestream)
- [RotatingFileStream(filename, options)](#new-rotatingfilestreamfilename-options)
- [filename](#filename-stringfunction)
- [options](#options-object)
- [compress](#compress)
- [history](#history)
- [immutable](#immutable)
- [initialRotation](#initialrotation)
- [interval](#interval)
- [maxFiles](#maxfiles)
- [maxSize](#maxsize)
- [path](#path)
- [rotate](#rotate)
- [rotationTime](#rotationtime)
- [size](#size)
- [Events](#events)
- [Rotation logic](#rotation-logic)
- [Under the hood](#under-the-hood)
- [Compatibility](#compatibility)
- [TypeScript](#typescript)
- [Licence](#licence)
- [Bugs](#bugs)
- [ChangeLog](#changelog)
- [Donating](#donating)
# API
```javascript
require("rotating-file-stream");
```
Returns **RotatingFileStream** constructor.
## Class: RotatingFileStream
Extends [stream.Writable](https://nodejs.org/api/stream.html#stream_class_stream_writable).
## [new] RotatingFileStream(filename, options)
Returns a new **RotatingFileStream** to _filename_ as
[fs.createWriteStream](https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options) does.
The file is rotated following _options_ rules.
### filename {String|Function}
The most complex problem about file name is: "how to call the rotated file name?"
The answer to this question may vary in many forms depending on application requirements and/or specifications.
If there are no requirements, a _String_ can be used and _default rotated file name generator_ will be used;
otherwise a _Function_ which returns the _rotated file name_ can be used.
#### function filename(time, index)
- time: {Date} If both rotation by interval is enabled and **options.rotationTime** [(see below)](#rotationtime) is
**false**, the start time of rotation period, otherwise the time when rotation job started. If **null**, the
_not-rotated file name_ must be returned.
- index {Number} The progressive index of rotation by size in the same rotation period.
An example of a complex _rotated file name generator_ function could be:
```javascript
function pad(num) {
return (num > 9 ? "" : "0") + num;
}
function generator(time, index) {
if (!time) return "file.log";
var month = time.getFullYear() + "" + pad(time.getMonth() + 1);
var day = pad(time.getDate());
var hour = pad(time.getHours());
var minute = pad(time.getMinutes());
return month + "/" + month + day + "-" + hour + minute + "-" + index + "-file.log";
}
var rfs = require("rotating-file-stream");
var stream = rfs(generator, {
size: "10M",
interval: "30m"
});
```
**Note:**
if both rotation by interval and rotation by time are used, returned _rotated file name_ **must** be function of both
parameters _time_ and _index_. Alternatively, **rotationTime** _option_ can be used (to see below).
If classical **logrotate** behaviour is enabled _rotated file name_ is only a function of _index_.
#### function filename(index)
- index {Number} The progressive index of rotation. If **null**, the _not-rotated file name_ must be returned.
**Note:**
The _not-rotated file name_ **must** be only the _filename_, to specify a _path_ the appropriate option **must** be used.
```javascript
rfs("path/to/file.log"); // wrong
rfs("file.log", { path: "path/to" }); // OK
```
**Note:**
if part of returned destination path does not exists, the rotation job will try to create it.
### options {Object}
- compress: {String|Function|True} (default: null) Specifies compression method of rotated files.
- highWaterMark: {Number} (default: null) Proxied to [new stream.Writable](https://nodejs.org/api/stream.html#stream_constructor_new_stream_writable_options)
- history: {String} (default: null) Specifies the _history filename_.
- immutable: {Boolean} (default: null) Never mutates file names.
- initialRotation: {Boolean} (default: null) Initial rotation based on _not-rotated file_ timestamp.
- interval: {String} (default: null) Specifies the time interval to rotate the file.
- maxFiles: {Integer} (default: null) Specifies the maximum number of rotated files to keep.
- maxSize: {String} (default: null) Specifies the maximum size of rotated files to keep.
- mode: {Integer} (default: null) Proxied to [fs.createWriteStream](https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options)
- path: {String} (default: null) Specifies the base path for files.
- rotate: {Integer} (default: null) Enables the classical UNIX **logrotate** behaviour.
- rotationTime: {Boolean} (default: null) Makes rotated file name with time of rotation.
- size: {String} (default: null) Specifies the file size to rotate the file.
#### path
If present, it is prepended to generated file names as well as for history file.
#### size
Accepts a positive integer followed by one of these possible letters:
- **B**: Bites
- **K**: KiloBites
- **M**: MegaBytes
- **G**: GigaBytes
```javascript
size: '300B', // rotates the file when size exceeds 300 Bytes
// useful for tests
```
```javascript
size: '300K', // rotates the file when size exceeds 300 KiloBytes
```
```javascript
size: '100M', // rotates the file when size exceeds 100 MegaBytes
```
```javascript
size: '1G', // rotates the file when size exceeds a GigaByte
```
#### interval
Accepts a positive integer followed by one of these possible letters:
- **s**: seconds. Accepts integer divider of 60.
- **m**: minutes. Accepts integer divider of 60.
- **h**: hours. Accepts integer divider of 24.
- **d**: days. Accepts integer.
- **M**: months. Accepts integer. **EXPERIMENTAL**
```javascript
interval: '5s', // rotates at seconds 0, 5, 10, 15 and so on
// useful for tests
```
```javascript
interval: '5m', // rotates at minutes 0, 5, 10, 15 and so on
```
```javascript
interval: '2h', // rotates at midnight, 02:00, 04:00 and so on
```
```javascript
interval: '1d', // rotates at every midnight
```
```javascript
interval: '1M', // rotates at every midnight between two distinct months
```
#### compress
Due the nature of **Node.js** compression may be done with an external command (to use other CPUs than the one used
by **Node.js**) or with internal code (to use the CPU used by **Node.js**). This decision is left to you.
Following fixed strings are allowed to compress the files with internal libraries:
- bzip2 (**not implemented yet**)
- gzip
To enable external compression, a _function_ can be used or simply the _boolean_ **true** value to use default
external compression.
The function should accept _source_ and _dest_ file names and must return the shell command to be executed to
compress the file.
The two following code snippets have exactly the same effect:
```javascript
var rfs = require("rotating-file-stream");
var stream = rfs("file.log", {
size: "10M",
compress: true
});
```
```javascript
var rfs = require("rotating-file-stream");
var stream = rfs("file.log", {
size: "10M",
compress: function(source, dest) {
return "cat " + source + " | gzip -c9 > " + dest;
}
});
```
**Note:**
this option is ignored if **immutable** is set to **true**.
**Note:**
the shell command to compress the rotated file should not remove the source file, it will be removed by the package
if rotation job complete with success.
#### initialRotation
When program stops in a rotation period then restarts in a new rotation period, logs of different rotation period will
go in the next rotated file; in a few words: a rotation job is lost. If this option is set to **true** an initial check
is performed against the _not-rotated file_ timestamp and, if it falls in a previous rotation period, an initial
rotation job is done as well.
**Note:**
this option is ignored if **rotationTime** is set to **true**.
#### rotate
If specified, classical UNIX **logrotate** behaviour is enabled and the value of this option has same effect in
_logrotate.conf_ file.
**Note:**
following options are ignored if **rotate** option is specified.
#### immutable
If set to **true**, names of generated files never changes. New files are immediately generated with their rotated
name. In other words the _rotated file name generator_ is never called with a **null** _time_ parameter unless to
determinate the _history file_ name; this can happen if **maxFiles** or **maxSize** are used without **history**
option. **rotation** _event_ now has a _filename_ parameter with the newly created file name.
Useful to send logs to logstash through filebeat.
**Note:**
if this option is set to **true**, **compress** is ignored.
**Note:**
this option is ignored if **interval** is not set.
#### rotationTime
As specified above, if rotation by interval is enabled, the parameter _time_ passed to _rotated file name generator_ is the
start time of rotation period. Setting this option to **true**, parameter _time_ passed is time when rotation job
started.
**Note:**
if this option is set to **true**, **initialRotation** is ignored.
#### history
Due to the complexity that _rotated file names_ can have because of the _filename generator function_, if number or
size of rotated files should not exceed a given limit, the package needs a file where to store this information. This
option specifies the name _history file_. This option takes effect only if at least one of **maxFiles** or **maxSize**
is used. If **null**, the _not rotated filename_ with the '.txt' suffix is used.
#### maxFiles
If specified, it's value is the maximum number of _rotated files_ to be kept.
#### maxSize
If specified, it's value must respect same syntax of [size](#size) option and is the maximum size of _rotated files_
to be kept.
## Events
Custom _Events_ are emitted by the stream.
```javascript
var rfs = require('rotating-file-stream');
var stream = rfs(...);
stream.on('error', function(err) {
// here are reported blocking errors
// once this event is emitted, the stream will be closed as well
});
stream.on('open', function(filename) {
// no rotated file is open (emitted after each rotation as well)
// filename: useful if immutable option is true
});
stream.on('removed', function(filename, number) {
// rotation job removed the specified old rotated file
// number == true, the file was removed to not exceed maxFiles
// number == false, the file was removed to not exceed maxSize
});
stream.on('rotation', function() {
// rotation job started
});
stream.on('rotated', function(filename) {
// rotation job completed with success producing given filename
});
stream.on('warning', function(err) {
// here are reported non blocking errors
});
```
## Rotation logic
Regardless of when and why rotation happens, the content of a single
[stream.write](https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback)
will never be split among two files.
### by size
Once the _not-rotated_ file is opened first time, its size is checked and if it is greater or equal to
size limit, a first rotation happens. After each
[stream.write](https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback),
the same check is performed.
### by interval
The package sets a [Timeout](https://nodejs.org/api/timers.html#timers_settimeout_callback_delay_args)
to start a rotation job at the right moment.
## Under the hood
Logs should be handled so carefully, so this package tries to never overwrite files.
At stream creation, if the _not-rotated_ log file already exists and its size exceeds the rotation size,
an initial rotation attempt is done.
At each rotation attempt a check is done to verify that destination rotated file does not exists yet;
if this is not the case a new destination _rotated file name_ is generated and the same check is
performed before going on. This is repeated until a not existing destination file name is found or the
package is exhausted. For this reason the _rotated file name generator_ function may be called several
times for each rotation job.
If requested by **maxFiles** or **maxSize** options, at the end of a rotation job, a check is performed to ensure that
given limits are respected. This means that **while rotation job is running both the limits could be not respected**,
the same can happen (if **maxFiles** or **maxSize** are changed) till the end of first _rotation job_.
The first check performed is the one against **maxFiles**, in case some files are removed, then the check against
**maxSize** is performed, finally other files can be removed. When **maxFiles** or **maxSize** are enabled for first
time, an _history file_ can be created with one _rotated filename_ (as returned by _filename generator function_) at
each line.
Once an **error** _event_ is emitted, nothing more can be done: the stream is closed as well.
## Compatibility
The package is tested under [all Node.js versions](https://travis-ci.org/iccicci/rotating-file-stream)
currently supported accordingly to [Node.js Release](https://github.com/nodejs/Release).
## TypeScript
To import the package in a **TypeScript** project, use following import statement.
```typescript
import rfs from "rotating-file-stream";
```
## Licence
[MIT Licence](https://github.com/iccicci/rotating-file-stream/blob/master/LICENSE)
## Bugs
Do not hesitate to report any bug or inconsistency [@github](https://github.com/iccicci/rotating-file-stream/issues).
## ChangeLog
[ChangeLog](https://github.com/iccicci/rotating-file-stream/blob/master/CHANGELOG.md)
## Donating
If you find useful this package, please consider the opportunity to donate some satoshis to this bitcoin address:
**12p1p5q7sK75tPyuesZmssiMYr4TKzpSCN**
+244
View File
@@ -0,0 +1,244 @@
"use strict";
var cp = require("child_process");
var fs = require("fs");
var path = require("path");
var utils = require("./utils");
var zlib = require("zlib");
function classical(count) {
var prevName;
var thisName;
var self = this;
if(this.options.rotate === count) delete this.rotatedName;
var callback = function(err) {
if(err) return self.emit("error", err);
self.open();
if(self.options.compress) self.compress(thisName);
else {
self.emit("rotated", self.rotatedName);
self.interval();
}
};
try {
prevName = count === 1 ? this.name : this.generator(count - 1);
thisName = this.generator(count);
}
catch(e) {
return callback(e);
}
var doIt = function(done) {
fs.rename(prevName, thisName, function(err) {
if(err) {
if(err.code !== "ENOENT") return callback(err);
return utils.makePath(thisName, function(err) {
if(err) return callback(err);
fs.rename(prevName, thisName, function(err) {
if(err) return callback(err);
process.nextTick(done);
});
});
}
process.nextTick(done);
});
};
fs.stat(prevName, function(err) {
if(! err) {
if(! self.rotatedName) self.rotatedName = thisName;
if(count !== 1) return doIt(self.classical.bind(self, count - 1));
if(self.options.compress)
return self.findName({}, true, function(err, name) {
if(err) return callback(err);
thisName = name;
doIt(callback);
});
return doIt(callback);
}
if(err.code !== "ENOENT") return callback(err);
self.classical(count - 1);
});
}
function compress(tmp) {
var self = this;
this.findName({}, false, function(err, name) {
if(err) return self.emit("error", err);
self.touch(name, function(err) {
if(err) return self.emit("error", err);
var done = function(err) {
if(err) return self.emit("error", err);
fs.unlink(tmp, function(err) {
if(err) self.emit("warning", err);
if(self.options.rotate) self.emit("rotated", self.rotatedName);
else self.emit("rotated", name);
self.interval();
});
};
if(typeof self.options.compress === "function") self.external(tmp, name, done);
else self.gzip(tmp, name, done);
/*
if(self.options.compress == "gzip")
self.gzip(tmp, name, done);
else
throw new Error("Not implemented yet");
*/
});
});
}
function external(src, dst, callback) {
var att = {};
var cont;
var self = this;
try {
cont = self.options.compress(src, dst);
}
catch(e) {
return process.nextTick(callback.bind(null, e));
}
att[dst] = 1;
self.findName(att, true, function(err, name) {
if(err) return callback(err);
fs.open(name, "w", parseInt("777", 8), function(err, fd) {
if(err) return callback(err);
var unlink = function(err) {
fs.unlink(name, function(err2) {
if(err2) self.emit("warning", err2);
callback(err);
});
};
fs.write(fd, cont, function(err) {
fs.close(fd, function(err2) {
if(err) {
if(err2) self.emit("warning", err2);
return unlink(err);
}
if(err2) return unlink(err2);
if(name.indexOf(path.sep) === -1) name = "." + path.sep + name;
cp.exec(name, unlink);
});
});
});
});
}
function exhausted(attempts) {
var err = new Error("Too many destination file attempts");
err.code = "RFS-TOO-MANY";
if(attempts) err.attempts = attempts;
return err;
}
function findName(attempts, tmp, callback) {
var count = 0;
for(var i in attempts) count += attempts[i];
if(count >= 1000) return callback(this.exhausted(attempts));
var name = this.name + "." + count + ".rfs.tmp";
var self = this;
if(! tmp)
try {
var pars = [count + 1];
if(! this.options.rotate)
if(this.options.interval && ! this.options.rotationTime) pars.unshift(new Date(this.prev));
else pars.unshift(this.rotation);
name = this.generator.apply(this, pars);
}
catch(e) {
return process.nextTick(callback.bind(null, e));
}
if(name in attempts) {
attempts[name]++;
return self.findName(attempts, tmp, callback);
}
fs.stat(name, function(err) {
if(! err || err.code !== "ENOENT") {
attempts[name] = 1;
return self.findName(attempts, tmp, callback);
}
callback(null, name);
});
}
function gzip(src, dst, callback) {
const inp = fs.createReadStream(src);
const out = fs.createWriteStream(dst);
const zip = zlib.createGzip();
[inp, out, zip].map(e => e.once("error", callback));
out.once("finish", callback);
inp.pipe(zip).pipe(out);
}
function touch(name, callback, retry) {
var self = this;
fs.open(name, "a", function(err, fd) {
if(err && err.code !== "ENOENT" && ! retry) return callback(err);
if(! err) return fs.close(fd, callback);
utils.makePath(name, function(err) {
if(err) return callback(err);
self.touch(name, callback, true);
});
});
}
module.exports = {
classical: classical,
compress: compress,
exhausted: exhausted,
external: external,
findName: findName,
gzip: gzip,
touch: touch
};
+21
View File
@@ -0,0 +1,21 @@
import { WriteStream } from "fs";
export interface RfsOptions {
compress?: string | Function | boolean;
highWaterMark?: number;
history?: string;
immutable?: boolean;
initialRotation?: boolean;
interval?: string;
maxFiles?: number;
maxSize?: string;
mode?: number;
path?: string;
rotate?: number;
rotationTime?: boolean;
size?: string;
}
declare function RotatingFileStream(fileName: string | Function, options: RfsOptions): WriteStream;
export default RotatingFileStream;
+304
View File
@@ -0,0 +1,304 @@
"use strict";
var compress = require("./compress");
var fs = require("fs");
var interval = require("./interval");
var path = require("path");
var util = require("util");
var utils = require("./utils");
var Writable = require("stream").Writable;
function RotatingFileStream(filename, options) {
if(! (this instanceof RotatingFileStream)) return new RotatingFileStream(filename, options);
options = utils.checkOptions(options);
if(typeof filename === "function") this.generator = filename;
else if(typeof filename === "string")
if(options.rotate) this.generator = utils.createClassical(filename);
else this.generator = utils.createGenerator(filename);
else throw new Error("Don't know how to handle 'filename' type: " + typeof filename);
if(options.path) {
var generator = this.generator;
this.generator = function(time, index) {
return path.join(options.path, generator(time, index));
};
}
var opt = {};
if(options.highWaterMark) opt.highWaterMark = options.highWaterMark;
if(options.mode) opt.mode = options.mode;
Writable.call(this, opt);
this.chunks = [];
this.options = options;
this.size = 0;
this.write = this.write; // https://github.com/iccicci/rotating-file-stream/issues/19
utils.setEvents(this);
process.nextTick(this.firstOpen.bind(this));
}
util.inherits(RotatingFileStream, Writable);
RotatingFileStream.prototype._close = function(done) {
if(this.stream) {
this.stream.on("finish", done);
this.stream.end();
this.stream = null;
}
else done();
};
RotatingFileStream.prototype._rewrite = function() {
const self = this;
const callback = function() {
if(self.ending) self._close(Writable.prototype.end.bind(self));
};
if(this.err) {
const chunks = this.chunks;
this.chunks = [];
chunks.map(e => {
if(e.cb) e.cb();
});
return callback();
}
if(this.writing || this.rotation) return;
if(this.options.size && this.size >= this.options.size) return this.rotate();
if(! this.stream) return;
if(! this.chunks.length) return callback();
const chunk = this.chunks[0];
this.chunks.shift();
this.size += chunk.chunk.length;
this.writing = true;
this.stream.write(chunk.chunk, function(err) {
self.writing = false;
if(err) self.emit("error", err);
if(chunk.cb) chunk.cb();
process.nextTick(self._rewrite.bind(self));
});
};
RotatingFileStream.prototype._write = function(chunk, encoding, callback) {
this.chunks.push({ chunk: chunk, cb: callback });
this._rewrite();
};
RotatingFileStream.prototype._writev = function(chunks, callback) {
chunks[chunks.length - 1].cb = callback;
this.chunks = this.chunks.concat(chunks);
this._rewrite();
};
RotatingFileStream.prototype.end = function() {
var args = [];
for(var i = 0; i < arguments.length; ++i) {
if("function" === typeof arguments[i]) {
this.once("finish", arguments[i]);
break;
}
if(i > 1) break;
args.push(arguments[i]);
}
this.ending = true;
if(args.length) this.write.apply(this, args);
else this._rewrite();
};
RotatingFileStream.prototype.firstOpen = function() {
var self = this;
if(this.options.immutable) return this.immutate(true);
try {
this.name = this.generator(null);
}
catch(e) {
return this.emit("error", e);
}
this.once("open", this.interval.bind(this));
fs.stat(this.name, function(err, stats) {
if(err) {
if(err.code === "ENOENT") return self.open();
return self.emit("error", err);
}
if(! stats.isFile()) return self.emit("error", new Error("Can't write on: " + self.name + " (it is not a file)"));
if(self.options.initialRotation) {
var prev;
self._interval(self.now());
prev = self.prev;
self._interval(stats.mtime.getTime());
if(prev !== self.prev) return self.rotate();
}
self.size = stats.size;
if(! self.options.size || stats.size < self.options.size) return self.open();
if(self.options.interval) self._interval(self.now());
self.rotate();
});
};
RotatingFileStream.prototype.immutate = function(first, index, now) {
if(! index) {
index = 1;
now = new Date(this.now());
}
if(index >= 1001) return this.emit("error", this.exhausted());
try {
this.name = this.generator(now, index);
}
catch(e) {
return this.emit("error", e);
}
var open = function(size) {
this.size = size;
this.open();
this.once(
"open",
function() {
if(! first) this.emit("rotated", this.last);
this.last = this.name;
this.interval();
}.bind(this)
);
}.bind(this);
fs.stat(
this.name,
function(err, stats) {
if(err) {
if(err.code === "ENOENT") return open(0);
return this.emit("error", err);
}
if(! stats.isFile()) return this.emit("error", new Error("Can't write on: " + this.name + " (it is not a file)"));
if(this.options.size && stats.size >= this.options.size) return this.immutate(first, index + 1, now);
open(stats.size);
}.bind(this)
);
};
RotatingFileStream.prototype.move = function(retry) {
var name;
var self = this;
var callback = function(err) {
if(err) return self.emit("error", err);
self.open();
if(self.options.compress) self.compress(name);
else {
self.emit("rotated", name);
self.interval();
}
};
this.findName({}, self.options.compress, function(err, found) {
if(err) return callback(err);
name = found;
fs.rename(self.name, name, function(err) {
if(err && err.code !== "ENOENT" && ! retry) return callback(err);
if(! err) return callback();
utils.makePath(name, function(err) {
if(err) return callback(err);
self.move(true);
});
});
});
};
RotatingFileStream.prototype.now = function() {
return Date.now();
};
RotatingFileStream.prototype.open = function(retry) {
var fd;
var self = this;
var options = { flags: "a" };
var callback = function(err) {
if(err) self.emit("error", err);
process.nextTick(self._rewrite.bind(self));
};
if("mode" in this.options) options.mode = this.options.mode;
var stream = fs.createWriteStream(this.name, options);
stream.once("open", function() {
self.stream = stream;
self.emit("open", self.name);
callback();
});
stream.once("error", function(err) {
if(err.code !== "ENOENT" && ! retry) return callback(err);
utils.makePath(self.name, function(err) {
if(err) return callback(err);
self.open(true);
});
});
};
RotatingFileStream.prototype.rotate = function() {
this.size = 0;
this.rotation = new Date();
this.emit("rotation");
this._clear();
this._close(this.options.rotate ? this.classical.bind(this, this.options.rotate) : this.options.immutable ? this.immutate.bind(this) : this.move.bind(this));
};
for(var i in compress) RotatingFileStream.prototype[i] = compress[i];
for(i in interval) RotatingFileStream.prototype[i] = interval[i];
module.exports = RotatingFileStream;
module.exports.default = RotatingFileStream;
+168
View File
@@ -0,0 +1,168 @@
"use strict";
var fs = require("fs");
var util = require("util");
function _clear(done) {
if(this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
function __interval(now) {
now = new Date(now);
var year = now.getFullYear();
var month = now.getMonth();
var day = now.getDate();
var hours = now.getHours();
var num = this.options.interval.num;
var unit = this.options.interval.unit;
if(unit === "M") {
day = 1;
hours = 0;
}
else if(unit === "d") hours = 0;
else hours = parseInt(hours / num, 10) * num;
this.prev = new Date(year, month, day, hours, 0, 0, 0).getTime();
if(unit === "M") month += num;
else if(unit === "d") day += num;
else hours += num;
this.next = new Date(year, month, day, hours, 0, 0, 0).getTime();
}
function _interval(now) {
var unit = this.options.interval.unit;
if(unit === "M" || unit === "d" || unit === "h") this.__interval(now);
else {
var period = 1000 * this.options.interval.num;
if(unit === "m") period *= 60;
this.prev = parseInt(now / period, 10) * period;
this.next = this.prev + period;
}
return new Date(this.prev);
}
function interval() {
if(! this.options.interval) return;
this._interval(this.now());
var self = this;
var set = function() {
var time = self.next - self.now();
self.timer = time > self.maxTimeout ? setTimeout(set, self.maxTimeout) : setTimeout(self.rotate.bind(self), time);
self.timer.unref();
};
set();
}
function historyWrite(self, res) {
var files = [];
res.map(e => files.push(e.name));
self.files = files;
fs.writeFile(self.options.history, files.join("\n"), "utf8", function(err) {
if(err) self.emit("warning", err);
self.emit("history");
});
}
function historyRemove(self, res, step, number) {
var file = res.shift();
fs.unlink(file.name, function(err) {
if(err) self.emit("warning", err);
else self.emit("removed", file.name, number);
step(self, res);
});
}
function historyCheckSize(self, res) {
if(! self.options.maxSize) return historyWrite(self, res);
var size = 0;
res.map(e => (size += e.size));
if(size <= self.options.maxSize) return historyWrite(self, res);
historyRemove(self, res, historyCheckSize, false);
}
function historyCheckFiles(self, res) {
res.sort(function(a, b) {
return a.time - b.time;
});
if(! self.options.maxFiles || res.length <= self.options.maxFiles) return historyCheckSize(self, res);
historyRemove(self, res, historyCheckFiles, true);
}
function historyGather(self, files, idx, res) {
if(idx === files.length) return historyCheckFiles(self, res);
fs.stat(files[idx], function(err, stats) {
if(err) {
if(err.code !== "ENOENT") return self.emit("warning", err);
}
else if(stats.isFile())
res.push({
name: files[idx],
size: stats.size,
time: stats.ctime.getTime()
});
else self.emit("warning", "File '" + files[idx] + "' contained in history is not a regular file");
historyGather(self, files, idx + 1, res);
});
}
function history(lastfile) {
var filename = this.options.history;
var self = this;
if(this.files) {
this.files.push(lastfile);
return historyGather(self, this.files, 0, []);
}
if(! filename) this.options.history = filename = this.generator(null) + ".txt";
fs.readFile(filename, "utf8", function(err, data) {
if(err) {
if(err.code !== "ENOENT") return self.emit("warning", err);
return historyGather(self, [lastfile], 0, []);
}
var files = data.split("\n");
files.push(lastfile);
historyGather(self, files, 0, []);
});
}
module.exports = {
__interval: __interval,
_clear: _clear,
_interval: _interval,
history: history,
interval: interval,
maxTimeout: 2147483640
};
+44
View File
@@ -0,0 +1,44 @@
{
"name": "rotating-file-stream",
"version": "1.4.6",
"description": "Opens a stream.Writable to a file rotated by interval and/or size. A logrotate alternative.",
"scripts": {
"all": "npm run npmignore && npm run eslint && npm run coverage && npm run ts",
"coverage": "TZ=\"Europe/Rome\" ./node_modules/.bin/nyc -r lcov -r text -r text-summary npm test",
"debug": "node --inspect-brk ./node_modules/.bin/_mocha test",
"eslint": "./node_modules/.bin/eslint *.js test/*js",
"npmignore": "echo '.codeclimate.yml\\n.eslintrc\\n.gitignore\\n.gitattributes\\n.travis.yml\\n.vscode\\nCHANGELOG.md\\nREADME.md\\ntest' > .npmignore ; cat .gitignore >> .npmignore",
"test": "TZ=\"Europe/Rome\" ./node_modules/.bin/_mocha test",
"ts": "node_modules/.bin/tsc index.d.ts --lib es6"
},
"bugs": "https://github.com/iccicci/rotating-file-stream/issues",
"repository": "https://github.com/iccicci/rotating-file-stream",
"keywords": [
"log",
"rotate",
"logrotate"
],
"engines": {
"node": ">=6.0"
},
"author": "Daniele Ricci <daniele.icc@gmail.com> (https://github.com/iccicci)",
"contributors": [
"cicci (https://www.trinityteam.it/DanieleRicci#en)",
"allevo",
"rakshith-ravi",
"kbirger",
"Jorge Silva <jorgemsrs@gmail.com>",
"Jan Christoph Bernack <jc.bernack@gmail.com>",
"cchare (https://github.com/cchare)"
],
"license": "MIT",
"readmeFilename": "README.md",
"types": "index.d.ts",
"devDependencies": {
"eslint": "6.5.1",
"mocha": "6.2.2",
"nyc": "14.1.1",
"typescript": "3.6.4",
"@types/node": "12.11.1"
}
}
+234
View File
@@ -0,0 +1,234 @@
"use strict";
var fs = require("fs");
var path = require("path");
function buildNumberCheck(field) {
return function(typ, options, val) {
var value = parseInt(val, 10);
if(value !== val || value <= 0 || typ !== "number") throw new Error("'" + field + "' option must be a positive integer number");
};
}
function buildStringCheck(field, check) {
return function(typ, options, val) {
if(typ !== "string") throw new Error("Don't know how to handle 'options." + field + "' type: " + typ);
options[field] = check(val);
};
}
function checkMeasure(v, what, units) {
var ret = {};
ret.num = parseInt(v, 10);
if(isNaN(ret.num)) throw new Error("Unknown 'options." + what + "' format: " + v);
if(ret.num <= 0) throw new Error("A positive integer number is expected for 'options." + what + "'");
ret.unit = v.replace(/^[ 0]*/g, "").substr((ret.num + "").length, 1);
if(ret.unit.length === 0) throw new Error("Missing unit for 'options." + what + "'");
if(! units[ret.unit]) throw new Error("Unknown 'options." + what + "' unit: " + ret.unit);
return ret;
}
var intervalUnits = {
M: true,
d: true,
h: true,
m: true,
s: true
};
function checkIntervalUnit(ret, unit, amount) {
if(parseInt(amount / ret.num, 10) * ret.num !== amount) throw new Error("An integer divider of " + amount + " is expected as " + unit + " for 'options.interval'");
}
function checkInterval(v) {
var ret = checkMeasure(v, "interval", intervalUnits);
switch(ret.unit) {
case "h":
checkIntervalUnit(ret, "hours", 24);
break;
case "m":
checkIntervalUnit(ret, "minutes", 60);
break;
case "s":
checkIntervalUnit(ret, "seconds", 60);
break;
}
return ret;
}
var sizeUnits = {
B: true,
G: true,
K: true,
M: true
};
function checkSize(v) {
var ret = checkMeasure(v, "size", sizeUnits);
if(ret.unit === "K") return ret.num * 1024;
if(ret.unit === "M") return ret.num * 1048576;
if(ret.unit === "G") return ret.num * 1073741824;
return ret.num;
}
var checks = {
compress: function(typ, options, val) {
if(! val) throw new Error("A value for 'options.compress' must be specified");
if(typ === "boolean")
options.compress = function(src, dst) {
return "cat " + src + " | gzip -c9 > " + dst;
};
else if(typ === "string") {
//if(val != "bzip" && val != "gzip")
if(val !== "gzip") throw new Error("Don't know how to handle compression method: " + val);
}
else if(typ !== "function") throw new Error("Don't know how to handle 'options.compress' type: " + typ);
},
highWaterMark: function() {},
history: function(typ) {
if(typ !== "string") throw new Error("Don't know how to handle 'options.history' type: " + typ);
},
immutable: function() {},
initialRotation: function() {},
interval: buildStringCheck("interval", checkInterval),
maxFiles: buildNumberCheck("maxFiles"),
maxSize: buildStringCheck("maxSize", checkSize),
mode: function() {},
path: function(typ) {
if(typ !== "string") throw new Error("Don't know how to handle 'options.path' type: " + typ);
},
rotate: buildNumberCheck("rotate"),
rotationTime: function() {},
size: buildStringCheck("size", checkSize)
};
function checkOptions(options) {
if(! options) return {};
if(typeof options !== "object") throw new Error("Don't know how to handle 'options' type: " + typeof options);
var ret = {};
for(var opt in options) {
var val = options[opt];
var typ = typeof val;
if(! (opt in checks)) throw new Error("Unknown option: " + opt);
ret[opt] = options[opt];
checks[opt](typ, ret, val);
}
if(! ret.interval) {
delete ret.immutable;
delete ret.initialRotation;
delete ret.rotationTime;
}
if(ret.rotate) {
delete ret.history;
delete ret.immutable;
delete ret.maxFiles;
delete ret.maxSize;
delete ret.rotationTime;
}
if(ret.immutable) delete ret.compress;
if(ret.rotationTime) delete ret.initialRotation;
return ret;
}
function pad(num) {
return (num > 9 ? "" : "0") + num;
}
function createClassical(filename) {
return function(index) {
if(! index) return filename;
return filename + "." + index;
};
}
function createGenerator(filename) {
return function(time, index) {
if(! time) return filename;
var month = time.getFullYear() + "" + pad(time.getMonth() + 1);
var day = pad(time.getDate());
var hour = pad(time.getHours());
var minute = pad(time.getMinutes());
return month + day + "-" + hour + minute + "-" + pad(index) + "-" + filename;
};
}
function makePath(name, callback) {
var dir = path.parse(name).dir;
fs.mkdir(dir, function(e) {
if(e) {
if(e.code === "ENOENT") return makePath(dir, callback);
if(e.code !== "EEXIST") return callback(e);
}
callback();
});
}
function setEvents(self) {
self.once("error", function(err) {
self.err = err;
self.end();
});
self.once("finish", self._clear.bind(self));
self.on("rotated", function() {
self.rotation = null;
self._rewrite();
});
if((self.options.maxFiles || self.options.maxSize) && ! self.options.rotate) self.on(self.options.immutable ? "open" : "rotated", self.history.bind(self));
}
module.exports = {
checkOptions: checkOptions,
createClassical: createClassical,
createGenerator: createGenerator,
makePath: makePath,
setEvents: setEvents
};