Comparing version 1.0.0-rc.1 to 1.0.0
'use strict' | ||
const { readdir, stat } = require('fs/promises') | ||
const { readdir, stat, unlink } = require('fs/promises') | ||
const { dirname, join } = require('path') | ||
@@ -43,2 +43,13 @@ | ||
function validateLimitOptions (limit) { | ||
if (limit) { | ||
if (typeof limit !== 'object') { | ||
throw new Error('limit must be an object') | ||
} | ||
if (typeof limit.count !== 'number' || limit.count <= 0) { | ||
throw new Error('limit.count must be a number greater than 0') | ||
} | ||
} | ||
} | ||
function getNextDay (start) { | ||
@@ -66,10 +77,15 @@ return new Date(start + 24 * 60 * 60 * 1000).setHours(0, 0, 0, 0) | ||
function buildFileName (fileName, lastNumber = 1, extension) { | ||
if (!fileName) { | ||
function getFileName (fileVal) { | ||
if (!fileVal) { | ||
throw new Error('No file name provided') | ||
} | ||
return `${fileName}.${lastNumber}${extension ?? ''}` | ||
return typeof fileVal === 'function' ? fileVal() : fileVal | ||
} | ||
async function detectLastNumber (fileName, time = null) { | ||
function buildFileName (fileVal, lastNumber = 1, extension) { | ||
return `${getFileName(fileVal)}.${lastNumber}${extension ?? ''}` | ||
} | ||
async function detectLastNumber (fileVal, time = null) { | ||
const fileName = getFileName(fileVal) | ||
try { | ||
@@ -86,3 +102,3 @@ const numbers = await readFileTrailingNumbers(dirname(fileName), time) | ||
for (const file of await readdir(folder)) { | ||
if (time && !await isMatchingTime(join(folder, file), time)) { | ||
if (time && !(await isMatchingTime(join(folder, file), time))) { | ||
continue | ||
@@ -108,2 +124,19 @@ } | ||
module.exports = { buildFileName, detectLastNumber, parseFrequency, getNext, parseSize } | ||
async function checkFileRemoval (files, { count }) { | ||
if (files.length > count) { | ||
const filesToRemove = files.splice(0, files.length - 1 - count) | ||
await Promise.allSettled(filesToRemove.map(file => unlink(file))) | ||
} | ||
return files | ||
} | ||
module.exports = { | ||
buildFileName, | ||
checkFileRemoval, | ||
detectLastNumber, | ||
parseFrequency, | ||
getNext, | ||
parseSize, | ||
getFileName, | ||
validateLimitOptions | ||
} |
{ | ||
"name": "pino-roll", | ||
"version": "1.0.0-rc.1", | ||
"version": "1.0.0", | ||
"description": "A Pino transport that automatically rolls your log files", | ||
@@ -14,3 +14,3 @@ "main": "pino-roll.js", | ||
"type": "git", | ||
"url": "git+https://github.com/feugy/pino-roll.git" | ||
"url": "git+https://github.com/mcollina/pino-roll.git" | ||
}, | ||
@@ -22,10 +22,13 @@ "keywords": [ | ||
"author": "Damien Simonin Feugas <damien.feugas@gmail.com>", | ||
"contributors": [ | ||
"Matteo Collina <hello@matteocollina.com>" | ||
], | ||
"license": "MIT", | ||
"dependencies": { | ||
"sonic-boom": "^2.8.0" | ||
"sonic-boom": "^3.8.0" | ||
}, | ||
"devDependencies": { | ||
"date-fns": "^2.28.0", | ||
"husky": "^7.0.4", | ||
"pino": "^7.9.2", | ||
"date-fns": "^3.6.0", | ||
"husky": "^9.0.11", | ||
"pino": "^8.19.0", | ||
"snazzy": "^9.0.0", | ||
@@ -32,0 +35,0 @@ "standard": "^17.0.0-2", |
'use strict' | ||
const SonicBoom = require('sonic-boom') | ||
const { buildFileName, detectLastNumber, parseSize, parseFrequency, getNext } = require('./lib/utils') | ||
const { | ||
buildFileName, | ||
checkFileRemoval, | ||
detectLastNumber, | ||
parseSize, | ||
parseFrequency, | ||
getNext, | ||
validateLimitOptions | ||
} = require('./lib/utils') | ||
/** | ||
* A function that returns a string path to the base file name | ||
* | ||
* @typedef {function} LogFilePath | ||
* @returns {string} | ||
*/ | ||
/** | ||
* @typedef {object} Options | ||
* | ||
* @property {string} file - Absolute or relative path to the log file. | ||
* @property {string|LogFilePath} file - Absolute or relative path to the log file. | ||
* Your application needs the write right on the parent folder. | ||
* Number will be appened to this file name. | ||
* Number will be appended to this file name. | ||
* When the parent folder already contains numbered files, numbering will continue based on the highest number. | ||
@@ -28,5 +43,13 @@ * If this path does not exist, the logger with throw an error unless you set `mkdir` to `true`. | ||
* @property {string} extension? - When specified, appends a file extension after the file number. | ||
* | ||
* @property {LimitOptions} limit? - strategy used to remove oldest files when rotating them. | ||
*/ | ||
/** | ||
* @typedef {object} LimitOptions | ||
* | ||
* @property {number} count? -number of log files, **in addition to the currently used file**. | ||
*/ | ||
/** | ||
* @typedef {Options & import('sonic-boom').SonicBoomOpts} PinoRollOptions | ||
@@ -42,3 +65,11 @@ */ | ||
*/ | ||
module.exports = async function ({ file, size, frequency, extension, ...opts } = {}) { | ||
module.exports = async function ({ | ||
file, | ||
size, | ||
frequency, | ||
extension, | ||
limit, | ||
...opts | ||
} = {}) { | ||
validateLimitOptions(limit) | ||
const frequencySpec = parseFrequency(frequency) | ||
@@ -48,6 +79,8 @@ | ||
const fileName = buildFileName(file, number, extension) | ||
const createdFileNames = [fileName] | ||
let currentSize = 0 | ||
const maxSize = parseSize(size) | ||
const destination = new SonicBoom({ ...opts, dest: buildFileName(file, number, extension) }) | ||
const destination = new SonicBoom({ ...opts, dest: fileName }) | ||
@@ -74,3 +107,8 @@ let rollTimeout | ||
function roll () { | ||
destination.reopen(buildFileName(file, ++number, extension)) | ||
const fileName = buildFileName(file, ++number, extension) | ||
destination.reopen(fileName) | ||
if (limit) { | ||
createdFileNames.push(fileName) | ||
checkFileRemoval(createdFileNames, limit) | ||
} | ||
} | ||
@@ -77,0 +115,0 @@ |
# pino-roll | ||
[![npm version](https://img.shields.io/npm/v/pino-roll)](https://www.npmjs.com/package/pino-roll) | ||
[![Build Status](https://img.shields.io/github/workflow/status/feugy/pino-roll/CI)](https://github.com/feugy/pino-roll/actions) | ||
[![Known Vulnerabilities](https://snyk.io/test/github/feugy/pino-roll/badge.svg)](https://snyk.io/test/github/feugy/pino-roll) | ||
<!-- [![Coverage Status](https://coveralls.io/repos/github/feugy/pino-roll/badge.svg?branch=master)](https://coveralls.io/github/feugy/pino-roll?branch=master) --> | ||
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/) | ||
A Pino transport that automatically rolls your log files | ||
A Pino transport that automatically rolls your log files. | ||
@@ -49,2 +44,3 @@ ## Install | ||
If this path does not exist, the logger with throw an error unless you set `mkdir` to `true`. | ||
`file` may also be a function that returns a string. | ||
@@ -63,6 +59,13 @@ * `size?`: the maximum size of a given log file. | ||
* `extension?` appends the provided string after the file number. | ||
* `extension?`: appends the provided string after the file number. | ||
* `limit?`: strategy used to remove oldest files when rotating them: | ||
* `limit.count?`: number of log files, **in addition to the currently used file**. | ||
Please not that `limit` only considers **created log files**. It will not consider any pre-existing files. | ||
Therefore, starting your logger with a limit will never tries deleting older log files, created during previous executions. | ||
## License | ||
MIT |
@@ -8,3 +8,11 @@ 'use strict' | ||
const { buildFileName, detectLastNumber, getNext, parseFrequency, parseSize } = require('../../lib/utils') | ||
const { | ||
buildFileName, | ||
detectLastNumber, | ||
getNext, | ||
parseFrequency, | ||
parseSize, | ||
getFileName, | ||
validateLimitOptions | ||
} = require('../../lib/utils') | ||
const { cleanAndCreateFolder, sleep } = require('../utils') | ||
@@ -58,2 +66,9 @@ | ||
test('getFileName()', async ({ equal, throws }) => { | ||
const strFunc = () => 'my-func' | ||
throws(getFileName, 'throws on empty input') | ||
equal(getFileName('my-file'), 'my-file', 'returns string when string given') | ||
equal(getFileName(strFunc), 'my-func', 'invokes function when function given') | ||
}) | ||
test('buildFileName()', async ({ equal, throws }) => { | ||
@@ -63,2 +78,3 @@ const ext = '.json' | ||
equal(buildFileName('my-file'), 'my-file.1', 'appends 1 by default') | ||
equal(buildFileName(() => 'my-func'), 'my-func.1', 'appends 1 by default') | ||
equal(buildFileName('my-file', 5, ext), 'my-file.5.json', 'appends number and extension') | ||
@@ -73,2 +89,3 @@ }) | ||
const fileName = join(folder, 'file.5') | ||
const fileNameFunc = () => fileName | ||
await writeFile(join(folder, 'file.1'), '') | ||
@@ -79,2 +96,3 @@ await writeFile(join(folder, 'file.5'), '') | ||
equal(await detectLastNumber(fileName), 10, 'detects highest existing number') | ||
equal(await detectLastNumber(fileNameFunc), 10, 'detects highest existing number when given func') | ||
}) | ||
@@ -110,1 +128,10 @@ | ||
}) | ||
test('validateLimitOptions()', async ({ doesNotThrow, throws }) => { | ||
doesNotThrow(() => validateLimitOptions(), 'allows no limit') | ||
doesNotThrow(() => validateLimitOptions({ count: 2 }), 'allows valid count') | ||
throws(() => validateLimitOptions(true), { message: 'limit must be an object' }, 'throws when limit is not an object') | ||
throws(() => validateLimitOptions({ count: [] }), { message: 'limit.count must be a number greater than 0' }, 'throws when limit.count is not an number') | ||
throws(() => validateLimitOptions({ count: -2 }), { message: 'limit.count must be a number greater than 0' }, 'throws when limit.count is negative') | ||
throws(() => validateLimitOptions({ count: 0 }), { message: 'limit.count must be a number greater than 0' }, 'throws when limit.count is 0') | ||
}) |
'use strict' | ||
const { once } = require('events') | ||
const { stat, readFile, writeFile } = require('fs/promises') | ||
const { stat, readFile, writeFile, readdir } = require('fs/promises') | ||
const { join } = require('path') | ||
const { test, beforeEach } = require('tap') | ||
const { format } = require('date-fns') | ||
@@ -38,2 +39,17 @@ const { buildStream, cleanAndCreateFolder, sleep } = require('./utils') | ||
test('rotate file based on time and parse filename func', async ({ ok, notOk, rejects }) => { | ||
const file = join(logFolder, 'log') | ||
const fileFunc = () => `${file}-${format(new Date(), 'HH-mm-ss-SSS')}` | ||
const stream = await buildStream({ frequency: 100, file: fileFunc }) | ||
stream.write('logged message #1\n') | ||
stream.write('logged message #2\n') | ||
await sleep(110) | ||
stream.write('logged message #3\n') | ||
stream.write('logged message #4\n') | ||
await sleep(110) | ||
stream.end() | ||
const files = await readdir(logFolder) | ||
ok(files.length === 3, 'created three files') | ||
}) | ||
test('rotate file based on size', async ({ ok, rejects }) => { | ||
@@ -67,8 +83,75 @@ const file = join(logFolder, 'log') | ||
equal(await readFile(`${file}.6`, 'utf8'), `${previousContent}${newContent}`, 'old and new content were written') | ||
equal( | ||
await readFile(`${file}.6`, 'utf8'), | ||
`${previousContent}${newContent}`, | ||
'old and new content were written' | ||
) | ||
rejects(stat(`${file}.1`), 'no other files created') | ||
}) | ||
test('remove files based on count', async ({ ok, rejects }) => { | ||
const file = join(logFolder, 'log') | ||
const stream = await buildStream({ | ||
size: '20b', | ||
file, | ||
limit: { count: 1 } | ||
}) | ||
for (let i = 1; i <= 5; i++) { | ||
stream.write(`logged message #${i}\n`) | ||
await sleep(20) | ||
} | ||
stream.end() | ||
await stat(`${file}.2`) | ||
let content = await readFile(`${file}.2`, 'utf8') | ||
ok(content.includes('#3'), 'second file contains thrid log') | ||
ok(content.includes('#4'), 'second file contains fourth log') | ||
await stat(`${file}.3`) | ||
content = await readFile(`${file}.3`, 'utf8') | ||
ok(content.includes('#5'), 'third file contains fifth log') | ||
await rejects(stat(`${file}.1`), 'first file was deleted') | ||
await rejects(stat(`${file}.4`), 'no other files created') | ||
}) | ||
test('do not remove pre-existing file when removing files based on count', async ({ | ||
ok, | ||
equal, | ||
rejects | ||
}) => { | ||
const file = join(logFolder, 'log') | ||
await writeFile(`${file}.1`, 'oldest content') | ||
await writeFile(`${file}.2`, 'old content') | ||
const stream = await buildStream({ | ||
size: '20b', | ||
file, | ||
limit: { count: 2 } | ||
}) | ||
for (let i = 1; i <= 7; i++) { | ||
stream.write(`logged message #${i}\n`) | ||
await sleep(20) | ||
} | ||
stream.end() | ||
await stat(`${file}.1`) | ||
let content = await readFile(`${file}.1`, 'utf8') | ||
equal(content, 'oldest content', 'oldest file was not touched') | ||
await stat(`${file}.3`) | ||
content = await readFile(`${file}.3`, 'utf8') | ||
ok(content.includes('#3'), 'second file contains third log') | ||
ok(content.includes('#4'), 'second file contains fourth log') | ||
await stat(`${file}.4`) | ||
content = await readFile(`${file}.4`, 'utf8') | ||
ok(content.includes('#5'), 'third file contains fifth log') | ||
ok(content.includes('#6'), 'third file contains sixth log') | ||
await stat(`${file}.5`) | ||
content = await readFile(`${file}.5`, 'utf8') | ||
ok(content.includes('#7'), 'fourth file contains seventh log') | ||
await rejects(stat(`${file}.2`), 'resumed file was deleted') | ||
await rejects(stat(`${file}.6`), 'no other files created') | ||
}) | ||
test('throw on missing file parameter', async ({ rejects }) => { | ||
rejects(buildStream(), { message: 'No file name provided' }, 'throws on missing file parameters') | ||
rejects( | ||
buildStream(), | ||
{ message: 'No file name provided' }, | ||
'throws on missing file parameters' | ||
) | ||
}) | ||
@@ -90,3 +173,3 @@ | ||
{ message: `${size} is not a valid size in KB, MB or GB` }, | ||
'throws on unexisting folder' | ||
'throws on unparseable size' | ||
) | ||
@@ -99,5 +182,37 @@ }) | ||
buildStream({ file: join(logFolder, 'log'), frequency }), | ||
{ message: `${frequency} is neither a supported frequency or a number of milliseconds` }, | ||
'throws on unexisting folder' | ||
{ | ||
message: `${frequency} is neither a supported frequency or a number of milliseconds` | ||
}, | ||
'throws on unparseable frequency' | ||
) | ||
}) | ||
test('throw on unparseable limit object', async ({ rejects }) => { | ||
rejects( | ||
buildStream({ file: join(logFolder, 'log'), limit: 10 }), | ||
{ | ||
message: 'limit must be an object' | ||
}, | ||
'throws on limit option not being an object' | ||
) | ||
}) | ||
test('throw when limit.count is not a number', async ({ rejects }) => { | ||
rejects( | ||
buildStream({ file: join(logFolder, 'log'), limit: { count: true } }), | ||
{ | ||
message: 'limit.count must be a number greater than 0' | ||
}, | ||
'throws on limit.count not being a number' | ||
) | ||
}) | ||
test('throw when limit.count is 0', async ({ rejects }) => { | ||
rejects( | ||
buildStream({ file: join(logFolder, 'log'), limit: { count: 0 } }), | ||
{ | ||
message: 'limit.count must be a number greater than 0' | ||
}, | ||
'throws on limit.count being 0' | ||
) | ||
}) |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
25704
556
1
70
1
+ Addedsonic-boom@3.8.1(transitive)
- Removedsonic-boom@2.8.0(transitive)
Updatedsonic-boom@^3.8.0