Skip to content

Commit

Permalink
Adds symlink option (#89)
Browse files Browse the repository at this point in the history
* Adds symlink option

* Adds documentation
  • Loading branch information
GasparAdragna authored Aug 30, 2024
1 parent 40b656f commit 62d16ee
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 3 deletions.
4 changes: 4 additions & 0 deletions README.md
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ You can specify any of [Sonic-Boom options](https://github.com/pinojs/sonic-boom

* `extension?`: appends the provided string after the file number.

* `symlink?`: creates a symlink to the current log file.
The symlink will be updated to the latest log file upon rotation.
The name of the symlink is always called `current.log`.

* `limit?`: strategy used to remove oldest files when rotating them:

* `limit.count?`: number of log files, **in addition to the currently used file**.
Expand Down
25 changes: 24 additions & 1 deletion lib/utils.js
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict'

const { readdir, stat, unlink } = require('fs/promises')
const { readdir, stat, unlink, symlink, lstat, readlink } = require('fs/promises')
const { dirname, join } = require('path')

function parseSize (size) {
Expand Down Expand Up @@ -135,9 +135,32 @@ async function checkFileRemoval (files, { count }) {
return files
}

async function checkSymlink (fileName, linkPath) {
const stats = await lstat(linkPath).then(stats => stats, () => null)
if (stats?.isSymbolicLink()) {
const existingTarget = await readlink(linkPath)
if (existingTarget === fileName) {
return false
}
await unlink(linkPath)
}
return true
}

async function createSymlink (fileVal) {
const fileName = getFileName(fileVal)
const linkPath = join(dirname(fileName), 'current.log')
const shouldCreateSymlink = await checkSymlink(fileName, linkPath)
if (shouldCreateSymlink) {
await symlink(fileName.split(/(\\|\/)/g).pop(), linkPath)
}
}

module.exports = {
buildFileName,
checkFileRemoval,
checkSymlink,
createSymlink,
detectLastNumber,
parseFrequency,
getNext,
Expand Down
11 changes: 11 additions & 0 deletions pino-roll.js
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const SonicBoom = require('sonic-boom')
const {
buildFileName,
checkFileRemoval,
createSymlink,
detectLastNumber,
parseSize,
parseFrequency,
Expand Down Expand Up @@ -42,6 +43,8 @@ const {
*
* @property {string} extension? - When specified, appends a file extension after the file number.
*
* @property {boolean} symlink? - When specified, creates a symlink to the current log file.
*
* @property {LimitOptions} limit? - strategy used to remove oldest files when rotating them.
*/

Expand All @@ -68,6 +71,7 @@ module.exports = async function ({
frequency,
extension,
limit,
symlink,
...opts
} = {}) {
validateLimitOptions(limit)
Expand All @@ -82,6 +86,10 @@ module.exports = async function ({

const destination = new SonicBoom({ ...opts, dest: fileName })

if (symlink) {
createSymlink(fileName)
}

let rollTimeout
if (frequencySpec) {
destination.once('close', () => {
Expand All @@ -104,6 +112,9 @@ module.exports = async function ({

function roll () {
destination.reopen(fileName)
if (symlink) {
createSymlink(fileName)
}
if (limit) {
createdFileNames.push(fileName)
checkFileRemoval(createdFileNames, limit)
Expand Down
42 changes: 41 additions & 1 deletion test/lib/utils.test.js
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
'use strict'

const { addDays, addHours, startOfDay, startOfHour } = require('date-fns')
const { writeFile, rm, stat } = require('fs/promises')
const { writeFile, rm, stat, readlink, symlink } = require('fs/promises')
const { join } = require('path')
const { test } = require('tap')

const {
buildFileName,
checkSymlink,
createSymlink,
getFileSize,
detectLastNumber,
getNext,
Expand Down Expand Up @@ -147,3 +149,41 @@ test('validateLimitOptions()', async ({ doesNotThrow, throws }) => {
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')
})

test('checkSymlink()', async ({ test, beforeEach }) => {
const folder = join('logs', 'utils')
beforeEach(() => cleanAndCreateFolder(folder))

test('given a new symlink (should return true)', async ({ equal }) => {
const fileName = join(folder, 'file.log')
const linkPath = join(folder, 'current.log')
await writeFile(fileName, 'test content')
const result = await checkSymlink(fileName, linkPath)
equal(result, true, 'returns true when symlink does not exist')
})

test('given an existing symlink pointing to the same file (should return false)', async ({ equal }) => {
const fileName = join(folder, 'file.log')
const linkPath = join(folder, 'current.log')
await writeFile(fileName, 'test content')
await symlink(fileName, linkPath)
const result = await checkSymlink(fileName, linkPath)
equal(result, false, 'returns false when symlink points to the same file')
const linkTarget = await readlink(linkPath)
equal(linkTarget, fileName, 'symlink remains unchanged')
})
})

test('createSymlink()', async ({ beforeEach }) => {
const folder = join('logs', 'utils')
beforeEach(() => cleanAndCreateFolder(folder))

test('given a new symlink (should create symlink)', async ({ equal }) => {
const fileName = join(folder, 'file.log')
const linkPath = join(folder, 'current.log')
await writeFile(fileName, 'test content')
await createSymlink(fileName)
const linkTarget = await readlink(linkPath)
equal(linkTarget, fileName, 'creates correct symlink')
})
})
34 changes: 33 additions & 1 deletion test/pino-roll.test.js
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict'

const { once } = require('events')
const { stat, readFile, writeFile, readdir } = require('fs/promises')
const { stat, readFile, writeFile, readdir, lstat, readlink } = require('fs/promises')
const { join } = require('path')
const { test, beforeEach } = require('tap')
const { format } = require('date-fns')
Expand Down Expand Up @@ -211,3 +211,35 @@ test('throw when limit.count is 0', async ({ rejects }) => {
'throws on limit.count being 0'
)
})

test('creates symlink if prop is set', async ({ equal, resolves }) => {
const file = join(logFolder, 'log')
const linkPath = join(logFolder, 'current.log')
const stream = await buildStream({ file, symlink: true })
stream.write('test content\n')
stream.end()
await sleep(200)
await resolves(lstat(linkPath), 'symlink was created')
const linkTarget = await readlink(linkPath)
equal(linkTarget, 'log.1', 'symlink points to the correct file')
const content = await readFile(linkPath, 'utf8')
equal(content, 'test content\n', 'symlink contains correct content')
})

test('symlink rotates on roll', async ({ equal, ok, resolves }) => {
const file = join(logFolder, 'log')
const linkPath = join(logFolder, 'current.log')
const stream = await buildStream({ frequency: 100, file, symlink: true })
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')
stream.end()
await sleep(200)
await resolves(lstat(linkPath), 'symlink was created')
const linkTarget = await readlink(linkPath)
equal(linkTarget, 'log.2', 'symlink points to the correct file')
const content = await readFile(linkPath, 'utf8')
ok(content.includes('#4'), 'symlink contains fourth log')
})
Empty file modified test/utils.js
100644 → 100755
Empty file.

0 comments on commit 62d16ee

Please sign in to comment.