From c599b59d8ad86911f0f22da636eaedc3ea95d63b Mon Sep 17 00:00:00 2001 From: Ronan Lenouvel Date: Fri, 2 Oct 2026 12:52:07 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20feat(rotate-logs):=20archive=20?= =?UTF-8?q?puis=20tronque=20les=20logs=20de=20plus=20de=2010=20Mo,=203=20a?= =?UTF-8?q?rchives=20conserv=C3=A9es=20(#606)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/rotate-logs.sh | 62 ++++++++++++++++ tests/bash/rotate-logs-test.sh | 132 +++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100755 bin/rotate-logs.sh create mode 100644 tests/bash/rotate-logs-test.sh diff --git a/bin/rotate-logs.sh b/bin/rotate-logs.sh new file mode 100755 index 00000000..38a0d7e5 --- /dev/null +++ b/bin/rotate-logs.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# ============================================================================= +# HomeCloud — Rotation des logs applicatifs d'une instance (#606) +# +# Usage : bash bin/rotate-logs.sh [dossier_de_logs] (défaut : var/log) +# +# Constat du 2026-10-02 : var/log/messenger.log atteignait ~50 Mo sur chacune +# des 7 instances, sans aucune purge. Le worker Messenger (cron toutes les +# minutes, `>> messenger.log`) réimprime sa bannière à chaque démarrage : ce +# fichier ne contient quasiment que ce bruit, mais y reçoit aussi les erreurs. +# +# Pour chaque *.log de plus de ROTATE_LOGS_MAX_BYTES (défaut 10 Mo) : +# - archive compressée dans .1.gz (les anciennes sont décalées, au +# plus ROTATE_LOGS_KEEP archives conservées, défaut 3) ; +# - le fichier est TRONQUÉ, jamais renommé : un worker qui tourne garde son +# descripteur ouvert (`>>` = O_APPEND) et continue d'écrire au bon endroit, +# sans qu'il faille le redémarrer. Quelques lignes écrites entre la copie +# et la troncature peuvent être perdues — acceptable pour des logs. +# +# Observation pure : si l'archivage échoue, le log n'est PAS tronqué, et le +# script sort toujours en 0 — il ne doit jamais faire échouer son appelant +# (le déploiement nocturne). +# ============================================================================= + +set -uo pipefail + +LOG_DIR="${1:-var/log}" +MAX_BYTES="${ROTATE_LOGS_MAX_BYTES:-10485760}" +KEEP="${ROTATE_LOGS_KEEP:-3}" + +# Les archives contiennent des logs : même confidentialité que les logs. +umask 077 + +[[ -d "$LOG_DIR" ]] || exit 0 + +rotate_one() { + local file="$1" size i + size=$(stat -c %s "$file" 2>/dev/null) || return 0 + [[ "$size" -gt "$MAX_BYTES" ]] || return 0 + + # Archiver d'abord dans un fichier temporaire : tant que la copie n'est pas + # complète, rien n'est décalé ni tronqué. + if ! gzip -c "$file" > "${file}.new.gz" 2>/dev/null; then + rm -f "${file}.new.gz" + echo "⚠ rotation : archivage de ${file} impossible, log conservé tel quel" >&2 + return 0 + fi + + rm -f "${file}.${KEEP}.gz" + for ((i = KEEP - 1; i >= 1; i--)); do + [[ -f "${file}.${i}.gz" ]] && mv "${file}.${i}.gz" "${file}.$((i + 1)).gz" + done + mv "${file}.new.gz" "${file}.1.gz" + : > "$file" + echo "↻ rotation : ${file} ($((size / 1024)) Ko) archivé dans ${file}.1.gz" +} + +for log in "$LOG_DIR"/*.log; do + [[ -f "$log" ]] && rotate_one "$log" +done + +exit 0 diff --git a/tests/bash/rotate-logs-test.sh b/tests/bash/rotate-logs-test.sh new file mode 100644 index 00000000..d32b02a4 --- /dev/null +++ b/tests/bash/rotate-logs-test.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +# ============================================================================= +# Tests de bin/rotate-logs.sh (#606) — rotation des logs applicatifs. +# Constat 2026-10-02 : var/log/messenger.log atteignait ~50 Mo sur les 7 +# instances (le worker Messenger réimprime sa bannière à chaque démarrage, +# cron toutes les minutes), sans aucune purge. +# ============================================================================= + +ROTATE_SCRIPT="${PROJECT_ROOT}/bin/rotate-logs.sh" + +_rotate_setup() { + ROT_DIR="$(mktemp -d)" + export ROTATE_LOGS_MAX_BYTES=100 ROTATE_LOGS_KEEP=3 +} + +_rotate_teardown() { + rm -rf "$ROT_DIR" + unset ROTATE_LOGS_MAX_BYTES ROTATE_LOGS_KEEP + hash -r +} + +_big_content() { printf 'ligne-%s-' "$1"; head -c 200 /dev/zero | tr '\0' 'x'; echo; } + +test_log_sous_le_seuil_est_intact() { + _rotate_setup + echo "petit" > "${ROT_DIR}/messenger.log" + + bash "$ROTATE_SCRIPT" "$ROT_DIR" > /dev/null 2>&1 + + assert_equals "petit" "$(cat "${ROT_DIR}/messenger.log")" + assert_file_not_exists "${ROT_DIR}/messenger.log.1.gz" + + _rotate_teardown +} + +test_log_au_dessus_du_seuil_est_archive_puis_vide() { + _rotate_setup + _big_content A > "${ROT_DIR}/messenger.log" + local before + before="$(cat "${ROT_DIR}/messenger.log")" + + bash "$ROTATE_SCRIPT" "$ROT_DIR" > /dev/null 2>&1 + local exit_code=$? + + assert_equals "0" "$exit_code" + assert_file_exists "${ROT_DIR}/messenger.log.1.gz" + assert_equals "$before" "$(gunzip -c "${ROT_DIR}/messenger.log.1.gz")" "l'archive contient exactement l'ancien contenu" + assert_equals "0" "$(wc -c < "${ROT_DIR}/messenger.log" | tr -d ' ')" "le log est vidé" + + _rotate_teardown +} + +test_les_archives_sont_decalees_et_limitees_a_keep() { + _rotate_setup + echo "ancienne-1" | gzip > "${ROT_DIR}/messenger.log.1.gz" + echo "ancienne-2" | gzip > "${ROT_DIR}/messenger.log.2.gz" + echo "ancienne-3" | gzip > "${ROT_DIR}/messenger.log.3.gz" + _big_content NEW > "${ROT_DIR}/messenger.log" + + bash "$ROTATE_SCRIPT" "$ROT_DIR" > /dev/null 2>&1 + + assert_contains "$(gunzip -c "${ROT_DIR}/messenger.log.1.gz")" "ligne-NEW" + assert_equals "ancienne-1" "$(gunzip -c "${ROT_DIR}/messenger.log.2.gz")" + assert_equals "ancienne-2" "$(gunzip -c "${ROT_DIR}/messenger.log.3.gz")" + assert_file_not_exists "${ROT_DIR}/messenger.log.4.gz" + + _rotate_teardown +} + +test_seuls_les_fichiers_log_sont_traites() { + _rotate_setup + _big_content X > "${ROT_DIR}/notes.txt" + _big_content Y > "${ROT_DIR}/messenger.log" + + bash "$ROTATE_SCRIPT" "$ROT_DIR" > /dev/null 2>&1 + + assert_contains "$(cat "${ROT_DIR}/notes.txt")" "ligne-X" + assert_file_not_exists "${ROT_DIR}/notes.txt.1.gz" + assert_file_exists "${ROT_DIR}/messenger.log.1.gz" + + _rotate_teardown +} + +# Le worker Messenger écrit via « >> messenger.log » et garde son descripteur +# ouvert : la rotation doit tronquer, pas renommer, et les écritures suivantes +# doivent repartir du début (O_APPEND), sans trou ni octets nuls. +test_ecritures_du_worker_apres_rotation_repartent_de_zero() { + _rotate_setup + _big_content W > "${ROT_DIR}/messenger.log" + exec 9>>"${ROT_DIR}/messenger.log" + + bash "$ROTATE_SCRIPT" "$ROT_DIR" > /dev/null 2>&1 + echo "apres-rotation" >&9 + exec 9>&- + + assert_equals "apres-rotation" "$(cat "${ROT_DIR}/messenger.log")" "uniquement les nouvelles lignes" + assert_equals "15" "$(wc -c < "${ROT_DIR}/messenger.log" | tr -d ' ')" "pas de trou (octets nuls) en tête de fichier" + + _rotate_teardown +} + +# Si l'archivage échoue, on ne tronque JAMAIS : perdre des logs pour rien serait pire. +test_echec_de_gzip_laisse_le_log_intact_et_ne_fait_pas_echouer() { + _rotate_setup + local stubs + stubs="$(mktemp -d)" + printf '#!/bin/bash\nexit 1\n' > "${stubs}/gzip" + chmod +x "${stubs}/gzip" + _big_content F > "${ROT_DIR}/messenger.log" + local before + before="$(cat "${ROT_DIR}/messenger.log")" + + PATH="${stubs}:${PATH}" bash "$ROTATE_SCRIPT" "$ROT_DIR" > /dev/null 2>&1 + local exit_code=$? + + assert_equals "0" "$exit_code" "la rotation ne doit jamais faire échouer l'appelant" + assert_equals "$before" "$(cat "${ROT_DIR}/messenger.log")" "log intact si l'archivage échoue" + + rm -rf "$stubs" + _rotate_teardown +} + +test_dossier_absent_ou_vide_ne_fait_pas_echouer() { + _rotate_setup + + bash "$ROTATE_SCRIPT" "${ROT_DIR}/inexistant" > /dev/null 2>&1 + assert_equals "0" "$?" "dossier absent" + bash "$ROTATE_SCRIPT" "$ROT_DIR" > /dev/null 2>&1 + assert_equals "0" "$?" "dossier vide" + + _rotate_teardown +} From c249eb829277830e641bd3d8578cba7ae3f931de Mon Sep 17 00:00:00 2001 From: Ronan Lenouvel Date: Fri, 2 Oct 2026 12:52:07 +0200 Subject: [PATCH 2/3] =?UTF-8?q?=E2=9C=A8=20feat(deploy-nightly):=20lance?= =?UTF-8?q?=20la=20rotation=20des=20logs=20avant=20toute=20d=C3=A9cision?= =?UTF-8?q?=20de=20d=C3=A9ploiement=20(#606)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/deploy-nightly.sh | 9 +++++++ tests/bash/deploy-nightly-test.sh | 40 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/bin/deploy-nightly.sh b/bin/deploy-nightly.sh index ac5acd12..be797b1b 100755 --- a/bin/deploy-nightly.sh +++ b/bin/deploy-nightly.sh @@ -31,6 +31,15 @@ COMPOSER_BIN="$HC_COMPOSER_BIN" cd "$INSTANCE_PATH" || exit 1 mkdir -p var/log +# Rotation des logs (#606) — avant toute décision de déploiement, donc même +# quand l'instance est à jour ou quand le déploiement est reporté. Ce script est +# déjà planifié et étalé sur les 7 instances : pas de nouveau cron à poser. +# Observation pure : un échec ne doit jamais empêcher le déploiement. Lancé via +# "$BASH" (chemin absolu du bash courant) et non « bash » : indépendant du PATH +# du cron et des stubs des tests. +ROTATE_LOGS_SCRIPT="${DEPLOY_NIGHTLY_ROTATE_SCRIPT:-${SCRIPT_DIR}/rotate-logs.sh}" +"$BASH" "$ROTATE_LOGS_SCRIPT" var/log || echo "⚠ ${PRENOM} : rotation des logs en échec (sans effet sur le déploiement)" >&2 + report_line() { # ||<étape ou ->| echo "${PRENOM}|${1}|${2:--}|${3:--}" >> "$REPORT_FILE" diff --git a/tests/bash/deploy-nightly-test.sh b/tests/bash/deploy-nightly-test.sh index 784d1c22..1d1bdf12 100644 --- a/tests/bash/deploy-nightly-test.sh +++ b/tests/bash/deploy-nightly-test.sh @@ -454,3 +454,43 @@ test_pas_de_rollback_si_le_code_en_place_est_deja_la_cible() { _teardown_fixture } + +# ── Rotation des logs (#606) ──────────────────────────────────────────────── +# Le script nocturne est déjà planifié et étalé sur les 7 instances : il porte +# la rotation (pas de nouveau cron à poser à la main sur chaque serveur). + +test_rotation_des_logs_meme_si_l_instance_est_a_jour() { + _setup_fixture + echo "abc1234" > "${FIXTURE_DIR}/instance/.deployed-sha" + _write_all_success_stubs "abc1234" + head -c 500 /dev/zero | tr '\0' 'x' > "${FIXTURE_DIR}/instance/var/log/messenger.log" + export ROTATE_LOGS_MAX_BYTES=100 + + _run_nightly + + assert_file_exists "${FIXTURE_DIR}/instance/var/log/messenger.log.1.gz" + assert_equals "0" "$(wc -c < "${FIXTURE_DIR}/instance/var/log/messenger.log" | tr -d ' ')" "log vidé" + assert_contains "$(_report)" "yannick|skipped" + + unset ROTATE_LOGS_MAX_BYTES + _teardown_fixture +} + +test_echec_de_la_rotation_ne_bloque_pas_le_deploiement() { + _setup_fixture + echo "old0000" > "${FIXTURE_DIR}/instance/.deployed-sha" + _write_all_success_stubs "new1111" + printf '#!/bin/bash\nexit 1\n' > "${FIXTURE_DIR}/rotate-fails.sh" + export DEPLOY_NIGHTLY_ROTATE_SCRIPT="${FIXTURE_DIR}/rotate-fails.sh" + + _run_nightly + local exit_code=$? + + assert_equals "0" "$exit_code" "une rotation en échec ne doit pas empêcher le déploiement" + assert_contains "$(_report)" "yannick|ok" + assert_equals "new1111" "$(cat "${FIXTURE_DIR}/instance/.deployed-sha")" + + unset DEPLOY_NIGHTLY_ROTATE_SCRIPT + _teardown_fixture +} + From 4dd2b66c02a078e736f4cbfca0018a273641c8eb Mon Sep 17 00:00:00 2001 From: Ronan Lenouvel Date: Fri, 2 Oct 2026 12:52:07 +0200 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=93=9D=20docs(deploiement):=20rotatio?= =?UTF-8?q?n=20des=20logs=20et=20suivi=20#606?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/deploiement.md | 10 ++++++++++ .github/avancement.md | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/.claude/deploiement.md b/.claude/deploiement.md index 7d56712a..78cd59c7 100644 --- a/.claude/deploiement.md +++ b/.claude/deploiement.md @@ -639,6 +639,16 @@ Ce qui est en place depuis, **commun aux trois scripts** (`bin/lib/deploy-common - Un correctif du script nocturne n'est actif sur une instance qu'**à la nuit suivant celle où elle l'a récupéré** (le script du cron est celui du disque au lancement). Après un correctif de script, passer par `bash bin/deploy-all.sh` (exécuté depuis le poste) plutôt que d'attendre le cron. - Tests : `bash tests/bash/run.sh` (28 tests). Ils ne tournent pas dans la CI. +### Rotation des logs — `bin/rotate-logs.sh` (#606) + +Constat du 2026-10-02 : `var/log/messenger.log` atteignait ~50 Mo sur chacune des 7 instances, sans aucune purge. Le worker Messenger (cron toutes les minutes, `>> messenger.log`) réimprime sa bannière à chaque démarrage : le fichier ne contient presque que ce bruit, mais y reçoit aussi les erreurs du worker. + +- **Qui l'appelle** : `bin/deploy-nightly.sh`, au tout début (avant toute décision de déploiement : instance à jour, déploiement reporté ou non). Il est déjà planifié et étalé sur les 7 instances : **aucun nouveau cron à poser**. Un échec de rotation est signalé mais ne bloque jamais le déploiement. +- **Règle** : chaque `var/log/*.log` de plus de 10 Mo (`ROTATE_LOGS_MAX_BYTES`) est archivé dans `.1.gz` ; les archives sont décalées, 3 conservées (`ROTATE_LOGS_KEEP`). Mesuré sur un fichier de 50 Mo de bannières : archive de 245 Ko, 0,2 s, 3 Mo de mémoire. +- **Troncature, pas renommage** : le worker garde son descripteur ouvert (`>>` = O_APPEND) et continue d'écrire au bon endroit sans redémarrage. Quelques lignes écrites entre la copie et la troncature peuvent être perdues. Si l'archivage échoue, le log n'est **pas** tronqué. +- **Quand ça prend effet** : un correctif de script n'est actif sur une instance qu'à la nuit suivant celle où elle l'a récupéré (le script du cron est celui du disque). Pour purger tout de suite : `bash bin/rotate-logs.sh` dans le dossier de l'instance, ou `bash bin/deploy-all.sh` pour ronan. +- Les logs en eux-mêmes sont inchangés (niveau, contenu) : réduire le bruit à la source (bannière du worker) reste possible séparément. + ### Crons cPanel — créés et actifs depuis le 2026-09-12 > Vérifié en SSH le 2026-09-12 22h : les 8 crons ci-dessous sont bien diff --git a/.github/avancement.md b/.github/avancement.md index 34b3ace6..94b4a181 100644 --- a/.github/avancement.md +++ b/.github/avancement.md @@ -48,6 +48,17 @@ --- +## 🚧 Rotation des logs applicatifs (2026-10-02, #606, branche `chore/606-log-rotation`) + +- Constat : `var/log/messenger.log` à ~50 Mo sur chacune des 7 instances, sans purge ; 1,24 million de lignes dont aucune utile (la bannière du worker, réimprimée à chaque démarrage par le cron toutes les minutes, ~103 000 démarrages). +- `bin/rotate-logs.sh` : `*.log` > 10 Mo archivé en `.1.gz` (3 archives conservées), puis **tronqué** (le worker garde son descripteur `>>`). Pas d'archivage → pas de troncature ; sort toujours en 0. Mesuré : 50 Mo → 245 Ko, 0,2 s, 3 Mo de mémoire. +- Appelé en début de `bin/deploy-nightly.sh` (déjà planifié et étalé sur les 7 instances) : aucun nouveau cron, aucune intervention manuelle par serveur. Un échec n'empêche jamais le déploiement. +- Tests bash : 37/37 (7 pour la rotation dont l'écriture O_APPEND après troncature, 2 d'intégration nocturne). +- Effectif sur une instance à la nuit suivant celle où elle récupère le script ; purge immédiate possible avec `bash bin/rotate-logs.sh`. +- Reste : revue, PR (label + `Closes #606` + assignee + board), CI verte, merge ; la purge réelle des 7 instances se constate après un passage nocturne (ou à la demande). + +--- + ## ✅ Mises à jour composer — Symfony 8.0 → 8.1 (2026-10-02, #538, PR #562 mergée) - Contraintes `symfony/*` et `extra.symfony.require` passées de `8.0.*` à `8.1.*` (un `composer update` seul n'aurait rien monté). Symfony en 8.1.8, Doctrine ORM 3.7.3, DoctrineBundle 3.3.2, phpunit 13.4.0, twig 3.30, monolog 3.12.1.