Spaces:
Paused
Paused
File size: 3,426 Bytes
35743bd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | /**
* Console Log Interceptor — captures console output to a log file.
*
* Monkey-patches console.log, console.info, console.warn, console.error,
* and console.debug to also append JSON log entries to a file. This allows
* the Console Log Viewer to display application logs in real-time.
*
* Call initConsoleInterceptor() once at server startup (before any logging).
*
* @module lib/consoleInterceptor
*/
import { appendFileSync, existsSync, mkdirSync } from "fs";
import { dirname, resolve } from "path";
import { getAppLogFilePath, getAppLogToFile } from "./logEnv";
const logToFile = getAppLogToFile();
const logFilePath = resolve(getAppLogFilePath());
declare global {
var __omnirouteConsoleInterceptorInit: boolean | undefined;
}
/**
* Map console method names to log levels.
*/
const LEVEL_MAP: Record<string, string> = {
debug: "debug",
log: "info",
info: "info",
warn: "warn",
error: "error",
};
/**
* Ensure the log directory exists.
*/
function ensureDir() {
const dir = dirname(logFilePath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
}
/**
* Try to extract component name from message patterns like [COMPONENT] or [component].
*/
function extractComponent(msg: string): string {
const match = msg.match(/^\[([^\]]+)\]/);
return match ? match[1] : "app";
}
/**
* Convert arguments to a string message, handling objects and errors.
*/
function argsToMessage(args: unknown[]): string {
return args
.map((arg) => {
if (arg instanceof Error) return `${arg.message}\n${arg.stack || ""}`;
if (typeof arg === "object" && arg !== null) {
try {
return JSON.stringify(arg);
} catch {
return String(arg);
}
}
return String(arg);
})
.join(" ");
}
/**
* Append a JSON log entry to the log file.
*/
function writeEntry(level: string, args: unknown[]) {
try {
const message = argsToMessage(args);
const entry = {
timestamp: new Date().toISOString(),
level,
component: extractComponent(message),
message,
};
appendFileSync(logFilePath, JSON.stringify(entry) + "\n");
} catch {
// Silently fail — never break the app over log writing
}
}
/**
* Initialize the console interceptor.
* Patches console.log, console.info, console.warn, console.error, console.debug
* to also write to the log file.
*
* Safe to call multiple times — only initializes once.
*/
export function initConsoleInterceptor(): void {
if (!logToFile || globalThis.__omnirouteConsoleInterceptorInit) return;
try {
ensureDir();
} catch {
// Can't create log dir — skip interception
return;
}
globalThis.__omnirouteConsoleInterceptorInit = true;
// Save original methods
const originalMethods = {
log: console.log.bind(console),
info: console.info.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console),
debug: console.debug.bind(console),
};
// Patch each console method
for (const [method, level] of Object.entries(LEVEL_MAP)) {
const original = originalMethods[method as keyof typeof originalMethods];
if (!original) continue;
(console as unknown as Record<string, unknown>)[method] = (...args: unknown[]) => {
// Call original console method
original(...args);
// Also write to file
writeEntry(level, args);
};
}
}
|