const fs = require('fs').promises; const path = require('path'); const profileManager = require('../config/profile-manager'); /** * Service de gestion des logs * Supporte plusieurs emplacements de logs : * - IIS logs: \\\inetpub\logs\LogFiles\Mecalux (avec sous-dossiers) * - ETL middleware: \\\ProgramData\Mecalux\ETLLogs * * LOGS_PATH dans .env peut contenir le placeholder "{host}" qui est remplacé * par le host du profil actif à chaque appel. * * En mode SaaS (profile.saas === true) l'accès aux logs est bloqué car le * WMS est hébergé dans le cloud et le filesystem n'est pas joignable. */ const DEFAULT_LOG_PATHS = [ '\\\\{host}\\inetpub\\logs\\LogFiles\\Mecalux', '\\\\{host}\\ProgramData\\Mecalux\\ETLLogs', ]; /** * Retourne les chemins de logs à scanner pour le profil actif. * Substitue {host} par l'hostname du profil. * Lance une erreur si le profil est en mode SaaS. */ function getLogPaths() { const profile = profileManager.getCurrent(); if (profile.saas) { throw new Error(`Log access is disabled for SaaS profile "${profile.name}". The WMS is cloud-hosted — local log files are not reachable. Use the WMS API (query/command/workflow tools) instead.`); } const templates = process.env.LOGS_PATH ? process.env.LOGS_PATH.split(';').map(p => p.trim()).filter(Boolean) : DEFAULT_LOG_PATHS; return templates.map(t => t.replace(/\{host\}/g, profile.host)); } /** * Scanne récursivement un dossier pour trouver tous les fichiers .log * @param {string} dir - Dossier à scanner * @param {Array} fileList - Liste accumulée des fichiers (pour la récursion) * @returns {Promise} - Liste des fichiers trouvés */ async function scanLogsRecursively(dir, fileList = []) { try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { // Récursion dans les sous-dossiers await scanLogsRecursively(fullPath, fileList); } else if (entry.isFile() && entry.name.endsWith('.log')) { try { const stats = await fs.stat(fullPath); fileList.push({ name: entry.name, path: fullPath, relativePath: path.relative(dir, fullPath), directory: path.dirname(fullPath), size: stats.size, modified: stats.mtime, }); } catch (statErr) { // Ignorer les fichiers inaccessibles console.error(`[Logs] Cannot access file ${fullPath}: ${statErr.message}`); } } } } catch (err) { // Ne pas planter si un dossier n'existe pas ou n'est pas accessible console.error(`[Logs] Cannot scan directory ${dir}: ${err.message}`); } return fileList; } /** * Liste tous les fichiers de logs dans tous les répertoires configurés * @returns {Promise} - Liste des fichiers de logs */ async function listLogFiles() { const logPaths = getLogPaths(); const allFiles = []; // Scanner chaque chemin configuré for (const logPath of logPaths) { const files = await scanLogsRecursively(logPath); allFiles.push(...files); } if (allFiles.length === 0) { const pathsList = logPaths.join(', '); throw new Error(`No log files found in configured paths: ${pathsList}`); } // Trier par date de modification (plus récent en premier) allFiles.sort((a, b) => b.modified - a.modified); return allFiles; } /** * Trouve le fichier de log le plus récent * @returns {Promise} - Chemin du fichier le plus récent */ async function findLatestLogFile() { const files = await listLogFiles(); if (files.length === 0) { throw new Error('No log files found'); } return files[0].path; } /** * Vérifie si un fichier existe * @param {string} filePath * @returns {Promise} */ async function fileExists(filePath) { try { await fs.access(filePath); return true; } catch { return false; } } /** * Résout le chemin d'un fichier de log en 3 passes : * 1. Chemin direct (LOGS_PATH + logFile, peut inclure un sous-dossier) * 2. Nom de fichier exact dans chaque sous-dossier immédiat de LOGS_PATH * 3. Correspondance floue (partielle, insensible à la casse) * En cas d'échec, retourne une erreur avec la liste des fichiers disponibles. * @param {string} logFile - Nom ou chemin partiel du fichier * @returns {Promise} - Chemin absolu résolu */ async function resolveLogFilePath(logFile) { // Si chemin absolu fourni directement if (path.isAbsolute(logFile)) { if (await fileExists(logFile)) return logFile; throw new Error(`File not found: ${logFile}`); } const basePath = getLogPaths()[0]; // Passe 1 — Chemin direct (ex: "ApplicationDictionary\ApplicationDictionary.log") const directPath = path.join(basePath, logFile); if (await fileExists(directPath)) return directPath; // Passe 2 & 3 — Scanner les sous-dossiers immédiats const fileName = path.basename(logFile); const fileNameNoExt = path.basename(fileName, path.extname(fileName)).toLowerCase(); let subEntries = []; try { subEntries = await fs.readdir(basePath, { withFileTypes: true }); } catch (err) { console.error(`[Logs] Cannot read base log path ${basePath}: ${err.message}`); } const subDirs = subEntries.filter(e => e.isDirectory()).map(e => e.name); // Passe 2 — Nom exact dans les sous-dossiers for (const dir of subDirs) { const candidate = path.join(basePath, dir, fileName); if (await fileExists(candidate)) return candidate; } // Passe 3 — Correspondance floue for (const dir of subDirs) { let dirFiles = []; try { dirFiles = await fs.readdir(path.join(basePath, dir)); } catch { continue; } for (const f of dirFiles) { if (!f.endsWith('.log')) continue; const fNoExt = path.basename(f, path.extname(f)).toLowerCase(); if (fNoExt.includes(fileNameNoExt) || fileNameNoExt.includes(fNoExt)) { return path.join(basePath, dir, f); } } } // Aucun fichier trouvé — construire une liste utile const available = []; for (const dir of subDirs) { try { const files = await fs.readdir(path.join(basePath, dir)); files.filter(f => f.endsWith('.log')).forEach(f => available.push(`${dir}\\${f}`)); } catch { /* ignorer */ } } const availableMsg = available.length > 0 ? `Available log files:\n${available.join('\n')}` : `No log files found in ${basePath}`; throw new Error(`Log file "${logFile}" not found.\n${availableMsg}`); } /** * Lit les dernières lignes d'un fichier de log * @param {number} count - Nombre de lignes à lire * @param {string} logFile - Nom ou chemin partiel du fichier (optionnel, prend le plus récent par défaut) * @returns {Promise} - Lignes du log */ async function readRecentLogs(count = 100, logFile = null) { try { const filePath = logFile ? await resolveLogFilePath(logFile) : await findLatestLogFile(); // Lire le fichier complet const content = await fs.readFile(filePath, 'utf8'); const lines = content.split('\n').filter((line) => line.trim() !== ''); // Prendre les dernières lignes const recentLines = lines.slice(-count); return { file: path.basename(filePath), fullPath: filePath, totalLines: lines.length, returnedLines: recentLines.length, lines: recentLines, }; } catch (err) { throw new Error(`Failed to read log file: ${err.message}`); } } /** * Recherche un mot-clé dans les fichiers de logs * @param {string} keyword - Mot-clé à rechercher * @param {number} maxResults - Nombre maximum de résultats * @param {number} contextLines - Nombre de lignes de contexte avant/après * @returns {Promise} - Résultats de la recherche */ async function searchLogs(keyword, maxResults = 50, contextLines = 2) { try { const files = await listLogFiles(); const results = []; const searchPattern = keyword.toLowerCase(); // Rechercher dans chaque fichier (du plus récent au plus ancien) for (const file of files) { if (results.length >= maxResults) break; try { const content = await fs.readFile(file.path, 'utf8'); const lines = content.split('\n'); // Rechercher dans chaque ligne for (let i = 0; i < lines.length; i++) { if (results.length >= maxResults) break; const line = lines[i]; if (line.toLowerCase().includes(searchPattern)) { // Extraire le contexte (lignes avant et après) const start = Math.max(0, i - contextLines); const end = Math.min(lines.length, i + contextLines + 1); const context = lines.slice(start, end); results.push({ file: file.name, fullPath: file.path, directory: file.directory, lineNumber: i + 1, line: line.trim(), context: context.map((l, idx) => ({ lineNumber: start + idx + 1, text: l.trim(), isMatch: start + idx === i, })), }); } } } catch (readErr) { // Ignorer les fichiers illisibles console.error(`[Logs] Cannot read file ${file.path}: ${readErr.message}`); } } return { keyword, totalResults: results.length, results, scannedPaths: getLogPaths(), }; } catch (err) { throw new Error(`Search failed: ${err.message}`); } } module.exports = { listLogFiles, findLatestLogFile, readRecentLogs, searchLogs, };