Mise en place d'un journal de connexions

This commit is contained in:
2026-04-21 17:14:16 +02:00
parent b00879cdfa
commit 272ad6d80a
6 changed files with 250 additions and 23 deletions

View File

@@ -391,15 +391,22 @@ def depassement_depuis_30min(site: str, sonde: str, seuil: float) -> bool:
# ========= Synology Chat =========
def send_synology_chat(message: str, *, username: str | None = None) -> bool:
webhook = (
_env_str(f"SYNO_CHAT_WEBHOOK_MONITOR_{SITE}") or
_env_str(f"SYNO_CHAT_WEBHOOK_MONITOR_{SITE.upper()}") or
_env_str("SYNO_CHAT_WEBHOOK_MONITOR") or
_env_str(f"SYNO_CHAT_WEBHOOK_{SITE}") or
_env_str(f"SYNO_CHAT_WEBHOOK_MONITOR") or
_env_str(f"SYNO_CHAT_WEBHOOK_{SITE.upper()}") or
_env_str("SYNO_CHAT_WEBHOOK")
)
if not webhook:
log.info("Synology Chat non configuré.")
return False
botname = username or _env_str("SYNO_CHAT_BOTNAME")
botname = (
username
or _env_str("SYNO_CHAT_BOTNAME_MONITOR")
or _env_str("SYNO_CHAT_BOTNAME")
)
timeout = int(_env_str("SYNO_CHAT_TIMEOUT", "10"))
verify_ssl = _env_bool("SYNO_CHAT_VERIFY_SSL", True)
@@ -407,7 +414,9 @@ def send_synology_chat(message: str, *, username: str | None = None) -> bool:
if botname:
chat_payload["username"] = botname
form_data = {"payload": json.dumps(chat_payload, ensure_ascii=False)}
form_data = {
"payload": json.dumps(chat_payload, ensure_ascii=False)
}
try:
response = requests.post(

View File

@@ -307,15 +307,22 @@ def depassement_depuis_30min(site: str, sonde: str, seuil: float) -> bool:
# ========= Synology Chat =========
def send_synology_chat(message: str, *, username: str | None = None) -> bool:
webhook = (
_env_str(f"SYNO_CHAT_WEBHOOK_MONITOR_{SITE}") or
_env_str(f"SYNO_CHAT_WEBHOOK_MONITOR_{SITE.upper()}") or
_env_str("SYNO_CHAT_WEBHOOK_MONITOR") or
_env_str(f"SYNO_CHAT_WEBHOOK_{SITE}") or
_env_str(f"SYNO_CHAT_WEBHOOK_MONITOR_SACLAY") or
_env_str(f"SYNO_CHAT_WEBHOOK_{SITE.upper()}") or
_env_str("SYNO_CHAT_WEBHOOK")
)
if not webhook:
log.info("Synology Chat non configuré.")
return False
botname = username or _env_str("SYNO_CHAT_BOTNAME")
botname = (
username
or _env_str("SYNO_CHAT_BOTNAME_MONITOR")
or _env_str("SYNO_CHAT_BOTNAME")
)
timeout = int(_env_str("SYNO_CHAT_TIMEOUT", "10"))
verify_ssl = _env_bool("SYNO_CHAT_VERIFY_SSL", True)

183
app/Monitor_connexions.py Normal file
View File

@@ -0,0 +1,183 @@
import os
import time
import logging
from typing import Optional
import json
import pymysql
import requests
from dotenv import load_dotenv
load_dotenv()
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(message)s"
)
log = logging.getLogger("journal_connexions")
def env_str(name: str, default: Optional[str] = None) -> Optional[str]:
value = os.getenv(name, default)
if value is None:
return None
value = value.strip()
return value if value else default
DB_CONFIG = {
"host": env_str("DB_HOST"),
"port": int(env_str("DB_PORT", "3306")),
"user": env_str("DB_USER2"),
"password": env_str("DB_PASS2"),
"database": env_str("DB_NAME2", "Acces"),
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
"autocommit": True,
}
SYNO_CHAT_WEBHOOK = env_str("SYNO_CHAT_WEBHOOK_CONNEXIONS")
SYNO_CHAT_BOTNAME = env_str("SYNO_CHAT_BOTNAME_CONNEXIONS", "Journal Connexions")
POLL_INTERVAL = int(env_str("POLL_INTERVAL", "10"))
def get_connection():
return pymysql.connect(**DB_CONFIG)
def format_message(row: dict) -> str:
return (
f"[Connexion MySQL]\n"
f"Utilisateur : {row.get('NomUtilisateur', '')}\n"
f"Poste : {row.get('PosteClient', '')}\n"
f"Tableur : {row.get('TableurSource', '')}\n"
f"Windows : {row.get('UtilisateurWindows', '')}\n"
f"Site : {row.get('SiteDemande', '')}\n"
f"Service : {row.get('ServiceDemande', '')}\n"
f"DSN : {row.get('DSN', '')}\n"
f"BDD : {row.get('BDD', '')}\n"
f"Statut : {row.get('Statut', '')}\n"
f"Motif : {row.get('Motif', '')}\n"
f"Heure : {row.get('DateHeure', '')}\n"
f"Session : {row.get('SessionID', '')}"
)
def send_synology_chat(message: str) -> None:
if not SYNO_CHAT_WEBHOOK:
raise RuntimeError("SYNO_CHAT_WEBHOOK_CONNEXIONS non configuré")
syno_payload = {
"text": message
}
response = requests.post(
SYNO_CHAT_WEBHOOK,
data={"payload": json.dumps(syno_payload, ensure_ascii=False)},
timeout=10
)
log.info("Synology Chat HTTP=%s body=%s", response.status_code, response.text)
response.raise_for_status()
body = response.json()
if not body.get("success", False):
raise RuntimeError(f"Synology Chat erreur: {body}")
def fetch_pending_rows(conn) -> list[dict]:
sql = """
SELECT
Id_Journal,
DateHeure,
NomUtilisateur,
PosteClient,
TableurSource,
UtilisateurWindows,
SiteDemande,
ServiceDemande,
DSN,
BDD,
Statut,
Motif,
SessionID
FROM `Acces`.`JournalConnexions`
WHERE NotificationEnvoyee = 0
ORDER BY DateHeure ASC, Id_Journal ASC
LIMIT 50
"""
print("SQL fetch_pending_rows =")
print(sql)
with conn.cursor() as cur:
cur.execute(sql)
return cur.fetchall()
def mark_sent(conn, row_id: int) -> None:
sql = """
UPDATE Acces.JournalConnexions
SET NotificationEnvoyee = 1,
DateNotification = NOW(),
ErreurNotification = NULL
WHERE Id_Journal = %s
"""
with conn.cursor() as cur:
cur.execute(sql, (row_id,))
def mark_error(conn, row_id: int, error_msg: str) -> None:
sql = """
UPDATE `Acces`.`JournalConnexions`
SET ErreurNotification = %s
WHERE Id_Journal = %s
"""
with conn.cursor() as cur:
cur.execute(sql, (error_msg[:255], row_id))
def process_once() -> None:
with get_connection() as conn:
rows = fetch_pending_rows(conn)
if not rows:
log.info("Aucune nouvelle connexion à notifier.")
return
log.info("%s connexion(s) à notifier.", len(rows))
for row in rows:
row_id = row["Id_Journal"]
try:
message = format_message(row)
send_synology_chat(message)
mark_sent(conn, row_id)
log.info("Notification envoyée pour Id_Journal=%s", row_id)
except Exception as exc:
log.exception("Erreur d'envoi pour Id_Journal=%s", row_id)
try:
mark_error(conn, row_id, str(exc))
except Exception:
log.exception("Impossible d'écrire ErreurNotification pour Id_Journal=%s", row_id)
def main():
missing = [k for k, v in DB_CONFIG.items() if v is None and k != "port"]
if missing:
raise RuntimeError(f"Variables d'environnement manquantes : {', '.join(missing)}")
if not SYNO_CHAT_WEBHOOK:
raise RuntimeError("SYNO_CHAT_WEBHOOK_CONNEXIONS manquant")
log.info("Surveillance de JournalConnexions démarrée.")
while True:
try:
process_once()
except Exception:
log.exception("Erreur générale dans la boucle de surveillance")
time.sleep(POLL_INTERVAL)
if __name__ == "__main__":
main()

