-
-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Decompose Logger, Target, Console, Formatter from metalog.js
- Loading branch information
1 parent
634d8b2
commit fa9f05f
Showing
6 changed files
with
486 additions
and
428 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 GitHub Actions / build (16, ubuntu-latest)
Check warning on line 29 in lib/console.js GitHub Actions / build (16, macos-latest)
Check warning on line 29 in lib/console.js GitHub Actions / build (18, ubuntu-latest)
Check warning on line 29 in lib/console.js GitHub Actions / build (18, macos-latest)
Check warning on line 29 in lib/console.js GitHub Actions / build (19, ubuntu-latest)
|
||
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
Oops, something went wrong.