Select the types of activity you want to include in your feed.
feat: Add more options for file logging
* 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
···9292 * */
9393 console?: LogLevel
9494 /**
9595- * Specify the minimum log level to output to rotating files. If `false` no log files will be created.
9696- * */
9797- file?: LogLevel | false
9898- /**
9999- * The full path and filename to use for log files
100100- *
101101- * If using rolling files the filename will be appended with `.N` (a number) BEFORE the extension based on rolling status
102102- *
103103- * May also be specified using env LOG_PATH or a function that returns a string
104104- *
105105- * @default 'CWD/logs/app.log
9595+ * Specify the minimum log level to output for files or a log file options object. If `false` no log files will be created.
10696 * */
107107- filePath?: string | (() => string)
9797+ file?: LogLevel | false | FileLogOptions
10898}
10999```
110100Available `LogLevel` levels, from lowest to highest:
···128118 file: 'warn' // file will log `warn` and higher
129119});
130120```
121121+122122+### File Options
123123+124124+`file` in `LogOptions` may be an object that specifies more behavior log files.
125125+126126+<details>
127127+128128+<summary>File Options</summary>
129129+130130+```ts
131131+export interface FileOptions {
132132+ /**
133133+ * The path and filename to use for log files.
134134+ *
135135+ * If using rolling files the filename will be appended with `.N` (a number) BEFORE the extension based on rolling status.
136136+ *
137137+ * May also be specified using env LOG_PATH or a function that returns a string.
138138+ *
139139+ * If path is relative the absolute path will be derived from the current working directory.
140140+ *
141141+ * @default 'CWD/logs/app.log'
142142+ * */
143143+ path?: string | (() => string)
144144+ /**
145145+ * For rolling log files
146146+ *
147147+ * When
148148+ * * value passed to rolling destination is a string (`filePath` from LogOptions is a string)
149149+ * * `frequency` is defined
150150+ *
151151+ * This determines the format of the datetime inserted into the log file name:
152152+ *
153153+ * * `unix` - unix epoch timestamp in milliseconds
154154+ * * `iso` - Full [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) datetime IE '2024-03-07T20:11:34Z'
155155+ * * `auto`
156156+ * * When frequency is `daily` only inserts date IE YYYY-MM-DD
157157+ * * Otherwise inserts full ISO8601 datetime
158158+ *
159159+ * @default 'auto'
160160+ * */
161161+ timestamp?: 'unix' | 'iso' | 'auto'
162162+ /**
163163+ * The maximum size of a given rolling log file.
164164+ *
165165+ * Can be combined with frequency. Use k, m and g to express values in KB, MB or GB.
166166+ *
167167+ * Numerical values will be considered as MB.
168168+ * */
169169+ size?: number | string
170170+ /**
171171+ * The amount of time a given rolling log file is used. Can be combined with size.
172172+ *
173173+ * Use `daily` or `hourly` to rotate file every day (or every hour). Existing file within the current day (or hour) will be re-used.
174174+ *
175175+ * Numerical values will be considered as a number of milliseconds. Using a numerical value will always create a new file upon startup.
176176+ *
177177+ * @default 'daily'
178178+ * */
179179+ frequency?: 'daily' | 'hourly' | number
180180+}
181181+```
182182+183183+</details>
131184132185## Usage
133186
+54-24
src/destinations.ts
···11import pinoRoll from 'pino-roll';
22-import {LogLevelStreamEntry, LogLevel, LogOptions, StreamDestination, FileDestination} from "./types.js";
22+import {
33+ LogLevelStreamEntry,
44+ LogLevel,
55+ StreamDestination,
66+ FileDestination,
77+} from "./types.js";
38import {DestinationStream, pino, destination} from "pino";
49import prettyDef, {PrettyOptions} from "pino-pretty";
510import {prettyConsole, prettyFile} from "./pretty.js";
···1217 if (level === false) {
1318 return undefined;
1419 }
1515- const {path: logPath, ...rest} = options;
2020+ const {
2121+ path: logPath,
2222+ size,
2323+ frequency,
2424+ timestamp = 'auto',
2525+ ...rest
2626+ } = options;
2727+2828+ if(size === undefined && frequency === undefined) {
2929+ throw new Error(`For rolling files must specify at least one of 'frequency' , 'size'`);
3030+ }
3131+3232+ const testPath = typeof logPath === 'function' ? logPath() : logPath;
16331734 try {
1818- const testPath = typeof logPath === 'function' ? logPath() : logPath;
1935 fileOrDirectoryIsWriteable(testPath);
3636+ } catch (e: any) {
3737+ 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});
3838+ }
20392121- const pInfo = path.parse(testPath);
2222- let filePath: string | (() => string);
2323- if(typeof logPath === 'string') {
2424- filePath = () => path.resolve(pInfo.dir, `${pInfo.name}-${new Date().toISOString().split('T')[0]}`)
2525- } else {
2626- filePath = logPath;
4040+ const pInfo = path.parse(testPath);
4141+ let filePath: string | (() => string);
4242+ if(typeof logPath === 'string' && frequency !== undefined) {
4343+ filePath = () => {
4444+ let dtStr: string;
4545+ switch(timestamp) {
4646+ case 'unix':
4747+ dtStr = Date.now().toString();
4848+ break;
4949+ case 'iso':
5050+ dtStr = new Date().toISOString();
5151+ break;
5252+ case 'auto':
5353+ dtStr = frequency === 'daily' ? new Date().toISOString().split('T')[0] : new Date().toISOString();
5454+ break;
5555+ }
5656+ return path.resolve(pInfo.dir, `${pInfo.name}-${dtStr}`)
2757 }
2828- const rollingDest = await pRoll({
2929- file: filePath,
3030- size: 10,
3131- frequency: 'daily',
3232- extension: pInfo.ext,
3333- mkdir: true,
3434- sync: false,
3535- });
5858+ } else {
5959+ filePath = path.resolve(pInfo.dir, pInfo.name);
6060+ }
6161+ const rollingDest = await pRoll({
6262+ file: filePath,
6363+ size,
6464+ frequency,
6565+ extension: pInfo.ext,
6666+ mkdir: true,
6767+ sync: false,
6868+ });
36693737- return {
3838- level: level,
3939- stream: prettyDef.default({...prettyFile, ...rest, destination: rollingDest})
4040- };
4141- } catch (e: any) {
4242- 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});
4343- }
7070+ return {
7171+ level: level,
7272+ stream: prettyDef.default({...prettyFile, ...rest, destination: rollingDest})
7373+ };
4474}
45754676export const buildDestinationFile = (level: LogLevel | false, options: FileDestination): LogLevelStreamEntry | undefined => {
···11import {DestinationStream, Level, Logger as PinoLogger, StreamEntry} from 'pino';
22import {ErrorWithCause} from "pony-cause";
33import {PrettyOptions} from "pino-pretty";
44+import {MarkRequired} from "ts-essentials";
4556export type LogLevel = typeof LOG_LEVELS[number];
67export const LOG_LEVELS= ['silent', 'fatal', 'error', 'warn', 'info', 'log', 'verbose', 'debug'] as const;
···1516 * */
1617 level?: LogLevel
1718 /**
1818- * Specify the minimum log level to output to rotating files. If `false` no log files will be created.
1919- * */
2020- file?: LogLevel | false
2121- /**
2222- * The full path and filename to use for log files
2323- *
2424- * If using rolling files the filename will be appended with `.N` (a number) BEFORE the extension based on rolling status
2525- *
2626- * May also be specified using env LOG_PATH or a function that returns a string
2727- *
2828- * @default 'CWD/logs/app.log'
1919+ * Specify the minimum log level to output to rotating files or file output options. If `false` no log files will be created.
2920 * */
3030- filePath?: string | (() => string)
2121+ file?: LogLevel | false | FileLogOptions
3122 /**
3223 * Specify the minimum log level streamed to the console (or docker container)
3324 * */
···4031 return true;
4132 }
4233 const t = typeof val;
4343- if(key === 'filePath') {
4444- return t === 'string' || t === 'function';
3434+ if(key === 'file') {
3535+ if(t === 'object') {
3636+ return isFileLogOptions(t);
3737+ }
3838+ return t === 'string' || val === false;
4539 }
4646- if(t !== 'string' && t !== 'boolean') {
4040+ if(t !== 'string') {
4741 return false;
4842 }
4949- if (key !== 'file') {
5050- return LOG_LEVELS.includes(val.toLocaleLowerCase());
5151- }
5252- return val === false || LOG_LEVELS.includes(val.toLocaleLowerCase());
4343+ return LOG_LEVELS.includes(val.toLocaleLowerCase());
5344 });
5445}
5546···6960 msg: string | Error | ErrorWithCause
7061}
71627272-export type FileDestination = Omit<PrettyOptions, 'destination' | 'sync'> & {path: string | (() => string)};
6363+export interface PinoRollOptions {
6464+ /**
6565+ * The maximum size of a given rolling log file.
6666+ *
6767+ * Can be combined with frequency. Use k, m and g to express values in KB, MB or GB.
6868+ *
6969+ * Numerical values will be considered as MB.
7070+ *
7171+ * @default '10MB'
7272+ * */
7373+ size?: number | string
7474+ /**
7575+ * The amount of time a given rolling log file is used. Can be combined with size.
7676+ *
7777+ * Use `daily` or `hourly` to rotate file every day (or every hour). Existing file within the current day (or hour) will be re-used.
7878+ *
7979+ * Numerical values will be considered as a number of milliseconds. Using a numerical value will always create a new file upon startup.
8080+ *
8181+ * @default 'daily'
8282+ * */
8383+ frequency?: 'daily' | 'hourly' | number
8484+}
8585+8686+export interface RollOptions {
8787+ /**
8888+ * For rolling log files
8989+ *
9090+ * When
9191+ * * value passed to rolling destination is a string (`filePath` from LogOptions is a string)
9292+ * * `frequency` is defined
9393+ *
9494+ * This determines the format of the datetime inserted into the log file name:
9595+ *
9696+ * * `unix` - unix epoch timestamp in milliseconds
9797+ * * `iso` - Full [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) datetime IE '2024-03-07T20:11:34Z'
9898+ * * `auto`
9999+ * * When frequency is `daily` only inserts date IE YYYY-MM-DD
100100+ * * Otherwise inserts full ISO8601 datetime
101101+ *
102102+ * @default 'auto'
103103+ * */
104104+ timestamp?: 'unix' | 'iso' | 'auto'
105105+}
106106+107107+export interface FileOptions extends PinoRollOptions, RollOptions {
108108+ /**
109109+ * The path and filename to use for log files.
110110+ *
111111+ * If using rolling files the filename will be appended with `.N` (a number) BEFORE the extension based on rolling status.
112112+ *
113113+ * May also be specified using env LOG_PATH or a function that returns a string.
114114+ *
115115+ * If path is relative the absolute path will be derived from the current working directory.
116116+ *
117117+ * @default 'CWD/logs/app.log'
118118+ * */
119119+ path?: string | (() => string)
120120+}
121121+122122+export type FileOptionsParsed = MarkRequired<FileOptions, 'path'>;
123123+124124+export interface FileLogOptions extends FileOptions {
125125+ /**
126126+ * Specify the minimum log level to output to rotating files. If `false` no log files will be created.
127127+ * */
128128+ level?: LogLevel | false
129129+}
130130+131131+export interface FileLogOptionsStrong extends FileLogOptions {
132132+ level: LogLevel
133133+ path: string | (() => string)
134134+}
135135+136136+export type FileLogOptionsParsed = (Omit<FileLogOptions, 'file'> & {level: false}) | FileLogOptionsStrong
137137+138138+const isFileLogOptions = (obj: any): obj is FileLogOptions => {
139139+ if (obj === null || typeof obj !== 'object') {
140140+ return false;
141141+ }
142142+ const levelOk = obj.level === undefined || ('level' in obj && obj.level === false || LOG_LEVELS.includes(obj.level.toLocaleLowerCase()));
143143+ const pathOk = obj.path === undefined || ('path' in obj && typeof obj.path === 'string' || typeof obj.path === 'function');
144144+ const frequencyOk = obj.frequency === undefined || ('frequency' in obj && typeof obj.frequency === 'string' || typeof obj.frequency === 'number');
145145+ const sizeOk = obj.size === undefined || ('size' in obj && typeof obj.size === 'string' || typeof obj.size === 'number');
146146+ const tsOk = obj.timestamp === undefined || ('timestamp' in obj && typeof obj.timestamp === 'string');
147147+148148+ return levelOk && pathOk && frequencyOk && sizeOk && tsOk;
149149+}
150150+151151+export type FileDestination = Omit<PrettyOptions, 'destination' | 'sync'> & FileOptionsParsed;
73152export type StreamDestination = Omit<PrettyOptions, 'destination'> & {destination: number | DestinationStream | NodeJS.WritableStream};
153153+154154+export type LogOptionsParsed = Omit<Required<LogOptions>, 'file'> & { file: FileLogOptionsParsed }