feat(ingestion): implement log monitoring script with webhook integration
- 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).
This commit is contained in:
423
scripts/logwhisperer.sh
Executable file
423
scripts/logwhisperer.sh
Executable file
@@ -0,0 +1,423 @@
|
||||
#!/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 "$@"
|
||||
Reference in New Issue
Block a user