#!/bin/bash # Copyright (C) 2026 Cédric Abonnel # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. set -u SCRIPT_NAME="$(basename "$0")" SCRIPT_PATH="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")" # shellcheck source=/opt/monitoring/lib/monitoring-lib.sh . /opt/monitoring/lib/monitoring-lib.sh || exit 3 load_conf_if_exists "/opt/monitoring/conf/alert-engine.conf" load_conf_if_exists "/opt/monitoring/conf/alert-engine.local.conf" lock_or_exit "alert-engine" require_cmd awk sed grep date tail stat cut tr LOG_SOURCE="${LOG_FILE:-/var/log/monitoring/events.jsonl}" STATE_FILE="${ALERT_STATE_FILE:-/var/lib/monitoring/alert-engine.offset}" DEDUP_FILE="${ALERT_DEDUP_FILE:-/var/lib/monitoring/alert-engine.dedup}" mkdir -p "$(dirname "$STATE_FILE")" "$(dirname "$DEDUP_FILE")" || fail_internal "Impossible de créer les répertoires d'état" touch "$STATE_FILE" "$DEDUP_FILE" || fail_internal "Impossible d'initialiser les fichiers d'état" json_get() { local key="$1" local line="$2" printf '%s\n' "$line" \ | sed -n "s/.*\"${key}\":\"\([^\"]*\)\".*/\1/p" \ | head -n1 } json_get_number() { local key="$1" local line="$2" printf '%s\n' "$line" \ | sed -n "s/.*\"${key}\":\([0-9][0-9]*\).*/\1/p" \ | head -n1 } get_last_offset() { local offset offset="$(cat "$STATE_FILE" 2>/dev/null || true)" if [[ "$offset" =~ ^[0-9]+$ ]]; then printf '%s\n' "$offset" else printf '0\n' fi } set_last_offset() { printf '%s\n' "$1" > "$STATE_FILE" } current_log_size() { stat -c '%s' "$LOG_SOURCE" 2>/dev/null || printf '0\n' } cleanup_dedup_file() { local now window tmp now="$(date +%s)" window="${ALERT_DEDUP_WINDOW:-3600}" tmp="$(mktemp "${MONITORING_STATE_DIR}/alert-engine.dedup.XXXXXX")" || return 0 awk -F'|' -v now="$now" -v window="$window" ' NF >= 2 { if ((now - $2) <= window) print $0 } ' "$DEDUP_FILE" > "$tmp" 2>/dev/null || true mv -f "$tmp" "$DEDUP_FILE" 2>/dev/null || true } dedup_key() { local host="$1" local app="$2" local level="$3" local event="$4" printf '%s|%s|%s|%s\n' "$host" "$app" "$level" "$event" } should_notify_dedup() { local key="$1" local now window found_ts now="$(date +%s)" window="${ALERT_DEDUP_WINDOW:-3600}" found_ts="$(awk -F'|' -v k="$key" ' $1 "|" $3 "|" $4 "|" $5 == k {print $2} ' "$DEDUP_FILE" | tail -n1)" if [[ "$found_ts" =~ ^[0-9]+$ ]]; then if [ $((now - found_ts)) -lt "$window" ]; then return 1 fi fi return 0 } save_dedup_entry() { local host="$1" local app="$2" local level="$3" local event="$4" local now now="$(date +%s)" printf '%s|%s|%s|%s|%s\n' "$host" "$now" "$app" "$level" "$event" >> "$DEDUP_FILE" } event_is_ignored() { local event="$1" ignored for ignored in ${ALERT_IGNORE_EVENTS:-}; do [ "$ignored" = "$event" ] && return 0 done return 1 } channels_for_event() { local level="$1" local event="$2" local varname value varname="ALERT_RULE_${event}" value="${!varname:-}" if [ -n "$value" ]; then printf '%s\n' "$value" return 0 fi case "$level" in WARNING) printf '%s\n' "${ALERT_DEFAULT_CHANNELS_WARNING:-ntfy}" ;; ERROR) printf '%s\n' "${ALERT_DEFAULT_CHANNELS_ERROR:-ntfy,mail}" ;; CRITICAL) printf '%s\n' "${ALERT_DEFAULT_CHANNELS_CRITICAL:-ntfy,mail}" ;; *) printf '\n' ;; esac } tags_for_level() { case "$1" in WARNING) printf '%s\n' "${NTFY_TAGS_WARNING:-warning}" ;; ERROR) printf '%s\n' "${NTFY_TAGS_ERROR:-warning,rotating_light}" ;; CRITICAL) printf '%s\n' "${NTFY_TAGS_CRITICAL:-skull,warning}" ;; *) printf '\n' ;; esac } send_ntfy() { local title="$1" local body="$2" local priority="$3" [ "${ALERT_NTFY_ENABLED:-true}" = "true" ] || return 0 [ -n "${NTFY_SERVER:-}" ] || return 1 [ -n "${NTFY_TOPIC:-}" ] || return 1 local url="${NTFY_SERVER%/}/${NTFY_TOPIC}" local curl_args=( -fsS -X POST -H "Title: ${title}" -H "Priority: ${priority}" -H "Tags: warning" -d "$body" ) # topic protégé if [ -n "${NTFY_TOKEN:-}" ]; then curl_args+=(-H "Authorization: Bearer ${NTFY_TOKEN}") fi curl "${curl_args[@]}" "$url" >/dev/null } send_mail() { local subject="$1" local body="$2" [ "${ALERT_MAIL_ENABLED:-true}" = "true" ] || return 0 [ -n "${DEST:-}" ] || return 1 [ -x "${ALERT_MAIL_BIN:-/usr/sbin/sendmail}" ] || return 1 { printf 'To: %s\n' "${DEST}" printf 'Subject: %s %s\n' "${ALERT_MAIL_SUBJECT_PREFIX:-[monitoring]}" "$subject" printf 'Content-Type: text/plain; charset=UTF-8\n' printf '\n' printf '%s\n' "$body" } | "${ALERT_MAIL_BIN:-/usr/sbin/sendmail}" -t } priority_for_level() { case "$1" in CRITICAL) printf 'urgent\n' ;; ERROR) printf 'high\n' ;; WARNING) printf 'default\n' ;; *) printf 'default\n' ;; esac } build_title() { local host="$1" local app="$2" local level="$3" local event="$4" printf '%s [%s] %s %s\n' "$host" "$app" "$level" "$event" } build_body() { local ts="$1" local host="$2" local app="$3" local level="$4" local event="$5" local message="$6" local line="$7" cat <