- Add logwhisperer.sh script for tailing and monitoring system logs - Implement pattern matching for critical errors (FATAL, ERROR, OOM, segfault) - Add JSON payload generation with severity levels - Implement rate limiting and offset tracking per log source - Add install.sh with interactive configuration and systemd support - Create comprehensive test suite with pytest - Add technical specification documentation - Update CHANGELOG.md following Common Changelog standard All 12 tests passing. Follows Metodo Sacchi (Safety first, little often, double check).
424 lines
11 KiB
Bash
Executable File
424 lines
11 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# LogWhisperer Agent - Script di monitoraggio log
|
|
# Legge log di sistema, rileva errori critici e invia alert via webhook
|
|
#
|
|
|
|
set -euo pipefail
|
|
|
|
# ============================================================================
|
|
# CONFIGURAZIONE DEFAULT
|
|
# ============================================================================
|
|
SCRIPT_NAME="$(basename "$0")"
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
VERSION="1.0.0"
|
|
|
|
# Default config paths
|
|
CONFIG_FILE="/etc/logwhisperer/config.env"
|
|
OFFSET_DIR="/var/lib/logwhisperer"
|
|
DEBUG_LOG="/var/log/logwhisperer/debug.log"
|
|
|
|
# Default values
|
|
WEBHOOK_URL=""
|
|
CLIENT_ID=""
|
|
LOG_SOURCES=""
|
|
POLL_INTERVAL=5
|
|
MAX_LINE_LENGTH=2000
|
|
|
|
# Error patterns (case-insensitive)
|
|
PATTERNS=(
|
|
"FATAL"
|
|
"ERROR"
|
|
"OOM"
|
|
"Out of memory"
|
|
"segfault"
|
|
"disk full"
|
|
"No space left on device"
|
|
"Connection refused"
|
|
"Permission denied"
|
|
)
|
|
|
|
# Rate limiting (seconds)
|
|
RATE_LIMIT=30
|
|
|
|
# ============================================================================
|
|
# FUNZIONI DI UTILITÀ
|
|
# ============================================================================
|
|
|
|
log_debug() {
|
|
if [[ "${DEBUG:-0}" == "1" ]]; then
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] DEBUG: $*" >&2
|
|
fi
|
|
}
|
|
|
|
log_error() {
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2
|
|
}
|
|
|
|
log_info() {
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO: $*"
|
|
}
|
|
|
|
# ============================================================================
|
|
# USAGE E HELP
|
|
# ============================================================================
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $SCRIPT_NAME [OPTIONS]
|
|
|
|
LogWhisperer Agent - Monitora log di sistema e invia alert critici via webhook
|
|
|
|
OPTIONS:
|
|
-c, --config FILE Percorso file di configurazione (default: $CONFIG_FILE)
|
|
-d, --dry-run Modalità test: non invia webhook, stampa output
|
|
-t, --test-line LINE Testa il matching su una singola riga
|
|
-s, --test-source SRC Specifica la sorgente per il test (default: /var/log/syslog)
|
|
-v, --validate Valida la configurazione e esce
|
|
--debug Abilita log di debug
|
|
-h, --help Mostra questo help
|
|
--version Mostra versione
|
|
|
|
ESEMPI:
|
|
$SCRIPT_NAME --help
|
|
$SCRIPT_NAME --validate --config /etc/logwhisperer/config.env
|
|
$SCRIPT_NAME --dry-run --test-line "FATAL: database error"
|
|
$SCRIPT_NAME --config /etc/logwhisperer/config.env
|
|
|
|
EOF
|
|
}
|
|
|
|
# ============================================================================
|
|
# GESTIONE CONFIGURAZIONE
|
|
# ============================================================================
|
|
|
|
load_config() {
|
|
if [[ -f "$CONFIG_FILE" ]]; then
|
|
# shellcheck source=/dev/null
|
|
source "$CONFIG_FILE"
|
|
fi
|
|
}
|
|
|
|
validate_config() {
|
|
local errors=0
|
|
|
|
if [[ -z "${WEBHOOK_URL:-}" ]]; then
|
|
log_error "WEBHOOK_URL non configurato"
|
|
errors=$((errors + 1))
|
|
fi
|
|
|
|
if [[ -z "${CLIENT_ID:-}" ]]; then
|
|
log_error "CLIENT_ID non configurato"
|
|
errors=$((errors + 1))
|
|
fi
|
|
|
|
if [[ -z "${LOG_SOURCES:-}" ]]; then
|
|
log_error "LOG_SOURCES non configurato"
|
|
errors=$((errors + 1))
|
|
fi
|
|
|
|
if [[ $errors -gt 0 ]]; then
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# ============================================================================
|
|
# PATTERN MATCHING
|
|
# ============================================================================
|
|
|
|
match_pattern() {
|
|
local line="$1"
|
|
local pattern
|
|
|
|
for pattern in "${PATTERNS[@]}"; do
|
|
if echo "$line" | grep -qi "$pattern"; then
|
|
echo "$pattern"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
get_severity() {
|
|
local pattern="$1"
|
|
|
|
case "$pattern" in
|
|
"FATAL"|"OOM"|"Out of memory"|"segfault")
|
|
echo "critical"
|
|
;;
|
|
"ERROR"|"disk full"|"No space left on device")
|
|
echo "medium"
|
|
;;
|
|
*)
|
|
echo "low"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ============================================================================
|
|
# PAYLOAD JSON
|
|
# ============================================================================
|
|
|
|
build_payload() {
|
|
local source="$1"
|
|
local raw_log="$2"
|
|
local matched_pattern="$3"
|
|
local severity
|
|
severity=$(get_severity "$matched_pattern")
|
|
|
|
# Escape JSON special characters
|
|
raw_log=$(echo "$raw_log" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g')
|
|
|
|
cat <<EOF
|
|
{
|
|
"client_id": "$CLIENT_ID",
|
|
"hostname": "$(hostname)",
|
|
"source": "$source",
|
|
"severity": "$severity",
|
|
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
"raw_log": "$raw_log",
|
|
"matched_pattern": "$matched_pattern"
|
|
}
|
|
EOF
|
|
}
|
|
|
|
# ============================================================================
|
|
# DISPATCH WEBHOOK
|
|
# ============================================================================
|
|
|
|
dispatch_webhook() {
|
|
local payload="$1"
|
|
|
|
if [[ -z "$WEBHOOK_URL" ]]; then
|
|
log_error "WEBHOOK_URL non configurato"
|
|
return 1
|
|
fi
|
|
|
|
local response
|
|
local http_code
|
|
|
|
response=$(curl -s -w "\n%{http_code}" \
|
|
-X POST \
|
|
-H "Content-Type: application/json" \
|
|
-d "$payload" \
|
|
"$WEBHOOK_URL" 2>/dev/null) || {
|
|
log_error "Impossibile connettersi al webhook"
|
|
return 1
|
|
}
|
|
|
|
http_code=$(echo "$response" | tail -n1)
|
|
|
|
if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then
|
|
return 0
|
|
else
|
|
log_error "Webhook ha restituito HTTP $http_code"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# TEST MODE
|
|
# ============================================================================
|
|
|
|
test_line() {
|
|
local line="$1"
|
|
local source="${2:-/var/log/syslog}"
|
|
local matched_pattern
|
|
|
|
matched_pattern=$(match_pattern "$line") || {
|
|
echo "No match"
|
|
return 0
|
|
}
|
|
|
|
# Stampa solo il JSON su stdout per permettere parsing
|
|
build_payload "$source" "$line" "$matched_pattern"
|
|
return 0
|
|
}
|
|
|
|
# ============================================================================
|
|
# MAIN LOOP
|
|
# ============================================================================
|
|
|
|
main_loop() {
|
|
log_info "Avvio LogWhisperer Agent v$VERSION"
|
|
log_info "Client ID: $CLIENT_ID"
|
|
log_info "Log sources: $LOG_SOURCES"
|
|
log_info "Webhook URL: $WEBHOOK_URL"
|
|
|
|
# Array per tracciare ultimi alert (rate limiting)
|
|
declare -A last_alert
|
|
|
|
# Converte LOG_SOURCES in array
|
|
IFS=',' read -ra SOURCES <<< "$LOG_SOURCES"
|
|
|
|
# Inizializza offset directory
|
|
mkdir -p "$OFFSET_DIR"
|
|
|
|
while true; do
|
|
for source in "${SOURCES[@]}"; do
|
|
# Rimuovi spazi
|
|
source=$(echo "$source" | xargs)
|
|
|
|
if [[ ! -r "$source" ]]; then
|
|
log_debug "File non leggibile: $source"
|
|
continue
|
|
fi
|
|
|
|
# Offset file per questa sorgente
|
|
local offset_file="$OFFSET_DIR/$(echo "$source" | tr '/' '_').offset"
|
|
local last_pos=0
|
|
|
|
if [[ -f "$offset_file" ]]; then
|
|
last_pos=$(cat "$offset_file")
|
|
fi
|
|
|
|
# Ottieni dimensione attuale
|
|
local current_size
|
|
current_size=$(stat -c%s "$source" 2>/dev/null || echo 0)
|
|
|
|
# Se il file è stato troncato o ruotato
|
|
if [[ $current_size -lt $last_pos ]]; then
|
|
last_pos=0
|
|
fi
|
|
|
|
# Leggi nuove righe
|
|
tail -c +$((last_pos + 1)) "$source" 2>/dev/null | while IFS= read -r line; do
|
|
# Trunca se troppo lunga
|
|
if [[ ${#line} -gt $MAX_LINE_LENGTH ]]; then
|
|
line="${line:0:$MAX_LINE_LENGTH}..."
|
|
fi
|
|
|
|
local matched_pattern
|
|
if matched_pattern=$(match_pattern "$line"); then
|
|
local now
|
|
now=$(date +%s)
|
|
local source_key="$source:$matched_pattern"
|
|
|
|
# Rate limiting
|
|
if [[ -n "${last_alert[$source_key]:-}" ]]; then
|
|
local last_time=${last_alert[$source_key]}
|
|
if [[ $((now - last_time)) -lt $RATE_LIMIT ]]; then
|
|
log_debug "Rate limited: $source_key"
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
log_info "Rilevato: $matched_pattern in $source"
|
|
|
|
local payload
|
|
payload=$(build_payload "$source" "$line" "$matched_pattern")
|
|
|
|
if dispatch_webhook "$payload"; then
|
|
last_alert[$source_key]=$now
|
|
log_debug "Alert inviato con successo"
|
|
else
|
|
log_error "Fallimento invio alert"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# Salva nuova posizione
|
|
echo "$current_size" > "$offset_file"
|
|
done
|
|
|
|
sleep "$POLL_INTERVAL"
|
|
done
|
|
}
|
|
|
|
# ============================================================================
|
|
# PARSING ARGOMENTI
|
|
# ============================================================================
|
|
|
|
DRY_RUN=0
|
|
VALIDATE=0
|
|
TEST_LINE=""
|
|
TEST_SOURCE="/var/log/syslog"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
-c|--config)
|
|
CONFIG_FILE="$2"
|
|
shift 2
|
|
;;
|
|
-d|--dry-run)
|
|
DRY_RUN=1
|
|
shift
|
|
;;
|
|
-t|--test-line)
|
|
TEST_LINE="$2"
|
|
shift 2
|
|
;;
|
|
-s|--test-source)
|
|
TEST_SOURCE="$2"
|
|
shift 2
|
|
;;
|
|
-v|--validate)
|
|
VALIDATE=1
|
|
shift
|
|
;;
|
|
--debug)
|
|
DEBUG=1
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
--version)
|
|
echo "$SCRIPT_NAME v$VERSION"
|
|
exit 0
|
|
;;
|
|
*)
|
|
log_error "Opzione sconosciuta: $1"
|
|
usage
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# ============================================================================
|
|
# MAIN
|
|
# ============================================================================
|
|
|
|
main() {
|
|
# Carica configurazione
|
|
load_config
|
|
|
|
# Modalità validazione
|
|
if [[ $VALIDATE -eq 1 ]]; then
|
|
if validate_config; then
|
|
log_info "Configurazione valida"
|
|
exit 0
|
|
else
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Modalità test linea
|
|
if [[ -n "$TEST_LINE" ]]; then
|
|
if [[ $DRY_RUN -eq 1 ]]; then
|
|
test_line "$TEST_LINE" "$TEST_SOURCE"
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
# Verifica configurazione prima di avviare
|
|
if ! validate_config; then
|
|
log_error "Configurazione invalida. Usa --validate per dettagli."
|
|
exit 1
|
|
fi
|
|
|
|
# Modalità dry-run senza test line
|
|
if [[ $DRY_RUN -eq 1 ]]; then
|
|
log_info "Modalità dry-run: nessun webhook verrà inviato"
|
|
fi
|
|
|
|
# Avvia loop principale
|
|
main_loop
|
|
}
|
|
|
|
main "$@"
|