diff --git a/src/funcs.ts b/src/funcs.ts index 3130c7b..4a7d2cc 100644 --- a/src/funcs.ts +++ b/src/funcs.ts @@ -1,5 +1,13 @@ import process from "process"; -import {FileLogOptions, FileLogOptionsParsed, LOG_LEVEL_NAMES, LogLevel, LogOptions, LogOptionsParsed} from "./types.js"; +import { + FileLogOptions, + FileLogOptionsParsed, + FileLogPathOptions, + LOG_LEVEL_NAMES, + LogLevel, + LogOptions, + LogOptionsParsed +} from "./types.js"; import {projectDir, logPathRelative} from "./constants.js"; import {isAbsolute, resolve} from 'node:path'; @@ -36,7 +44,7 @@ const isFileLogOptions = (obj: any): obj is FileLogOptions => { /** * Takes an object and parses it into a fully-populated LogOptions object based on opinionated defaults * */ -export const parseLogOptions = (config: LogOptions = {}, baseDir?: string): LogOptionsParsed => { +export const parseLogOptions = (config: LogOptions = {}, options?: FileLogPathOptions): LogOptionsParsed => { if (!isLogOptions(config)) { throw new Error(`Logging levels were not valid. Must be one of: 'silent', 'fatal', 'error', 'warn', 'info', 'verbose', 'debug', -- 'file' may be false.`) } @@ -55,7 +63,7 @@ export const parseLogOptions = (config: LogOptions = {}, baseDir?: string): LogO if (file.level === false) { fileObj = {level: false}; } else { - const path = typeof file.path === 'function' ? file.path : getLogPath(file.path, baseDir); + const path = typeof file.path === 'function' ? file.path : getLogPath(file.path, options); fileObj = { level: configLevel || defaultLevel, ...file, @@ -67,7 +75,7 @@ export const parseLogOptions = (config: LogOptions = {}, baseDir?: string): LogO } else { fileObj = { level: file, - path: getLogPath(undefined, baseDir) + path: getLogPath(undefined, options) }; } @@ -83,14 +91,18 @@ export const parseLogOptions = (config: LogOptions = {}, baseDir?: string): LogO }; } -export const getLogPath = (path?: string, baseDir = projectDir) => { +export const getLogPath = (path?: string, options: FileLogPathOptions = {}) => { + const { + logBaseDir: baseDir = projectDir, + logDefaultPath = logPathRelative + } = options; let pathVal: string; if (path !== undefined) { pathVal = path; } else if (typeof process.env.LOG_PATH === 'string') { pathVal = process.env.LOG_PATH; } else { - pathVal = logPathRelative; + pathVal = logDefaultPath; } if (isAbsolute(pathVal)) { diff --git a/src/loggers.ts b/src/loggers.ts index 41bbd0d..29b87dc 100644 --- a/src/loggers.ts +++ b/src/loggers.ts @@ -98,10 +98,9 @@ export const loggerApp = (config: LogOptions | object = {}, extras?: LoggerAppEx pretty = {}, destinations = [], pino, - logBaseDir } = extras || {}; - const options = parseLogOptions(config, logBaseDir); + const options = parseLogOptions(config, extras); const streams: LogLevelStreamEntry[] = [ buildDestinationStdout(options.console, pretty), ...destinations @@ -135,10 +134,9 @@ export const loggerAppRolling = async (config: LogOptions | object = {}, extras? pretty = {}, destinations = [], pino, - logBaseDir } = extras || {}; - const options = parseLogOptions(config, logBaseDir); + const options = parseLogOptions(config, extras); const streams: LogLevelStreamEntry[] = [ buildDestinationStdout(options.console, pretty), ...destinations diff --git a/src/types.ts b/src/types.ts index 5ae0f27..ad6585e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -222,10 +222,31 @@ export type JsonPrettyDestination = StreamDestination & { export type LogOptionsParsed = Omit, 'file'> & { file: FileLogOptionsParsed } +export interface FileLogPathOptions { + /** + * The base path to use when parsing file logging options. + * + * @see FileOptions + * + * @default 'CWD' + * */ + logBaseDir?: string + /** + * The default path to use when parsing file logging options. + * + * If this path is relative it is joined with `logBaseDir` + * + * @see FileOptions + * + * @default './logs/app.log' + * */ + logDefaultPath?: string +} + /** * Additional settings and Pino Transports to apply to the returned Logger. * */ -export interface LoggerAppExtras { +export interface LoggerAppExtras extends FileLogPathOptions { /** * Additional pino-pretty options that are applied to the built-in console/log streams * */ @@ -238,15 +259,6 @@ export interface LoggerAppExtras { * Additional [Pino Log options](https://getpino.io/#/docs/api?id=options) that are passed to `pino()` on logger creation * */ pino?: PinoLoggerOptions - - /** - * The base path to use when parsing file logging options. - * - * @see FileOptions - * - * @default 'CWD' - * */ - logBaseDir?: string } /** diff --git a/tests/index.test.ts b/tests/index.test.ts index a35e06c..4b5d839 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -11,7 +11,7 @@ import {PassThrough, Transform} from "node:stream"; import chai, {expect} from "chai"; import {pEvent} from 'p-event'; import {sleep} from "../src/util.js"; -import {LogData, LOG_LEVEL_NAMES, PRETTY_ISO8601} from "../src/types.js"; +import { LogData, LOG_LEVEL_NAMES, PRETTY_ISO8601, FileLogPathOptions } from "../src/types.js"; import withLocalTmpDir from 'with-local-tmp-dir'; import {readdirSync,} from 'node:fs'; import { @@ -29,7 +29,7 @@ const dateFormat = dateFormatDef as unknown as typeof dateFormatDef.default; const testConsoleLogger = (config?: object, colorize = false): [Logger, Transform, Transform] => { - const opts = parseLogOptions(config, process.cwd()); + const opts = parseLogOptions(config, {logBaseDir: process.cwd()}); const testStream = new PassThrough(); const rawStream = new PassThrough(); const logger = buildLogger('debug', [ @@ -50,7 +50,7 @@ const testConsoleLogger = (config?: object, colorize = false): [Logger, Transfor } const testObjectLogger = (config?: object, object?: boolean): [Logger, Transform, Transform] => { - const opts = parseLogOptions(config, process.cwd()); + const opts = parseLogOptions(config, {logBaseDir: process.cwd()}); const testStream = new PassThrough({objectMode: true}); const rawStream = new PassThrough(); const logger = buildLogger('debug', [ @@ -71,8 +71,12 @@ const testObjectLogger = (config?: object, object?: boolean): [Logger, Transform return [logger, testStream, rawStream]; } -const testFileRollingLogger = async (config?: object, logBaseDir = process.cwd()) => { - const opts = parseLogOptions(config, logBaseDir); +const testFileRollingLogger = async (config?: object, options: FileLogPathOptions = {}) => { + const fileOpts: FileLogPathOptions = { + logBaseDir: process.cwd(), + ...options + } + const opts = parseLogOptions(config, fileOpts); const { file: { level, @@ -95,8 +99,12 @@ const testFileRollingLogger = async (config?: object, logBaseDir = process.cwd() ]); }; -const testFileLogger = async (config?: object, logBaseDir = process.cwd()) => { - const opts = parseLogOptions(config, logBaseDir); +const testFileLogger = async (config?: object, options: FileLogPathOptions = {}) => { + const fileOpts: FileLogPathOptions = { + logBaseDir: process.cwd(), + ...options + } + const opts = parseLogOptions(config, fileOpts); const { file: { path: logPath, @@ -118,8 +126,8 @@ const testFileLogger = async (config?: object, logBaseDir = process.cwd()) => { }; const testRollingAppLogger = async (config: LogOptions | object = {}, extras: LoggerAppExtras = {}): Promise<[Logger, Transform, Transform]> => { - const {destinations = [], pretty, logBaseDir = process.cwd(), ...restExtras} = extras; - const opts = parseLogOptions(config, logBaseDir); + const {destinations = [], pretty, logBaseDir, ...restExtras} = extras; + const opts = parseLogOptions(config, {logBaseDir: process.cwd(), ...extras}); const testStream = new PassThrough(); const rawStream = new PassThrough(); const streams: LogLevelStreamEntry[] = [ @@ -136,14 +144,13 @@ const testRollingAppLogger = async (config: LogOptions | object = {}, extras: Lo stream: rawStream } ]; - const logger = await loggerAppRolling({...config, console: 'silent'}, {destinations: [...destinations, ...streams], pretty, logBaseDir, ...restExtras}); + const logger = await loggerAppRolling({...opts, console: 'silent'}, {destinations: [...destinations, ...streams], pretty, logBaseDir, ...restExtras}); return [logger, testStream, rawStream]; } const testAppLogger = (config: LogOptions | object = {}, extras: LoggerAppExtras = {}): [Logger, Transform, Transform] => { - const {destinations = [], pretty = {}, logBaseDir = process.cwd(), ...restExtras} = extras; - - const opts = parseLogOptions(config, logBaseDir); + const {destinations = [], pretty = {}, logBaseDir, ...restExtras} = extras; + const opts = parseLogOptions(config, {logBaseDir: process.cwd(), ...extras}); const testStream = new PassThrough(); const rawStream = new PassThrough(); @@ -163,7 +170,7 @@ const testAppLogger = (config: LogOptions | object = {}, extras: LoggerAppExtras stream: rawStream }, ]; - const logger = loggerApp({...config, console: 'silent'}, {destinations: [...destinations, ...streams], pretty, logBaseDir, ...restExtras}); + const logger = loggerApp({...opts, console: 'silent'}, {destinations: [...destinations, ...streams], pretty, logBaseDir, ...restExtras}); return [logger, testStream, rawStream]; } @@ -218,6 +225,79 @@ describe('Config Parsing', function () { expect(config.console).eq('warn') expect(config.file.level).eq('error') }); + + describe('Log File Options', function() { + + it(`uses CWD for base path when none is specified`, async function () { + const config = parseLogOptions({ + level: 'debug' + }); + expect(config.file.path).includes(process.cwd()) + }); + + it(`uses user-specified base path when specified`, async function () { + await withLocalTmpDir(async () => { + const config = parseLogOptions({ + level: 'debug' + }, {logBaseDir: process.cwd()}); + expect(config.file.path).includes(process.cwd()) + }, {unsafeCleanup: false}); + }); + + it(`uses 'logs/app.log' for default log path when none is specified`, async function () { + const config = parseLogOptions({ + level: 'debug' + }); + expect(config.file.path).includes('logs/app.log') + }); + + it(`uses user-specified default log path when none is specified`, async function () { + const config = parseLogOptions({ + level: 'debug', + }, {logDefaultPath: 'logs/myApp.log'}); + expect(config.file.path).includes('logs/myApp.log') + }); + + it(`uses config-specified absolute path`, async function () { + const specificPath = '/my/absolute/path/app.log'; + const config = parseLogOptions({ + level: 'debug', + file: { + path: specificPath + } + }); + expect(config.file.path).eq(specificPath) + }); + + it(`uses config-specified relative path with base path`, async function () { + const relativePath = './my/relative/path/app.log'; + const config = parseLogOptions({ + level: 'debug', + file: { + path: relativePath + } + }); + expect(config.file.path).eq(path.join(process.cwd(), relativePath)); + + const configWithDefault = parseLogOptions({ + level: 'debug', + file: { + path: relativePath + } + }, {logDefaultPath: 'logs/myApp.log'}); + expect(configWithDefault.file.path).eq(path.join(process.cwd(), relativePath)); + }); + + it(`uses ENV-specified path`, async function () { + const specificPath = '/my/absolute/path/app.log'; + process.env.LOG_PATH = specificPath; + const config = parseLogOptions({ + level: 'debug', + }); + delete process.env.LOG_PATH; + expect(config.file.path).eq(specificPath) + }); + }); }) describe('Transports', function () { @@ -464,6 +544,22 @@ describe('Transports', function () { expect(paths.length).eq(1); }, {unsafeCleanup: true}); }); + + it('It writes to file with a different default log path', async function () { + await withLocalTmpDir(async () => { + const [logger, testStream, rawStream] = testAppLogger({file: 'debug'}, {logDefaultPath: './myApp.log'}); + const race = Promise.race([ + pEvent(testStream, 'data'), + sleep(10) + ]) as Promise; + logger.debug('Test'); + await sleep(20); + const res = await race; + const paths = readdirSync('.'); + expect(paths.length).eq(1); + expect(paths[0]).includes('myApp.log'); + }, {unsafeCleanup: true}); + }); }); });