version mise à jour par claude , à tester
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
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: \\<host>\inetpub\logs\LogFiles\Mecalux (avec sous-dossiers)
|
||||
* - ETL middleware: \\<host>\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',
|
||||
];
|
||||
|
||||
const LOG_FILE_PATTERN = process.env.LOG_FILE_PATTERN || '*.log';
|
||||
|
||||
/**
|
||||
* 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<Array>} - 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(`Cannot access file ${fullPath}: ${statErr.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Ne pas planter si un dossier n'existe pas ou n'est pas accessible
|
||||
console.error(`Cannot scan directory ${dir}: ${err.message}`);
|
||||
}
|
||||
|
||||
return fileList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste tous les fichiers de logs dans tous les répertoires configurés
|
||||
* @returns {Promise<Array>} - 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<string>} - 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<boolean>}
|
||||
*/
|
||||
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<string>} - 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(`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<Object>} - 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<Object>} - 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(`Cannot read file ${file.path}: ${readErr.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
keyword,
|
||||
totalResults: results.length,
|
||||
results,
|
||||
scannedPaths: getLogPaths(),
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(`Search failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche des erreurs dans les logs récents
|
||||
* @param {number} maxResults - Nombre maximum de résultats
|
||||
* @returns {Promise<Object>} - Erreurs trouvées
|
||||
*/
|
||||
async function findRecentErrors(maxResults = 20) {
|
||||
const errorPatterns = ['error', 'exception', 'failed', 'fatal', 'critical'];
|
||||
const allErrors = [];
|
||||
|
||||
try {
|
||||
for (const pattern of errorPatterns) {
|
||||
if (allErrors.length >= maxResults) break;
|
||||
|
||||
const results = await searchLogs(pattern, maxResults - allErrors.length, 1);
|
||||
allErrors.push(...results.results);
|
||||
}
|
||||
|
||||
// Dédupliquer par numéro de ligne et fichier
|
||||
const unique = allErrors.filter(
|
||||
(error, index, self) =>
|
||||
index ===
|
||||
self.findIndex(
|
||||
(e) => e.fullPath === error.fullPath && e.lineNumber === error.lineNumber
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
totalErrors: unique.length,
|
||||
errors: unique.slice(0, maxResults),
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to find errors: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lit tout le contenu d'un fichier de log spécifique
|
||||
* @param {string} logFilePath - Chemin complet du fichier
|
||||
* @returns {Promise<Object>} - Contenu du fichier
|
||||
*/
|
||||
async function readFullLog(logFilePath) {
|
||||
try {
|
||||
const content = await fs.readFile(logFilePath, 'utf8');
|
||||
const lines = content.split('\n').filter((line) => line.trim() !== '');
|
||||
|
||||
return {
|
||||
file: path.basename(logFilePath),
|
||||
fullPath: logFilePath,
|
||||
totalLines: lines.length,
|
||||
content: lines,
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to read log file: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient des statistiques sur les logs
|
||||
* @returns {Promise<Object>} - Statistiques
|
||||
*/
|
||||
async function getLogStats() {
|
||||
try {
|
||||
const files = await listLogFiles();
|
||||
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
|
||||
|
||||
// Grouper par dossier
|
||||
const byDirectory = {};
|
||||
files.forEach(file => {
|
||||
const dir = file.directory;
|
||||
if (!byDirectory[dir]) {
|
||||
byDirectory[dir] = {
|
||||
directory: dir,
|
||||
count: 0,
|
||||
totalSize: 0,
|
||||
files: [],
|
||||
};
|
||||
}
|
||||
byDirectory[dir].count++;
|
||||
byDirectory[dir].totalSize += file.size;
|
||||
byDirectory[dir].files.push({
|
||||
name: file.name,
|
||||
sizeMB: (file.size / (1024 * 1024)).toFixed(2),
|
||||
modified: file.modified.toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
configuredPaths: getLogPaths(),
|
||||
totalFiles: files.length,
|
||||
totalSize,
|
||||
totalSizeMB: (totalSize / (1024 * 1024)).toFixed(2),
|
||||
oldestFile: files[files.length - 1]?.name,
|
||||
newestFile: files[0]?.name,
|
||||
byDirectory: Object.values(byDirectory).sort((a, b) => b.count - a.count),
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to get log stats: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listLogFiles,
|
||||
findLatestLogFile,
|
||||
readRecentLogs,
|
||||
searchLogs,
|
||||
findRecentErrors,
|
||||
readFullLog,
|
||||
getLogStats,
|
||||
};
|
||||
Reference in New Issue
Block a user