feat: better log handling

This commit is contained in:
Strix 2025-02-05 11:24:37 +01:00
parent 4b00caf7d4
commit 3fd3881d70
8 changed files with 108 additions and 80 deletions

View file

@ -1,45 +1,24 @@
import { statSync } from "node:fs";
import { IDispatch, IDispatchConfig } from "./IDispatch";
import { ILogItem } from "./ILogItem";
import { LogLevel } from "./LogLevel";
export class Dispatch implements IDispatch {
private dispatches: IDispatch[] = [];
export abstract class Dispatch implements IDispatch {
children: IDispatch[] = [];
constructor(config: IDispatchConfig = {}) {}
trace(...args: any[]): void {
this.process({ timestamp: new Date(), level: LogLevel.Trace, data: args });
}
debug(...args: any[]): void {
this.process({ timestamp: new Date(), level: LogLevel.Debug, data: args });
}
info(...args: any[]): void {
this.process({ timestamp: new Date(), level: LogLevel.Info, data: args });
}
warn(...args: any[]): void {
this.process({ timestamp: new Date(), level: LogLevel.Warn, data: args });
}
error(...args: any[]): void {
this.process({ timestamp: new Date(), level: LogLevel.Error, data: args });
}
fatal(...args: any[]): void {
this.process({ timestamp: new Date(), level: LogLevel.Fatal, data: args });
}
constructor(config: IDispatchConfig = {}) { }
process(item: ILogItem): void {
for (const dispatch of this.dispatches) {
this.output(item);
}
output(item: ILogItem): void {
for (const dispatch of this.children) {
dispatch.process(item);
}
}
add(dispatch: IDispatch): Dispatch {
this.dispatches.push(dispatch);
chain(dispatch: IDispatch): Dispatch {
this.children.push(dispatch);
return this;
}
}

View file

@ -2,6 +2,7 @@ import * as fs from "fs";
import { FormattedDispatch } from "./FormattedDispatch";
import { App } from "../App";
import { ILogItem } from "./ILogItem";
import { LOGE, LOGW } from "./common";
export class FileDispatch extends FormattedDispatch {
path: string;
@ -12,9 +13,7 @@ export class FileDispatch extends FormattedDispatch {
this.path = path;
this.stream = fs.createWriteStream(path, { flags: "a" });
this.stream.on("error", (err) => {
App.logger.error(`Error writing to file: ${this.path}`, err);
});
this.stream.on("error", (err) => LOGE(`Error writing to file: ${this.path}`, err));
}
process(item: ILogItem): void {

View file

@ -1,34 +1,46 @@
import { object } from "zod";
import { Dispatch } from "./Dispatch";
import { ILogItem } from "./ILogItem";
import { LogLevel } from "./LogLevel";
export class FormattedDispatch extends Dispatch {
private formatString: string = "%t %l <%o>: %m";
private levelFixedLength = Object.keys(LogLevel).reduce((acc, cur) => cur.length > acc ? cur.length : acc, 0)
private fmtPart(x: any) {
switch (typeof x) {
case "string":
case "number":
return x;
case "object":
return JSON.stringify(x);
default:
return x;
}
}
format(item: ILogItem): string {
let data;
// Array to string
if (Array.isArray(item.data))
data = item.data
.map((item: any) => this.fmtPart(item))
.join(" ");
else data = this.fmtPart(item.data);
return this.formatString
.replace("%t", item.timestamp.toISOString())
.replace("%l", LogLevel[item.level])
.replace("%l", LogLevel[item.level].padEnd(this.levelFixedLength))
.replace("%o", item.origin || "")
.replace(
"%m",
item.data
.map((item: any) => {
switch (typeof item) {
case "string":
case "number":
return item;
case "object":
return JSON.stringify(item);
default:
return item;
}
})
.join(" "),
);
data);
}
process(item: ILogItem): void {
if (item.flags?.includes("skip_formatting")) return super.process(item);
super.process({
...item,
data: this.format(item),

View file

@ -3,5 +3,7 @@ import { ILogItem } from "./ILogItem";
export interface IDispatchConfig {}
export interface IDispatch {
process(item: ILogItem): void;
chain(dispatch: IDispatch): void; // add dispatches
process(item: ILogItem): void; // process a line; format, stats, etc.
output(item: ILogItem): void; // output a line
}

View file

@ -3,6 +3,7 @@ import { LogLevel } from "./LogLevel";
export interface ILogItem {
timestamp: Date;
origin?: string;
flags?: string[];
level: LogLevel;
data: any;
}

View file

@ -1,44 +1,54 @@
import { statSync } from "fs";
import { Dispatch } from "./Dispatch";
import { ILogItem } from "./ILogItem";
import { LogLevel } from "./LogLevel";
export class SmartDispatch extends Dispatch {
process(item: ILogItem) {
let err = new Error();
// we need to look through the stack and keep track of intermediate dispatches
let stack = err.stack!.split("\n");
let i = 0;
while (!stack[i].includes("at SmartDispatch.process")) i++;
let src = stack[i + 2].trim();
let path = null;
let origin = item.origin;
if (!item.origin) {
let err = new Error();
// we need to look through the stack and keep track of intermediate dispatches
let stack = err.stack!.split("\n");
let i = 0;
while (!stack[i].includes("at SmartDispatch.process")) i++;
// now we need to format the src (at ...:..:.. or at App.<anonymous> ..., etc)
if (src.includes("at ")) src = src.split("at ")[1];
if (src.includes(" (")) src = src.split(" (")[1];
if (src.includes(")")) src = src.split(")")[0];
let src = stack[i + 2].trim();
let path = null;
let file = {
path: src.split(":")[0],
line: src.split(":")[1],
column: src.split(":")[2],
};
// now we need to format the src (at ...:..:.. or at App.<anonymous> ..., etc)
if (src.includes("at ")) src = src.split("at ")[1];
if (src.includes(" (")) src = src.split(" (")[1];
if (src.includes(")")) src = src.split(")")[0];
try {
path = statSync(file.path).isFile() ? file.path : null;
} catch (e) {
path = null;
let file = {
path: src.split(":")[0],
line: src.split(":")[1],
column: src.split(":")[2],
};
try {
path = statSync(file.path).isFile() ? file.path : null;
} catch (e) {
path = null;
}
if (path) {
// trim the path to the project root
if (path.startsWith(process.cwd()))
path = "." + path.slice(process.cwd().length);
src = `${path}:${file.line}:${file.column}`;
}
origin = src;
}
if (path) {
// trim the path to the project root
if (path.startsWith(process.cwd()))
path = "." + path.slice(process.cwd().length);
src = `${path}:${file.line}:${file.column}`;
}
if (!item.level) item.level = LogLevel.Trace;
if (!item.timestamp) item.timestamp = new Date();
super.process({
...item,
origin: src,
origin
});
}
}

View file

@ -4,8 +4,7 @@ import { ILogItem } from "./ILogItem";
import { LogLevel } from "./LogLevel";
export class StdoutDispatch extends FormattedDispatch {
process(item: ILogItem) {
process.stdout.write(this.format(item) + "\n");
super.process(item);
output(item: ILogItem) {
process.stdout.write(item.data + "\n");
}
}

26
common.ts Normal file
View file

@ -0,0 +1,26 @@
import { Dispatch } from "./Dispatch";
import { ILogItem } from "./ILogItem";
import { LogLevel } from "./LogLevel";
let CURRENT_DISPATCH: Dispatch = new class extends Dispatch {
process(item: ILogItem): void {
console.log(item.data);
}
};
export const setDispatch = (dispatch: Dispatch) => CURRENT_DISPATCH = dispatch;
export const LOGT = (...args) => CURRENT_DISPATCH.process({ timestamp: new Date(), level: LogLevel.Trace, data: args });
export const LOGD = (...args) => CURRENT_DISPATCH.process({ timestamp: new Date(), level: LogLevel.Debug, data: args });
export const LOGI = (...args) => CURRENT_DISPATCH.process({ timestamp: new Date(), level: LogLevel.Info, data: args });
export const LOGW = (...args) => CURRENT_DISPATCH.process({ timestamp: new Date(), level: LogLevel.Warn, data: args });
export const LOGE = (...args) => CURRENT_DISPATCH.process({ timestamp: new Date(), level: LogLevel.Error, data: args });
export const LOGF = (...args) => CURRENT_DISPATCH.process({ timestamp: new Date(), level: LogLevel.Fatal, data: args });
export const LOG = (item: Partial<ILogItem>) => CURRENT_DISPATCH.process({
flags: item.flags ?? [],
origin: item.origin ?? "unknown",
level: item.level ?? LogLevel.Trace,
timestamp: item.timestamp ?? new Date(),
data: item.data ?? ""
})