cordis/logger/logger.ts

266 lines
11 KiB
TypeScript
Raw Normal View History

2021-10-30 17:26:41 +00:00
import * as util from 'util';
import * as path from 'path';
2021-11-02 04:29:24 +00:00
import * as fs from 'fs/promises';
import * as moment from 'moment';
import * as colors from 'colors/safe';
import { SourceMapConsumer } from 'source-map';
2021-10-30 17:26:41 +00:00
import * as StackTrace from '../stack-trace/stack-trace';
2021-11-07 21:57:09 +00:00
const LINE_NUM_LENGTH = 3;
const MAX_FILE_NAME_PADDING = 22;
const baseDir = path.join(__dirname, '..', '..');
2021-11-07 21:57:09 +00:00
/** Adds padding to the left of a string */
function padLeft(string: string, padding: string, minLength: number) {
while (string.length < minLength) {
string = padding + string;
2021-10-30 17:26:41 +00:00
}
2021-11-07 21:57:09 +00:00
return string;
2021-10-30 17:26:41 +00:00
}
2021-11-07 21:57:09 +00:00
/** Adds padding to the right of a string */
function padRight(string: string, padding: string, minLength: number) {
while (string.length < minLength) {
string = string + padding;
}
return string;
}
2021-11-02 04:29:24 +00:00
/**
* @param jsFile The compiled javascript file
* @param tsRelativeFile The source typescript file relative to jsFile's directory
* @returns The path to tsRelativeFile based on the root directory
*/
function getPrintedPath(jsFile: string, tsRelativeFile: string) {
let rootFile = path.join(__dirname, '..', '..');
let jsDir = path.dirname(jsFile);
let tsPath = path.resolve(jsDir, tsRelativeFile);
return path.relative(rootFile, tsPath);
}
/**
* @param dir The directory to search
* @returns an async iterator to all files in a directory (recursively searched)
*/
async function* getAllFiles(dir: string): AsyncIterable<string> {
let filesAndDirs = await fs.readdir(dir, { withFileTypes: true });
for (let next of filesAndDirs) {
if (next.isFile()) {
yield path.join(dir, next.name);
} else if (next.isDirectory()) {
let nextFiles = getAllFiles(path.join(dir, next.name));
for await (let nextFile of nextFiles) {
yield nextFile;
}
}
}
}
/**
* @param sourceMaps A mapping from .js file to SourceMapConsumer created from a corresponding .js.map file
* @param frame The StackTrace.Frame to try to convert into typescript format
* @returns A StackTrace.Frame that is in terms of the corresponding typescript file or the same StackTrace.Frame
* if the conversion couldn't be done (i.e. it's an internal node code)
*/
2021-11-02 04:29:24 +00:00
function tryCreateTSFrame(sourceMaps: Map<string, SourceMapConsumer>, frame: StackTrace.Frame): StackTrace.Frame {
if (!frame.fileName) return frame;
let mapping = sourceMaps.get(frame.fileName);
if (!mapping) return frame;
if (!frame.line || !frame.column) return frame;
let pos = mapping.originalPositionFor({ line: frame.line, column: frame.column });
if (!pos.source) return frame;
return {
functionName: frame.functionName,
fileName: path.resolve(path.dirname(frame.fileName), pos.source),
2021-11-02 04:29:24 +00:00
line: pos.line ?? undefined,
column: pos.column ?? undefined
}
}
// I know, I know. But it takes all this disgusting code to make sure we only have to load the source maps once per module instance
// instead of spamming the hard drive. To be honest, why do you care about this file anyway. It's just the logger :)
2021-11-02 04:29:24 +00:00
let STATIC_SOURCE_MAPS_LOADING = false;
let STATIC_SOURCE_MAPS_CALLBACKS: ((sourceMaps: Map<string, SourceMapConsumer>) => void)[] = [];
let STATIC_SOURCE_MAPS: Map<string, SourceMapConsumer> | null = null;
async function getStaticSourceMaps(): Promise<Map<string, SourceMapConsumer>> {
if (STATIC_SOURCE_MAPS !== null) {
return STATIC_SOURCE_MAPS;
} else if (STATIC_SOURCE_MAPS_LOADING) {
return new Promise<Map<string, SourceMapConsumer>>((resolve) => {
STATIC_SOURCE_MAPS_CALLBACKS.push(resolve);
});
} else {
STATIC_SOURCE_MAPS_LOADING = true;
let sourceMaps = new Map<string, SourceMapConsumer>();
for await (let file of getAllFiles(path.join(__dirname, '..'))) {
if (!file.endsWith('.js')) continue;
if (!await fs.stat(file + '.map').then(() => true).catch(() => false)) continue; // make sure .map file exists
let fileBuff = await fs.readFile(file + '.map');
if (typeof window !== 'undefined' && URL && URL.createObjectURL) {
// Only run this stuff if we are running in a browser window
let mappingsWasmBuffer = await fs.readFile(path.join(__dirname, '../../node_modules/source-map/lib/mappings.wasm'));
let mappingsWasmBlob = new Blob([ mappingsWasmBuffer ], { type: 'application/wasm' });
//@ts-ignore Typescript is missing this function... Probably because it is static
SourceMapConsumer.initialize({ "lib/mappings.wasm": URL.createObjectURL(mappingsWasmBlob) });
}
let sourceMapConsumer = await new SourceMapConsumer(fileBuff.toString());
sourceMaps.set(file, sourceMapConsumer);
}
STATIC_SOURCE_MAPS = sourceMaps;
STATIC_SOURCE_MAPS_LOADING = false;
for (let callback of STATIC_SOURCE_MAPS_CALLBACKS) {
callback(sourceMaps);
}
STATIC_SOURCE_MAPS_CALLBACKS = [];
return STATIC_SOURCE_MAPS;
}
}
enum LoggerLevel {
Fatal, Error, Warn,
Info, Debug, Silly
};
/**
* Colorizes text
* @param level The color level
* @param text The text to colorize
* @returns The colored text
*/
function colorize(level: LoggerLevel, text: string): string {
switch (level) {
case LoggerLevel.Fatal: return colors.bgRed(colors.white(text));
case LoggerLevel.Error: return colors.red(text);
case LoggerLevel.Warn: return colors.yellow(text);
case LoggerLevel.Info: return colors.green(text);
case LoggerLevel.Debug: return colors.blue(text);
case LoggerLevel.Silly: return colors.magenta(text);
default: return colors.bgWhite(text);
}
}
2021-10-30 17:26:41 +00:00
export default class Logger {
private name: string;
private console: Console;
2021-11-02 04:29:24 +00:00
private sourceMaps = new Map<string, SourceMapConsumer>();
2021-11-07 20:13:59 +00:00
private hasSourceMaps = false;
private sourceMapsWaiters: (() => void)[] = [];
2021-11-07 21:57:09 +00:00
private longestFileNameLength: number = 0;
2021-10-30 17:26:41 +00:00
2021-11-02 04:29:24 +00:00
private constructor(name: string, processConsole?: Console) {
2021-10-30 17:26:41 +00:00
this.name = name;
this.console = processConsole ?? console;
}
/** Creates a new logger. The logger will print in terms of the dist .js files until it loads the TypeScript mappings */
2021-11-02 04:29:24 +00:00
static create(name: string, processConsole?: Console) {
let log = new Logger(name, processConsole);
(async () => {
let sourceMaps = await getStaticSourceMaps();
log.sourceMaps = sourceMaps;
2021-11-07 20:13:59 +00:00
log.onGetSourceMaps();
2021-11-02 04:29:24 +00:00
})();
return log;
}
2021-11-07 20:13:59 +00:00
private onGetSourceMaps() {
this.hasSourceMaps = true;
for (let sourceMapWaiter of this.sourceMapsWaiters) {
sourceMapWaiter();
}
2021-11-07 21:57:09 +00:00
let longestFileName = Array.from(this.sourceMaps.keys())
.map(filePath => path.basename(filePath))
.reduce((longestFileName, fileName) => fileName.length > longestFileName.length ? fileName : longestFileName, '');
this.longestFileNameLength = longestFileName.length;
2021-11-07 20:13:59 +00:00
}
public async ensureSourceMaps(): Promise<void> {
if (this.hasSourceMaps) return;
return new Promise((resolve) => {
this.sourceMapsWaiters.push(resolve);
});
}
public getTSStackFrames(err: Error): StackTrace.Frame[] {
let jsFrames: StackTrace.Frame[] = StackTrace.parse(err);
let tsFrames = jsFrames.map(jsFrame => tryCreateTSFrame(this.sourceMaps, jsFrame));
tsFrames = tsFrames.map(tsFrame => StackTrace.relativizeFilePath(tsFrame, baseDir));
return tsFrames;
}
/** Logs a message and potentially corresponding data */
2021-10-30 17:26:41 +00:00
private log(level: LoggerLevel, message: string | null, data?: Error | any): void {
let frames: StackTrace.Frame[] = StackTrace.parse(new Error());
2021-11-02 04:29:24 +00:00
let jsFrame = frames[2];
let tsFrame = tryCreateTSFrame(this.sourceMaps, jsFrame);
2021-10-30 17:26:41 +00:00
2021-11-07 21:57:09 +00:00
let fileName: string;
let line: number;
if (tsFrame.fileName) {
fileName = path.basename(getPrintedPath(jsFrame.fileName, tsFrame.fileName));
line = tsFrame.line ?? NaN;
} else {
fileName = this.name;
line = NaN;
}
2021-10-30 17:26:41 +00:00
2021-11-07 21:57:09 +00:00
let prefixFileName = padLeft(fileName, ' ', Math.min(this.longestFileNameLength, MAX_FILE_NAME_PADDING));
let prefixLine = padRight(line + '', ' ', LINE_NUM_LENGTH);
let prefixLogType = colorize(level, LoggerLevel[level].charAt(0).toLowerCase());
let prefixTime = moment().format('HH:mm:ss.SSS');
let prefix: string = `${prefixTime} ${prefixFileName}:${prefixLine} ${prefixLogType}`;
2021-10-30 17:26:41 +00:00
let out: string = '';
if (message !== null) {
out += message.split('\n').map(o => `${prefix}: ${o}`).join('\n');
}
2021-11-02 04:29:24 +00:00
let handleData = (data: Error | any) => {
2021-10-30 17:26:41 +00:00
if (data) {
if (message !== null) {
out += '\n';
}
if (data instanceof Error) {
if (data.stack) {
let errorFrames = this.getTSStackFrames(data);
2021-11-02 04:29:24 +00:00
let tsStackLines = errorFrames
.map((frame: StackTrace.Frame) => StackTrace.getFrameLine(frame));
2021-11-02 04:29:24 +00:00
out += `${prefix}# ${data.name}: ${data.message}`;
out += `\n${prefix}# ${tsStackLines.join(`\n${prefix}# `)}`;
//out += `${prefix}# ${data.stack.split('\n').join(`\n${prefix}# `)}`;
2021-10-30 17:26:41 +00:00
} else {
out += `${prefix}# ${data.name}: ${data.message}`;
}
2021-11-02 04:29:24 +00:00
// Use the source map to create a typescript version of the stack
// This will be printed asynchronously because SourceMapConsumer
2021-10-30 17:26:41 +00:00
} else {
2021-11-07 21:57:09 +00:00
let s = util.inspect(data, { colors: true, depth: 2 });
2021-10-30 17:26:41 +00:00
s = s.split('\n').map(o => `${prefix}$ ${o}`).join('\n');
out += s;
}
}
}
if (Array.isArray(data)) {
data.forEach(handleData);
} else {
handleData(data);
}
this.console.log(out);
}
public fatal(message: string | null, data?: Error | any): void { this.log(LoggerLevel.Fatal, message, data); }
public error(message: string | null, data?: Error | any): void { this.log(LoggerLevel.Error, message, data); }
public warn( message: string | null, data?: Error | any): void { this.log(LoggerLevel.Warn, message, data); }
public info( message: string | null, data?: Error | any): void { this.log(LoggerLevel.Info, message, data); }
public debug(message: string | null, data?: Error | any): void { this.log(LoggerLevel.Debug, message, data); }
public silly(message: string | null, data?: Error | any): void { this.log(LoggerLevel.Silly, message, data); }
// Wrapper for util.inspect in case we want to do anything special in the future
2021-10-30 17:26:41 +00:00
public inspect(object: any, showHidden?: boolean, depth?: number, color?: boolean): string {
return util.inspect(object, showHidden, depth, color);
}
}