File size: 6,488 Bytes
eb846d0 |
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 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 |
// filepath: /Users/sunmeng/code/github/mcphub/src/services/logService.ts
import { spawn, ChildProcess } from 'child_process';
import { EventEmitter } from 'events';
import * as os from 'os';
import * as process from 'process';
interface LogEntry {
timestamp: number;
type: 'info' | 'error' | 'warn' | 'debug';
source: string;
message: string;
processId?: string;
}
// ANSI color codes for console output
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
dim: '\x1b[2m',
underscore: '\x1b[4m',
blink: '\x1b[5m',
reverse: '\x1b[7m',
hidden: '\x1b[8m',
black: '\x1b[30m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
bgBlack: '\x1b[40m',
bgRed: '\x1b[41m',
bgGreen: '\x1b[42m',
bgYellow: '\x1b[43m',
bgBlue: '\x1b[44m',
bgMagenta: '\x1b[45m',
bgCyan: '\x1b[46m',
bgWhite: '\x1b[47m',
};
// Level colors for different log types
const levelColors = {
info: colors.green,
error: colors.red,
warn: colors.yellow,
debug: colors.cyan,
};
// Maximum number of logs to keep in memory
const MAX_LOGS = 1000;
class LogService {
private logs: LogEntry[] = [];
private logEmitter = new EventEmitter();
private mainProcessId: string;
private hostname: string;
constructor() {
this.mainProcessId = process.pid.toString();
this.hostname = os.hostname();
this.overrideConsole();
}
// Format a timestamp for display
private formatTimestamp(timestamp: number): string {
const date = new Date(timestamp);
return date.toISOString();
}
// Format a log message for console output
private formatLogMessage(
type: 'info' | 'error' | 'warn' | 'debug',
source: string,
message: string,
processId?: string,
): string {
const timestamp = this.formatTimestamp(Date.now());
const pid = processId || this.mainProcessId;
const level = type.toUpperCase();
const levelColor = levelColors[type];
return `${colors.dim}[${timestamp}]${colors.reset} ${levelColor}${colors.bright}[${level}]${colors.reset} ${colors.blue}[${pid}]${colors.reset} ${colors.magenta}[${source}]${colors.reset} ${message}`;
}
// Override console methods to capture logs
private overrideConsole() {
const originalConsoleLog = console.log;
const originalConsoleError = console.error;
const originalConsoleWarn = console.warn;
const originalConsoleDebug = console.debug;
// Helper method to handle common logic for all console methods
const handleConsoleMethod = (
type: 'info' | 'error' | 'warn' | 'debug',
originalMethod: (...args: any[]) => void,
...args: any[]
) => {
const firstArg = args.length > 0 ? this.formatArgument(args[0]) : { text: '' };
const remainingArgs = args.slice(1).map((arg) => this.formatArgument(arg).text);
const combinedMessage = [firstArg.text, ...remainingArgs].join(' ');
const source = firstArg.source || 'main';
const processId = firstArg.processId;
this.addLog(type, source, combinedMessage, processId);
originalMethod.apply(console, [
this.formatLogMessage(type, source, combinedMessage, processId),
]);
};
console.log = (...args: any[]) => {
handleConsoleMethod('info', originalConsoleLog, ...args);
};
console.error = (...args: any[]) => {
handleConsoleMethod('error', originalConsoleError, ...args);
};
console.warn = (...args: any[]) => {
handleConsoleMethod('warn', originalConsoleWarn, ...args);
};
console.debug = (...args: any[]) => {
handleConsoleMethod('debug', originalConsoleDebug, ...args);
};
}
// Format an argument for logging and extract structured information
private formatArgument(arg: any): { text: string; source?: string; processId?: string } {
// Handle null and undefined
if (arg === null) return { text: 'null' };
if (arg === undefined) return { text: 'undefined' };
// Handle objects
if (typeof arg === 'object') {
try {
return { text: JSON.stringify(arg, null, 2) };
} catch (e) {
return { text: String(arg) };
}
}
// Handle strings with potential structured information
const argStr = String(arg);
// Check for patterns like [processId] [source] message or [processId] [source-processId] message
const structuredPattern = /^\s*\[([^\]]+)\]\s*\[([^\]]+)\]\s*(.*)/;
const match = argStr.match(structuredPattern);
if (match) {
const [_, firstBracket, secondBracket, remainingText] = match;
// Check if the second bracket has a format like 'source-processId'
const sourcePidPattern = /^([^-]+)-(.+)$/;
const sourcePidMatch = secondBracket.match(sourcePidPattern);
if (sourcePidMatch) {
// If we have a 'source-processId' format in the second bracket
const [_, source, extractedProcessId] = sourcePidMatch;
return {
text: remainingText.trim(),
source: source.trim(),
processId: firstBracket.trim(),
};
}
// Otherwise treat first bracket as processId and second as source
return {
text: remainingText.trim(),
source: secondBracket.trim(),
processId: firstBracket.trim(),
};
}
// Return original string if no structured format is detected
return { text: argStr };
}
// Add a log entry to the logs array
private addLog(
type: 'info' | 'error' | 'warn' | 'debug',
source: string,
message: string,
processId?: string,
) {
const log: LogEntry = {
timestamp: Date.now(),
type,
source,
message,
processId: processId || this.mainProcessId,
};
this.logs.push(log);
// Limit the number of logs kept in memory
if (this.logs.length > MAX_LOGS) {
this.logs.shift();
}
// Emit the log event for SSE subscribers
this.logEmitter.emit('log', log);
}
// Get all logs
public getLogs(): LogEntry[] {
return this.logs;
}
// Subscribe to log events
public subscribe(callback: (log: LogEntry) => void): () => void {
this.logEmitter.on('log', callback);
return () => {
this.logEmitter.off('log', callback);
};
}
// Clear all logs
public clearLogs(): void {
this.logs = [];
this.logEmitter.emit('clear');
}
}
// Export a singleton instance
const logService = new LogService();
export default logService;
|