Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .claude/deploiement.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<fichier>.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
Expand Down
11 changes: 11 additions & 0 deletions .github/avancement.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 9 additions & 0 deletions bin/deploy-nightly.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
# <prenom>|<statut>|<étape ou ->|<sha ou ->
echo "${PRENOM}|${1}|${2:--}|${3:--}" >> "$REPORT_FILE"
Expand Down
62 changes: 62 additions & 0 deletions bin/rotate-logs.sh
Original file line number Diff line number Diff line change
@@ -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 <fichier>.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
40 changes: 40 additions & 0 deletions tests/bash/deploy-nightly-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

132 changes: 132 additions & 0 deletions tests/bash/rotate-logs-test.sh
Original file line number Diff line number Diff line change
@@ -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
}
Loading