From abba8e352ae225289dec306c23b8c52f14a60010 Mon Sep 17 00:00:00 2001 From: Manus AI Date: Tue, 18 Aug 2026 08:09:38 +0000 Subject: [PATCH] feat(ops): ajouter supervision automatique des applications --- ops/README.md | 11 ++++ ops/healthcheck-apps.sh | 120 ++++++++++++++++++++++++++++++++++ ops/install-healthcheck.sh | 15 +++++ ops/manus-apps-health.service | 13 ++++ ops/manus-apps-health.timer | 12 ++++ 5 files changed, 171 insertions(+) create mode 100644 ops/README.md create mode 100644 ops/healthcheck-apps.sh create mode 100644 ops/install-healthcheck.sh create mode 100644 ops/manus-apps-health.service create mode 100644 ops/manus-apps-health.timer diff --git a/ops/README.md b/ops/README.md new file mode 100644 index 0000000..f0ef587 --- /dev/null +++ b/ops/README.md @@ -0,0 +1,11 @@ +# Supervision des applications + +Le script `healthcheck-apps.sh` est exécuté par le timer systemd toutes les cinq minutes et trois minutes après chaque démarrage serveur. Il vérifie les conteneurs Docker Compose, leur healthcheck, leur politique de redémarrage et les routes HTTPS Traefik déclarées. Une tâche ponctuelle terminée avec succès est reconnue comme telle. Toute réponse HTTP inférieure à 500 confirme la joignabilité d’une route, y compris une API sans page racine ou un tableau protégé. En cas d’anomalie, il produit un rapport et une entrée de journal, **sans redémarrer de conteneur**. Les redémarrages automatiques restent assurés par les politiques Docker `restart` et par `manus-apps.service` au démarrage du serveur. + +Le dernier rapport est écrit dans `/var/lib/manus-apps-health/latest.txt`. Les anomalies sont aussi consignées dans le journal système avec le tag `manus-apps-health`. + +Après mise à jour Git, installer ou actualiser le service avec : + +```bash +sudo ./ops/install-healthcheck.sh +``` diff --git a/ops/healthcheck-apps.sh b/ops/healthcheck-apps.sh new file mode 100644 index 0000000..6e9cfbc --- /dev/null +++ b/ops/healthcheck-apps.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# Vérifie les conteneurs Compose, leur healthcheck, leur redémarrage et leurs routes Traefik. +# Ce contrôle est volontairement non intrusif : Docker et manus-apps.service assurent les redémarrages. +set -Eeuo pipefail + +readonly STATE_DIR="/var/lib/manus-apps-health" +readonly REPORT_FILE="${STATE_DIR}/latest.txt" +readonly LOCK_FILE="/run/manus-apps-health.lock" +readonly MODE="${1:---check-only}" + +mkdir -p "$STATE_DIR" +exec 9>"$LOCK_FILE" +flock -n 9 || exit 0 + +declare -A SEEN_HTTP_HOSTS=() +declare -a REPORT_LINES=() +HAS_FAILURE=false + +log_line() { + local level="$1" + local message="$2" + local line="[$(date -Is)] [$level] $message" + REPORT_LINES+=("$line") + echo "$line" +} + +mark_failure() { + HAS_FAILURE=true + log_line "KO" "$1" +} + +is_healthy_http_status() { + # Toute réponse HTTP (< 500) confirme que Traefik et le service restent joignables. + # Les 401/404 sont légitimes pour les tableaux protégés ou API sans page racine. + [[ "$1" =~ ^[1-4][0-9][0-9]$ ]] +} + +check_http_route() { + local container_id="$1" + local project="$2" + local labels host_rule host status + + labels="$(docker inspect -f '{{range $key, $value := .Config.Labels}}{{$key}}={{$value}}{{"\n"}}{{end}}' "$container_id")" + host_rule="$(printf '%s\n' "$labels" | grep -oE 'Host\(`[^`]+`\)' | head -n 1 || true)" + [[ -n "$host_rule" ]] || return 0 + + host="${host_rule#Host(\`}" + host="${host%\`)}" + [[ -n "$host" ]] || return 0 + [[ -z "${SEEN_HTTP_HOSTS[$host]:-}" ]] || return 0 + SEEN_HTTP_HOSTS["$host"]=1 + + status="$(curl --silent --show-error --location --max-redirs 3 --connect-timeout 5 --max-time 8 \ + --output /dev/null --write-out '%{http_code}' "https://${host}" 2>/dev/null || true)" + if is_healthy_http_status "$status"; then + log_line "OK" "${project}: HTTPS ${host} → ${status}" + else + # Un échec HTTPS peut dépendre de Traefik, DNS ou d’une redirection applicative. + # Il déclenche une alerte, jamais un redémarrage Compose automatique. + mark_failure "${project}: HTTPS ${host} → ${status:-erreur réseau}" + fi +} + +check_containers() { + local container_id name project workdir config_files status health restart_policy exit_code + mapfile -t container_ids < <(docker ps -aq --filter label=com.docker.compose.project) + + if [[ "${#container_ids[@]}" -eq 0 ]]; then + mark_failure "Aucun conteneur Docker Compose détecté" + return + fi + + for container_id in "${container_ids[@]}"; do + name="$(docker inspect -f '{{.Name}}' "$container_id" | sed 's#^/##')" + project="$(docker inspect -f '{{index .Config.Labels "com.docker.compose.project"}}' "$container_id")" + workdir="$(docker inspect -f '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' "$container_id")" + config_files="$(docker inspect -f '{{index .Config.Labels "com.docker.compose.project.config_files"}}' "$container_id")" + status="$(docker inspect -f '{{.State.Status}}' "$container_id")" + health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$container_id")" + restart_policy="$(docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' "$container_id")" + exit_code="$(docker inspect -f '{{.State.ExitCode}}' "$container_id")" + + if [[ "$status" != "running" ]]; then + if [[ "$status" == "exited" && "$exit_code" == "0" ]]; then + log_line "INFO" "${project}/${name}: tâche ponctuelle terminée" + continue + fi + mark_failure "${project}/${name}: état Docker ${status}" + continue + fi + if [[ "$health" == "unhealthy" ]]; then + mark_failure "${project}/${name}: healthcheck unhealthy" + continue + fi + if [[ "$restart_policy" != "unless-stopped" && "$restart_policy" != "always" ]]; then + mark_failure "${project}/${name}: politique de redémarrage ${restart_policy:-none}" + continue + fi + + log_line "OK" "${project}/${name}: running, health=${health}, restart=${restart_policy}" + check_http_route "$container_id" "$project" + done +} + +check_containers + +{ + echo "Supervision Santinova / Itinova" + echo "Généré : $(date -Is)" + echo "Statut : $([[ "$HAS_FAILURE" == true ]] && echo KO || echo OK)" + printf '%s\n' "${REPORT_LINES[@]}" +} > "${REPORT_FILE}.tmp" +mv "${REPORT_FILE}.tmp" "$REPORT_FILE" + +if [[ "$HAS_FAILURE" == true ]]; then + logger -p daemon.err -t manus-apps-health "Anomalie détectée : consulter ${REPORT_FILE}" + exit 1 +fi + +logger -p daemon.info -t manus-apps-health "Toutes les applications contrôlées sont opérationnelles" diff --git a/ops/install-healthcheck.sh b/ops/install-healthcheck.sh new file mode 100644 index 0000000..1c63c97 --- /dev/null +++ b/ops/install-healthcheck.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Installe la supervision depuis une copie versionnée du dépôt manus-dashboard. +set -Eeuo pipefail + +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +install -D -m 0750 "${SCRIPT_DIR}/healthcheck-apps.sh" /opt/manus-deploy/scripts/healthcheck-apps.sh +install -D -m 0644 "${SCRIPT_DIR}/manus-apps-health.service" /etc/systemd/system/manus-apps-health.service +install -D -m 0644 "${SCRIPT_DIR}/manus-apps-health.timer" /etc/systemd/system/manus-apps-health.timer + +systemctl daemon-reload +systemctl enable --now manus-apps-health.timer +# Un échec de contrôle doit rester visible dans systemd, sans bloquer l’installation du timer. +systemctl start manus-apps-health.service || true +systemctl status manus-apps-health.service --no-pager || true diff --git a/ops/manus-apps-health.service b/ops/manus-apps-health.service new file mode 100644 index 0000000..5fcbf92 --- /dev/null +++ b/ops/manus-apps-health.service @@ -0,0 +1,13 @@ +[Unit] +Description=Contrôle de santé des applications Santinova / Itinova +Documentation=https://git.santinova-soft.org/manus-admin/manus-dashboard +After=docker.service network-online.target manus-apps.service +Requires=docker.service +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/opt/manus-deploy/scripts/healthcheck-apps.sh --check-only +TimeoutStartSec=5min +StandardOutput=journal +StandardError=journal diff --git a/ops/manus-apps-health.timer b/ops/manus-apps-health.timer new file mode 100644 index 0000000..70388e9 --- /dev/null +++ b/ops/manus-apps-health.timer @@ -0,0 +1,12 @@ +[Unit] +Description=Planification de la supervision des applications Santinova / Itinova + +[Timer] +OnBootSec=3min +OnUnitActiveSec=5min +RandomizedDelaySec=30s +Persistent=true +Unit=manus-apps-health.service + +[Install] +WantedBy=timers.target