Skip to content

Commit

Permalink
feat: Add more options for file logging
Browse files Browse the repository at this point in the history
* Add pino-roll options to file options
* Implement file datetime formatting based on frequency option
* Consolidate filePath into path on a new `file` option object
  • Loading branch information
FoxxMD committed Mar 7, 2024
1 parent a153ff3 commit e9b40db
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 81 deletions.
77 changes: 65 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,19 +92,9 @@ interface LogOptions {
* */
console?: LogLevel
/**
* Specify the minimum log level to output to rotating files. If `false` no log files will be created.
* Specify the minimum log level to output for files or a log file options object. If `false` no log files will be created.
* */
file?: LogLevel | false
/**
* The full path and filename to use for log files
*
* If using rolling files the filename will be appended with `.N` (a number) BEFORE the extension based on rolling status
*
* May also be specified using env LOG_PATH or a function that returns a string
*
* @default 'CWD/logs/app.log
* */
filePath?: string | (() => string)
file?: LogLevel | false | FileLogOptions
}
```
Available `LogLevel` levels, from lowest to highest:
Expand All @@ -129,6 +119,69 @@ const logger = loggerApp({
});
```

### File Options

`file` in `LogOptions` may be an object that specifies more behavior log files.

<details>

<summary>File Options</summary>

```ts
export interface FileOptions {
/**
* The path and filename to use for log files.
*
* If using rolling files the filename will be appended with `.N` (a number) BEFORE the extension based on rolling status.
*
* May also be specified using env LOG_PATH or a function that returns a string.
*
* If path is relative the absolute path will be derived from the current working directory.
*
* @default 'CWD/logs/app.log'
* */
path?: string | (() => string)
/**
* For rolling log files
*
* When
* * value passed to rolling destination is a string (`filePath` from LogOptions is a string)
* * `frequency` is defined
*
* This determines the format of the datetime inserted into the log file name:
*
* * `unix` - unix epoch timestamp in milliseconds
* * `iso` - Full [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) datetime IE '2024-03-07T20:11:34Z'
* * `auto`
* * When frequency is `daily` only inserts date IE YYYY-MM-DD
* * Otherwise inserts full ISO8601 datetime
*
* @default 'auto'
* */
timestamp?: 'unix' | 'iso' | 'auto'
/**
* The maximum size of a given rolling log file.
*
* Can be combined with frequency. Use k, m and g to express values in KB, MB or GB.
*
* Numerical values will be considered as MB.
* */
size?: number | string
/**
* The amount of time a given rolling log file is used. Can be combined with size.
*
* Use `daily` or `hourly` to rotate file every day (or every hour). Existing file within the current day (or hour) will be re-used.
*
* Numerical values will be considered as a number of milliseconds. Using a numerical value will always create a new file upon startup.
*
* @default 'daily'
* */
frequency?: 'daily' | 'hourly' | number
}
```

</details>

## Usage

