diff --git a/README.md b/README.md index e69de29..bfde52a 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,83 @@ +# logwatcher + +Logwatcher est un outil sur-mesure d'analyse et de tri automatique des logs LAME MDC pour le support N2. + +Les techniciens N2 reçoivent quotidiennement des compte-rendus de suivi +d'imports NOSYMAG contenant des centaines de lignes d'erreurs. La plupart +ne les concernent pas. Logwatcher analyse ces compte-rendus, identifie +les erreurs pertinentes pour le support N2, et génère trois rapports +(n2, autres, tout) prêts à être transmis. + +--- + +## 📦 Installation + +### Prérequis +- Python >= 3.14 +- `uv` (recommandé) ou `pip` + +### Procédure + +#### uv +```bash +uv pip install -e ".[dev]" +``` + +#### pip +```bash +pip install -e ".[dev]" +``` + +--- + +## 🚀 Commandes + +### Analyser un fichier de logs +```bash +logwatcher --input-files chemin/vers/CR_fichier.txt +``` + +### Analyser tous les logs d'un répertoire +```bash +logwatcher --input-dir chemin/vers/dossier +``` + +### Afficher la version +```bash +logwatcher --version +``` + +Le guide complet se trouve dans [usage](docs/usage.md). + +--- + +## 🏗️ Architecture +Le détail de l'architecture se trouve dans [architecture](docs/architecture.md). + +--- + +## 🧪 Tests +Les tests se trouvent dans `tests/`. Le projet est couvert à ~95% par les tests unitaires et d'intégration. + + +### Lancer tous les tests +```bash +pytest +``` + +### Lancer un test spécifique (ex: `test_parser.py`) +```bash +pytest tests/test_parser.py +``` + + +### Générer un rapport de couverture +```bash +pytest --cov=logwatcher --cov-report=term-missing +``` + +--- + +## 📄 Licence + +MIT — voir [LICENSE](LICENSE). \ No newline at end of file diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..0c800ae --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,105 @@ +# Architecture + +## Vue d'ensemble + +Logwatcher est un outil en ligne de commande Python qui analyse des fichiers +de logs LAME MDC, identifie les erreurs pertinentes pour le support N2, et +génère des rapports texte. + +Le traitement suit un pipeline linéaire : + +```text +fichiers de logs + │ + ▼ + parser.py lit et parse les fichiers en LogEntry + │ + ▼ + classifier.py classe chaque entrée : N2 ou hors N2 + │ + ▼ + reporter.py génère les trois rapports (all, n2, other) + │ + ▼ + rapports .log +``` + +## Structure du package + +```text +src/logwatcher/ +├── __init__.py # expose __version__ +├── __main__.py # point d'entrée : python -m logwatcher +├── cli.py # interface en ligne de commande (Typer) +├── config.py # constantes : chemins, formats +├── logging_config.py # configuration du logging +├── models.py # LogEntry : structure de données +├── parser.py # lecture et parsing des fichiers +├── classifier.py # classification N2 / hors N2 +└── reporter.py # génération des rapports texte +``` + +### Modules +#### cli.py — point d'entrée + +* Définit `app = typer.Typer()` et la commande principale. +* Valide les arguments (`--input-files` / `--input-dir` mutuellement exclusifs). +* Orchestre le pipeline : parsing → classification → rapports. +* Gère les erreurs utilisateur (`BadParameter`, code de sortie 2) et +* les erreurs d'exécution (code de sortie 1). +* Expose `--version` (eager, affiche et quitte). + +#### models.py — LogEntry + +Dataclass immuable (par convention) représentant une ligne de log parsée. + +Champs principaux : + +* `server_ip`, `mdc_server_name` : origine du log +* `store_name` : magasin concerné +* `start_time`, error_time : horodatages +* `error_message` : message d'erreur complet +* `error_name` : nom du pattern N2 correspondant (rempli par le classifier) +* `raw_line` : ligne brute d'origine +* `ligne` : numéro de ligne dans le fichier source + +LogEntry est créée uniquement par le parser. Le classifier l'enrichit +(remplit `error_name`). Le modèle lui-même ne contient pas de logique métier. +[`parser.py`]() — lecture et parsing + +* `LOG_PATTERN` : expression régulière du format d'une ligne LAME MDC. +* `parse_log_file()` : lit un fichier, retourne list[LogEntry]. +* Les lignes non conformes (système, en-têtes, corrompues) sont ignorées silencieusement. + +#### classifier.py — classification + +* N2_PATTERNS : dictionnaire des motifs d'erreurs pertinents pour le N2, fournis par les techniciens. +* classify_log_entries() : sépare les entrées en deux listes (relevant, irrelevant) et remplit entry.error_name pour les entrées pertinentes. + +#### reporter.py — rapports + +* Templates texte (`string.Template`) : `BASE_TEMPLATE`, `N2_SUPPORT_TEMPLATE`, `OTHER_TEMPLATE`, `ERROR_TEMPLATE`. +* `build_reports()` : construit les trois rapports (`dict n2, other, all`). +* `write_log_report()` : écrit les rapports sur disque en Windows-1252. +* `_get_period()` : calcule les bornes min/max des error_time. + +#### logging_config.py — logging + +* `setup_logging()` : configure le logger racine. +* Handler console (`INFO`, ou `DEBUG` si verbose) + handler fichier. +* Le fichier de log `logwatcher.log` est horodaté et écrit dans le dossier de sortie. + +#### config.py — constantes + +* Chemins (`OUTPUT_PATH`, `FIXTURE_PATH` ...). +* Formats de date (`DATETIME_FORMAT`). + +## Dépendances + +| Package | Rôle | +|---------|------| +| `typer` | Interface en ligne de commande | +| `pytest` | Tests (dev) | +| `pytest-cov` | Couverture (dev) | +| `ruff` | Linting/formatage (dev) | +| `mypy` | Typage statique (dev) | diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..c012d38 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,142 @@ +# Guide d'utilisation + +## Introduction + +Logwatcher analyse les comptes-rendus de suivi d'imports NOSYMAG (fichiers +`CR_*.txt` et autres logs LAME MDC) pour identifier les erreurs pertinentes +pour le support N2. Il génère trois rapports : un pour les erreurs N2, un +pour les erreurs hors périmètre N2, et un rapport complet. + +--- + +## Entrées + +### Analyser un fichier de logs + +```bash +logwatcher --input-files chemin/vers/CR_fichier.txt +``` + +### Analyser plusieurs fichiers de logs +L'option `--input-files` accepte plusieurs fichiers. Répéter l'option pour +chaque fichier : + +```bash +logwatcher --input-files fichier1.txt --input-files fichier2.txt +``` + +### Analyser tous les logs d'un répertoire + +```bash +logwatcher --input-dir chemin/vers/dossier +``` + +L'option `--input-dir` analyse tous les fichiers `.log`, `.txt` ou sans +extension contenus dans le répertoire. Les autres fichiers sont ignorés. + +> ### Règle d'exclusivité +> `--input-files` et `--input-dir` sont mutuellement exclusifs. Fournir +les deux options lève une erreur et le traitement est interrompu. Ne fournir +aucune des deux lève également une erreur. + +## Sorties + +### Emplacement des rapports + +Par défaut, les rapports sont écrits dans le répertoire `output/` du dossier +courant. Un autre emplacement peut être fourni avec `--output-dir` : + +```bash +logwatcher --input-dir chemin/vers/dossier --output-dir chemin/vers/sortie +``` + +### Les rapports générés + +Chaque analyse produit trois rapports différents : `all.log`, `n2.log` et `other.log`. + +- `all.log` : Toutes les erreurs analysées, N2 et hors N2. +- `n2.log` : Uniquement les erreurs pertinentes pour le support N2. +- `other.log` : Uniquement les erreurs hors périmètre N2 + + +#### Contenu d'un rapport + +Chaque rapport contient un en-tête avec la période analysée, le nombre de +fichiers lus et le nombre total d'erreurs, suivi du détail des erreurs. +Un rapport se présente ainsi : + +```text +RAPPORT D'ANALYSE DE LOGS +========================= +Période : 01/09/2026 11:07:53 -> 01/09/2026 15:04:18 +Fichier(s) lu(s) : 1 +Nombre total d'erreur(s) : 3 + +================================= +ERREURS SUPPORT N2 +================================= +Nombre d'erreurs : 2 + +[1] BL_AUTO_BESTSELLER_REJECTED + Magasin: DUMAS-DELAGE + MDC: MDC_340 + Serveur: 192.168.13.23 + Heure: 28/07/2026 08:05:27 + Raison: Erreur : 1099_2622609667_DESADV : / 00098/000 ... ne correspond +``` + +## Options + +- `--output-dir` : Répertoire de sortie des rapports. Par défaut : `output/`. +```bash +logwatcher --input-dir logs/ --output-dir rapports/2026-09-01/ +``` + +- `--version` : Affiche la version du package et quitte. +```bash +logwatcher --version +# logwatcher version: 0.0.1 +``` + +## Cas particuliers + +### Fichier vide + +Un fichier vide est lu sans erreur. Les trois rapports sont générés avec `Nombre total d'erreur(s) : 0`. + + +### Répertoire vide + +Un répertoire vide est traité sans erreur. Les trois rapports sont générés +avec `Fichier(s) lu(s) : 0 et Nombre total d'erreur(s) : 0`. + + +### Logs non reconnus + +Les lignes qui ne correspondent pas au format LAME MDC (lignes système, +en-têtes de répertoire, lignes corrompues) sont ignorées silencieusement +et ne produisent pas d'entrée dans les rapports. + + +### Encodage des fichiers + +Les fichiers d'entrée sont lus en Windows-1252 (encodage natif des logs +LAME MDC). Les rapports générés sont également écrits en Windows-1252. + +## Exemple complet + +Analyser tous les logs d'un répertoire et écrire les rapports dans un +dossier dédié : +```bash +logwatcher --input-dir logs/2026-09-01/ --output-dir rapports/2026-09-01/ +``` + +Après exécution, les fichiers suivants sont créés dans `rapports/2026-09-01/` : +```text +all.log # toutes les erreurs +n2.log # erreurs pertinentes pour le N2 +other.log # erreurs hors périmètre N2 +``` + +Le rapport `n2.log` peut ensuite être transmis aux techniciens N2, et +`other.log` aux équipes concernées par les autres erreurs. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 566eafe..4aaeb4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "logwatcher" version = "0.0.1" description = "Trier, classifier et extraire automatiquement les entrées pertinentes d'un fichier de logs applicatif." readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.14" dependencies = [ "typer>=0.27.2", ] diff --git a/src/logwatcher/sorter.py b/src/logwatcher/sorter.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_reporter.py b/tests/test_reporter.py index 26969b6..5e97aad 100644 --- a/tests/test_reporter.py +++ b/tests/test_reporter.py @@ -128,7 +128,7 @@ def test_render_target_report_relevant( relevant = get_only_relevant_log_entries[0] target_report = _render_target_report(relevant, N2_SUPPORT_TEMPLATE) assert "ERREURS SUPPORT N2" in target_report - assert target_report.find("Nombre d'erreurs\t : " + str(len(relevant))) != -1 + assert target_report.find("Nombre d'erreur(s)\t : " + str(len(relevant))) != -1 assert _render_entries(relevant) in target_report @@ -139,7 +139,7 @@ def test_render_target_report_irrelevant( irrelevant = get_only_irrelevant_log_entries[1] target_report = _render_target_report(irrelevant, OTHER_TEMPLATE) assert "AUTRES ERREURS" in target_report - assert target_report.find("Nombre d'erreurs\t : " + str(len(irrelevant))) != -1 + assert target_report.find("Nombre d'erreur(s)\t : " + str(len(irrelevant))) != -1 assert _render_entries(irrelevant) in target_report @@ -165,10 +165,10 @@ def test_build_reports( assert "all" in reports and "n2" in reports and "other" in reports assert reports["all"].find(f"Période\t : {start_date} -> {end_date}") != -1 - assert reports["all"].find(f"Fichiers lu\t : {nb_files}") != -1 + assert reports["all"].find(f"Fichier(s) lu(s)\t : {nb_files}") != -1 assert ( reports["all"].find( - f"Nombre total d'erreurs\t: {len(get_all_log_entries[0] + get_all_log_entries[1])}" + f"Nombre total d'erreur(s)\t: {len(get_all_log_entries[0] + get_all_log_entries[1])}" ) != -1 ) @@ -181,12 +181,12 @@ def test_build_reports( and "AUTRES ERREURS" in reports["other"] ) assert ( - reports["n2"].find(f"Nombre total d'erreurs\t: {len(get_all_log_entries[0])}") + reports["n2"].find(f"Nombre total d'erreur(s)\t: {len(get_all_log_entries[0])}") != -1 ) assert ( reports["other"].find( - f"Nombre total d'erreurs\t: {len(get_all_log_entries[1])}" + f"Nombre total d'erreur(s)\t: {len(get_all_log_entries[1])}" ) != -1 ) @@ -224,21 +224,21 @@ def _test_write_log_report( assert reports["all"] == all_file.read_text(encoding="windows-1252") assert ( all_file.read_text(encoding="windows-1252").find( - f"Nombre total d'erreurs\t: {nb_errors['all']}" + f"Nombre total d'erreur(s)\t: {nb_errors['all']}" ) != -1 ) assert reports["n2"] == n2_file.read_text(encoding="windows-1252") assert ( n2_file.read_text(encoding="windows-1252").find( - f"Nombre total d'erreurs\t: {nb_errors['n2']}" + f"Nombre total d'erreur(s)\t: {nb_errors['n2']}" ) != -1 ) assert reports["other"] == other_file.read_text(encoding="windows-1252") assert ( other_file.read_text(encoding="windows-1252").find( - f"Nombre total d'erreurs\t: {nb_errors['other']}" + f"Nombre total d'erreur(s)\t: {nb_errors['other']}" ) != -1 )