backend logging
This commit is contained in:
+110
@@ -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
@@ -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
@@ -0,0 +1,427 @@
|
||||
# rotating-file-stream
|
||||
|
||||
[](https://travis-ci.org/iccicci/rotating-file-stream?branch=master)
|
||||
[](https://codeclimate.com/github/iccicci/rotating-file-stream)
|
||||
[](https://codeclimate.com/github/iccicci/rotating-file-stream/coverage)
|
||||
[](https://blockchain.info/address/12p1p5q7sK75tPyuesZmssiMYr4TKzpSCN)
|
||||
|
||||
[](https://www.npmjs.com/package/rotating-file-stream)
|
||||
[](https://david-dm.org/iccicci/rotating-file-stream)
|
||||
[](https://david-dm.org/iccicci/rotating-file-stream?type=dev)
|
||||
|
||||
[](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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
};
|
||||
Reference in New Issue
Block a user