2026-08-14 18:02:25 +02:00
|
|
|
"""Healthcheck endpoint - GMAO Collège"""
|
|
|
|
|
from flask import Blueprint, jsonify
|
|
|
|
|
from app_new.extensions import db
|
|
|
|
|
from app_new.core.models import User
|
|
|
|
|
from app_new.version import get_version
|
|
|
|
|
import subprocess
|
|
|
|
|
import os
|
|
|
|
|
import time
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
health_bp = Blueprint('health', __name__, url_prefix='/health')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Constantes
|
|
|
|
|
WATCHDOG_STARTUP_GRACE_SECONDS = 60
|
|
|
|
|
APP_START_TIME = time.time()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _check_watchdog(name, start_time_threshold=None):
|
|
|
|
|
"""Verifie si un processus watchdog tourne via /proc."""
|
|
|
|
|
try:
|
|
|
|
|
pids = []
|
|
|
|
|
for pid in os.listdir('/proc'):
|
|
|
|
|
if not pid.isdigit():
|
|
|
|
|
continue
|
|
|
|
|
try:
|
|
|
|
|
cmdline_path = f'/proc/{pid}/cmdline'
|
|
|
|
|
with open(cmdline_path, 'rb') as f:
|
|
|
|
|
cmdline = f.read().decode('utf-8', errors='replace').replace('\x00', ' ')
|
|
|
|
|
if name in cmdline and 'grep' not in cmdline:
|
|
|
|
|
pids.append(int(pid))
|
|
|
|
|
except (FileNotFoundError, PermissionError):
|
|
|
|
|
continue
|
|
|
|
|
if pids:
|
|
|
|
|
return {'running': True, 'pid': pids[0], 'instances': len(pids)}
|
|
|
|
|
# Grace period au demarrage
|
|
|
|
|
if start_time_threshold and time.time() - APP_START_TIME < start_time_threshold:
|
|
|
|
|
return {'running': False, 'starting': True}
|
|
|
|
|
return {'running': False}
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return {'running': False, 'error': str(e)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _check_database():
|
|
|
|
|
"""Verifie la connexion a la base de donnees."""
|
|
|
|
|
try:
|
|
|
|
|
db.session.execute(db.text('SELECT 1'))
|
|
|
|
|
return {'connected': True, 'dialect': str(db.engine.url).split('://')[0]}
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return {'connected': False, 'error': str(e)}
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 20:02:32 +02:00
|
|
|
def _check_integration_watchdog(process_name, integration, start_time_threshold=None):
|
|
|
|
|
"""Un watchdog non configure est volontairement arrete et reste sain."""
|
|
|
|
|
try:
|
|
|
|
|
from watchdog_gate import integration_is_configured
|
|
|
|
|
|
|
|
|
|
if not integration_is_configured(integration):
|
|
|
|
|
return {'running': False, 'enabled': False}
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
return {'running': False, 'enabled': False, 'configuration_error': str(exc)}
|
|
|
|
|
|
|
|
|
|
status = _check_watchdog(process_name, start_time_threshold=start_time_threshold)
|
|
|
|
|
status['enabled'] = True
|
|
|
|
|
return status
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 18:02:25 +02:00
|
|
|
@health_bp.route('/')
|
|
|
|
|
def health():
|
|
|
|
|
"""Endpoint public de healthcheck."""
|
|
|
|
|
db_status = _check_database()
|
|
|
|
|
|
|
|
|
|
# Le watchdog pronote met parfois plus de temps a demarrer (connexion ENT)
|
|
|
|
|
watchdogs = {
|
|
|
|
|
'flask': _check_watchdog('gunicorn'),
|
2026-08-14 20:02:32 +02:00
|
|
|
'gmao_watchdog': _check_integration_watchdog('gmao_watchdog.py', 'outlook'),
|
|
|
|
|
'ent_watchdog': _check_integration_watchdog('ent_watchdog.py', 'ent'),
|
|
|
|
|
'pronote_watchdog': _check_integration_watchdog(
|
|
|
|
|
'pronote_watchdog.py',
|
|
|
|
|
'pronote',
|
|
|
|
|
start_time_threshold=WATCHDOG_STARTUP_GRACE_SECONDS,
|
|
|
|
|
),
|
|
|
|
|
'watchdog_dnd': _check_integration_watchdog('watchdog_dnd.py', 'yeastar'),
|
2026-08-14 18:02:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
all_watchdogs_ok = all(
|
2026-08-14 20:02:32 +02:00
|
|
|
w.get('enabled') is False or w.get('running') or w.get('starting')
|
2026-08-14 18:02:25 +02:00
|
|
|
for w in watchdogs.values()
|
|
|
|
|
)
|
|
|
|
|
healthy = db_status.get('connected') and all_watchdogs_ok and not any(w.get('starting') for w in watchdogs.values())
|
|
|
|
|
|
|
|
|
|
git_commit = 'unknown'
|
|
|
|
|
try:
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
['git', '-C', '/app', 'rev-parse', '--short', 'HEAD'],
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
timeout=5
|
|
|
|
|
)
|
|
|
|
|
if result.returncode == 0:
|
|
|
|
|
git_commit = result.stdout.strip()
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
# Fallback si git n'est pas disponible dans l'image
|
|
|
|
|
if git_commit == 'unknown':
|
|
|
|
|
version_file = '/app/.git-version'
|
|
|
|
|
if os.path.exists(version_file):
|
|
|
|
|
try:
|
|
|
|
|
with open(version_file) as f:
|
|
|
|
|
git_commit = f.read().strip()
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
response = {
|
|
|
|
|
'status': 'healthy' if healthy else 'degraded',
|
|
|
|
|
'version': get_version(),
|
|
|
|
|
'timestamp': datetime.utcnow().isoformat() + 'Z',
|
|
|
|
|
'database': db_status,
|
|
|
|
|
'watchdogs': watchdogs,
|
|
|
|
|
'git_commit': git_commit,
|
|
|
|
|
'environment': os.environ.get('FLASK_ENV', 'production'),
|
|
|
|
|
'uptime_seconds': int(time.time() - APP_START_TIME)
|
|
|
|
|
}
|
|
|
|
|
return jsonify(response), 200 if healthy else 503
|