feat(ops): ajouter supervision automatique des applications

This commit is contained in:
Manus AI
2026-08-18 07:31:02 +00:00
parent 9500ca2d70
commit 5e8d4796d4
5 changed files with 208 additions and 0 deletions

11
ops/README.md Normal file
View File

@@ -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. En cas d’anomalie, une remise en route Compose ciblée est tentée une fois.
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
```

158
ops/healthcheck-apps.sh Normal file
View File

@@ -0,0 +1,158 @@
#!/usr/bin/env bash
# Vérifie les conteneurs Compose, leur healthcheck, leur redémarrage et leurs routes Traefik.
# En cas d'écart, un redémarrage Compose ciblé est tenté une fois avant le prochain cycle.
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:---repair}"
mkdir -p "$STATE_DIR"
exec 9>"$LOCK_FILE"
flock -n 9 || exit 0
declare -A PROJECT_CONFIGS=()
declare -A PROJECT_NAMES=()
declare -A SEEN_HTTP_HOSTS=()
declare -A FAILED_PROJECTS=()
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"
}
mark_project_failure() {
FAILED_PROJECTS["$1"]=1
mark_failure "$2"
}
is_healthy_http_status() {
[[ "$1" =~ ^2[0-9][0-9]$ || "$1" =~ ^3[0-9][0-9]$ ]]
}
check_http_route() {
local container_id="$1"
local project="$2"
local workdir="$3"
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 10 --max-time 25 \
--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
mark_project_failure "$workdir" "${project}: HTTPS ${host} → ${status:-erreur réseau}"
fi
}
check_containers() {
local container_id name project workdir config_files status health restart_policy
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")"
PROJECT_NAMES["$workdir"]="$project"
PROJECT_CONFIGS["$workdir"]="$config_files"
if [[ "$status" != "running" ]]; then
mark_project_failure "$workdir" "${project}/${name}: état Docker ${status}"
continue
fi
if [[ "$health" == "unhealthy" ]]; then
mark_project_failure "$workdir" "${project}/${name}: healthcheck unhealthy"
continue
fi
if [[ "$restart_policy" != "unless-stopped" && "$restart_policy" != "always" ]]; then
mark_project_failure "$workdir" "${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" "$workdir"
done
}
repair_projects() {
local workdir config_files project config
for workdir in "${!FAILED_PROJECTS[@]}"; do
project="${PROJECT_NAMES[$workdir]}"
[[ -d "$workdir" ]] || continue
config_files="${PROJECT_CONFIGS[$workdir]}"
local -a compose_args=()
if [[ -n "$config_files" ]]; then
IFS=',' read -r -a configs <<< "$config_files"
for config in "${configs[@]}"; do
compose_args+=("-f" "$config")
done
elif [[ -f "$workdir/docker-compose.yml" ]]; then
compose_args+=("-f" "$workdir/docker-compose.yml")
else
mark_failure "${project}: fichier Compose introuvable pour la réparation"
continue
fi
log_line "INFO" "${project}: tentative de réparation Compose"
if (cd "$workdir" && docker compose "${compose_args[@]}" up -d --remove-orphans); then
log_line "INFO" "${project}: réparation Compose demandée"
else
mark_failure "${project}: échec de réparation Compose"
fi
done
}
check_containers
if [[ "$HAS_FAILURE" == true && "$MODE" == "--repair" ]]; then
repair_projects
log_line "INFO" "Nouvelle vérification après réparation"
sleep 15
exec "$0" --check-only
fi
{
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"

View File

@@ -0,0 +1,14 @@
#!/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
systemctl start manus-apps-health.service
systemctl status manus-apps-health.service --no-pager

View File

@@ -0,0 +1,13 @@
[Unit]
Description=Contrôle et réparation 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 --repair
TimeoutStartSec=10min
StandardOutput=journal
StandardError=journal

View File

@@ -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