Linter pass + fix logger sometimes not pruning old files

This commit is contained in:
Erik 2024-10-08 22:32:09 +03:00
parent 8c0ae9170f
commit 9e6188d7c5
5 changed files with 102 additions and 101 deletions

View File

@ -10,10 +10,10 @@ enum LogLevel {
export { LogLevel }; export { LogLevel };
const ColourCodes: { [key: string]: number } = { const ColourCodes: { [key: string]: number } = {
error: 0xe88388, error: 0xe88388,
warn: 0xf9d472, warn: 0xf9d472,
info: 0x76a9d8, info: 0x76a9d8,
debug: 0xd8abd7, debug: 0xd8abd7,
status: 0x72d4d7 status: 0x72d4d7
}; };
@ -30,10 +30,10 @@ const TypeStream = { // If none defined they are sent to default
}; };
const Colours = { const Colours = {
error: 'red', error: 'red',
warn: 'yellow', warn: 'yellow',
info: 'blue', info: 'blue',
debug: 'magenta', debug: 'magenta',
status: 'cyanBright' status: 'cyanBright'
}; };
@ -55,11 +55,11 @@ type LogLevelType = {
} }
const logLevelMapping = { const logLevelMapping = {
debug: LogLevel.debug, debug: LogLevel.debug,
info: LogLevel.info, info: LogLevel.info,
status: LogLevel.status, status: LogLevel.status,
warn: LogLevel.warn, warn: LogLevel.warn,
error: LogLevel.error error: LogLevel.error
}; };
type SharedOptionsType = { type SharedOptionsType = {
@ -74,10 +74,10 @@ type SharedOptionsType = {
const SharedOptions: SharedOptionsType = { const SharedOptions: SharedOptionsType = {
guard, guard,
customStreams, customStreams,
logLevel: LogLevel.info, logLevel: LogLevel.info,
logLevelMapping, logLevelMapping,
customTypes: [], customTypes: [],
labels: [] labels: []
}; };
export type LoggerMasterOptions = SharedOptionsType & { export type LoggerMasterOptions = SharedOptionsType & {
@ -94,11 +94,11 @@ export type LoggerMasterOptions = SharedOptionsType & {
const MasterOptions: LoggerMasterOptions = { const MasterOptions: LoggerMasterOptions = {
...SharedOptions, ...SharedOptions,
fileRotationFreq: 1, fileRotationFreq: 1,
directory: './logs', directory: './logs',
customTypes, customTypes,
customTypeMapping, customTypeMapping,
customColours, customColours,
broadcastLevel: 4, broadcastLevel: 4,
webhook, webhook,
pruneDays, pruneDays,
skipFileWrite skipFileWrite

View File

@ -30,7 +30,7 @@ class LoggerClient implements Logger
#types: string[]; #types: string[];
#labels: string[]; #labels: string[];
constructor (opts: LoggerClientOptions = Defaults.ClientOptions) constructor (opts: LoggerClientOptions = Defaults.ClientOptions)
{ {
this.#_name = opts.name || opts.constructor.name; this.#_name = opts.name || opts.constructor.name;
if (this.#_name === 'Object') if (this.#_name === 'Object')
@ -61,17 +61,17 @@ class LoggerClient implements Logger
} }
} }
get logLevel () get logLevel ()
{ {
return this.#_logLevel; return this.#_logLevel;
} }
get logLevels () get logLevels ()
{ {
return Object.keys(this.#_logLevelMapping); return Object.keys(this.#_logLevelMapping);
} }
setLogLevel (level = 'info') setLogLevel (level = 'info')
{ {
if (typeof level === 'number') if (typeof level === 'number')
this.#_logLevel = level; this.#_logLevel = level;
@ -81,7 +81,7 @@ class LoggerClient implements Logger
throw new Error(`Invalid log level type, expected string or number, got ${typeof level}`); throw new Error(`Invalid log level type, expected string or number, got ${typeof level}`);
} }
#transport (type = 'info', ...args: [...entries: Loggable[], options: TransportOptions]) #transport (type = 'info', ...args: [...entries: Loggable[], options: TransportOptions])
{ {
if (this.#_logLevelMapping[type] < this.#_logLevel) if (this.#_logLevelMapping[type] < this.#_logLevel)
return; return;
@ -102,7 +102,7 @@ class LoggerClient implements Logger
message += inspect(makePlainError(entry)) + ' '; message += inspect(makePlainError(entry)) + ' ';
else if (typeof entry === 'string' || typeof entry === 'number') else if (typeof entry === 'string' || typeof entry === 'number')
message += entry + ' '; message += entry + ' ';
else else
message += inspect(entry) + ' '; message += inspect(entry) + ' ';
} }
@ -117,28 +117,28 @@ class LoggerClient implements Logger
} }
// These methods are dynamically implemented by the constructor, simply here to provide IDE hints // These methods are dynamically implemented by the constructor, simply here to provide IDE hints
// eslint-disable-next-line @typescript-eslint/no-unused-vars
error (..._args: [...entries: Loggable[], options: WriteOptions|Loggable]): void error (..._args: [...entries: Loggable[], options: WriteOptions|Loggable]): void
{ {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
warn (..._args: [...entries: Loggable[], options: WriteOptions|Loggable]): void warn (..._args: [...entries: Loggable[], options: WriteOptions|Loggable]): void
{ {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
status (..._args: [...entries: Loggable[], options: WriteOptions|Loggable]): void status (..._args: [...entries: Loggable[], options: WriteOptions|Loggable]): void
{ {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info (..._args: [...entries: Loggable[], options: WriteOptions|Loggable]): void info (..._args: [...entries: Loggable[], options: WriteOptions|Loggable]): void
{ {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
debug (..._args: [...entries: Loggable[], options: WriteOptions|Loggable]): void debug (..._args: [...entries: Loggable[], options: WriteOptions|Loggable]): void
{ {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }

View File

@ -28,7 +28,7 @@ type WriteStreams = {
[key:string]: fs.WriteStream [key:string]: fs.WriteStream
} }
class MasterLogger implements Logger class MasterLogger implements Logger
{ {
[key: string]: LogFunction | unknown; [key: string]: LogFunction | unknown;
@ -54,7 +54,7 @@ class MasterLogger implements Logger
#skipFileWrite: boolean; #skipFileWrite: boolean;
#labels: string[]; #labels: string[];
constructor (config = Defaults.MasterOptions) constructor (config = Defaults.MasterOptions)
{ {
const { const {
directory, customTypes = [], customStreams = [], customTypeMapping, directory, customTypes = [], customStreams = [], customTypeMapping,
@ -62,17 +62,17 @@ class MasterLogger implements Logger
webhook, broadcastLevel, pruneDays, skipFileWrite, labels = [] webhook, broadcastLevel, pruneDays, skipFileWrite, labels = []
} = { ...Defaults.MasterOptions, ...config }; } = { ...Defaults.MasterOptions, ...config };
if (!directory) if (!directory)
throw new Error('Missing directory for log files'); throw new Error('Missing directory for log files');
this.#directory = path.resolve(directory); this.#directory = path.resolve(directory);
if (!fs.existsSync(this.#directory)) if (!fs.existsSync(this.#directory))
fs.mkdirSync(this.#directory, { recursive: true }); fs.mkdirSync(this.#directory, { recursive: true });
this.#_broadcastLevel = broadcastLevel as number; this.#_broadcastLevel = broadcastLevel as number;
this.#_logLevel = logLevel as number; this.#_logLevel = logLevel as number;
this.#_logLevelMapping = { ...Defaults.MasterOptions.logLevelMapping, ...logLevelMapping }; this.#_logLevelMapping = { ...Defaults.MasterOptions.logLevelMapping, ...logLevelMapping };
if (logLevelMapping) if (logLevelMapping)
Object.entries(logLevelMapping).forEach(([ name, level ]) => Object.entries(logLevelMapping).forEach(([ name, level ]) =>
{ {
addLogLevel(name, level); addLogLevel(name, level);
}); });
@ -80,7 +80,7 @@ class MasterLogger implements Logger
this.#skipFileWrite = skipFileWrite as boolean; this.#skipFileWrite = skipFileWrite as boolean;
this.#types = [ ...customTypes, ...Defaults.Types ]; this.#types = [ ...customTypes, ...Defaults.Types ];
for (const type of this.#types) for (const type of this.#types)
{ {
if (typeof this.#_logLevelMapping[type] === 'undefined') if (typeof this.#_logLevelMapping[type] === 'undefined')
throw new Error(`Missing logLevelMapping for type ${type}`); throw new Error(`Missing logLevelMapping for type ${type}`);
@ -90,8 +90,8 @@ class MasterLogger implements Logger
}); });
} }
this.#colours = { ...Defaults.Colours, ...customColours }; this.#colours = { ...Defaults.Colours, ...customColours };
this.#colourFuncs = Object.entries(this.#colours).reduce((prev: FuncsType, [ type, colour ]: (string | number)[]) => this.#colourFuncs = Object.entries(this.#colours).reduce((prev: FuncsType, [ type, colour ]: (string | number)[]) =>
{ {
if (typeof colour === 'number') if (typeof colour === 'number')
colour = `#${colour.toString(16)}`; colour = `#${colour.toString(16)}`;
@ -104,7 +104,7 @@ class MasterLogger implements Logger
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
prev[type] = { func: chalk[colour], int: Defaults.ColourCodes[type as string] || Defaults.ColourCodes.info }; prev[type] = { func: chalk[colour], int: Defaults.ColourCodes[type as string] || Defaults.ColourCodes.info };
return prev; return prev;
}, {} as FuncsType); }, {} as FuncsType);
@ -141,68 +141,68 @@ class MasterLogger implements Logger
clearInterval(this.#pruneInterval); clearInterval(this.#pruneInterval);
const streams = Object.keys(this.#writeStreams); const streams = Object.keys(this.#writeStreams);
for (const type of streams) for (const type of streams)
this.#writeStreams[type].end(); this.#writeStreams[type].end();
clearTimeout(this.#rotateTO); clearTimeout(this.#rotateTO);
} }
get guard () get guard ()
{ {
return this.#_guard; return this.#_guard;
} }
get logLevel () get logLevel ()
{ {
return this.#_logLevel; return this.#_logLevel;
} }
get logLevels () get logLevels ()
{ {
return Object.keys(this.#_logLevelMapping); return Object.keys(this.#_logLevelMapping);
} }
pruneLogFiles () pruneLogFiles ()
{ {
const directory = fs.readdirSync(this.#directory, { withFileTypes: true }); const directory = fs.readdirSync(this.#directory, { withFileTypes: true });
const now = Date.now(); const now = Date.now();
const limit = this.#pruneDays * 24 * 60 * 60 * 1000; const limit = this.#pruneDays * 24 * 60 * 60 * 1000;
for (const entry of directory) for (const entry of directory)
{ {
if (!entry.isFile() || !(/\d{4}-\d{2}-\d{2}-[a-z]+\.log/u).test(entry.name)) if (!entry.isFile() || !(/\d{4}-\d{2}-\d{2}-[a-z]+\.log/iu).test(entry.name))
continue; continue;
const [ year, month, day ] = entry.name.split('-'); const [ year, month, day ] = entry.name.split('-');
const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day)).getTime(); const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day)).getTime();
if ((now - date) < limit) if ((now - date) < limit)
continue; continue;
fs.unlinkSync(path.join(this.#directory, entry.name)); fs.unlinkSync(path.join(this.#directory, entry.name));
} }
} }
setLogLevel (level: number) setLogLevel (level: number)
{ {
if (LogLevel[level] in this.#_logLevelMapping) if (LogLevel[level] in this.#_logLevelMapping)
this.#_logLevel = level; this.#_logLevel = level;
else else
throw new Error('Not a valid log level'); throw new Error('Not a valid log level');
} }
setBroadcastLevel (level: number) setBroadcastLevel (level: number)
{ {
if (LogLevel[level] in this.#_logLevelMapping) if (LogLevel[level] in this.#_logLevelMapping)
this.#_broadcastLevel = level; this.#_broadcastLevel = level;
else else
throw new Error('Not a valid log level'); throw new Error('Not a valid log level');
} }
attach (shard: Shard) attach (shard: Shard)
{ {
shard.on('message', (msg: IPCMessage) => shard.on('message', (msg: IPCMessage) =>
{ {
if (!msg[this.#_guard]) if (!msg[this.#_guard])
return; return;
const { message, type, header, broadcast, labels } = msg; const { message, type, header, broadcast, labels } = msg;
const func = this[type] as LogFunction; const func = this[type] as LogFunction;
if (!func) if (!func)
throw new Error(`Attempted use of invalid logging function of type: ${type}, ensure client and master have the same type definitions.`); throw new Error(`Attempted use of invalid logging function of type: ${type}, ensure client and master have the same type definitions.`);
func(message, { subheader: header, shard, broadcast, labels }); func(message, { subheader: header, shard, broadcast, labels });
}); });
@ -214,22 +214,22 @@ class MasterLogger implements Logger
let { subheader = '', shard, broadcast = false, labels = [] }: WriteOptions = {}; let { subheader = '', shard, broadcast = false, labels = [] }: WriteOptions = {};
if (typeof last === 'object' && isWriteOptions(last)) if (typeof last === 'object' && isWriteOptions(last))
{ {
({ subheader = '', shard, broadcast = false, labels =[] } = last); ({ subheader = '', shard, broadcast = false, labels =[] } = last);
args.pop(); args.pop();
} }
let colour = this.#colourFuncs[type]; let colour = this.#colourFuncs[type];
if (!colour) if (!colour)
colour = this.#colourFuncs.info; colour = this.#colourFuncs.info;
let text = ''; let text = '';
for (const entry of args as Loggable[]) for (const entry of args as Loggable[])
{ {
if (entry instanceof Error) if (entry instanceof Error)
text += inspect(makePlainError(entry)) + ' '; text += inspect(makePlainError(entry)) + ' ';
else if (typeof entry === 'string' || typeof entry === 'number') else if (typeof entry === 'string' || typeof entry === 'number')
text += entry + ' '; text += entry + ' ';
else else
text += inspect(text) + ' '; text += inspect(text) + ' ';
} }
text = text.trim(); text = text.trim();
@ -238,107 +238,108 @@ class MasterLogger implements Logger
const maxChars = Math.max(...this.#types.map(t => t.length)); const maxChars = Math.max(...this.#types.map(t => t.length));
const spacer = ' '.repeat(maxChars - type.length); const spacer = ' '.repeat(maxChars - type.length);
if (this.#_logLevelMapping[type] >= this.#_logLevel) if (this.#_logLevelMapping[type] >= this.#_logLevel || process.env.NODE_ENV === 'development')
{ {
const out = `${colour.func(type)}${spacer} ${colour.func(header)}: ${chalk.bold(subheader)}${text}`; const out = `${colour.func(type)}${spacer} ${colour.func(header)}: ${chalk.bold(subheader)}${text}`;
if (type === 'error') if (type === 'error')
console.error(out); // eslint-disable-line no-console console.error(out); // eslint-disable-line no-console
else else
console.log(out); // eslint-disable-line no-console console.log(out); // eslint-disable-line no-console
} }
if ((broadcast || (this.#_broadcastLevel <= this.#_logLevelMapping[type])) && this.#webhook) if ((broadcast || (this.#_broadcastLevel <= this.#_logLevelMapping[type])) && this.#webhook)
{ {
const description = (subheader.length ? `**${subheader.trim()}**: ${process.env.NODE_ENV ?? 'production'}\n` : '') + `\`\`\`${text}\`\`\``; const description = (subheader.length ? `**${subheader.trim()}**` : '**ENV**') + `: ${process.env.NODE_ENV ?? 'production'}`;
const content = `\`\`\`${text}\`\`\``;
this.#webhook.send({ this.#webhook.send({
embeds: [{ embeds: [{
title: `[__${type.toUpperCase()}__] ${this._shard(shard)}`, title: `[__${type.toUpperCase()}__] ${this._shard(shard)}`,
description, description: `${description}\n${content}`,
color: colour.int, color: colour.int,
footer: { footer: {
text: [ ...labels, ...this.#labels ].join(', ') ?? '' text: [ ...labels, ...this.#labels ].join(', ') ?? ''
} }
}] }]
}); });
} }
if (this.#skipFileWrite) if (this.#skipFileWrite)
return; return;
const streamType = this.#streamTypeMapping[type] || 'default'; const streamType = this.#streamTypeMapping[type] || 'default';
if (this.#writeStreams[streamType]) if (this.#writeStreams[streamType])
this.#writeStreams[streamType].write(`\n${type}${spacer} ${header}: ${subheader}${text}`); this.#writeStreams[streamType].write(`\n${type}${spacer} ${header}: ${subheader}${text}`);
else else
console.log(`${chalk.red(`[LOGGER] Missing file stream for ${streamType}`)}`); // eslint-disable-line no-console console.log(`${chalk.red(`[LOGGER] Missing file stream for ${streamType}`)}`); // eslint-disable-line no-console
} }
rotateLogFiles () rotateLogFiles ()
{ {
const streams = Object.keys(this.#writeStreams); const streams = Object.keys(this.#writeStreams);
for (const type of streams) for (const type of streams)
{ {
this.#writeStreams[type].write('\nRotating log file'); this.#writeStreams[type].write('\nRotating log file');
this.#writeStreams[type].end(); this.#writeStreams[type].end();
this.#writeStreams[type] = this.loadFile(type); this.#writeStreams[type] = this.loadFile(type);
} }
const nextTime = Math.floor(Date.now() / this.#rotationFreq) * this.#rotationFreq + this.#rotationFreq; const nextTime = Math.floor(Date.now() / this.#rotationFreq) * this.#rotationFreq + this.#rotationFreq;
if (this.#rotateTO) if (this.#rotateTO)
clearTimeout(this.#rotateTO); clearTimeout(this.#rotateTO);
this.#rotateTO = setTimeout(this.rotateLogFiles.bind(this), nextTime - Date.now()); this.#rotateTO = setTimeout(this.rotateLogFiles.bind(this), nextTime - Date.now());
} }
loadFile (type: string, date = Date.now()) loadFile (type: string, date = Date.now())
{ {
if (!type) if (!type)
throw new Error('Missing file type'); throw new Error('Missing file type');
const fileName = `${moment(date).format('YYYY-MM-DD')}-${type}.log`; const fileName = `${moment(date).format('YYYY-MM-DD')}-${type}.log`;
const filePath = path.join(this.#directory, fileName); const filePath = path.join(this.#directory, fileName);
if (!fs.existsSync(filePath)) if (!fs.existsSync(filePath))
fs.writeFileSync(filePath, ''); fs.writeFileSync(filePath, '');
return fs.createWriteStream(filePath, { flags: 'a' }); return fs.createWriteStream(filePath, { flags: 'a' });
} }
_shard (shard?: Shard) _shard (shard?: Shard)
{ {
if (!shard) if (!shard)
return 'controller'; return 'controller';
let id = '??'; let id = '??';
if ('id' in shard) if ('id' in shard)
id = `${shard.id < 10 ? `0${shard.id}` : shard.id}`; id = `${shard.id < 10 ? `0${shard.id}` : shard.id}`;
return `shard-${id}`; return `shard-${id}`;
} }
get date () get date ()
{ {
return moment().format('YYYY-MM-DD HH:mm:ss'); return moment().format('YYYY-MM-DD HH:mm:ss');
} }
// These methods are dynamically implemented by the constructor // These methods are dynamically implemented by the constructor
// eslint-disable-next-line @typescript-eslint/no-unused-vars
error (..._args: [...entries: Loggable[], options: WriteOptions | Loggable]): void error (..._args: [...entries: Loggable[], options: WriteOptions | Loggable]): void
{ {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
warn (..._args: [...entries: Loggable[], options: WriteOptions | Loggable]): void warn (..._args: [...entries: Loggable[], options: WriteOptions | Loggable]): void
{ {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
status (..._args: [...entries: Loggable[], options: WriteOptions | Loggable]): void status (..._args: [...entries: Loggable[], options: WriteOptions | Loggable]): void
{ {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info (..._args: [...entries: Loggable[], options: WriteOptions | Loggable]): void info (..._args: [...entries: Loggable[], options: WriteOptions | Loggable]): void
{ {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
debug (..._args: [...entries: Loggable[], options: WriteOptions | Loggable]): void debug (..._args: [...entries: Loggable[], options: WriteOptions | Loggable]): void
{ {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }

View File

@ -3,14 +3,14 @@ import { WriteOptions } from './Types';
export const makePlainError = (err: Error) => export const makePlainError = (err: Error) =>
{ {
return { return {
name: err.name, name: err.name,
message: err.message, message: err.message,
stack: err.stack stack: err.stack
}; };
}; };
const validKeys = [ 'subheader', 'shard', 'broadcast', 'labels' ]; const validKeys = [ 'subheader', 'shard', 'broadcast', 'labels' ];
export const isWriteOptions = (obj: unknown, extended = false): obj is WriteOptions => export const isWriteOptions = (obj: unknown, extended = false): obj is WriteOptions =>
{ {
if (!obj || typeof obj !== 'object') if (!obj || typeof obj !== 'object')
return false; return false;

View File

@ -5,7 +5,7 @@ type Shard = {
} & EventEmitter; } & EventEmitter;
type WriteOptions = { type WriteOptions = {
subheader?: string, subheader?: string,
shard?: Shard, shard?: Shard,
broadcast?: boolean, broadcast?: boolean,
labels?: string[] labels?: string[]