mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
fix: enhance stdio log (#103)
This commit is contained in:
@@ -17,7 +17,7 @@ const LogViewer: React.FC<LogViewerProps> = ({ logs, isLoading = false, error =
|
|||||||
const [autoScroll, setAutoScroll] = useState(true);
|
const [autoScroll, setAutoScroll] = useState(true);
|
||||||
const [filter, setFilter] = useState<string>('');
|
const [filter, setFilter] = useState<string>('');
|
||||||
const [typeFilter, setTypeFilter] = useState<Array<'info' | 'error' | 'warn' | 'debug'>>(['info', 'error', 'warn', 'debug']);
|
const [typeFilter, setTypeFilter] = useState<Array<'info' | 'error' | 'warn' | 'debug'>>(['info', 'error', 'warn', 'debug']);
|
||||||
const [sourceFilter, setSourceFilter] = useState<Array<'main' | 'child-process'>>(['main', 'child-process']);
|
const [sourceFilter, setSourceFilter] = useState<Array<'main' | 'child'>>(['main', 'child']);
|
||||||
|
|
||||||
// Auto scroll to bottom when new logs come in if autoScroll is enabled
|
// Auto scroll to bottom when new logs come in if autoScroll is enabled
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -30,7 +30,7 @@ const LogViewer: React.FC<LogViewerProps> = ({ logs, isLoading = false, error =
|
|||||||
const filteredLogs = logs.filter(log => {
|
const filteredLogs = logs.filter(log => {
|
||||||
const matchesText = filter ? log.message.toLowerCase().includes(filter.toLowerCase()) : true;
|
const matchesText = filter ? log.message.toLowerCase().includes(filter.toLowerCase()) : true;
|
||||||
const matchesType = typeFilter.includes(log.type);
|
const matchesType = typeFilter.includes(log.type);
|
||||||
const matchesSource = sourceFilter.includes(log.source as 'main' | 'child-process');
|
const matchesSource = sourceFilter.includes(log.source as 'main' | 'child');
|
||||||
return matchesText && matchesType && matchesSource;
|
return matchesText && matchesType && matchesSource;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,10 +48,19 @@ const LogViewer: React.FC<LogViewerProps> = ({ logs, isLoading = false, error =
|
|||||||
// Get badge color based on log type
|
// Get badge color based on log type
|
||||||
const getLogTypeColor = (type: string) => {
|
const getLogTypeColor = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'error': return 'bg-red-500';
|
case 'error': return 'bg-red-400';
|
||||||
case 'warn': return 'bg-yellow-500';
|
case 'warn': return 'bg-yellow-400';
|
||||||
case 'debug': return 'bg-purple-500';
|
case 'debug': return 'bg-purple-400';
|
||||||
default: return 'bg-blue-500';
|
default: return 'bg-blue-400';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get badge color based on log source
|
||||||
|
const getSourceColor = (source: string) => {
|
||||||
|
switch (source) {
|
||||||
|
case 'main': return 'bg-green-400';
|
||||||
|
case 'child': return 'bg-orange-400';
|
||||||
|
default: return 'bg-gray-400';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,11 +101,11 @@ const LogViewer: React.FC<LogViewerProps> = ({ logs, isLoading = false, error =
|
|||||||
|
|
||||||
{/* Log source filters */}
|
{/* Log source filters */}
|
||||||
<div className="flex gap-1 items-center ml-2">
|
<div className="flex gap-1 items-center ml-2">
|
||||||
{(['main', 'child-process'] as const).map(source => (
|
{(['main', 'child'] as const).map(source => (
|
||||||
<Badge
|
<Badge
|
||||||
key={source}
|
key={source}
|
||||||
variant={sourceFilter.includes(source) ? 'default' : 'outline'}
|
variant={sourceFilter.includes(source) ? 'default' : 'outline'}
|
||||||
className="cursor-pointer"
|
className={`cursor-pointer ${sourceFilter.includes(source) ? getSourceColor(source) : ''}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (sourceFilter.includes(source)) {
|
if (sourceFilter.includes(source)) {
|
||||||
setSourceFilter(prev => prev.filter(s => s !== source));
|
setSourceFilter(prev => prev.filter(s => s !== source));
|
||||||
@@ -156,14 +165,17 @@ const LogViewer: React.FC<LogViewerProps> = ({ logs, isLoading = false, error =
|
|||||||
<div
|
<div
|
||||||
key={`${log.timestamp}-${index}`}
|
key={`${log.timestamp}-${index}`}
|
||||||
className={`py-1 border-b border-gray-100 dark:border-gray-800 ${log.type === 'error' ? 'text-red-500' :
|
className={`py-1 border-b border-gray-100 dark:border-gray-800 ${log.type === 'error' ? 'text-red-500' :
|
||||||
log.type === 'warn' ? 'text-yellow-500' : ''
|
log.type === 'warn' ? 'text-yellow-500' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="text-gray-400">[{formatTimestamp(log.timestamp)}]</span>
|
<span className="text-gray-400">[{formatTimestamp(log.timestamp)}]</span>
|
||||||
<Badge className={`ml-2 mr-1 ${getLogTypeColor(log.type)}`}>
|
<Badge className={`ml-2 mr-1 ${getLogTypeColor(log.type)}`}>
|
||||||
{log.type}
|
{log.type}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="outline" className="mr-2">
|
<Badge
|
||||||
|
variant="default"
|
||||||
|
className={`mr-2 ${getSourceColor(log.source)}`}
|
||||||
|
>
|
||||||
{log.source === 'main' ? t('logs.main') : t('logs.child')}
|
{log.source === 'main' ? t('logs.main') : t('logs.child')}
|
||||||
{log.processId ? ` (${log.processId})` : ''}
|
{log.processId ? ` (${log.processId})` : ''}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const colors = {
|
|||||||
blink: '\x1b[5m',
|
blink: '\x1b[5m',
|
||||||
reverse: '\x1b[7m',
|
reverse: '\x1b[7m',
|
||||||
hidden: '\x1b[8m',
|
hidden: '\x1b[8m',
|
||||||
|
|
||||||
black: '\x1b[30m',
|
black: '\x1b[30m',
|
||||||
red: '\x1b[31m',
|
red: '\x1b[31m',
|
||||||
green: '\x1b[32m',
|
green: '\x1b[32m',
|
||||||
@@ -30,7 +30,7 @@ const colors = {
|
|||||||
magenta: '\x1b[35m',
|
magenta: '\x1b[35m',
|
||||||
cyan: '\x1b[36m',
|
cyan: '\x1b[36m',
|
||||||
white: '\x1b[37m',
|
white: '\x1b[37m',
|
||||||
|
|
||||||
bgBlack: '\x1b[40m',
|
bgBlack: '\x1b[40m',
|
||||||
bgRed: '\x1b[41m',
|
bgRed: '\x1b[41m',
|
||||||
bgGreen: '\x1b[42m',
|
bgGreen: '\x1b[42m',
|
||||||
@@ -38,7 +38,7 @@ const colors = {
|
|||||||
bgBlue: '\x1b[44m',
|
bgBlue: '\x1b[44m',
|
||||||
bgMagenta: '\x1b[45m',
|
bgMagenta: '\x1b[45m',
|
||||||
bgCyan: '\x1b[46m',
|
bgCyan: '\x1b[46m',
|
||||||
bgWhite: '\x1b[47m'
|
bgWhite: '\x1b[47m',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Level colors for different log types
|
// Level colors for different log types
|
||||||
@@ -46,7 +46,7 @@ const levelColors = {
|
|||||||
info: colors.green,
|
info: colors.green,
|
||||||
error: colors.red,
|
error: colors.red,
|
||||||
warn: colors.yellow,
|
warn: colors.yellow,
|
||||||
debug: colors.cyan
|
debug: colors.cyan,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Maximum number of logs to keep in memory
|
// Maximum number of logs to keep in memory
|
||||||
@@ -55,7 +55,6 @@ const MAX_LOGS = 1000;
|
|||||||
class LogService {
|
class LogService {
|
||||||
private logs: LogEntry[] = [];
|
private logs: LogEntry[] = [];
|
||||||
private logEmitter = new EventEmitter();
|
private logEmitter = new EventEmitter();
|
||||||
private childProcesses: { [id: string]: ChildProcess } = {};
|
|
||||||
private mainProcessId: string;
|
private mainProcessId: string;
|
||||||
private hostname: string;
|
private hostname: string;
|
||||||
|
|
||||||
@@ -72,12 +71,17 @@ class LogService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Format a log message for console output
|
// Format a log message for console output
|
||||||
private formatLogMessage(type: 'info' | 'error' | 'warn' | 'debug', source: string, message: string, processId?: string): string {
|
private formatLogMessage(
|
||||||
|
type: 'info' | 'error' | 'warn' | 'debug',
|
||||||
|
source: string,
|
||||||
|
message: string,
|
||||||
|
processId?: string,
|
||||||
|
): string {
|
||||||
const timestamp = this.formatTimestamp(Date.now());
|
const timestamp = this.formatTimestamp(Date.now());
|
||||||
const pid = processId || this.mainProcessId;
|
const pid = processId || this.mainProcessId;
|
||||||
const level = type.toUpperCase();
|
const level = type.toUpperCase();
|
||||||
const levelColor = levelColors[type];
|
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}`;
|
return `${colors.dim}[${timestamp}]${colors.reset} ${levelColor}${colors.bright}[${level}]${colors.reset} ${colors.blue}[${pid}]${colors.reset} ${colors.magenta}[${source}]${colors.reset} ${message}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,97 +92,117 @@ class LogService {
|
|||||||
const originalConsoleWarn = console.warn;
|
const originalConsoleWarn = console.warn;
|
||||||
const originalConsoleDebug = console.debug;
|
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[]) => {
|
console.log = (...args: any[]) => {
|
||||||
const message = args.map(arg => this.formatArgument(arg)).join(' ');
|
handleConsoleMethod('info', originalConsoleLog, ...args);
|
||||||
this.addLog('info', 'main', message);
|
|
||||||
originalConsoleLog.apply(console, [this.formatLogMessage('info', 'main', message)]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
console.error = (...args: any[]) => {
|
console.error = (...args: any[]) => {
|
||||||
const message = args.map(arg => this.formatArgument(arg)).join(' ');
|
handleConsoleMethod('error', originalConsoleError, ...args);
|
||||||
this.addLog('error', 'main', message);
|
|
||||||
originalConsoleError.apply(console, [this.formatLogMessage('error', 'main', message)]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
console.warn = (...args: any[]) => {
|
console.warn = (...args: any[]) => {
|
||||||
const message = args.map(arg => this.formatArgument(arg)).join(' ');
|
handleConsoleMethod('warn', originalConsoleWarn, ...args);
|
||||||
this.addLog('warn', 'main', message);
|
|
||||||
originalConsoleWarn.apply(console, [this.formatLogMessage('warn', 'main', message)]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
console.debug = (...args: any[]) => {
|
console.debug = (...args: any[]) => {
|
||||||
const message = args.map(arg => this.formatArgument(arg)).join(' ');
|
handleConsoleMethod('debug', originalConsoleDebug, ...args);
|
||||||
this.addLog('debug', 'main', message);
|
|
||||||
originalConsoleDebug.apply(console, [this.formatLogMessage('debug', 'main', message)]);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format an argument for logging
|
// Format an argument for logging and extract structured information
|
||||||
private formatArgument(arg: any): string {
|
private formatArgument(arg: any): { text: string; source?: string; processId?: string } {
|
||||||
if (arg === null) return 'null';
|
// Handle null and undefined
|
||||||
if (arg === undefined) return 'undefined';
|
if (arg === null) return { text: 'null' };
|
||||||
|
if (arg === undefined) return { text: 'undefined' };
|
||||||
|
|
||||||
|
// Handle objects
|
||||||
if (typeof arg === 'object') {
|
if (typeof arg === 'object') {
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(arg, null, 2);
|
return { text: JSON.stringify(arg, null, 2) };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return String(arg);
|
return { text: String(arg) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 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
|
// Add a log entry to the logs array
|
||||||
private addLog(type: 'info' | 'error' | 'warn' | 'debug', source: string, message: string, processId?: string) {
|
private addLog(
|
||||||
|
type: 'info' | 'error' | 'warn' | 'debug',
|
||||||
|
source: string,
|
||||||
|
message: string,
|
||||||
|
processId?: string,
|
||||||
|
) {
|
||||||
const log: LogEntry = {
|
const log: LogEntry = {
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
type,
|
type,
|
||||||
source,
|
source,
|
||||||
message,
|
message,
|
||||||
processId: processId || this.mainProcessId
|
processId: processId || this.mainProcessId,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.logs.push(log);
|
this.logs.push(log);
|
||||||
|
|
||||||
// Limit the number of logs kept in memory
|
// Limit the number of logs kept in memory
|
||||||
if (this.logs.length > MAX_LOGS) {
|
if (this.logs.length > MAX_LOGS) {
|
||||||
this.logs.shift();
|
this.logs.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit the log event for SSE subscribers
|
// Emit the log event for SSE subscribers
|
||||||
this.logEmitter.emit('log', log);
|
this.logEmitter.emit('log', log);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture output from a child process
|
|
||||||
public captureChildProcess(command: string, args: string[], processId: string): ChildProcess {
|
|
||||||
const childProcess = spawn(command, args);
|
|
||||||
this.childProcesses[processId] = childProcess;
|
|
||||||
|
|
||||||
childProcess.stdout.on('data', (data) => {
|
|
||||||
const output = data.toString().trim();
|
|
||||||
if (output) {
|
|
||||||
this.addLog('info', 'child-process', output, processId);
|
|
||||||
console.log(this.formatLogMessage('info', 'child-process', output, processId));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
childProcess.stderr.on('data', (data) => {
|
|
||||||
const output = data.toString().trim();
|
|
||||||
if (output) {
|
|
||||||
this.addLog('error', 'child-process', output, processId);
|
|
||||||
console.error(this.formatLogMessage('error', 'child-process', output, processId));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
childProcess.on('close', (code) => {
|
|
||||||
const message = `Process exited with code ${code}`;
|
|
||||||
this.addLog('info', 'child-process', message, processId);
|
|
||||||
console.log(this.formatLogMessage('info', 'child-process', message, processId));
|
|
||||||
delete this.childProcesses[processId];
|
|
||||||
});
|
|
||||||
|
|
||||||
return childProcess;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all logs
|
// Get all logs
|
||||||
public getLogs(): LogEntry[] {
|
public getLogs(): LogEntry[] {
|
||||||
return this.logs;
|
return this.logs;
|
||||||
@@ -201,4 +225,4 @@ class LogService {
|
|||||||
|
|
||||||
// Export a singleton instance
|
// Export a singleton instance
|
||||||
const logService = new LogService();
|
const logService = new LogService();
|
||||||
export default logService;
|
export default logService;
|
||||||
|
|||||||
@@ -114,9 +114,10 @@ export const initializeClientsFromSettings = (isInit: boolean): ServerInfo[] =>
|
|||||||
command: conf.command,
|
command: conf.command,
|
||||||
args: conf.args,
|
args: conf.args,
|
||||||
env: env,
|
env: env,
|
||||||
|
stderr: 'pipe',
|
||||||
});
|
});
|
||||||
transport.stderr?.on('data', (data) => {
|
transport.stderr?.on('data', (data) => {
|
||||||
console.error(`Error from server ${name}: ${data}`);
|
console.log(`[${name}] [child] ${data}`);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Skipping server '${name}': missing required configuration`);
|
console.warn(`Skipping server '${name}': missing required configuration`);
|
||||||
|
|||||||
Reference in New Issue
Block a user