Files
Gestion_sondes/app/surveillance_releves.py

228 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# Surveillance des réceptions de données dans les tables (par site) + mail d'alerte / retour à la normale
from __future__ import annotations
from datetime import datetime, timedelta
import os
import sys
import logging
import tempfile
from contextlib import closing
import mysql.connector # important pour cibler les exceptions MySQL
from dotenv import load_dotenv
import utils_db
from utils_mail import envoyer_mail
# -------------------- PATHS (portable Windows/Linux) --------------------
APP_DIR = os.path.dirname(os.path.abspath(__file__)) # .../Gestion_sondes/app
ROOT_DIR = os.path.abspath(os.path.join(APP_DIR, "..")) # .../Gestion_sondes
ENV_PATH = os.path.join(ROOT_DIR, ".env")
LOG_DIR = os.path.join(ROOT_DIR, "Logs")
os.makedirs(LOG_DIR, exist_ok=True)
log_filename = os.path.join(LOG_DIR, datetime.now().strftime("surveillance_%Y-%m-%d.log"))
# -------------------- LOGGING (UTF-8 + fichier + console) --------------------
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[
logging.FileHandler(log_filename, encoding="utf-8"),
logging.StreamHandler(sys.stdout),
],
force=True, # Python 3.8+
)
# Fix Windows/PyCharm : forcer l'encodage UTF-8 sur le StreamHandler (évite UnicodeEncodeError cp1252)
for h in logging.getLogger().handlers:
if isinstance(h, logging.StreamHandler):
try:
h.stream.reconfigure(encoding="utf-8", errors="replace")
except Exception:
# Certains streams/IDEs ne supportent pas reconfigure
pass
# -------------------- ENV --------------------
# override=True : si cron/supervisor/IDE a des variables vides, le .env reprend la main
load_dotenv(ENV_PATH, override=True)
# -------------------- PARAMETRES --------------------
# Mets ici les tables réelles à surveiller (noms exacts des tables MySQL)
tables = ["Saclay", "Meudon"]
DELAI_MINUTES = 15
RAPPEL_HEURES = 6
# State portable (Windows/Linux)
STATE_DIR = os.path.join(tempfile.gettempdir(), "surveillance_states")
os.makedirs(STATE_DIR, exist_ok=True)
TABLES_SET = set(tables) # whitelist simple
# -------------------- STATE MANAGEMENT --------------------
def _state_file(site: str) -> str:
return os.path.join(STATE_DIR, f"{site}.state")
def _read_last_alert(site: str) -> datetime | None:
sf = _state_file(site)
if not os.path.exists(sf):
return None
try:
with open(sf, "r", encoding="utf-8") as f:
raw = f.read().strip()
return datetime.fromisoformat(raw)
except (OSError, ValueError) as e:
logging.warning(f"Etat corrompu pour {site} ({sf}) : {e}. On ignorera cet état.")
return None
def mark_alert_sent(site: str, when: datetime | None = None) -> None:
"""Écrit/actualise l'état *après* un envoi mail réussi."""
sf = _state_file(site)
when = when or datetime.now()
try:
with open(sf, "w", encoding="utf-8") as f:
f.write(when.isoformat())
except OSError as e:
logging.warning(f"Impossible d'écrire l'état {sf} : {e} (l'anti-spam sera moins efficace).")
def should_send_alert(site: str) -> bool:
"""
Retourne True si on doit envoyer une alerte maintenant (première fois ou rappel).
IMPORTANT : ne modifie PAS l'état ici.
L'état est mis à jour uniquement après envoi réussi (mark_alert_sent).
"""
last_alert = _read_last_alert(site)
if last_alert is None:
return True
return (datetime.now() - last_alert) >= timedelta(hours=RAPPEL_HEURES)
def clear_state(site: str) -> None:
sf = _state_file(site)
try:
if os.path.exists(sf):
os.remove(sf)
except OSError as e:
logging.warning(f"Impossible de supprimer l'état {sf} : {e}")
# -------------------- MAIN --------------------
def main() -> None:
limite = datetime.now() - timedelta(minutes=DELAI_MINUTES)
# pour logs plus clairs
defauts_en_cours: list[str] = [] # défaut détecté (même si déjà signalé)
alertes_envoyees: list[str] = [] # alertes envoyées à CE run (pour mail groupé)
erreurs_sql: list[str] = [] # SQL KO sur tables
# 1) Connexion MySQL (une seule fois)
try:
with closing(utils_db.connect_to_mysql()) as cnx, closing(cnx.cursor()) as cursor:
# 2) Surveillance par table
for table in tables:
if table not in TABLES_SET:
logging.warning(f"Table ignorée (non whitelistée) : {table}")
continue
# 2a) Lecture de la dernière date
try:
cursor.execute(f"SELECT MAX(Date) FROM `{table}`")
row = cursor.fetchone()
last_update = row[0] if row else None
except mysql.connector.Error as e:
msg = f"Erreur SQL sur {table} : {e}"
erreurs_sql.append(table)
logging.error(msg)
# Alerte SQL (anti-spam)
if should_send_alert(table):
try:
envoyer_mail(
f"⚠️ ALERTE : erreur SQL sur {table} (voir logs).",
f"Erreur SQL détectée sur la table {table}.\n\nDétail:\n{e}"
)
mark_alert_sent(table)
alertes_envoyees.append(f"{table} (SQL)")
except Exception as mail_e:
logging.error(f"Impossible d'envoyer le mail d'erreur SQL pour {table} : {mail_e}")
else:
logging.info(f"{table} : erreur SQL déjà signalée, rappel dans {RAPPEL_HEURES}h.")
continue
# 2b) Logique métier
if (last_update is None) or (last_update < limite):
defauts_en_cours.append(table)
if should_send_alert(table):
logging.warning(f"⚠️ {table} en défaut (dernier relevé : {last_update})")
# On envoie le mail tout de suite (alerte individuelle)
try:
envoyer_mail(
f"⚠️ ALERTE : {table} absence de relevés",
f"Pas de relevés depuis > {DELAI_MINUTES} min.\nDernier relevé : {last_update}"
)
mark_alert_sent(table)
alertes_envoyees.append(f"{table} (dernier : {last_update})")
except Exception as mail_e:
# pas de mark_alert_sent => on retentera au prochain run
logging.error(f"Erreur envoi mail alerte pour {table} : {mail_e}")
else:
logging.info(f"{table} déjà signalé, rappel dans {RAPPEL_HEURES}h.")
else:
# OK => si on avait un état, on envoie un "retour à la normale"
if os.path.exists(_state_file(table)):
message = f"{table} : relevés à nouveau reçus (dernier : {last_update}). Situation normale."
try:
envoyer_mail(f"✅ OK : {table} relevés reçus", message)
clear_state(table)
logging.info(f"📩 Retour à la normale envoyé pour {table}.")
except Exception as mail_e:
# on conserve le state => on retentera le retour à la normale
logging.error(f"Erreur envoi mail retour à la normale pour {table} : {mail_e}")
else:
logging.info(f"{table} OK (dernier relevé : {last_update})")
except mysql.connector.Error as e:
logging.error(f"MySQL KO : {e}")
try:
envoyer_mail(
"⚠️ ALERTE : Base MySQL inaccessible (surveillance impossible).",
"Connexion MySQL impossible : la surveillance des relevés ne peut pas sexécuter."
)
except Exception as mail_e:
logging.error(f"Impossible d'envoyer le mail MySQL KO : {mail_e}")
return
# 3) Logs de synthèse (fidèles à la réalité)
if alertes_envoyees:
logging.info("📧 Mail(s) envoyé(s) : " + ", ".join(alertes_envoyees))
elif defauts_en_cours or erreurs_sql:
# défauts en cours mais déjà signalés (ou erreurs SQL déjà signalées)
bloc = []
if defauts_en_cours:
bloc.append("défaut(s) relevés: " + ", ".join(defauts_en_cours))
if erreurs_sql:
bloc.append("erreur(s) SQL: " + ", ".join(erreurs_sql))
logging.info("⚠️ " + " | ".join(bloc) + " (déjà signalé / pas de mail envoyé à ce run)")
else:
logging.info("👍 Tout est OK, aucun Mail envoyé.")
if __name__ == "__main__":
main()