sfcc-dev-mcp
Version:
MCP server for Salesforce B2C Commerce Cloud development assistance including logs, debugging, and development tools
255 lines • 11 kB
JavaScript
/**
* SFCC WebDAV Log Client
*
* This module handles all interactions with SFCC's WebDAV log interface.
* It provides methods to fetch, parse, and analyze log files from the
* Salesforce B2C Commerce Cloud logging system.
*/
import { createClient } from 'webdav';
import { getCurrentDate, formatBytes, parseLogEntries, extractUniqueErrors, normalizeFilePath, } from '../utils/utils.js';
import { Logger } from '../utils/logger.js';
// Create a logger instance for this module
const logger = new Logger('LogClient');
/**
* Client for accessing SFCC logs via WebDAV
*
* This class encapsulates all the logic for connecting to SFCC's WebDAV interface
* and performing various log-related operations like fetching, searching, and analyzing logs.
*/
export class SFCCLogClient {
webdavClient;
config;
/**
* Initialize the SFCC log client
*
* @param config - SFCC configuration with authentication details
*/
constructor(config) {
this.config = config;
this.setupWebDAVClient();
}
/**
* Set up the WebDAV client with appropriate authentication
* Supports both basic authentication and OAuth credentials
*
* @private
*/
setupWebDAVClient() {
const webdavUrl = `https://${this.config.hostname}/on/demandware.servlet/webdav/Sites/Logs/`;
let authConfig = {};
if (this.config.username && this.config.password) {
// Basic authentication
authConfig = {
username: this.config.username,
password: this.config.password,
};
}
else if (this.config.clientId && this.config.clientSecret) {
// OAuth authentication (using client ID as username and client secret as password for WebDAV)
authConfig = {
username: this.config.clientId,
password: this.config.clientSecret,
};
}
else {
throw new Error('Either username/password or clientId/clientSecret must be provided');
}
this.webdavClient = createClient(webdavUrl, authConfig);
}
/**
* Get list of log files for a specific date
*
* @param date - Date in YYYYMMDD format (defaults to today)
* @returns Array of log file names for the specified date
*/
async getLogFiles(date) {
const targetDate = date ?? getCurrentDate();
logger.methodEntry('getLogFiles', { date: targetDate });
const startTime = Date.now();
const contents = await this.webdavClient.getDirectoryContents('/');
logger.timing('webdav_getDirectoryContents', startTime);
const logFiles = contents
.filter((item) => item.type === 'file' &&
item.filename.includes(targetDate) &&
item.filename.endsWith('.log'))
.map((item) => item.filename);
logger.debug(`Found ${logFiles.length} log files for date ${targetDate}:`, logFiles);
logger.methodExit('getLogFiles', { count: logFiles.length });
return logFiles;
}
/**
* Get the latest log entries for a specific log level
*
* @param level - Log level to fetch (error, warn, info)
* @param limit - Maximum number of entries to return
* @param date - Date in YYYYMMDD format (defaults to today)
* @returns Formatted log entries
*/
async getLatestLogs(level, limit, date) {
const targetDate = date ?? getCurrentDate();
logger.methodEntry('getLatestLogs', { level, limit, date: targetDate });
const startTime = Date.now();
const logFiles = await this.getLogFiles(targetDate);
// Filter files for the specific log level
const levelFiles = logFiles.filter(file => {
const filename = normalizeFilePath(file);
return filename.startsWith(`${level}-`);
});
logger.debug(`Filtered to ${levelFiles.length} ${level} log files:`, levelFiles);
if (levelFiles.length === 0) {
const availableFiles = logFiles.map(f => normalizeFilePath(f)).join(', ');
const result = `No ${level} log files found for date ${targetDate}. Available files: ${availableFiles}`;
logger.warn(result);
logger.methodExit('getLatestLogs', { result: 'no_files' });
return result;
}
// Get the most recent log file (sort by filename, latest timestamp should be last)
const latestFile = levelFiles.sort().pop();
logger.debug(`Processing latest file: ${latestFile}`);
const fileStartTime = Date.now();
const logContent = await this.webdavClient.getFileContents(latestFile, { format: 'text' });
logger.timing('webdav_getFileContents', fileStartTime);
const logEntries = parseLogEntries(logContent, level.toUpperCase());
const latestEntries = logEntries.slice(-limit).reverse();
logger.debug(`Parsed ${logEntries.length} total entries, returning latest ${latestEntries.length}`);
logger.timing('getLatestLogs', startTime);
logger.methodExit('getLatestLogs', { entriesReturned: latestEntries.length });
return `Latest ${limit} ${level} messages from ${normalizeFilePath(latestFile)}:\n\n${latestEntries.join('\n\n---\n\n')}`;
}
/**
* Generate a comprehensive summary of logs for a specific date
*
* @param date - Date in YYYYMMDD format (defaults to today)
* @returns Detailed log summary with counts and key issues
*/
async summarizeLogs(date) {
const targetDate = date ?? getCurrentDate();
const logFiles = await this.getLogFiles(targetDate);
if (logFiles.length === 0) {
return `No log files found for date ${targetDate}`;
}
const summary = {
date: targetDate,
errorCount: 0,
warningCount: 0,
infoCount: 0,
debugCount: 0,
keyIssues: [],
files: logFiles,
};
// Analyze each log file for counts and patterns
for (const file of logFiles) {
try {
const content = await this.webdavClient.getFileContents(file, { format: 'text' });
const lines = content.split('\n');
// Count different log levels
for (const line of lines) {
if (line.includes(' ERROR ')) {
summary.errorCount++;
}
if (line.includes(' WARN ')) {
summary.warningCount++;
}
if (line.includes(' INFO ')) {
summary.infoCount++;
}
if (line.includes(' DEBUG ')) {
summary.debugCount++;
}
}
// Extract key error patterns from error files
const filename = normalizeFilePath(file);
if (filename.startsWith('error-')) {
const errors = parseLogEntries(content, 'ERROR');
const uniqueErrors = extractUniqueErrors(errors);
summary.keyIssues.push(...uniqueErrors);
}
}
catch (error) {
logger.error(`Error reading file ${file}:`, error);
}
}
// Remove duplicate key issues
summary.keyIssues = [...new Set(summary.keyIssues)];
return this.formatLogSummary(summary);
}
/**
* Search for specific patterns across log files
*
* @param pattern - Search pattern or keyword
* @param logLevel - Optional log level to restrict search to
* @param limit - Maximum number of matches to return
* @param date - Date in YYYYMMDD format (defaults to today)
* @returns Formatted search results
*/
async searchLogs(pattern, logLevel, limit = 20, date) {
const targetDate = date ?? getCurrentDate();
const logFiles = await this.getLogFiles(targetDate);
let filesToSearch = logFiles;
if (logLevel) {
filesToSearch = logFiles.filter(file => {
const filename = normalizeFilePath(file);
return filename.startsWith(`${logLevel}-`);
});
}
const matchingEntries = [];
for (const file of filesToSearch) {
try {
const content = await this.webdavClient.getFileContents(file, { format: 'text' });
const lines = content.split('\n');
for (const line of lines) {
if (line.toLowerCase().includes(pattern.toLowerCase()) && matchingEntries.length < limit) {
matchingEntries.push(`[${normalizeFilePath(file)}] ${line.trim()}`);
}
}
}
catch (error) {
logger.error(`Error searching file ${file}:`, error);
}
}
return matchingEntries.length > 0
? `Found ${matchingEntries.length} matches for "${pattern}":\n\n${matchingEntries.join('\n\n')}`
: `No matches found for "${pattern}" in logs for ${targetDate}`;
}
/**
* List all available log files with metadata
*
* @returns Formatted list of log files with size and modification dates
*/
async listLogFiles() {
try {
const contents = await this.webdavClient.getDirectoryContents('/');
const logFiles = contents
.filter((item) => item.type === 'file' && item.filename.endsWith('.log'))
.map((item) => ({
name: item.filename,
size: item.size,
lastModified: item.lastmod,
}));
return `Available log files:\n\n${logFiles.map((file) => `📄 ${file.name}\n Size: ${formatBytes(file.size)}\n Modified: ${file.lastModified}`).join('\n\n')}`;
}
catch (error) {
throw new Error(`Failed to list log files: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Format a log summary into a readable string
*
* @param summary - Log summary data
* @returns Formatted summary string
* @private
*/
formatLogSummary(summary) {
return `Log Summary for ${summary.date}:\n\n` +
'📊 Counts:\n' +
`- Errors: ${summary.errorCount}\n` +
`- Warnings: ${summary.warningCount}\n` +
`- Info: ${summary.infoCount}\n` +
`- Debug: ${summary.debugCount}\n\n` +
`📁 Log Files (${summary.files.length}):\n` +
`${summary.files.map((f) => `- ${f}`).join('\n')}\n\n` +
'🔥 Key Issues:\n' +
`${summary.keyIssues.length > 0 ? summary.keyIssues.map((issue) => `- ${issue}`).join('\n') : 'No major issues detected'}`;
}
}
//# sourceMappingURL=log-client.js.map