UNPKG

sfcc-dev-mcp

Version:

MCP server for Salesforce B2C Commerce Cloud development assistance including logs, debugging, and development tools

418 lines (415 loc) 19.5 kB
/** * Main log client - lightweight orchestrator that composes specialized modules */ import { Logger } from '../../utils/logger.js'; import { getCurrentDate, normalizeFilePath } from '../../utils/utils.js'; import { WebDAVClientManager } from './webdav-client-manager.js'; import { LogFileReader } from './log-file-reader.js'; import { LogFileDiscovery } from './log-file-discovery.js'; import { LogProcessor } from './log-processor.js'; import { LogAnalyzer } from './log-analyzer.js'; import { LogFormatter } from './log-formatter.js'; import { LOG_CONSTANTS, LOG_MESSAGES, JOB_LOG_CONSTANTS } from './log-constants.js'; export class SFCCLogClient { logger; webdavManager; fileReader; fileDiscovery; processor; analyzer; constructor(config, logger) { this.logger = logger ?? Logger.getChildLogger('LogClient'); this.webdavManager = new WebDAVClientManager(this.logger); // Convert SFCCConfig to WebDAVClientConfig for backward compatibility const webdavConfig = { hostname: config.hostname, username: config.username, password: config.password, clientId: config.clientId, clientSecret: config.clientSecret, }; // Setup WebDAV client and initialize modules const webdavClient = this.webdavManager.setupClient(webdavConfig); this.fileReader = new LogFileReader(webdavClient, this.logger); this.fileDiscovery = new LogFileDiscovery(webdavClient, this.logger); this.processor = new LogProcessor(this.logger); this.analyzer = new LogAnalyzer(this.logger); } /** * Get the latest log entries for a specific log level */ async getLatestLogs(level, limit, date) { const targetDate = date ?? getCurrentDate(); this.logger.methodEntry('getLatestLogs', { level, limit, date: targetDate }); const startTime = Date.now(); // Get and filter log files const levelFiles = await this.fileDiscovery.getLogFilesByLevel(level, targetDate); if (levelFiles.length === 0) { const allFiles = await this.fileDiscovery.getLogFiles(targetDate); const availableFiles = allFiles.map(f => normalizeFilePath(f.filename)); const result = LogFormatter.formatNoFilesFound(level, targetDate, availableFiles); this.logger.warn(result); this.logger.methodExit('getLatestLogs', { result: 'no_files' }); return result; } // Sort files by date (newest first) const sortedFiles = this.fileDiscovery.sortFilesByDate(levelFiles, true); // Read file contents const fileContents = await this.fileReader.readMultipleFiles(sortedFiles.map(f => f.filename), { maxBytes: LOG_CONSTANTS.DEFAULT_TAIL_BYTES }); // Process log entries const allLogEntries = await this.processor.processLogFiles(sortedFiles, level, fileContents); const sortedEntries = this.processor.sortAndLimitEntries(allLogEntries, limit); const latestEntries = this.processor.extractFormattedEntries(sortedEntries); // Format response const fileList = sortedFiles.map(f => normalizeFilePath(f.filename)); const result = LogFormatter.formatLatestLogs(latestEntries, level, limit, fileList); this.logger.debug(LogFormatter.formatProcessingSummary(latestEntries.length, sortedFiles.length, allLogEntries.length)); this.logger.timing('getLatestLogs', startTime); this.logger.methodExit('getLatestLogs', { entriesReturned: latestEntries.length, filesProcessed: sortedFiles.length, }); return result; } /** * Get list of log files for a specific date (backward compatibility) */ async getLogFiles(date) { const targetDate = date ?? getCurrentDate(); this.logger.methodEntry('getLogFiles', { date: targetDate }); const logFiles = await this.fileDiscovery.getLogFiles(targetDate); this.logger.methodExit('getLogFiles', { count: logFiles.length }); return logFiles; } /** * Generate a comprehensive summary of logs for a specific date */ async summarizeLogs(date) { const targetDate = date ?? getCurrentDate(); this.logger.methodEntry('summarizeLogs', { date: targetDate }); const logFiles = await this.fileDiscovery.getLogFiles(targetDate); if (logFiles.length === 0) { const result = `No log files found for date ${targetDate}`; this.logger.methodExit('summarizeLogs', { result: 'no_files' }); return result; } // Read file contents const fileContents = await this.fileReader.readMultipleFiles(logFiles.map(f => f.filename), { maxBytes: LOG_CONSTANTS.DEFAULT_TAIL_BYTES }); // Analyze logs const summary = await this.analyzer.analyzeLogs(logFiles, fileContents, targetDate); const result = LogFormatter.formatLogSummary(summary); this.logger.methodExit('summarizeLogs', { filesAnalyzed: logFiles.length }); return result; } async searchLogs(optionsOrPattern, logLevel, limit = LOG_CONSTANTS.DEFAULT_SEARCH_LIMIT, date) { // Handle both new options interface and legacy parameters const options = typeof optionsOrPattern === 'string' ? { pattern: optionsOrPattern, logLevel, limit, date, } : optionsOrPattern; const { pattern, logLevel: level, limit: searchLimit, date: searchDate } = options; const targetDate = searchDate ?? getCurrentDate(); this.logger.methodEntry('searchLogs', { pattern, logLevel: level, limit: searchLimit, date: targetDate }); const logFiles = await this.fileDiscovery.getLogFiles(targetDate); // Filter by log level if specified const filesToSearch = level ? this.fileDiscovery.filterLogFiles(logFiles, { level }) : logFiles; if (filesToSearch.length === 0) { const result = LOG_MESSAGES.NO_SEARCH_MATCHES(pattern, targetDate); this.logger.methodExit('searchLogs', { result: 'no_files' }); return result; } // Read file contents const fileContents = await this.fileReader.readMultipleFiles(filesToSearch.map(f => f.filename), { maxBytes: LOG_CONSTANTS.DEFAULT_TAIL_BYTES }); // Search for patterns const matches = this.processor.processSearchResults(filesToSearch, fileContents, pattern, searchLimit); const result = LogFormatter.formatSearchResults(matches, pattern, targetDate); this.logger.methodExit('searchLogs', { matchesFound: matches.length }); return result; } /** * List available log files with metadata */ async listLogFiles() { this.logger.methodEntry('listLogFiles'); const startTime = Date.now(); try { const files = await this.fileDiscovery.getAllLogFiles(); const result = LogFormatter.formatLogFilesList(files); this.logger.methodExit('listLogFiles', { fileCount: files.length }); return result; } catch (error) { const errorMessage = LogFormatter.formatError('list_log_files', error); this.logger.error(errorMessage); this.logger.methodExit('listLogFiles', { error: true }); throw new Error(`Failed to list log files: ${error.message}`); } finally { const duration = Date.now() - startTime; this.logger.debug(`listLogFiles completed in ${duration}ms`); } } /** * Get the complete contents of a specific log file */ async getLogFileContents(filename, maxBytes, tailOnly) { this.logger.methodEntry('getLogFileContents', { filename, maxBytes, tailOnly }); const startTime = Date.now(); try { // Use tailOnly flag to determine reading strategy if (tailOnly) { const content = await this.fileReader.getFileContentsTail(filename, { maxBytes: maxBytes ?? LOG_CONSTANTS.DEFAULT_TAIL_BYTES, }); const result = this.formatLogFileContents(filename, content, true); this.logger.methodExit('getLogFileContents', { tailOnly: true }); return result; } else { // Read full file from beginning with optional size limit const content = await this.fileReader.getFileContentsHead(filename, maxBytes); const result = this.formatLogFileContents(filename, content, false); this.logger.methodExit('getLogFileContents', { tailOnly: false }); return result; } } catch (error) { const errorMessage = LogFormatter.formatError('get_log_file_contents', error); this.logger.error(errorMessage); this.logger.methodExit('getLogFileContents', { error: true }); return errorMessage; } finally { const duration = Date.now() - startTime; this.logger.debug(`getLogFileContents completed in ${duration}ms`); } } /** * Format log file contents for display */ formatLogFileContents(filename, content, isTailOnly) { const lines = content.split('\n').filter(line => line.trim()); const readType = isTailOnly ? 'tail' : 'full'; return `# Log File Contents: ${filename} (${readType} read) Total lines: ${lines.length} Content size: ${content.length} bytes --- ${content}`; } /** * Get advanced log analysis with patterns and recommendations */ async getAdvancedAnalysis(date) { const targetDate = date ?? getCurrentDate(); this.logger.methodEntry('getAdvancedAnalysis', { date: targetDate }); const logFiles = await this.fileDiscovery.getLogFiles(targetDate); if (logFiles.length === 0) { return `No log files found for date ${targetDate}`; } // Read file contents const fileContents = await this.fileReader.readMultipleFiles(logFiles.map(f => f.filename), { maxBytes: LOG_CONSTANTS.DEFAULT_TAIL_BYTES }); // Perform comprehensive analysis const summary = await this.analyzer.analyzeLogs(logFiles, fileContents, targetDate); // Parse entries for pattern detection const allEntries = Array.from(fileContents.values()) .flatMap(content => content.split('\n')) .filter(line => line.trim()) .map(line => this.processor.parseLogEntry(line)); const patterns = this.analyzer.detectPatterns(allEntries); const healthScore = this.analyzer.calculateHealthScore(summary); const recommendations = this.analyzer.generateRecommendations(summary, patterns); const result = this.analyzer.formatAnalysisResults(summary, patterns, healthScore, recommendations); this.logger.methodExit('getAdvancedAnalysis', { filesAnalyzed: logFiles.length, entriesProcessed: allEntries.length, }); return result; } /** * Test WebDAV connection */ async testConnection() { return await this.webdavManager.testConnection(); } /** * Get log statistics for a date range */ async getLogStats(date) { const targetDate = date ?? getCurrentDate(); const stats = await this.fileDiscovery.getLogFileStats(targetDate); const sections = [ `Log Statistics for ${targetDate}:`, '', '📊 Overview:', `- Total Files: ${stats.totalFiles}`, `- Files by Level: ${LogFormatter.formatLogLevelStats(stats.filesByLevel)}`, '', '📁 File Info:', `- Newest: ${stats.newestFile ?? 'N/A'}`, `- Oldest: ${stats.oldestFile ?? 'N/A'}`, ]; return sections.join('\n'); } /** * Get latest job log files */ async getLatestJobLogFiles(limit) { this.logger.methodEntry('getLatestJobLogFiles', { limit }); try { const jobLogs = await this.fileDiscovery.getLatestJobLogFiles(limit); const result = LogFormatter.formatJobLogList(jobLogs); this.logger.methodExit('getLatestJobLogFiles', { count: jobLogs.length }); return result; } catch (error) { const errorMessage = LogFormatter.formatError('get_latest_job_log_files', error); this.logger.error(errorMessage); this.logger.methodExit('getLatestJobLogFiles', { error: true }); return errorMessage; } } /** * Search job logs by job name */ async searchJobLogsByName(jobName, limit) { this.logger.methodEntry('searchJobLogsByName', { jobName, limit }); try { const jobLogs = await this.fileDiscovery.searchJobLogsByName(jobName, limit); const result = LogFormatter.formatJobLogList(jobLogs); this.logger.methodExit('searchJobLogsByName', { count: jobLogs.length }); return result; } catch (error) { const errorMessage = LogFormatter.formatError('search_job_logs_by_name', error); this.logger.error(errorMessage); this.logger.methodExit('searchJobLogsByName', { error: true }); return errorMessage; } } /** * Get job log entries for a specific log level or all levels */ async getJobLogEntries(level = 'all', limit = JOB_LOG_CONSTANTS.DEFAULT_JOB_LOG_LIMIT, jobName) { this.logger.methodEntry('getJobLogEntries', { level, limit, jobName }); try { // Get job logs based on filter const jobLogs = jobName ? await this.fileDiscovery.searchJobLogsByName(jobName, limit) : await this.fileDiscovery.getLatestJobLogFiles(limit); if (jobLogs.length === 0) { const result = jobName ? `No job logs found for job name: ${jobName}` : 'No job logs found'; this.logger.methodExit('getJobLogEntries', { result: 'no_logs' }); return result; } // Read job log contents const fileContents = await this.fileReader.readMultipleFiles(jobLogs.map(job => job.logFile), { maxBytes: LOG_CONSTANTS.DEFAULT_TAIL_BYTES }); // Process job log entries const jobLogEntries = await this.processor.processJobLogFiles(jobLogs, level, fileContents); const sortedEntries = this.processor.sortAndLimitEntries(jobLogEntries, limit); const latestEntries = this.processor.extractFormattedEntries(sortedEntries); // Format response const jobContext = jobName ? `job: ${jobName}` : 'latest jobs'; const result = LogFormatter.formatJobLogEntries(latestEntries, level, limit, jobContext); this.logger.methodExit('getJobLogEntries', { entriesReturned: latestEntries.length, jobLogsProcessed: jobLogs.length, }); return result; } catch (error) { const errorMessage = LogFormatter.formatError('get_job_log_entries', error); this.logger.error(errorMessage); this.logger.methodExit('getJobLogEntries', { error: true }); return errorMessage; } } /** * Search for patterns in job logs */ async searchJobLogs(pattern, level, limit = LOG_CONSTANTS.DEFAULT_SEARCH_LIMIT, jobName) { this.logger.methodEntry('searchJobLogs', { pattern, level, limit, jobName }); try { // Get job logs based on filter const jobLogs = jobName ? await this.fileDiscovery.searchJobLogsByName(jobName) : await this.fileDiscovery.getLatestJobLogFiles(); if (jobLogs.length === 0) { const result = jobName ? `No job logs found for job name: ${jobName}` : 'No job logs found'; this.logger.methodExit('searchJobLogs', { result: 'no_logs' }); return result; } // Read job log contents const fileContents = await this.fileReader.readMultipleFiles(jobLogs.map(job => job.logFile), { maxBytes: LOG_CONSTANTS.DEFAULT_TAIL_BYTES }); // Search for patterns in job logs const matches = []; for (const jobLog of jobLogs) { const content = fileContents.get(jobLog.logFile); if (!content) { continue; } const lines = content.split('\n'); for (const line of lines) { if (line.toLowerCase().includes(pattern.toLowerCase()) && matches.length < limit) { // Filter by level if specified if (level && level !== 'all') { const levelUpper = level.toUpperCase(); if (!line.includes(` ${levelUpper} `)) { continue; } } matches.push(`[${jobLog.jobName}] ${line.trim()}`); } } } const jobContext = jobName ? `job: ${jobName}` : 'job logs'; const result = LogFormatter.formatJobSearchResults(matches, pattern, jobContext); this.logger.methodExit('searchJobLogs', { matchesFound: matches.length }); return result; } catch (error) { const errorMessage = LogFormatter.formatError('search_job_logs', error); this.logger.error(errorMessage); this.logger.methodExit('searchJobLogs', { error: true }); return errorMessage; } } /** * Get job execution summary for a specific job */ async getJobExecutionSummary(jobName) { this.logger.methodEntry('getJobExecutionSummary', { jobName }); try { const jobLogs = await this.fileDiscovery.searchJobLogsByName(jobName, 1); if (jobLogs.length === 0) { const result = `No job logs found for job name: ${jobName}`; this.logger.methodExit('getJobExecutionSummary', { result: 'no_logs' }); return result; } const latestJobLog = jobLogs[0]; const content = await this.fileReader.getFileContentsTail(latestJobLog.logFile, { maxBytes: LOG_CONSTANTS.DEFAULT_TAIL_BYTES, }); const summary = this.processor.extractJobExecutionSummary(content); const result = LogFormatter.formatJobExecutionSummary(summary, jobName); this.logger.methodExit('getJobExecutionSummary', { jobLog: latestJobLog.logFile }); return result; } catch (error) { const errorMessage = LogFormatter.formatError('get_job_execution_summary', error); this.logger.error(errorMessage); this.logger.methodExit('getJobExecutionSummary', { error: true }); return errorMessage; } } } //# sourceMappingURL=log-client.js.map