[READ-ONLY] Mirror of https://github.com/FoxxMD/logging. A typed, opinionated, batteries-included, Pino-based logging solution for backend TS/JS projects foxxmd.github.io/logging
child-logger logging logging-library nodejs pinojs typescript-library
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: Improve log path extensibility

* Implement `filePath` in LogOptions
* Can be absolute, relative path, string function, or parsed from LOG_PATH
* Remove undocumented CONFIG_DIR env

FoxxMD (Mar 7, 2024, 1:51 PM EST) d4fca38d 58424b9a

+110 -65
+43 -41
patches/pino-roll+1.0.0-rc.1.patch
··· 1 1 diff --git a/node_modules/pino-roll/lib/utils.js b/node_modules/pino-roll/lib/utils.js 2 - index c84cd8f..228e03e 100644 2 + index c84cd8f..0be7f5e 100644 3 3 --- a/node_modules/pino-roll/lib/utils.js 4 4 +++ b/node_modules/pino-roll/lib/utils.js 5 - @@ -62,11 +62,11 @@ function getNext (frequency) { 5 + @@ -62,14 +62,19 @@ function getNext (frequency) { 6 6 return getNextCustom(frequency) 7 7 } 8 8 9 9 -function buildFileName (fileName, lastNumber = 1, extension) { 10 - +function buildFileName (fileName, lastNumber = 1, extension, prefix) { 11 - if (!fileName) { 10 + - if (!fileName) { 11 + +function getFileName (fileVal) { 12 + + if (!fileVal) { 12 13 throw new Error('No file name provided') 13 14 } 14 15 - return `${fileName}.${lastNumber}${extension ?? ''}` 15 - + return `${fileName}${prefix ?? ''}.${lastNumber}${extension ?? ''}` 16 + + return typeof fileVal === 'function' ? fileVal() : fileVal 16 17 } 17 18 18 - async function detectLastNumber (fileName, time = null) { 19 + -async function detectLastNumber (fileName, time = null) { 20 + +function buildFileName (fileVal, lastNumber = 1, extension) { 21 + + return `${getFileName(fileVal)}.${lastNumber}${extension ?? ''}` 22 + +} 23 + + 24 + +async function detectLastNumber (fileVal, time = null) { 25 + + const fileName = getFileName(fileVal) 26 + try { 27 + const numbers = await readFileTrailingNumbers(dirname(fileName), time) 28 + return numbers.sort((a, b) => b - a)[0] 29 + @@ -102,4 +107,4 @@ async function isMatchingTime (filePath, time) { 30 + return birthtimeMs >= time 31 + } 32 + 33 + -module.exports = { buildFileName, detectLastNumber, parseFrequency, getNext, parseSize } 34 + +module.exports = { buildFileName, detectLastNumber, parseFrequency, getNext, parseSize, getFileName } 19 35 diff --git a/node_modules/pino-roll/pino-roll.js b/node_modules/pino-roll/pino-roll.js 20 - index 800d59f..798dca3 100644 36 + index 800d59f..68c42e9 100644 21 37 --- a/node_modules/pino-roll/pino-roll.js 22 38 +++ b/node_modules/pino-roll/pino-roll.js 23 - @@ -25,6 +25,8 @@ const { buildFileName, detectLastNumber, parseSize, parseFrequency, getNext } = 24 - * Using a numerical value will always create a new file upon startup. 25 - * 26 - * @property {string} extension? - When specified, appends a file extension after the file number. 27 - + * 28 - + * @property {string} prefix? - When specified, is appended to file name before file number. 29 - */ 39 + @@ -3,12 +3,19 @@ 40 + const SonicBoom = require('sonic-boom') 41 + const { buildFileName, detectLastNumber, parseSize, parseFrequency, getNext } = require('./lib/utils') 30 42 43 + +/** 44 + + * A function that returns a string path to the base file name 45 + + * 46 + + * @typedef {function} LogFilePath 47 + + * @returns {string} 48 + + */ 49 + + 31 50 /** 32 - @@ -38,7 +40,7 @@ const { buildFileName, detectLastNumber, parseSize, parseFrequency, getNext } = 33 - * @param {PinoRollOptions} options - to configure file destionation, and rolling rules. 34 - * @returns {SonicBoom} the Sonic boom steam, usabled as Pino transport. 35 - */ 36 - -module.exports = async function ({ file, size, frequency, extension, ...opts } = {}) { 37 - +module.exports = async function ({ file, size, frequency, extension, prefix, ...opts } = {}) { 38 - const frequencySpec = parseFrequency(frequency) 39 - 40 - let number = await detectLastNumber(file, frequencySpec?.start) 41 - @@ -46,7 +48,7 @@ module.exports = async function ({ file, size, frequency, extension, ...opts } = 42 - let currentSize = 0 43 - const maxSize = parseSize(size) 44 - 45 - - const destination = new SonicBoom({ ...opts, dest: buildFileName(file, number, extension) }) 46 - + const destination = new SonicBoom({ ...opts, dest: buildFileName(file, number, extension, prefix) }) 47 - 48 - let rollTimeout 49 - if (frequencySpec) { 50 - @@ -68,7 +70,7 @@ module.exports = async function ({ file, size, frequency, extension, ...opts } = 51 - } 52 - 53 - function roll () { 54 - - destination.reopen(buildFileName(file, ++number, extension)) 55 - + destination.reopen(buildFileName(file, ++number, extension, prefix)) 56 - } 57 - 58 - function scheduleRoll () { 51 + * @typedef {object} Options 52 + * 53 + - * @property {string} file - Absolute or relative path to the log file. 54 + + * @property {string|LogFilePath} file - Absolute or relative path to the log file. 55 + * Your application needs the write right on the parent folder. 56 + - * Number will be appened to this file name. 57 + + * Number will be appended to this file name. 58 + * When the parent folder already contains numbered files, numbering will continue based on the highest number. 59 + * If this path does not exist, the logger with throw an error unless you set `mkdir` to `true`. 60 + *
+5 -7
src/constants.ts
··· 1 1 import path from "path"; 2 2 import process from "process"; 3 3 4 - export const projectDir = process.cwd(); 5 - export const configDir: string = path.resolve(projectDir, './config'); 6 - let logPath = path.resolve(configDir, `./logs`); 7 - if (typeof process.env.CONFIG_DIR === 'string') { 8 - logPath = path.resolve(process.env.CONFIG_DIR, './logs'); 9 - } 4 + const projectDir = process.cwd(); 5 + 6 + let logPath = path.resolve(projectDir, `./logs/app.log`); 10 7 11 8 export { 12 - logPath 9 + logPath, 10 + projectDir 13 11 }
+16 -8
src/destinations.ts
··· 15 15 const {path: logPath, ...rest} = options; 16 16 17 17 try { 18 - fileOrDirectoryIsWriteable(logPath); 18 + const testPath = typeof logPath === 'function' ? logPath() : logPath; 19 + fileOrDirectoryIsWriteable(testPath); 20 + 21 + const pInfo = path.parse(testPath); 22 + let filePath: string | (() => string); 23 + if(typeof logPath === 'string') { 24 + filePath = () => path.resolve(pInfo.dir, `${pInfo.name}-${new Date().toISOString().split('T')[0]}`) 25 + } else { 26 + filePath = logPath; 27 + } 19 28 const rollingDest = await pRoll({ 20 - file: path.resolve(logPath, 'app'), 29 + file: filePath, 21 30 size: 10, 22 31 frequency: 'daily', 23 - get extension() { 24 - return `-${new Date().toISOString().split('T')[0]}.log` 25 - }, 32 + extension: pInfo.ext, 26 33 mkdir: true, 27 34 sync: false, 28 35 }); ··· 41 48 return undefined; 42 49 } 43 50 44 - const {path: logPath, ...rest} = options; 51 + const {path: logPathVal, ...rest} = options; 45 52 46 53 try { 47 - fileOrDirectoryIsWriteable(logPath); 48 - const dest = destination({dest: logPath, mkdir: true, sync: false}) 54 + const filePath = typeof logPathVal === 'function' ? logPathVal() : logPathVal; 55 + fileOrDirectoryIsWriteable(filePath); 56 + const dest = destination({dest: filePath, mkdir: true, sync: false}) 49 57 50 58 return { 51 59 level: level,
+23 -2
src/funcs.ts
··· 1 1 import process from "process"; 2 2 import {isLogOptions, LogLevel, LogOptions} from "./types.js"; 3 + import {logPath, projectDir} from "./constants.js"; 4 + import {isAbsolute, resolve} from 'node:path'; 3 5 4 6 export const parseLogOptions = (config: LogOptions = {}): Required<LogOptions> => { 5 7 if (!isLogOptions(config)) { ··· 12 14 const { 13 15 level = configLevel || defaultLevel, 14 16 file = configLevel || defaultLevel, 15 - console = configLevel || 'debug' 17 + console = configLevel || 'debug', 18 + filePath 16 19 } = config; 17 20 18 21 return { 19 22 level, 20 23 file: file as LogLevel | false, 21 - console 24 + console, 25 + filePath: typeof filePath === 'function' ? filePath : getLogPath(filePath) 22 26 }; 23 27 } 28 + 29 + export const getLogPath = (path?: string) => { 30 + let pathVal: string; 31 + if (path !== undefined) { 32 + pathVal = path; 33 + } else if (typeof process.env.LOG_PATH === 'string') { 34 + pathVal = process.env.LOG_PATH; 35 + } else { 36 + pathVal = logPath; 37 + } 38 + 39 + if (isAbsolute(pathVal)) { 40 + return pathVal; 41 + } 42 + 43 + return resolve(projectDir, pathVal); 44 + }
+1 -1
src/loggers.ts
··· 52 52 53 53 let error: Error; 54 54 try { 55 - const file = buildDestinationFile(options.file, {path: path.resolve(logPath, 'app.log'), append: true}); 55 + const file = buildDestinationFile(options.file, {path: options.filePath, append: true}); 56 56 if (file !== undefined) { 57 57 streams.push(file); 58 58 }
+19 -3
src/types.ts
··· 19 19 * */ 20 20 file?: LogLevel | false 21 21 /** 22 + * The full path and filename to use for log files 23 + * 24 + * If using rolling files the filename will be appended with `.N` (a number) BEFORE the extension based on rolling status 25 + * 26 + * May also be specified using env LOG_PATH or a function that returns a string 27 + * 28 + * @default 'CWD/logs/app.log 29 + * */ 30 + filePath?: string | (() => string) 31 + /** 22 32 * Specify the minimum log level streamed to the console (or docker container) 23 33 * */ 24 34 console?: LogLevel ··· 26 36 27 37 export const isLogOptions = (obj: object = {}): obj is LogOptions => { 28 38 return Object.entries(obj).every(([key, val]) => { 39 + if(val === undefined) { 40 + return true; 41 + } 29 42 const t = typeof val; 43 + if(key === 'filePath') { 44 + return t === 'string' || t === 'function'; 45 + } 30 46 if(t !== 'string' && t !== 'boolean') { 31 47 return false; 32 48 } 33 49 if (key !== 'file') { 34 - return val === undefined || LOG_LEVELS.includes(val.toLocaleLowerCase()); 50 + return LOG_LEVELS.includes(val.toLocaleLowerCase()); 35 51 } 36 - return val === undefined || val === false || LOG_LEVELS.includes(val.toLocaleLowerCase()); 52 + return val === false || LOG_LEVELS.includes(val.toLocaleLowerCase()); 37 53 }); 38 54 } 39 55 ··· 53 69 msg: string | Error | ErrorWithCause 54 70 } 55 71 56 - export type FileDestination = Omit<PrettyOptions, 'destination' | 'sync'> & {path: string}; 72 + export type FileDestination = Omit<PrettyOptions, 'destination' | 'sync'> & {path: string | (() => string)}; 57 73 export type StreamDestination = Omit<PrettyOptions, 'destination'> & {destination: number | DestinationStream | NodeJS.WritableStream};
+3 -3
tests/index.test.ts
··· 247 247 describe('Combined', function() { 248 248 it('It writes to rolling file and console', async function () { 249 249 await withLocalTmpDir(async () => { 250 - const logPath = './logs'; 250 + const logPath = './logs/app.log'; 251 251 const [logger, testStream, rawStream] = await testRollingAppLogger({file: 'debug'}, logPath); 252 252 const race = Promise.race([ 253 253 pEvent(testStream, 'data'), ··· 258 258 const res = await race; 259 259 expect(res).to.not.be.undefined; 260 260 expect(res.toString()).to.include('DEBUG: Test'); 261 - const paths = readdirSync(logPath); 261 + const paths = readdirSync('./logs'); 262 262 expect(paths.length).eq(1); 263 - const fileContents = readFileSync(path.resolve(logPath, paths[0])).toString(); 263 + const fileContents = readFileSync(path.resolve('./logs', paths[0])).toString(); 264 264 expect(fileContents).includes('DEBUG: Test'); 265 265 }, {unsafeCleanup: true}); 266 266 });