figma-restoration-mcp-vue-tools
Version:
Professional Figma Component Restoration Kit - MCP tools with snapDOM-powered high-quality screenshots, intelligent shadow detection, and advanced diff analysis for Vue component restoration. Features enhanced figma_compare with color-coded region analysi
815 lines (684 loc) • 24.3 kB
JavaScript
import chalk from 'chalk';
/**
* OperationLogger - Enhanced logging with performance monitoring for MCP tools
*/
export class OperationLogger {
constructor(toolName, options = {}) {
this.toolName = toolName;
this.options = {
verbose: options.verbose || false,
logLevel: options.logLevel || 'info', // debug, info, warn, error
includeTimestamp: options.includeTimestamp !== false,
includePerformance: options.includePerformance !== false,
maxLogLength: options.maxLogLength || 1000
};
this.operations = new Map();
}
/**
* Log operation start
* @param {string} operationId - Unique operation identifier
* @param {string} operation - Operation name
* @param {Object} args - Operation arguments
*/
logStart(operationId, operation, args = {}) {
const startTime = Date.now();
const sanitizedArgs = this.sanitizeArgs(args);
this.operations.set(operationId, {
operation,
args: sanitizedArgs,
startTime,
status: 'running'
});
if (this.shouldLog('info')) {
const timestamp = this.getTimestamp();
console.log(chalk.blue(`${timestamp}🚀 [${this.toolName}] Starting ${operation}`));
if (this.options.verbose && Object.keys(sanitizedArgs).length > 0) {
console.log(chalk.gray(`${timestamp} Parameters: ${JSON.stringify(sanitizedArgs)}`));
}
}
}
/**
* Log operation success
* @param {string} operationId - Operation identifier
* @param {Object} result - Operation result
* @param {Object} metrics - Additional metrics
*/
logSuccess(operationId, result = {}, metrics = {}) {
const operation = this.operations.get(operationId);
if (!operation) {
this.logError(operationId, new Error('Operation not found for success logging'));
return;
}
const endTime = Date.now();
const duration = endTime - operation.startTime;
operation.status = 'completed';
operation.endTime = endTime;
operation.duration = duration;
operation.result = this.sanitizeResult(result);
operation.metrics = metrics;
if (this.shouldLog('info')) {
const timestamp = this.getTimestamp();
console.log(chalk.green(`${timestamp}✅ [${this.toolName}] Completed ${operation.operation}`));
if (this.options.includePerformance) {
console.log(chalk.gray(`${timestamp} Duration: ${duration}ms`));
if (Object.keys(metrics).length > 0) {
console.log(chalk.gray(`${timestamp} Metrics: ${JSON.stringify(metrics)}`));
}
}
if (this.options.verbose && result.success !== undefined) {
console.log(chalk.gray(`${timestamp} Success: ${result.success}`));
}
}
// Clean up old operations to prevent memory leaks
this.cleanupOldOperations();
}
/**
* Log operation error
* @param {string} operationId - Operation identifier
* @param {Error} error - Error that occurred
* @param {Object} context - Additional error context
*/
logError(operationId, error, context = {}) {
const operation = this.operations.get(operationId);
const endTime = Date.now();
const duration = operation ? endTime - operation.startTime : 0;
if (operation) {
operation.status = 'failed';
operation.endTime = endTime;
operation.duration = duration;
operation.error = {
message: error.message,
name: error.name,
code: error.code,
stack: error.stack
};
operation.context = context;
}
if (this.shouldLog('error')) {
const timestamp = this.getTimestamp();
const operationName = operation ? operation.operation : 'Unknown Operation';
console.error(chalk.red(`${timestamp}❌ [${this.toolName}] Failed ${operationName}`));
console.error(chalk.red(`${timestamp} Error: ${error.message}`));
if (this.options.includePerformance && duration > 0) {
console.error(chalk.gray(`${timestamp} Duration: ${duration}ms`));
}
if (this.options.verbose && error.stack) {
console.error(chalk.gray(`${timestamp} Stack: ${this.truncateText(error.stack)}`));
}
if (Object.keys(context).length > 0) {
console.error(chalk.gray(`${timestamp} Context: ${JSON.stringify(context)}`));
}
}
}
/**
* Log operation timeout
* @param {string} operationId - Operation identifier
* @param {number} timeoutMs - Timeout duration
*/
logTimeout(operationId, timeoutMs) {
const operation = this.operations.get(operationId);
const endTime = Date.now();
const duration = operation ? endTime - operation.startTime : timeoutMs;
if (operation) {
operation.status = 'timeout';
operation.endTime = endTime;
operation.duration = duration;
}
if (this.shouldLog('warn')) {
const timestamp = this.getTimestamp();
const operationName = operation ? operation.operation : 'Unknown Operation';
console.warn(chalk.yellow(`${timestamp}⏰ [${this.toolName}] Timeout ${operationName}`));
console.warn(chalk.yellow(`${timestamp} Timeout: ${timeoutMs}ms, Actual: ${duration}ms`));
}
}
/**
* Log operation retry
* @param {string} operationId - Operation identifier
* @param {number} attempt - Retry attempt number
* @param {Error} error - Error that caused retry
*/
logRetry(operationId, attempt, error) {
const operation = this.operations.get(operationId);
if (operation) {
if (!operation.retries) {
operation.retries = [];
}
operation.retries.push({
attempt,
error: error.message,
timestamp: Date.now()
});
}
if (this.shouldLog('warn')) {
const timestamp = this.getTimestamp();
const operationName = operation ? operation.operation : 'Unknown Operation';
console.warn(chalk.yellow(`${timestamp}🔄 [${this.toolName}] Retry ${operationName} (attempt ${attempt})`));
console.warn(chalk.yellow(`${timestamp} Reason: ${error.message}`));
}
}
/**
* Log debug information
* @param {string} message - Debug message
* @param {Object} data - Additional debug data
*/
logDebug(message, data = {}) {
if (this.shouldLog('debug')) {
const timestamp = this.getTimestamp();
console.log(chalk.gray(`${timestamp}🔍 [${this.toolName}] ${message}`));
if (Object.keys(data).length > 0) {
console.log(chalk.gray(`${timestamp} Data: ${JSON.stringify(data)}`));
}
}
}
/**
* Log warning
* @param {string} message - Warning message
* @param {Object} data - Additional warning data
*/
logWarning(message, data = {}) {
if (this.shouldLog('warn')) {
const timestamp = this.getTimestamp();
console.warn(chalk.yellow(`${timestamp}⚠️ [${this.toolName}] ${message}`));
if (Object.keys(data).length > 0) {
console.warn(chalk.yellow(`${timestamp} Data: ${JSON.stringify(data)}`));
}
}
}
/**
* Get operation statistics
* @returns {Object} - Operation statistics
*/
getStatistics() {
const operations = Array.from(this.operations.values());
const completed = operations.filter(op => op.status === 'completed');
const failed = operations.filter(op => op.status === 'failed');
const timeout = operations.filter(op => op.status === 'timeout');
const running = operations.filter(op => op.status === 'running');
const durations = completed.map(op => op.duration).filter(d => d > 0);
const avgDuration = durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0;
const maxDuration = durations.length > 0 ? Math.max(...durations) : 0;
const minDuration = durations.length > 0 ? Math.min(...durations) : 0;
return {
total: operations.length,
completed: completed.length,
failed: failed.length,
timeout: timeout.length,
running: running.length,
successRate: operations.length > 0 ? (completed.length / operations.length) * 100 : 0,
performance: {
avgDuration: Math.round(avgDuration),
maxDuration,
minDuration,
totalDuration: durations.reduce((a, b) => a + b, 0)
}
};
}
/**
* Log step-by-step operation details (verbose mode)
* @param {string} operationId - Operation identifier
* @param {string} step - Step name
* @param {Object} stepData - Step data
*/
logStep(operationId, step, stepData = {}) {
const operation = this.operations.get(operationId);
if (operation) {
if (!operation.steps) {
operation.steps = [];
}
operation.steps.push({
step,
data: stepData,
timestamp: Date.now()
});
}
if (this.options.verbose && this.shouldLog('debug')) {
const timestamp = this.getTimestamp();
console.log(chalk.cyan(`${timestamp}🔍 [${this.toolName}] Step: ${step}`));
if (Object.keys(stepData).length > 0) {
console.log(chalk.gray(`${timestamp} Step Data: ${JSON.stringify(stepData)}`));
}
}
}
/**
* Log performance metrics
* @param {string} operationId - Operation identifier
* @param {Object} metrics - Performance metrics
*/
logMetrics(operationId, metrics) {
const operation = this.operations.get(operationId);
if (operation) {
if (!operation.performanceMetrics) {
operation.performanceMetrics = [];
}
operation.performanceMetrics.push({
...metrics,
timestamp: Date.now()
});
}
if (this.options.includePerformance && this.shouldLog('debug')) {
const timestamp = this.getTimestamp();
console.log(chalk.magenta(`${timestamp}📊 [${this.toolName}] Metrics: ${JSON.stringify(metrics)}`));
}
}
/**
* Log resource usage
* @param {string} operationId - Operation identifier
* @param {Object} resources - Resource usage data
*/
logResourceUsage(operationId, resources) {
const operation = this.operations.get(operationId);
if (operation) {
if (!operation.resourceUsage) {
operation.resourceUsage = [];
}
operation.resourceUsage.push({
...resources,
timestamp: Date.now()
});
}
if (this.options.verbose && this.shouldLog('debug')) {
const timestamp = this.getTimestamp();
console.log(chalk.blue(`${timestamp}💾 [${this.toolName}] Resources: ${JSON.stringify(resources)}`));
}
}
/**
* Generate performance report
* @param {string} operationId - Operation identifier (optional)
* @returns {Object} - Performance report
*/
generatePerformanceReport(operationId = null) {
if (operationId) {
const operation = this.operations.get(operationId);
if (!operation) {
return null;
}
return this.generateSingleOperationReport(operation);
}
// Generate report for all operations
const operations = Array.from(this.operations.values());
const stats = this.getStatistics();
const report = {
summary: stats,
operationBreakdown: {},
performanceTrends: this.analyzePerformanceTrends(),
resourceUsage: this.analyzeResourceUsage(),
errorPatterns: this.analyzeErrorPatterns()
};
// Group operations by type
operations.forEach(op => {
if (!report.operationBreakdown[op.operation]) {
report.operationBreakdown[op.operation] = {
count: 0,
avgDuration: 0,
successRate: 0,
errors: []
};
}
const breakdown = report.operationBreakdown[op.operation];
breakdown.count++;
if (op.duration) {
breakdown.avgDuration = (breakdown.avgDuration * (breakdown.count - 1) + op.duration) / breakdown.count;
}
if (op.status === 'failed' && op.error) {
breakdown.errors.push(op.error.message);
}
});
// Calculate success rates
Object.keys(report.operationBreakdown).forEach(opType => {
const breakdown = report.operationBreakdown[opType];
const successful = breakdown.count - breakdown.errors.length;
breakdown.successRate = (successful / breakdown.count) * 100;
});
return report;
}
/**
* Export operation logs
* @param {Object} options - Export options
* @returns {Object} - Exported logs
*/
exportLogs(options = {}) {
const {
format = 'json',
includeSteps = false,
includeMetrics = false,
timeRange = null
} = options;
let operations = Array.from(this.operations.values());
// Filter by time range if specified
if (timeRange) {
const { start, end } = timeRange;
operations = operations.filter(op => {
return op.startTime >= start && op.startTime <= end;
});
}
const exportData = {
toolName: this.toolName,
exportTime: new Date().toISOString(),
totalOperations: operations.length,
operations: operations.map(op => {
const exported = {
operation: op.operation,
status: op.status,
startTime: new Date(op.startTime).toISOString(),
duration: op.duration,
args: op.args
};
if (op.error) {
exported.error = op.error;
}
if (op.result) {
exported.result = op.result;
}
if (includeSteps && op.steps) {
exported.steps = op.steps;
}
if (includeMetrics && op.performanceMetrics) {
exported.performanceMetrics = op.performanceMetrics;
}
return exported;
})
};
return format === 'json' ? exportData : this.formatAsCSV(exportData);
}
/**
* Clear operation history
*/
clearHistory() {
this.operations.clear();
}
/**
* Set logging options
* @param {Object} options - New logging options
*/
setOptions(options) {
this.options = { ...this.options, ...options };
}
/**
* Get real-time monitoring data
* @returns {Object} - Real-time monitoring data
*/
getMonitoringData() {
const now = Date.now();
const operations = Array.from(this.operations.values());
// Operations in the last minute
const recentOps = operations.filter(op => now - op.startTime < 60000);
// Current running operations
const runningOps = operations.filter(op => op.status === 'running');
// Recent errors
const recentErrors = operations
.filter(op => op.status === 'failed' && now - op.startTime < 300000) // Last 5 minutes
.map(op => ({
operation: op.operation,
error: op.error?.message,
time: new Date(op.startTime).toISOString()
}));
return {
timestamp: new Date(now).toISOString(),
activeOperations: runningOps.length,
recentOperations: recentOps.length,
recentErrors: recentErrors.length,
errorDetails: recentErrors,
systemHealth: this.calculateSystemHealth(),
performanceMetrics: this.getRecentPerformanceMetrics()
};
}
// Private methods
shouldLog(level) {
const levels = { debug: 0, info: 1, warn: 2, error: 3 };
const currentLevel = levels[this.options.logLevel] || 1;
const messageLevel = levels[level] || 1;
return messageLevel >= currentLevel;
}
getTimestamp() {
if (!this.options.includeTimestamp) {
return '';
}
return `[${new Date().toISOString()}] `;
}
sanitizeArgs(args) {
const sanitized = { ...args };
// Remove sensitive information
if (sanitized.password) sanitized.password = '[REDACTED]';
if (sanitized.token) sanitized.token = '[REDACTED]';
if (sanitized.apiKey) sanitized.apiKey = '[REDACTED]';
// Truncate long strings
Object.keys(sanitized).forEach(key => {
if (typeof sanitized[key] === 'string' && sanitized[key].length > this.options.maxLogLength) {
sanitized[key] = this.truncateText(sanitized[key]);
}
});
return sanitized;
}
sanitizeResult(result) {
if (typeof result !== 'object' || result === null) {
return result;
}
const sanitized = { ...result };
// Remove large data from logs
if (sanitized.data && typeof sanitized.data === 'string' && sanitized.data.length > this.options.maxLogLength) {
sanitized.data = `[DATA: ${sanitized.data.length} chars]`;
}
return sanitized;
}
truncateText(text) {
if (text.length <= this.options.maxLogLength) {
return text;
}
return text.substring(0, this.options.maxLogLength) + '...';
}
cleanupOldOperations() {
// Keep only the last 100 operations to prevent memory leaks
if (this.operations.size > 100) {
const entries = Array.from(this.operations.entries());
const toKeep = entries.slice(-100);
this.operations.clear();
toKeep.forEach(([key, value]) => {
this.operations.set(key, value);
});
}
}
generateSingleOperationReport(operation) {
const report = {
operation: operation.operation,
status: operation.status,
duration: operation.duration,
startTime: new Date(operation.startTime).toISOString(),
endTime: operation.endTime ? new Date(operation.endTime).toISOString() : null
};
if (operation.steps) {
report.steps = operation.steps.map(step => ({
step: step.step,
data: step.data,
timestamp: new Date(step.timestamp).toISOString()
}));
}
if (operation.performanceMetrics) {
report.performanceMetrics = operation.performanceMetrics;
}
if (operation.resourceUsage) {
report.resourceUsage = operation.resourceUsage;
}
if (operation.error) {
report.error = operation.error;
}
return report;
}
analyzePerformanceTrends() {
const operations = Array.from(this.operations.values())
.filter(op => op.status === 'completed' && op.duration)
.sort((a, b) => a.startTime - b.startTime);
if (operations.length < 2) {
return { trend: 'insufficient_data', operations: operations.length };
}
const recentOps = operations.slice(-10); // Last 10 operations
const olderOps = operations.slice(0, -10);
if (olderOps.length === 0) {
return { trend: 'insufficient_data', operations: operations.length };
}
const recentAvg = recentOps.reduce((sum, op) => sum + op.duration, 0) / recentOps.length;
const olderAvg = olderOps.reduce((sum, op) => sum + op.duration, 0) / olderOps.length;
const change = ((recentAvg - olderAvg) / olderAvg) * 100;
return {
trend: change > 10 ? 'degrading' : change < -10 ? 'improving' : 'stable',
changePercent: Math.round(change),
recentAverage: Math.round(recentAvg),
historicalAverage: Math.round(olderAvg),
operations: operations.length
};
}
analyzeResourceUsage() {
const operations = Array.from(this.operations.values())
.filter(op => op.resourceUsage && op.resourceUsage.length > 0);
if (operations.length === 0) {
return { available: false };
}
const allResources = operations.flatMap(op => op.resourceUsage);
const memoryUsage = allResources
.filter(r => r.memory)
.map(r => r.memory);
const cpuUsage = allResources
.filter(r => r.cpu)
.map(r => r.cpu);
return {
available: true,
memory: memoryUsage.length > 0 ? {
avg: memoryUsage.reduce((a, b) => a + b, 0) / memoryUsage.length,
max: Math.max(...memoryUsage),
min: Math.min(...memoryUsage)
} : null,
cpu: cpuUsage.length > 0 ? {
avg: cpuUsage.reduce((a, b) => a + b, 0) / cpuUsage.length,
max: Math.max(...cpuUsage),
min: Math.min(...cpuUsage)
} : null
};
}
analyzeErrorPatterns() {
const failedOps = Array.from(this.operations.values())
.filter(op => op.status === 'failed' && op.error);
const errorCounts = {};
const errorsByType = {};
failedOps.forEach(op => {
const errorMsg = op.error.message;
const errorType = op.error.name || 'Unknown';
errorCounts[errorMsg] = (errorCounts[errorMsg] || 0) + 1;
if (!errorsByType[errorType]) {
errorsByType[errorType] = [];
}
errorsByType[errorType].push(errorMsg);
});
// Find most common errors
const commonErrors = Object.entries(errorCounts)
.sort(([,a], [,b]) => b - a)
.slice(0, 5)
.map(([error, count]) => ({ error, count }));
return {
totalErrors: failedOps.length,
uniqueErrors: Object.keys(errorCounts).length,
commonErrors,
errorsByType
};
}
calculateSystemHealth() {
const stats = this.getStatistics();
if (stats.total === 0) {
return { status: 'unknown', score: 0, factors: [] };
}
let score = 100;
const factors = [];
// Success rate factor (40% weight)
const successRateScore = stats.successRate;
score = score * 0.6 + successRateScore * 0.4;
factors.push({
factor: 'success_rate',
score: successRateScore,
weight: 0.4
});
// Performance factor (30% weight)
const avgDuration = stats.performance.avgDuration;
let performanceScore = 100;
if (avgDuration > 30000) { // > 30s is poor
performanceScore = 30;
} else if (avgDuration > 10000) { // > 10s is fair
performanceScore = 60;
} else if (avgDuration > 5000) { // > 5s is good
performanceScore = 80;
}
score = score * 0.7 + performanceScore * 0.3;
factors.push({
factor: 'performance',
score: performanceScore,
weight: 0.3
});
// Timeout factor (20% weight)
const timeoutRate = (stats.timeout / stats.total) * 100;
const timeoutScore = Math.max(0, 100 - timeoutRate * 10);
score = score * 0.8 + timeoutScore * 0.2;
factors.push({
factor: 'timeout_rate',
score: timeoutScore,
weight: 0.2
});
// Recent errors factor (10% weight)
const now = Date.now();
const recentErrors = Array.from(this.operations.values())
.filter(op => op.status === 'failed' && now - op.startTime < 300000); // Last 5 minutes
const recentErrorScore = Math.max(0, 100 - recentErrors.length * 20);
score = score * 0.9 + recentErrorScore * 0.1;
factors.push({
factor: 'recent_errors',
score: recentErrorScore,
weight: 0.1
});
let status;
if (score >= 90) status = 'excellent';
else if (score >= 75) status = 'good';
else if (score >= 60) status = 'fair';
else if (score >= 40) status = 'poor';
else status = 'critical';
return {
status,
score: Math.round(score),
factors
};
}
getRecentPerformanceMetrics() {
const now = Date.now();
const recentOps = Array.from(this.operations.values())
.filter(op => now - op.startTime < 300000); // Last 5 minutes
if (recentOps.length === 0) {
return { available: false };
}
const completedOps = recentOps.filter(op => op.status === 'completed' && op.duration);
if (completedOps.length === 0) {
return { available: false };
}
const durations = completedOps.map(op => op.duration);
const throughput = completedOps.length / 5; // Operations per minute
return {
available: true,
operationsPerMinute: throughput,
averageDuration: durations.reduce((a, b) => a + b, 0) / durations.length,
p95Duration: this.calculatePercentile(durations, 95),
p99Duration: this.calculatePercentile(durations, 99)
};
}
calculatePercentile(values, percentile) {
const sorted = values.slice().sort((a, b) => a - b);
const index = Math.ceil((percentile / 100) * sorted.length) - 1;
return sorted[index] || 0;
}
formatAsCSV(data) {
const headers = ['Operation', 'Status', 'Start Time', 'Duration', 'Error'];
const rows = data.operations.map(op => [
op.operation,
op.status,
op.startTime,
op.duration || '',
op.error?.message || ''
]);
return [headers, ...rows]
.map(row => row.map(cell => `"${cell}"`).join(','))
.join('\n');
}
}