### Child Loggers
Expand Down
78 changes: 54 additions & 24 deletions src/destinations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import pinoRoll from 'pino-roll';
import {LogLevelStreamEntry, LogLevel, LogOptions, StreamDestination, FileDestination} from "./types.js";
import {
LogLevelStreamEntry,
LogLevel,
StreamDestination,
FileDestination,
} from "./types.js";
import {DestinationStream, pino, destination} from "pino";
import prettyDef, {PrettyOptions} from "pino-pretty";
import {prettyConsole, prettyFile} from "./pretty.js";
Expand All @@ -12,35 +17,60 @@ export const buildDestinationRollingFile = async (level: LogLevel | false, optio
if (level === false) {
return undefined;
}
const {path: logPath, ...rest} = options;
const {
path: logPath,
size,
frequency,
timestamp = 'auto',
...rest
} = options;

if(size === undefined && frequency === undefined) {
throw new Error(`For rolling files must specify at least one of 'frequency' , 'size'`);
}

const testPath = typeof logPath === 'function' ? logPath() : logPath;

try {
const testPath = typeof logPath === 'function' ? logPath() : logPath;
fileOrDirectoryIsWriteable(testPath);
} catch (e: any) {
throw new ErrorWithCause<Error>('Cannot write logs to rotating file due to an error while trying to access the specified logging directory', {cause: e as Error});
}

const pInfo = path.parse(testPath);
let filePath: string | (() => string);
if(typeof logPath === 'string') {
filePath = () => path.resolve(pInfo.dir, `${pInfo.name}-${new Date().toISOString().split('T')[0]}`)
} else {
filePath = logPath;
const pInfo = path.parse(testPath);
let filePath: string | (() => string);
if(typeof logPath === 'string' && frequency !== undefined) {
filePath = () => {
let dtStr: string;
switch(timestamp) {
case 'unix':
dtStr = Date.now().toString();
break;
case 'iso':
dtStr = new Date().toISOString();
break;
case 'auto':
dtStr = frequency === 'daily' ? new Date().toISOString().split('T')[0] : new Date().toISOString();
break;
}
return path.resolve(pInfo.dir, `${pInfo.name}-${dtStr}`)
}
const rollingDest = await pRoll({
file: filePath,
size: 10,
frequency: 'daily',
extension: pInfo.ext,
mkdir: true,
sync: false,
});

return {
level: level,
stream: prettyDef.default({...prettyFile, ...rest, destination: rollingDest})
};
} catch (e: any) {
throw new ErrorWithCause<Error>('WILL NOT write logs to rotating file due to an error while trying to access the specified logging directory', {cause: e as Error});
} else {
filePath = path.resolve(pInfo.dir, pInfo.name);
}
const rollingDest = await pRoll({
file: filePath,
size,
frequency,
extension: pInfo.ext,
mkdir: true,
sync: false,
});

return {
level: level,
stream: prettyDef.default({...prettyFile, ...rest, destination: rollingDest})
};
}

export const buildDestinationFile = (level: LogLevel | false, options: FileDestination): LogLevelStreamEntry | undefined => {
Expand Down
35 changes: 30 additions & 5 deletions src/funcs.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import process from "process";
import {isLogOptions, LogLevel, LogOptions} from "./types.js";
import {FileLogOptions, FileLogOptionsParsed, isLogOptions, LogLevel, LogOptions, LogOptionsParsed} from "./types.js";
import {logPath, projectDir} from "./constants.js";
import {isAbsolute, resolve} from 'node:path';
import {MarkRequired} from "ts-essentials";

export const parseLogOptions = (config: LogOptions = {}): Required<LogOptions> => {
export const parseLogOptions = (config: LogOptions = {}): 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.`)
}
Expand All @@ -15,14 +16,38 @@ export const parseLogOptions = (config: LogOptions = {}): Required<LogOptions> =
level = configLevel || defaultLevel,
file = configLevel || defaultLevel,
console = configLevel || 'debug',
filePath
} = config;

let fileObj: FileLogOptionsParsed;
if (typeof file === 'object') {
if (file.level === false) {
fileObj = {level: false};
} else {
const path = typeof file.path === 'function' ? file.path : getLogPath(file.path);
fileObj = {
level: configLevel || defaultLevel,
...file,
path
}
}
} else if (file === false) {
fileObj = {level: false};
} else {
fileObj = {
level: file,
path: getLogPath()
};
}

if(fileObj.level !== false && fileObj.frequency === undefined && fileObj.size === undefined) {
// set default rolling log behavior
fileObj.frequency = 'daily';
}

return {
level,
file: file as LogLevel | false,
file: fileObj,
console,
filePath: typeof filePath === 'function' ? filePath : getLogPath(filePath)
};
}

Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
Logger,
LogLevelStreamEntry,
LogOptions,
FileLogOptions,
isLogOptions,
LogData,
LogLevel,
Expand All @@ -15,6 +16,7 @@ import {
import {childLogger, loggerApp, loggerDebug, loggerTest, loggerAppRolling} from './loggers.js';

export type {
FileLogOptions,
Logger,
LogLevelStreamEntry,
LogOptions,
Expand Down
31 changes: 17 additions & 14 deletions src/loggers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import {parseLogOptions} from "./funcs.js";
import {Logger, LogLevel, LogLevelStreamEntry, LogOptions} from "./types.js";
import {buildDestinationFile, buildDestinationRollingFile, buildDestinationStdout} from "./destinations.js";
import {pino} from "pino";
import {logPath} from "./constants.js";
import path from "path";

export const buildLogger = (defaultLevel: LogLevel, streams: LogLevelStreamEntry[]): Logger => {
const plogger = pino({
Expand Down Expand Up @@ -62,14 +60,17 @@ export const loggerApp = (config: LogOptions | object = {}) => {
const streams: LogLevelStreamEntry[] = [buildDestinationStdout(options.console)];

let error: Error;
try {
const file = buildDestinationFile(options.file, {path: options.filePath, append: true});
if (file !== undefined) {
streams.push(file);
if(options.file.level !== false) {
try {
const file = buildDestinationFile(options.file.level, {...options.file, append: true});
if (file !== undefined) {
streams.push(file);
}
} catch (e) {
error = e;
}
} catch (e) {
error = e;
}

const logger = buildLogger('debug' as LogLevel, streams);
if (error !== undefined) {
logger.warn(error);
Expand All @@ -82,13 +83,15 @@ export const loggerAppRolling = async (config: LogOptions | object = {}) => {
const streams: LogLevelStreamEntry[] = [buildDestinationStdout(options.console)];

let error: Error;
try {
const file = await buildDestinationRollingFile(options.file, {path: logPath});
if (file !== undefined) {
streams.push(file);
if(options.file.level !== false) {
try {
const file = await buildDestinationRollingFile(options.file.level, options.file);
if (file !== undefined) {
streams.push(file);
}
} catch (e) {
error = e;
}
} catch (e) {
error = e;
}
const logger = buildLogger('debug' as LogLevel, streams);
if (error !== undefined) {
Expand Down
Loading

0 comments on commit e9b40db

Please sign in to comment.