Compare commits

...

2 Commits

Author SHA1 Message Date
4e6b3418df
file rotation support 2023-03-23 18:50:49 +02:00
9be57d20c7
v1.2.0 2023-03-23 18:49:36 +02:00
8 changed files with 124 additions and 46 deletions

View File

@ -161,7 +161,7 @@
"no-var": "warn",
"no-void": "warn",
"no-whitespace-before-property": "error",
"nonblock-statement-body-position": "warn",
"nonblock-statement-body-position": ["warn", "below"],
"object-curly-spacing": [
"warn",
"always"

View File

@ -3,7 +3,6 @@
Simple logger I wrote to have a unified system for logging throughout my projects.
## TODO
- Automatic file rotation, currently only rotates files on startup
- Discord webhook, need to write a separate package for that, don't want to add the entirety of d.js as a dep just for a webhook
## Features
@ -19,10 +18,15 @@ The child processes are expected to be attached with the attach() method found i
## Logger Options
```
{
//////// SHARED BETWEEN CLIENT & MASTER
guard: '_logger', // Message guard, e.g this property has to be true in the IPC message for the logger to read it
debug: false, // On the master logger this determines whether the output is written to console (i.e. will always be written to file), on the client this determines whether it is sent to the master at all
customTypes: [], // Log types, defaults are 'error', 'warn', 'info', 'debug', 'status'. Each one of these has an associated shorthand function, the custom ones will receive one too, e.g. adding 'access' to the custom types will add a logger.access() function
/////// MASTER EXCLUSIVE
customStreams: [], // File streams, by default there are streams for error and default
customTypeMapping: {}, // This maps a type to a stream, e.g. adding "warn": "error" will pipe any warnings to the error log file
customColors: {}, // Supports any colours chalk.js supports, e.g. "warn": "green" will turn warning outputs green
debug: false
fileRotationFreq: 1, // How frequently to roate files in days - will not work with anything less than 1 day currently
directory: './logs', // Directory in which to write log files
}
```

View File

@ -1,6 +1,6 @@
{
"name": "@navy.gif/logger",
"version": "1.1.9",
"version": "1.2.0",
"description": "Logging thing",
"main": "index.js",
"author": "Navy.gif",

View File

@ -22,5 +22,20 @@ module.exports = {
info: 0x76a9d8,
debug: 0xd8abd7,
status: 0x72d4d7
},
MasterOptions: {
fileRotationFreq: 1,
directory: './logs',
customTypes: [],
customStreams: [],
customTypeMapping: {},
customColors: {},
debug: false,
guard: '_logger'
},
ClientOptions: {
debug: true,
guard: '_logger',
customStreams: []
}
};

View File

@ -4,13 +4,15 @@ class LoggerClient {
static MaxChars = 0;
constructor (opts) {
constructor (opts = Defaults.ClientOptions) {
this.name = opts.name || opts.constructor.name;
if (this.name === 'Object') this.name = 'unknown';
if (this.name === 'Object')
this.name = 'unknown';
this.name = this.name.toUpperCase();
if (this.name.length > LoggerClient.MaxChars) LoggerClient.MaxChars = this.name.length;
if (this.name.length > LoggerClient.MaxChars)
LoggerClient.MaxChars = this.name.length;
const { customTypes = [] } = opts;
this.types = [ ...customTypes, ...Defaults.Types ];
@ -20,17 +22,23 @@ class LoggerClient {
});
}
this.guard = opts.guard || Defaults.ClientOptions.guard;
}
transport (message, opts) {
if (typeof message !== 'string') message = inspect(message);
if (typeof message !== 'string')
message = inspect(message);
const spacer = ' '.repeat(LoggerClient.MaxChars - this.name.length);
const header = `${`[${this.name.substring(0, LoggerClient.MaxChars)}]${spacer}`} `;
// eslint-disable-next-line no-console
if (!process.send || !process.connected) console.log(`${header} ${message}`);
else process.send({ _logger: true, header, message, ...opts });
if (!process.send || !process.connected)
// eslint-disable-next-line no-console
console.log(`${header} ${message}`);
else
process.send({ [this.guard]: true, header, message, ...opts });
}

View File

@ -8,41 +8,67 @@ const { inspect } = require('node:util');
const Defaults = require('./Defaults');
const DAY = 1000 * 60 * 60 * 24;
const DefaultOpts = {
fileRotationFreq: 1,
directory: './logs',
customTypes: [],
customStreams: [],
customTypeMapping: {},
customColors: {},
debug: false,
guard: '_logger'
};
// TODO:
// - File rotation
// - Discord webhook, need to write a separate package for that, don't want to add the entirety of d.js as a dep just for a webhook
class MasterLogger {
constructor (options = {}) {
constructor ({
directory = './logs',
customTypes = [],
customStreams = [],
customTypeMapping = {},
customColors = {},
debug = false,
guard = '_logger',
fileRotationFreq = 1
} = DefaultOpts) {
this.directory = path.resolve(options.directory || './logs');
if (!fs.existsSync(this.directory)) fs.mkdirSync(this.directory, { recursive: true });
const { customTypes = [], customStreams = [], customTypeMapping = {}, customColors = {}, debug = false } = options;
this.directory = path.resolve(directory);
if (!fs.existsSync(this.directory))
fs.mkdirSync(this.directory, { recursive: true });
this._debug = debug;
this.types = [ ...customTypes, ...Defaults.Types ];
for (const type of this.types) {
Object.defineProperty(this, type, {
// Only write debug outputs if debug mode is enabled
value: (msg, opts) => msg.type === 'debug' && debug || msg.type !== 'debug'
? this.write(type, msg, opts) : null
value: (msg, opts) => this.write(type, msg, opts)
});
}
this.colours = { ...Defaults.Colours, ...customColors };
this.streamTypes = [ ...customStreams, 'error', 'default' ];
this.streamTypeMapping = { ...Defaults.TypeStream, ...customTypeMapping };
// eslint-disable-next-line no-return-assign
this.writeStreams = this.streamTypes.reduce((acc, type) => {
acc[type] = this.loadFile(type);
return acc;
}, {});
// Startup day, used to keep track of file rotation
this.rotationFreq = fileRotationFreq * DAY;
this.startDay = Math.floor(Date.now() / this.rotationFreq) * this.rotationFreq;
this.rotateTO = setTimeout(this.rotateLogFiles.bind(this), this.startDay + this.rotationFreq - Date.now());
this.guard = guard;
}
attach (shard) {
shard.on('message', (msg) => {
if (!msg._logger) return;
if (!msg[this.guard])
return;
const { message, type, header, broadcast } = msg;
this[type](message, { subheader: header, shard, broadcast });
});
@ -52,42 +78,62 @@ class MasterLogger {
type = type.toLowerCase();
let colour = this.colours[type];
if (!colour) colour = this.colours.info;
if (!colour)
colour = this.colours.info;
if (typeof text !== 'string') text = inspect(text);
if (typeof text !== 'string')
text = inspect(text);
const header = `[${this.date}] [${this._shard(shard)}]`;
const maxChars = Math.max(...this.types.map(t => t.length));
const spacer = ' '.repeat(maxChars - type.length);
console.log(`${chalk[colour](type)}${spacer} ${chalk[colour](header)}: ${chalk.bold(subheader)}${text}`); // eslint-disable-line no-console
if (type === 'debug' && this._debug || type !== 'debug')
console.log(`${chalk[colour](type)}${spacer} ${chalk[colour](header)}: ${chalk.bold(subheader)}${text}`); // eslint-disable-line no-console
if (broadcast) {
// Send to webhook - TODO
}
const streamType = this.streamTypeMapping[type] || 'default';
if (this.writeStreams[streamType]) this.writeStreams[streamType].write(`\n${type}${spacer} ${header}: ${subheader}${text}`);
else console.log(`${chalk.red(`[LOGGER] Missing file stream for ${streamType}`)}`); // eslint-disable-line no-console
if (this.writeStreams[streamType])
this.writeStreams[streamType].write(`\n${type}${spacer} ${header}: ${subheader}${text}`);
else
console.log(`${chalk.red(`[LOGGER] Missing file stream for ${streamType}`)}`); // eslint-disable-line no-console
}
loadFile (type) {
if (!type) throw new Error('Missing file type');
rotateLogFiles () {
const streams = Object.keys(this.writeStreams);
for (const type of streams) {
this.writeStreams[type].write('\nRotating log file');
this.writeStreams[type].end();
this.writeStreams[type] = this.loadFile(type);
}
const nextTime = Math.floor(Date.now() / this.rotationFreq) * this.rotationFreq + this.rotationFreq;
this.rotateTO = setTimeout(this.rotateLogFiles.bind(this), nextTime - Date.now());
}
const fileName = `${moment().format('YYYY-MM-DD')}-${type}.log`;
loadFile (type, date = Date.now()) {
if (!type)
throw new Error('Missing file type');
const fileName = `${moment(date).format('YYYY-MM-DD')}-${type}-${date}.log`;
const filePath = path.join(this.directory, fileName);
if (!fs.existsSync(filePath)) fs.writeFileSync(filePath, '');
if (!fs.existsSync(filePath))
fs.writeFileSync(filePath, '');
return fs.createWriteStream(filePath, { flags: 'a' });
}
_shard (shard) {
if (!shard) return 'controller';
if (!shard)
return 'controller';
let id = '??';
if ('id' in shard) id = `${shard.id < 10 ? `0${shard.id}` : shard.id}`;
if ('id' in shard)
id = `${shard.id < 10 ? `0${shard.id}` : shard.id}`;
return `shard-${id}`;
}

View File

@ -8,9 +8,17 @@ const logger = new LoggerClient({
customColors: { access: 'green' }
});
logger.info('Info test');
logger.status('Status test');
logger.debug('Debug test');
logger.warn('Warn test');
logger.error('Error test');
logger.access('Access test');
const main = async () => {
for (let i = 0; i < 10000; i++) {
if (i % 500 === 0)
await new Promise(resolve => setTimeout(resolve, 1000));
logger.info('Info test');
logger.status('Status test');
logger.debug('Debug test');
logger.warn('Warn test');
logger.error('Error test');
logger.access('Access test');
}
};
main();

View File

@ -15,7 +15,8 @@ const main = async () => {
customTypes: [ 'access' ],
customStreams: [ 'access' ],
customTypeMapping: { access: 'access', warn: 'error' },
customColors: { access: 'green' }
customColors: { access: 'green' },
fileRotationFreq: 0.0001
});
const child = ChildProcess.fork('./test/otherProcess.js');
logger.attach(child);
@ -25,13 +26,9 @@ const main = async () => {
const { types, colours, streamTypes, streamTypeMapping } = logger; // , writeStreams
console.log(types, colours, streamTypes, streamTypeMapping); // , writeStreams
logger.info('Info test');
logger.status('Status test');
logger.debug('Debug test');
logger.warn('Warn test');
logger.error('Error test');
logger.access('Access test');
for (let i = 0; i < 10; i++) await new Promise((resolve) => setTimeout(resolve, 1000));
for (let i = 0; i < 10; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
logger.info(`Iteration ${i}`);
}
};
main();