[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: Enable setting default log path with extras

FoxxMD (Mar 28, 2024, 12:18 PM EDT) d38e3281 8fcf1168

+152 -34
+18 -6
src/funcs.ts
··· 1 1 import process from "process"; 2 - import {FileLogOptions, FileLogOptionsParsed, LOG_LEVEL_NAMES, LogLevel, LogOptions, LogOptionsParsed} from "./types.js"; 2 + import { 3 + FileLogOptions, 4 + FileLogOptionsParsed, 5 + FileLogPathOptions, 6 + LOG_LEVEL_NAMES, 7 + LogLevel, 8 + LogOptions, 9 + LogOptionsParsed 10 + } from "./types.js"; 3 11 import {projectDir, logPathRelative} from "./constants.js"; 4 12 import {isAbsolute, resolve} from 'node:path'; 5 13 ··· 36 44 /** 37 45 * Takes an object and parses it into a fully-populated LogOptions object based on opinionated defaults 38 46 * */ 39 - export const parseLogOptions = (config: LogOptions = {}, baseDir?: string): LogOptionsParsed => { 47 + export const parseLogOptions = (config: LogOptions = {}, options?: FileLogPathOptions): LogOptionsParsed => { 40 48 if (!isLogOptions(config)) { 41 49 throw new Error(`Logging levels were not valid. Must be one of: 'silent', 'fatal', 'error', 'warn', 'info', 'verbose', 'debug', -- 'file' may be false.`) 42 50 } ··· 55 63 if (file.level === false) { 56 64 fileObj = {level: false}; 57 65 } else { 58 - const path = typeof file.path === 'function' ? file.path : getLogPath(file.path, baseDir); 66 + const path = typeof file.path === 'function' ? file.path : getLogPath(file.path, options); 59 67 fileObj = { 60 68 level: configLevel || defaultLevel, 61 69 ...file, ··· 67 75 } else { 68 76 fileObj = { 69 77 level: file, 70 - path: getLogPath(undefined, baseDir) 78 + path: getLogPath(undefined, options) 71 79 }; 72 80 } 73 81 ··· 83 91 }; 84 92 } 85 93 86 - export const getLogPath = (path?: string, baseDir = projectDir) => { 94 + export const getLogPath = (path?: string, options: FileLogPathOptions = {}) => { 95 + const { 96 + logBaseDir: baseDir = projectDir, 97 + logDefaultPath = logPathRelative 98 + } = options; 87 99 let pathVal: string; 88 100 if (path !== undefined) { 89 101 pathVal = path; 90 102 } else if (typeof process.env.LOG_PATH === 'string') { 91 103 pathVal = process.env.LOG_PATH; 92 104 } else { 93 - pathVal = logPathRelative; 105 + pathVal = logDefaultPath; 94 106 } 95 107 96 108 if (isAbsolute(pathVal)) {
+2 -4
src/loggers.ts
··· 98 98 pretty = {}, 99 99 destinations = [], 100 100 pino, 101 - logBaseDir 102 101 } = extras || {}; 103 102 104 - const options = parseLogOptions(config, logBaseDir); 103 + const options = parseLogOptions(config, extras); 105 104 const streams: LogLevelStreamEntry[] = [ 106 105 buildDestinationStdout(options.console, pretty), 107 106 ...destinations ··· 135 134 pretty = {}, 136 135 destinations = [], 137 136 pino, 138 - logBaseDir 139 137 } = extras || {}; 140 138 141 - const options = parseLogOptions(config, logBaseDir); 139 + const options = parseLogOptions(config, extras); 142 140 const streams: LogLevelStreamEntry[] = [ 143 141 buildDestinationStdout(options.console, pretty), 144 142 ...destinations
+22 -10
src/types.ts
··· 222 222 223 223 export type LogOptionsParsed = Omit<Required<LogOptions>, 'file'> & { file: FileLogOptionsParsed } 224 224 225 + export interface FileLogPathOptions { 226 + /** 227 + * The base path to use when parsing file logging options. 228 + * 229 + * @see FileOptions 230 + * 231 + * @default 'CWD' 232 + * */ 233 + logBaseDir?: string 234 + /** 235 + * The default path to use when parsing file logging options. 236 + * 237 + * If this path is relative it is joined with `logBaseDir` 238 + * 239 + * @see FileOptions 240 + * 241 + * @default './logs/app.log' 242 + * */ 243 + logDefaultPath?: string 244 + } 245 + 225 246 /** 226 247 * Additional settings and Pino Transports to apply to the returned Logger. 227 248 * */ 228 - export interface LoggerAppExtras { 249 + export interface LoggerAppExtras extends FileLogPathOptions { 229 250 /** 230 251 * Additional pino-pretty options that are applied to the built-in console/log streams 231 252 * */ ··· 238 259 * Additional [Pino Log options](https://getpino.io/#/docs/api?id=options) that are passed to `pino()` on logger creation 239 260 * */ 240 261 pino?: PinoLoggerOptions 241 - 242 - /** 243 - * The base path to use when parsing file logging options. 244 - * 245 - * @see FileOptions 246 - * 247 - * @default 'CWD' 248 - * */ 249 - logBaseDir?: string 250 262 } 251 263 252 264 /**
+110 -14
tests/index.test.ts
··· 11 11 import chai, {expect} from "chai"; 12 12 import {pEvent} from 'p-event'; 13 13 import {sleep} from "../src/util.js"; 14 - import {LogData, LOG_LEVEL_NAMES, PRETTY_ISO8601} from "../src/types.js"; 14 + import { LogData, LOG_LEVEL_NAMES, PRETTY_ISO8601, FileLogPathOptions } from "../src/types.js"; 15 15 import withLocalTmpDir from 'with-local-tmp-dir'; 16 16 import {readdirSync,} from 'node:fs'; 17 17 import { ··· 29 29 30 30 31 31 const testConsoleLogger = (config?: object, colorize = false): [Logger, Transform, Transform] => { 32 - const opts = parseLogOptions(config, process.cwd()); 32 + const opts = parseLogOptions(config, {logBaseDir: process.cwd()}); 33 33 const testStream = new PassThrough(); 34 34 const rawStream = new PassThrough(); 35 35 const logger = buildLogger('debug', [ ··· 50 50 } 51 51 52 52 const testObjectLogger = (config?: object, object?: boolean): [Logger, Transform, Transform] => { 53 - const opts = parseLogOptions(config, process.cwd()); 53 + const opts = parseLogOptions(config, {logBaseDir: process.cwd()}); 54 54 const testStream = new PassThrough({objectMode: true}); 55 55 const rawStream = new PassThrough(); 56 56 const logger = buildLogger('debug', [ ··· 71 71 return [logger, testStream, rawStream]; 72 72 } 73 73 74 - const testFileRollingLogger = async (config?: object, logBaseDir = process.cwd()) => { 75 - const opts = parseLogOptions(config, logBaseDir); 74 + const testFileRollingLogger = async (config?: object, options: FileLogPathOptions = {}) => { 75 + const fileOpts: FileLogPathOptions = { 76 + logBaseDir: process.cwd(), 77 + ...options 78 + } 79 + const opts = parseLogOptions(config, fileOpts); 76 80 const { 77 81 file: { 78 82 level, ··· 95 99 ]); 96 100 }; 97 101 98 - const testFileLogger = async (config?: object, logBaseDir = process.cwd()) => { 99 - const opts = parseLogOptions(config, logBaseDir); 102 + const testFileLogger = async (config?: object, options: FileLogPathOptions = {}) => { 103 + const fileOpts: FileLogPathOptions = { 104 + logBaseDir: process.cwd(), 105 + ...options 106 + } 107 + const opts = parseLogOptions(config, fileOpts); 100 108 const { 101 109 file: { 102 110 path: logPath, ··· 118 126 }; 119 127 120 128 const testRollingAppLogger = async (config: LogOptions | object = {}, extras: LoggerAppExtras = {}): Promise<[Logger, Transform, Transform]> => { 121 - const {destinations = [], pretty, logBaseDir = process.cwd(), ...restExtras} = extras; 122 - const opts = parseLogOptions(config, logBaseDir); 129 + const {destinations = [], pretty, logBaseDir, ...restExtras} = extras; 130 + const opts = parseLogOptions(config, {logBaseDir: process.cwd(), ...extras}); 123 131 const testStream = new PassThrough(); 124 132 const rawStream = new PassThrough(); 125 133 const streams: LogLevelStreamEntry[] = [ ··· 136 144 stream: rawStream 137 145 } 138 146 ]; 139 - const logger = await loggerAppRolling({...config, console: 'silent'}, {destinations: [...destinations, ...streams], pretty, logBaseDir, ...restExtras}); 147 + const logger = await loggerAppRolling({...opts, console: 'silent'}, {destinations: [...destinations, ...streams], pretty, logBaseDir, ...restExtras}); 140 148 return [logger, testStream, rawStream]; 141 149 } 142 150 143 151 const testAppLogger = (config: LogOptions | object = {}, extras: LoggerAppExtras = {}): [Logger, Transform, Transform] => { 144 - const {destinations = [], pretty = {}, logBaseDir = process.cwd(), ...restExtras} = extras; 145 - 146 - const opts = parseLogOptions(config, logBaseDir); 152 + const {destinations = [], pretty = {}, logBaseDir, ...restExtras} = extras; 153 + const opts = parseLogOptions(config, {logBaseDir: process.cwd(), ...extras}); 147 154 const testStream = new PassThrough(); 148 155 const rawStream = new PassThrough(); 149 156 ··· 163 170 stream: rawStream 164 171 }, 165 172 ]; 166 - const logger = loggerApp({...config, console: 'silent'}, {destinations: [...destinations, ...streams], pretty, logBaseDir, ...restExtras}); 173 + const logger = loggerApp({...opts, console: 'silent'}, {destinations: [...destinations, ...streams], pretty, logBaseDir, ...restExtras}); 167 174 return [logger, testStream, rawStream]; 168 175 } 169 176 ··· 218 225 expect(config.console).eq('warn') 219 226 expect(config.file.level).eq('error') 220 227 }); 228 + 229 + describe('Log File Options', function() { 230 + 231 + it(`uses CWD for base path when none is specified`, async function () { 232 + const config = parseLogOptions({ 233 + level: 'debug' 234 + }); 235 + expect(config.file.path).includes(process.cwd()) 236 + }); 237 + 238 + it(`uses user-specified base path when specified`, async function () { 239 + await withLocalTmpDir(async () => { 240 + const config = parseLogOptions({ 241 + level: 'debug' 242 + }, {logBaseDir: process.cwd()}); 243 + expect(config.file.path).includes(process.cwd()) 244 + }, {unsafeCleanup: false}); 245 + }); 246 + 247 + it(`uses 'logs/app.log' for default log path when none is specified`, async function () { 248 + const config = parseLogOptions({ 249 + level: 'debug' 250 + }); 251 + expect(config.file.path).includes('logs/app.log') 252 + }); 253 + 254 + it(`uses user-specified default log path when none is specified`, async function () { 255 + const config = parseLogOptions({ 256 + level: 'debug', 257 + }, {logDefaultPath: 'logs/myApp.log'}); 258 + expect(config.file.path).includes('logs/myApp.log') 259 + }); 260 + 261 + it(`uses config-specified absolute path`, async function () { 262 + const specificPath = '/my/absolute/path/app.log'; 263 + const config = parseLogOptions({ 264 + level: 'debug', 265 + file: { 266 + path: specificPath 267 + } 268 + }); 269 + expect(config.file.path).eq(specificPath) 270 + }); 271 + 272 + it(`uses config-specified relative path with base path`, async function () { 273 + const relativePath = './my/relative/path/app.log'; 274 + const config = parseLogOptions({ 275 + level: 'debug', 276 + file: { 277 + path: relativePath 278 + } 279 + }); 280 + expect(config.file.path).eq(path.join(process.cwd(), relativePath)); 281 + 282 + const configWithDefault = parseLogOptions({ 283 + level: 'debug', 284 + file: { 285 + path: relativePath 286 + } 287 + }, {logDefaultPath: 'logs/myApp.log'}); 288 + expect(configWithDefault.file.path).eq(path.join(process.cwd(), relativePath)); 289 + }); 290 + 291 + it(`uses ENV-specified path`, async function () { 292 + const specificPath = '/my/absolute/path/app.log'; 293 + process.env.LOG_PATH = specificPath; 294 + const config = parseLogOptions({ 295 + level: 'debug', 296 + }); 297 + delete process.env.LOG_PATH; 298 + expect(config.file.path).eq(specificPath) 299 + }); 300 + }); 221 301 }) 222 302 223 303 describe('Transports', function () { ··· 462 542 const res = await race; 463 543 const paths = readdirSync('./config/logs'); 464 544 expect(paths.length).eq(1); 545 + }, {unsafeCleanup: true}); 546 + }); 547 + 548 + it('It writes to file with a different default log path', async function () { 549 + await withLocalTmpDir(async () => { 550 + const [logger, testStream, rawStream] = testAppLogger({file: 'debug'}, {logDefaultPath: './myApp.log'}); 551 + const race = Promise.race([ 552 + pEvent(testStream, 'data'), 553 + sleep(10) 554 + ]) as Promise<Buffer>; 555 + logger.debug('Test'); 556 + await sleep(20); 557 + const res = await race; 558 + const paths = readdirSync('.'); 559 + expect(paths.length).eq(1); 560 + expect(paths[0]).includes('myApp.log'); 465 561 }, {unsafeCleanup: true}); 466 562 }); 467 563 });