Skip to content

Commit

Permalink
Decompose Logger, Target, Console, Formatter from metalog.js
Browse files Browse the repository at this point in the history
  • Loading branch information
georgolden authored and tshemsedinov committed Jun 28, 2023
1 parent 634d8b2 commit fa9f05f
Show file tree
Hide file tree
Showing 6 changed files with 486 additions and 428 deletions.
116 changes: 116 additions & 0 deletions lib/console.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
'use strict';

const readline = require('readline');
const util = require('util');

const INDENT = 2;

class Console {
#logger;
#groupIndent;
#counts;
#times;

constructor(logger) {
this.#logger = logger;
this.#groupIndent = 0;
this.#counts = new Map();
this.#times = new Map();
}

assert(assertion, ...args) {
try {
console.assert(assertion, ...args);
} catch (err) {
this.#logger.write('error', this.#groupIndent, err.stack);
}
}

clear() {

Check warning on line 29 in lib/console.js

View workflow job for this annotation

GitHub Actions / build (16, ubuntu-latest)

Expected 'this' to be used by class method 'clear'

Check warning on line 29 in lib/console.js

View workflow job for this annotation

GitHub Actions / build (16, macos-latest)

Expected 'this' to be used by class method 'clear'

Check warning on line 29 in lib/console.js

View workflow job for this annotation

GitHub Actions / build (18, ubuntu-latest)

Expected 'this' to be used by class method 'clear'

Check warning on line 29 in lib/console.js

View workflow job for this annotation

GitHub Actions / build (18, macos-latest)

Expected 'this' to be used by class method 'clear'

Check warning on line 29 in lib/console.js

View workflow job for this annotation

GitHub Actions / build (19, ubuntu-latest)

Expected 'this' to be used by class method 'clear'

Check warning on line 29 in lib/console.js

View workflow job for this annotation

GitHub Actions / build (20, ubuntu-latest)

Expected 'this' to be used by class method 'clear'
readline.cursorTo(process.stdout, 0, 0);
readline.clearScreenDown(process.stdout);
}

count(label = 'default') {
let cnt = this.#counts.get(label) || 0;
cnt++;
this.#counts.set(label, cnt);
this.#logger.write('debug', this.#groupIndent, `${label}: ${cnt}`);
}

countReset(label = 'default') {
this.#counts.delete(label);
}

debug(...args) {
this.#logger.write('debug', this.#groupIndent, ...args);
}

dir(...args) {
this.#logger.write('debug', this.#groupIndent, ...args);
}

trace(...args) {
const msg = util.format(...args);
const err = new Error(msg);
this.#logger.write('debug', this.#groupIndent, `Trace${err.stack}`);
}

info(...args) {
this.#logger.write('info', this.#groupIndent, ...args);
}

log(...args) {
this.#logger.write('log', this.#groupIndent, ...args);
}

warn(...args) {
this.#logger.write('warn', this.#groupIndent, ...args);
}

error(...args) {
this.#logger.write('error', this.#groupIndent, ...args);
}

group(...args) {
if (args.length !== 0) this.log(...args);
this.#groupIndent += INDENT;
}

groupCollapsed(...args) {
this.group(...args);
}

groupEnd() {
if (this.#groupIndent.length === 0) return;
this.#groupIndent -= INDENT;
}

table(tabularData) {
this.#logger.write('log', 0, JSON.stringify(tabularData));
}

time(label = 'default') {
this.#times.set(label, process.hrtime());
}

timeEnd(label = 'default') {
const startTime = this.#times.get(label);
const totalTime = process.hrtime(startTime);
const totalTimeMs = totalTime[0] * 1e3 + totalTime[1] / 1e6;
this.timeLog(label, `${label}: ${totalTimeMs}ms`);
this.#times.delete(label);
}

timeLog(label, ...args) {
const startTime = this.#times.get(label);
if (startTime === undefined) {
const msg = `Warning: No such label '${label}'`;
this.#logger.write('warn', this.#groupIndent, msg);
return;
}
this.#logger.write('debug', this.#groupIndent, ...args);
}
}

module.exports = { Console };
98 changes: 98 additions & 0 deletions lib/formatter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use strict';

const util = require('util');
const concolor = require('concolor');
const metautil = require('metautil');

const STACK_AT = ' at ';
const TYPE_LENGTH = 6;
const LINE_SEPARATOR = ';';
const DATE_LEN = 'YYYY-MM-DD'.length;
const TIME_START = DATE_LEN + 1;
const TIME_END = TIME_START + 'HH:MM:SS'.length;

const TYPE_COLOR = concolor({
log: 'b,black/white',
info: 'b,white/blue',
warn: 'b,black/yellow',
debug: 'b,white/green',
error: 'b,yellow/red',
});

const TEXT_COLOR = concolor({
log: 'white',
info: 'white',
warn: 'b,yellow',
debug: 'b,green',
error: 'red',
});

class Formatter {
#logger;

constructor(logger) {
this.#logger = logger;
}

format(type, indent, ...args) {
const normalize = type === 'error' || type === 'debug';
const s = `${' '.repeat(indent)}${util.format(...args)}`;
return normalize ? this.#logger.normalizeStack(s) : s;
}

pretty(type, indent, ...args) {
const dateTime = new Date().toISOString();
const message = this.format(type, indent, ...args);
const normalColor = TEXT_COLOR[type];
const markColor = TYPE_COLOR[type];
const time = normalColor(dateTime.substring(TIME_START, TIME_END));
const id = normalColor(this.#logger.workerId);
const mark = markColor(' ' + type.padEnd(TYPE_LENGTH));
const msg = normalColor(message);
return `${time} ${id} ${mark} ${msg}`;
}

file(type, indent, ...args) {
const dateTime = new Date().toISOString();
const message = this.format(type, indent, ...args);
const msg = metautil.replace(message, '\n', LINE_SEPARATOR);
return `${dateTime} [${type}] ${msg}`;
}

json(type, indent, ...args) {
const log = {
timestamp: new Date().toISOString(),
workerId: this.#logger.workerId,
level: type,
message: null,
};
if (metautil.isError(args[0])) {
log.err = this.expandError(args[0]);
args = args.slice(1);
} else if (typeof args[0] === 'object') {
Object.assign(log, args[0]);
if (metautil.isError(log.err)) log.err = this.expandError(log.err);
if (metautil.isError(log.error)) log.error = this.expandError(log.error);
args = args.slice(1);
}
log.message = util.format(...args);
return JSON.stringify(log);
}

normalizeStack(stack) {
if (!stack) return 'no data to log';
let res = metautil.replace(stack, STACK_AT, '');
if (this.#logger.home) res = metautil.replace(res, this.#logger.home, '');
return res;
}

expandError(err) {
return {
message: err.message,
stack: this.normalizeStack(err.stack),
...err,
};
}
}

module.exports = { Formatter };
105 changes: 105 additions & 0 deletions lib/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
'use strict';

const events = require('node:events');
const metautil = require('metautil');

const { Console } = require('./console.js');
const { Formatter } = require('./formatter.js');
const { FsTarget } = require('./target.js');

const DAY_MILLISECONDS = metautil.duration('1d');

const LOG_TYPES = ['log', 'info', 'warn', 'debug', 'error'];

const DEFAULT_FLAGS = {
log: false,
info: false,
warn: false,
debug: false,
error: false,
};

const logTypes = (types = LOG_TYPES) => {
const flags = { ...DEFAULT_FLAGS };
for (const type of types) {
flags[type] = true;
}
return flags;
};

const getNextReopen = () => {
const now = new Date();
const curTime = now.getTime();
const nextDate = now.setUTCHours(0, 0, 0, 0);
return nextDate - curTime + DAY_MILLISECONDS;
};

class Logger extends events.EventEmitter {
constructor(options) {
super();
const { workerId = 0, home, json } = options;
const { toFile = [], toStdout = [] } = options;
this.options = options;
this.active = false;
this.workerId = `W${workerId}`;
this.home = home;
this.json = Boolean(json);
this.reopenTimer = null;
this.toFile = logTypes(toFile);
this.toStdout = logTypes(toStdout);
this.console = new Console(this);
this.formatter = new Formatter(this);
this.fsEnabled = toFile.length !== 0;
this.target = null;
return this.open();
}

async open() {
if (this.active) return this;
if (!this.fsEnabled) {
this.active = true;
process.nextTick(() => this.emit('open'));
return this;
}
const nextReopen = getNextReopen();
this.reopenTimer = setTimeout(() => {
this.once('close', () => {
this.open();
});
this.close().catch((err) => {
process.stdout.write(`${err.stack}\n`);
this.emit('error', err);
});
}, nextReopen);
this.target = await new FsTarget(this);
this.active = true;
return this;
}

async close() {
if (!this.active) return;
if (this.target) await this.target.close();
this.active = false;
this.emit('close');
}

write(type, indent, ...args) {
const { formatter } = this;
if (this.toStdout[type]) {
const line = this.json
? formatter.json(type, indent, ...args)
: formatter.pretty(type, indent, ...args);
process.stdout.write(line + '\n');
}
if (this.toFile[type]) {
const line = this.json
? formatter.json(type, indent, ...args)
: formatter.file(type, indent, ...args);
this.target.write(line + '\n');
}
}
}

const openLog = async (args) => new Logger(args);

module.exports = { Logger, openLog };
Loading

0 comments on commit fa9f05f

Please sign in to comment.