204 lines
6.5 KiB
PHP
204 lines
6.5 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
/**
|
|
* Alert Engine - PHP Version
|
|
* Copyright (C) 2026 Cédric Abonnel
|
|
* License: GNU Affero General Public License v3
|
|
*/
|
|
|
|
require_once __DIR__ . '/../lib/monitoring-lib.php';
|
|
|
|
// --- Initialisation de la configuration spécifique ---
|
|
// On charge les fichiers de conf s'ils existent (format PHP attendu)
|
|
foreach (["/opt/monitoring/conf/alert-engine.conf.php", "/opt/monitoring/conf/alert-engine.conf.local.php"] as $conf) {
|
|
if (file_exists($conf)) {
|
|
$extra_conf = include $conf;
|
|
if (is_array($extra_conf)) {
|
|
$CONFIG = array_replace_recursive($CONFIG, $extra_conf);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Chemins des fichiers
|
|
$LOG_SOURCE = $CONFIG['LOG_FILE'] ?? '/var/log/monitoring/events.jsonl';
|
|
$STATE_FILE = $CONFIG['ALERT_STATE_FILE'] ?? '/var/lib/monitoring/alert-engine.offset';
|
|
$DEDUP_FILE = $CONFIG['ALERT_DEDUP_FILE'] ?? '/var/lib/monitoring/alert-engine.dedup';
|
|
$DEDUP_WINDOW = $CONFIG['ALERT_DEDUP_WINDOW'] ?? 3600;
|
|
|
|
// Création des répertoires si nécessaire
|
|
ensure_parent_dir($STATE_FILE);
|
|
ensure_parent_dir($DEDUP_FILE);
|
|
|
|
/**
|
|
* Nettoyage du fichier de déduplication (entrées expirées)
|
|
*/
|
|
function cleanup_dedup_file() {
|
|
global $DEDUP_FILE, $DEDUP_WINDOW;
|
|
if (!file_exists($DEDUP_FILE)) return;
|
|
|
|
$now = time();
|
|
$lines = file($DEDUP_FILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
|
$kept = [];
|
|
|
|
foreach ($lines as $line) {
|
|
$parts = explode('|', $line);
|
|
if (count($parts) >= 2 && ($now - (int)$parts[1]) <= $DEDUP_WINDOW) {
|
|
$kept[] = $line;
|
|
}
|
|
}
|
|
file_put_contents($DEDUP_FILE, implode("\n", $kept) . (empty($kept) ? "" : "\n"));
|
|
}
|
|
|
|
/**
|
|
* Vérifie si une alerte doit être envoyée (Déduplication)
|
|
*/
|
|
function should_notify_dedup($key) {
|
|
global $DEDUP_FILE, $DEDUP_WINDOW;
|
|
if (!file_exists($DEDUP_FILE)) return true;
|
|
|
|
$now = time();
|
|
$last_ts = 0;
|
|
|
|
$handle = fopen($DEDUP_FILE, 'r');
|
|
while (($line = fgets($handle)) !== false) {
|
|
$p = explode('|', trim($line));
|
|
if (count($p) >= 5) {
|
|
$current_key = "{$p[0]}|{$p[2]}|{$p[3]}|{$p[4]}";
|
|
if ($current_key === $key) {
|
|
$last_ts = (int)$p[1];
|
|
}
|
|
}
|
|
}
|
|
fclose($handle);
|
|
|
|
return ($now - $last_ts) >= $DEDUP_WINDOW;
|
|
}
|
|
|
|
/**
|
|
* Envoi vers ntfy
|
|
*/
|
|
function send_ntfy($title, $body, $level) {
|
|
global $CONFIG;
|
|
if (!($CONFIG['ALERT_NTFY_ENABLED'] ?? true)) return true;
|
|
if (empty($CONFIG['NTFY_SERVER']) || empty($CONFIG['NTFY_TOPIC'])) return false;
|
|
|
|
$priority = ['CRITICAL' => 'urgent', 'ERROR' => 'high', 'WARNING' => 'default'][$level] ?? 'default';
|
|
$tags = ($CONFIG['NTFY_TAGS'][$level]) ?? 'warning';
|
|
|
|
$url = rtrim($CONFIG['NTFY_SERVER'], '/') . '/' . $CONFIG['NTFY_TOPIC'];
|
|
$headers = ["Title: $title", "Priority: $priority", "Tags: $tags"];
|
|
|
|
if (!empty($CONFIG['NTFY_TOKEN'])) {
|
|
$headers[] = "Authorization: Bearer " . $CONFIG['NTFY_TOKEN'];
|
|
}
|
|
|
|
$ch = curl_init($url);
|
|
curl_setopt($ch, CURLOPT_POST, 1);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
|
|
curl_exec($ch);
|
|
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
return ($status >= 200 && $status < 300);
|
|
}
|
|
|
|
/**
|
|
* Envoi par mail
|
|
*/
|
|
function send_mail($subject, $body) {
|
|
global $CONFIG;
|
|
if (!($CONFIG['ALERT_MAIL_ENABLED'] ?? true)) return true;
|
|
if (empty($CONFIG['DEST'])) return false;
|
|
|
|
$prefix = $CONFIG['ALERT_MAIL_SUBJECT_PREFIX'] ?? '[monitoring]';
|
|
$headers = "From: monitoring@" . gethostname() . "\r\n" . "Content-Type: text/plain; charset=UTF-8";
|
|
|
|
return mail($CONFIG['DEST'], "$prefix $subject", $body, $headers);
|
|
}
|
|
|
|
/**
|
|
* Traitement d'une ligne de log
|
|
*/
|
|
function process_line($line) {
|
|
global $CONFIG, $DEDUP_FILE;
|
|
$data = json_decode($line, true);
|
|
if (!$data || !isset($data['level'], $data['event'])) return;
|
|
|
|
$level = strtoupper($data['level']);
|
|
$event = $data['event'];
|
|
|
|
if (in_array($level, ['DEBUG', 'INFO', 'NOTICE'])) return;
|
|
if (in_array($event, ($CONFIG['ALERT_IGNORE_EVENTS'] ?? []))) return;
|
|
|
|
// Déduplication
|
|
$key = "{$data['host']}|{$data['app']}|{$level}|{$event}";
|
|
if (!should_notify_dedup($key)) {
|
|
log_debug("alert_suppressed_dedup", "Alerte dédupliquée", ["event=$event", "host={$data['host']}"]);
|
|
return;
|
|
}
|
|
|
|
// Détermination des canaux (Règle spécifique puis défaut)
|
|
$channels_str = $CONFIG['RULES'][$event] ?? $CONFIG['DEFAULT_CHANNELS'][$level] ?? '';
|
|
if (empty($channels_str)) return;
|
|
$channels = explode(',', $channels_str);
|
|
|
|
$title = "{$data['host']} [{$data['app']}] $level $event";
|
|
$body = sprintf(
|
|
"Date: %s\nHôte: %s\nScript: %s\nNiveau: %s\nÉvénement: %s\n\nMessage:\n%s",
|
|
$data['ts'] ?? 'N/A', $data['host'], $data['app'], $level, $event, $data['message'] ?? ''
|
|
);
|
|
|
|
foreach ($channels as $ch) {
|
|
$ch = trim($ch);
|
|
$success = false;
|
|
if ($ch === 'ntfy') {
|
|
$success = send_ntfy($title, $body, $level);
|
|
$success ? log_info("alert_sent_ntfy", "Notification ntfy envoyée", ["event=$event"])
|
|
: log_error("alert_ntfy_failed", "Échec ntfy", ["event=$event"]);
|
|
} elseif ($ch === 'mail') {
|
|
$success = send_mail($title, $body);
|
|
$success ? log_info("alert_sent_mail", "Mail envoyé", ["event=$event"])
|
|
: log_error("alert_mail_failed", "Échec mail", ["event=$event"]);
|
|
}
|
|
}
|
|
|
|
// Enregistrement déduplication
|
|
$entry = sprintf("%s|%s|%s|%s|%s\n", $data['host'], time(), $data['app'], $level, $event);
|
|
file_put_contents($DEDUP_FILE, $entry, FILE_APPEND);
|
|
}
|
|
|
|
// --- Main ---
|
|
|
|
lock_or_exit("alert-engine");
|
|
|
|
if (!file_exists($LOG_SOURCE)) {
|
|
log_notice("alert_log_missing", "Fichier de log absent", ["file=$LOG_SOURCE"]);
|
|
exit(0);
|
|
}
|
|
|
|
$last_offset = file_exists($STATE_FILE) ? (int)file_get_contents($STATE_FILE) : 0;
|
|
$current_size = filesize($LOG_SOURCE);
|
|
|
|
// Gestion de la rotation de log
|
|
if ($last_offset > $current_size) {
|
|
log_notice("alert_offset_reset", "Rotation détectée", ["old=$last_offset", "new=0"]);
|
|
$last_offset = 0;
|
|
}
|
|
|
|
cleanup_dedup_file();
|
|
|
|
$fp = fopen($LOG_SOURCE, 'r');
|
|
if ($last_offset > 0) fseek($fp, $last_offset);
|
|
|
|
while (($line = fgets($fp)) !== false) {
|
|
if (trim($line) !== '') {
|
|
process_line($line);
|
|
}
|
|
}
|
|
fclose($fp);
|
|
|
|
file_put_contents($STATE_FILE, $current_size);
|
|
exit_with_status(); |