feat: better log handling
This commit is contained in:
parent
4b00caf7d4
commit
3fd3881d70
8 changed files with 108 additions and 80 deletions
41
Dispatch.ts
41
Dispatch.ts
|
@ -1,45 +1,24 @@
|
||||||
import { statSync } from "node:fs";
|
|
||||||
import { IDispatch, IDispatchConfig } from "./IDispatch";
|
import { IDispatch, IDispatchConfig } from "./IDispatch";
|
||||||
import { ILogItem } from "./ILogItem";
|
import { ILogItem } from "./ILogItem";
|
||||||
import { LogLevel } from "./LogLevel";
|
import { LogLevel } from "./LogLevel";
|
||||||
|
|
||||||
export class Dispatch implements IDispatch {
|
export abstract class Dispatch implements IDispatch {
|
||||||
private dispatches: IDispatch[] = [];
|
children: IDispatch[] = [];
|
||||||
|
|
||||||
constructor(config: IDispatchConfig = {}) {}
|
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 });
|
|
||||||
}
|
|
||||||
|
|
||||||
process(item: ILogItem): void {
|
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);
|
dispatch.process(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
add(dispatch: IDispatch): Dispatch {
|
chain(dispatch: IDispatch): Dispatch {
|
||||||
this.dispatches.push(dispatch);
|
this.children.push(dispatch);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import * as fs from "fs";
|
||||||
import { FormattedDispatch } from "./FormattedDispatch";
|
import { FormattedDispatch } from "./FormattedDispatch";
|
||||||
import { App } from "../App";
|
import { App } from "../App";
|
||||||
import { ILogItem } from "./ILogItem";
|
import { ILogItem } from "./ILogItem";
|
||||||
|
import { LOGE, LOGW } from "./common";
|
||||||
|
|
||||||
export class FileDispatch extends FormattedDispatch {
|
export class FileDispatch extends FormattedDispatch {
|
||||||
path: string;
|
path: string;
|
||||||
|
@ -12,9 +13,7 @@ export class FileDispatch extends FormattedDispatch {
|
||||||
this.path = path;
|
this.path = path;
|
||||||
this.stream = fs.createWriteStream(path, { flags: "a" });
|
this.stream = fs.createWriteStream(path, { flags: "a" });
|
||||||
|
|
||||||
this.stream.on("error", (err) => {
|
this.stream.on("error", (err) => LOGE(`Error writing to file: ${this.path}`, err));
|
||||||
App.logger.error(`Error writing to file: ${this.path}`, err);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process(item: ILogItem): void {
|
process(item: ILogItem): void {
|
||||||
|
|
|
@ -1,34 +1,46 @@
|
||||||
|
import { object } from "zod";
|
||||||
import { Dispatch } from "./Dispatch";
|
import { Dispatch } from "./Dispatch";
|
||||||
import { ILogItem } from "./ILogItem";
|
import { ILogItem } from "./ILogItem";
|
||||||
import { LogLevel } from "./LogLevel";
|
import { LogLevel } from "./LogLevel";
|
||||||
|
|
||||||
export class FormattedDispatch extends Dispatch {
|
export class FormattedDispatch extends Dispatch {
|
||||||
private formatString: string = "%t %l <%o>: %m";
|
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 {
|
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
|
return this.formatString
|
||||||
.replace("%t", item.timestamp.toISOString())
|
.replace("%t", item.timestamp.toISOString())
|
||||||
.replace("%l", LogLevel[item.level])
|
.replace("%l", LogLevel[item.level].padEnd(this.levelFixedLength))
|
||||||
.replace("%o", item.origin || "")
|
.replace("%o", item.origin || "")
|
||||||
.replace(
|
.replace(
|
||||||
"%m",
|
"%m",
|
||||||
item.data
|
data);
|
||||||
.map((item: any) => {
|
|
||||||
switch (typeof item) {
|
|
||||||
case "string":
|
|
||||||
case "number":
|
|
||||||
return item;
|
|
||||||
case "object":
|
|
||||||
return JSON.stringify(item);
|
|
||||||
default:
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.join(" "),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process(item: ILogItem): void {
|
process(item: ILogItem): void {
|
||||||
|
if (item.flags?.includes("skip_formatting")) return super.process(item);
|
||||||
super.process({
|
super.process({
|
||||||
...item,
|
...item,
|
||||||
data: this.format(item),
|
data: this.format(item),
|
||||||
|
|
|
@ -3,5 +3,7 @@ import { ILogItem } from "./ILogItem";
|
||||||
export interface IDispatchConfig {}
|
export interface IDispatchConfig {}
|
||||||
|
|
||||||
export interface IDispatch {
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { LogLevel } from "./LogLevel";
|
||||||
export interface ILogItem {
|
export interface ILogItem {
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
origin?: string;
|
origin?: string;
|
||||||
|
flags?: string[];
|
||||||
level: LogLevel;
|
level: LogLevel;
|
||||||
data: any;
|
data: any;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,44 +1,54 @@
|
||||||
import { statSync } from "fs";
|
import { statSync } from "fs";
|
||||||
import { Dispatch } from "./Dispatch";
|
import { Dispatch } from "./Dispatch";
|
||||||
import { ILogItem } from "./ILogItem";
|
import { ILogItem } from "./ILogItem";
|
||||||
|
import { LogLevel } from "./LogLevel";
|
||||||
|
|
||||||
export class SmartDispatch extends Dispatch {
|
export class SmartDispatch extends Dispatch {
|
||||||
process(item: ILogItem) {
|
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 origin = item.origin;
|
||||||
let path = null;
|
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)
|
let src = stack[i + 2].trim();
|
||||||
if (src.includes("at ")) src = src.split("at ")[1];
|
let path = null;
|
||||||
if (src.includes(" (")) src = src.split(" (")[1];
|
|
||||||
if (src.includes(")")) src = src.split(")")[0];
|
|
||||||
|
|
||||||
let file = {
|
// now we need to format the src (at ...:..:.. or at App.<anonymous> ..., etc)
|
||||||
path: src.split(":")[0],
|
if (src.includes("at ")) src = src.split("at ")[1];
|
||||||
line: src.split(":")[1],
|
if (src.includes(" (")) src = src.split(" (")[1];
|
||||||
column: src.split(":")[2],
|
if (src.includes(")")) src = src.split(")")[0];
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
let file = {
|
||||||
path = statSync(file.path).isFile() ? file.path : null;
|
path: src.split(":")[0],
|
||||||
} catch (e) {
|
line: src.split(":")[1],
|
||||||
path = null;
|
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) {
|
if (!item.level) item.level = LogLevel.Trace;
|
||||||
// trim the path to the project root
|
if (!item.timestamp) item.timestamp = new Date();
|
||||||
if (path.startsWith(process.cwd()))
|
|
||||||
path = "." + path.slice(process.cwd().length);
|
|
||||||
src = `${path}:${file.line}:${file.column}`;
|
|
||||||
}
|
|
||||||
super.process({
|
super.process({
|
||||||
...item,
|
...item,
|
||||||
origin: src,
|
origin
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,7 @@ import { ILogItem } from "./ILogItem";
|
||||||
import { LogLevel } from "./LogLevel";
|
import { LogLevel } from "./LogLevel";
|
||||||
|
|
||||||
export class StdoutDispatch extends FormattedDispatch {
|
export class StdoutDispatch extends FormattedDispatch {
|
||||||
process(item: ILogItem) {
|
output(item: ILogItem) {
|
||||||
process.stdout.write(this.format(item) + "\n");
|
process.stdout.write(item.data + "\n");
|
||||||
super.process(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
26
common.ts
Normal file
26
common.ts
Normal 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 ?? ""
|
||||||
|
})
|
Loading…
Reference in a new issue