#!/usr/bin/env php = 2) { $timestamp = (int)$parts[1]; if (($now - $timestamp) <= (int)$DEDUP_WINDOW) { $kept[] = $line; } else { $has_changed = true; // On a trouvé au moins une ligne à supprimer } } } // On ne réécrit le fichier que si des lignes ont été supprimées if ($has_changed) { $content = implode("\n", $kept); if (!empty($content)) { $content .= "\n"; } // LOCK_EX évite que deux instances n'écrivent en même temps file_put_contents($DEDUP_FILE, $content, LOCK_EX); } } /** * Vérifie si une alerte doit être envoyée (Déduplication) * La clé attendue est : "hostname|app|level|event" */ function should_notify_dedup(string $key): bool { global $DEDUP_FILE, $DEDUP_WINDOW; if (!file_exists($DEDUP_FILE)) { return true; } $now = time(); $last_ts = 0; $handle = fopen($DEDUP_FILE, 'r'); if (!$handle) { return true; // En cas d'erreur de lecture, on autorise l'alerte par sécurité } // On parcourt le fichier while (($line = fgets($handle)) !== false) { $line = trim($line); if (empty($line)) continue; $p = explode('|', $line); // Format du fichier : host|timestamp|app|level|event // On reconstruit la clé de comparaison (sans le timestamp index 1) if (count($p) >= 5) { $row_key = "{$p[0]}|{$p[2]}|{$p[3]}|{$p[4]}"; if ($row_key === $key) { $last_ts = (int)$p[1]; } } } fclose($handle); // Calcul de l'écart : vrai si on a dépassé la fenêtre ou si jamais vu (last_ts = 0) return ($now - $last_ts) >= (int)$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();