View File

@@ -57,10 +57,12 @@ TABLES_SET = set(TABLES)
DELAI_MINUTES = 15
RAPPEL_HEURES = 6
SYNOLOGY_CHAT_WEBHOOK_URL = os.getenv("SYNOLOGY_CHAT_WEBHOOK_URL", "").strip()
SYNOLOGY_CHAT_VERIFY_SSL = os.getenv("SYNOLOGY_CHAT_VERIFY_SSL", "true").strip().lower() in (
"1", "true", "yes", "on"
)
def _env_str(name: str, default: str = "") -> str:
return (os.getenv(name, default) or "").strip()
def _env_bool(name: str, default: bool) -> bool:
value = _env_str(name, "1" if default else "0").lower()
return value in ("1", "true", "yes", "on")
# ============================================================
@@ -201,19 +203,33 @@ def should_send_alert(site: str) -> bool:
# NOTIFICATIONS
# ============================================================
def envoyer_chat(titre: str, message: str) -> None:
if not SYNOLOGY_CHAT_WEBHOOK_URL:
logging.warning("Webhook Synology Chat non configuré : notification Chat ignorée.")
def envoyer_chat(site: str, titre: str, message: str) -> None:
webhook = (
_env_str(f"SYNO_CHAT_WEBHOOK_MONITOR_{site}") or
_env_str(f"SYNO_CHAT_WEBHOOK_MONITOR_{site.upper()}") or
_env_str("SYNO_CHAT_WEBHOOK_MONITOR") or
_env_str(f"SYNO_CHAT_WEBHOOK_{site}") or
_env_str(f"SYNO_CHAT_WEBHOOK_{site.upper()}") or
_env_str("SYNO_CHAT_WEBHOOK")
)
if not webhook:
logging.warning(f"Webhook Synology Chat monitor non configuré pour {site} : notification Chat ignorée.")
return
verify_ssl = _env_bool("SYNO_CHAT_VERIFY_SSL", True)
botname = _env_str("SYNO_CHAT_BOTNAME_MONITOR", "Injection données dans tables")
texte = f"{titre}\n{message}"
payload = {"text": texte}
payload: dict[str, str] = {"text": texte}
if botname:
payload["username"] = botname
response = requests.post(
SYNOLOGY_CHAT_WEBHOOK_URL,
webhook,
data={"payload": json.dumps(payload, ensure_ascii=False)},
timeout=10,
verify=SYNOLOGY_CHAT_VERIFY_SSL,
timeout=int(_env_str("SYNO_CHAT_TIMEOUT", "10")),
verify=verify_ssl,
)
response.raise_for_status()
@@ -224,10 +240,10 @@ def envoyer_chat(titre: str, message: str) -> None:
except ValueError:
pass
logging.info("💬 Notification Synology Chat envoyée.")
logging.info("💬 Notification Synology Chat envoyée pour %s.", site)
def envoyer_notifications(sujet: str, message: str) -> None:
def envoyer_notifications(site: str, sujet: str, message: str) -> None:
"""
Envoie mail + chat.
Lève une erreur si au moins un des deux canaux échoue.
@@ -241,7 +257,7 @@ def envoyer_notifications(sujet: str, message: str) -> None:
erreurs.append(f"mail: {e}")
try:
envoyer_chat(sujet, message)
envoyer_chat(site, sujet, message)
except Exception as e:
erreurs.append(f"chat: {e}")
@@ -293,6 +309,7 @@ def traiter_table(cursor, table: str, limite: datetime,
if should_send_alert(table):
try:
envoyer_notifications(
table,
f"⚠️ ALERTE : erreur SQL sur {table}",
f"Erreur SQL détectée sur la table {table}.\n\nDétail :\n{e}"
)
@@ -312,6 +329,7 @@ def traiter_table(cursor, table: str, limite: datetime,
logging.warning(f"⚠️ {table} en défaut (dernier relevé : {last_update})")
try:
envoyer_notifications(
table,
f"⚠️ ALERTE : {table} absence de relevés",
f"Pas de relevés depuis plus de {DELAI_MINUTES} min.\nDernier relevé : {last_update}"
)
@@ -339,6 +357,7 @@ def traiter_table(cursor, table: str, limite: datetime,
message = f"{table} : relevés à nouveau reçus (dernier : {last_update}). Situation normale."
try:
envoyer_notifications(
table,
f"✅ OK : {table} relevés reçus",
message
)
@@ -381,6 +400,7 @@ def main() -> None:
logging.error(f"MySQL KO : {e}")
try:
envoyer_notifications(
"GLOBAL",
"⚠️ ALERTE : Base MySQL inaccessible",
"Connexion MySQL impossible : la surveillance des relevés ne peut pas sexécuter."
)