Refactor exitrace to modular architecture
- Split monolithic exitrace.sh into focused lib/ modules - date_utils.sh: Date validation and generation functions - mx_lookup.sh: MX record lookup functionality - log_analyzer.awk: Core log analysis logic extracted from main script - Maintains full backward compatibility with existing interface - Improves maintainability, testability, and code organization 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env bash
|
||||
# date_utils.sh - Date utility functions for exitrace
|
||||
# Maintainer: ljp
|
||||
|
||||
# Constants
|
||||
readonly DATE_FMT="%Y-%m-%d"
|
||||
readonly MAX_DAYS_LIMIT=400
|
||||
|
||||
# Validation functions
|
||||
validate_positive_integer() {
|
||||
local value="$1"
|
||||
local param="$2"
|
||||
if [[ -z "$value" ]]; then
|
||||
echo "Fehler: Parameter $param benötigt einen Wert" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! [[ "$value" =~ ^[0-9]+$ ]]; then
|
||||
echo "Fehler: $param muss eine positive Zahl sein (gegeben: '$value')" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$value" -eq 0 ]]; then
|
||||
echo "Fehler: $param muss größer als 0 sein" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
validate_date() {
|
||||
local date_val="$1"
|
||||
local param="$2"
|
||||
if [[ -z "$date_val" ]]; then
|
||||
echo "Fehler: Parameter $param benötigt einen Wert" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! date -d "$date_val" >/dev/null 2>&1; then
|
||||
echo "Fehler: Ungültiges Datumsformat für $param: '$date_val'" >&2
|
||||
echo "Erwartetes Format: YYYY-MM-DD" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
validate_date_range() {
|
||||
local range="$1"
|
||||
if [[ -z "$range" ]]; then
|
||||
echo "Fehler: --range benötigt einen Wert" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$range" != *".."* ]]; then
|
||||
echo "Fehler: Ungültiges Range-Format: '$range'" >&2
|
||||
echo "Erwartetes Format: YYYY-MM-DD..YYYY-MM-DD" >&2
|
||||
exit 1
|
||||
fi
|
||||
local start_date="${range%%..*}"
|
||||
local end_date="${range##*..}"
|
||||
validate_date "$start_date" "--range Startdatum"
|
||||
validate_date "$end_date" "--range Enddatum"
|
||||
}
|
||||
|
||||
# Date generation functions
|
||||
gen_dates_hours_back() {
|
||||
local hrs="$1" start_ts end_ts cur next_ts
|
||||
end_ts="$(date +%s)" || { echo "Fehler: Kann aktuellen Timestamp nicht ermitteln" >&2; exit 1; }
|
||||
start_ts="$(date -d "-${hrs} hours" +%s)" || { echo "Fehler: Kann Start-Timestamp nicht berechnen" >&2; exit 1; }
|
||||
cur="$(date -d "@$start_ts" +${DATE_FMT})" || { echo "Fehler: Kann Startdatum nicht formatieren" >&2; exit 1; }
|
||||
|
||||
local iterations=0
|
||||
while :; do
|
||||
echo "$cur"
|
||||
next_ts="$(date -d "$cur +1 day" +%s 2>/dev/null)" || { echo "Fehler: Datumsberechnung fehlgeschlagen für $cur" >&2; exit 1; }
|
||||
[[ "$next_ts" -le "$end_ts" ]] || break
|
||||
cur="$(date -d "$cur +1 day" +${DATE_FMT} 2>/dev/null)" || { echo "Fehler: Datum-Inkrement fehlgeschlagen für $cur" >&2; exit 1; }
|
||||
|
||||
# Sicherheit gegen Endlosschleifen
|
||||
((iterations++))
|
||||
if [[ $iterations -gt $MAX_DAYS_LIMIT ]]; then
|
||||
echo "Fehler: Zu viele Tage im Zeitfenster (>$MAX_DAYS_LIMIT Tage)" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
gen_dates_days_back() {
|
||||
local days="$1" i
|
||||
for ((i=0;i<=days;i++)); do
|
||||
date -d "-$i day" +${DATE_FMT} || { echo "Fehler: Kann Datum für -$i day nicht berechnen" >&2; exit 1; }
|
||||
done
|
||||
}
|
||||
|
||||
gen_dates_range() {
|
||||
local a="${1%%..*}" b="${1##*..}" cur="$a"
|
||||
local iterations=0
|
||||
|
||||
while :; do
|
||||
echo "$cur"
|
||||
[[ "$cur" == "$b" ]] && break
|
||||
cur="$(date -d "$cur +1 day" +${DATE_FMT} 2>/dev/null)" || { echo "Fehler: Datum-Inkrement fehlgeschlagen für $cur" >&2; exit 1; }
|
||||
|
||||
# Sicherheit gegen Endlosschleifen
|
||||
((iterations++))
|
||||
if [[ $iterations -gt $MAX_DAYS_LIMIT ]]; then
|
||||
echo "Fehler: Zu großer Datumsbereich (>$MAX_DAYS_LIMIT Tage)" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
files_for_date() {
|
||||
local d="$1" y="${d:0:4}" m="${d:5:2}"
|
||||
local logroot="${LOGROOT:-/var/log/easyname}"
|
||||
local p="${logroot}/${y}/${m}/${d}.log"
|
||||
if [[ -f "$p" ]]; then
|
||||
printf "%s\n" "$p"
|
||||
fi
|
||||
if [[ -f "${p}.gz" ]]; then
|
||||
printf "%s\n" "${p}.gz"
|
||||
fi
|
||||
}
|
||||
|
||||
# Built with the help of Claude Code
|
||||
Executable
+178
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/awk -f
|
||||
# log_analyzer.awk - Exim log analyzer for exitrace
|
||||
# Maintainer: ljp
|
||||
# Processes Exim logs and classifies delivery status per Message-ID
|
||||
|
||||
function trim(s){ sub(/^[ \t\r\n]+/, "", s); sub(/[ \t\r\n]+$/, "", s); return s }
|
||||
function add_rcpt(id, rc){ if (!(rc in rcpts[id])) rcpts[id][rc]=1 }
|
||||
function incr(a, k){ a[k]++; return a[k] }
|
||||
|
||||
BEGIN { FS=" " }
|
||||
|
||||
{
|
||||
raw=$0
|
||||
|
||||
# Zeit + ID detektieren
|
||||
match(raw, /[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} [A-Za-z0-9-]+/, t)
|
||||
if (!t[0]) next
|
||||
split(t[0], p, " ")
|
||||
ts = p[1] " " p[2]
|
||||
|
||||
# Extrahiere Rest der Zeile nach Timestamp für weitere Verarbeitung
|
||||
raw_after = raw
|
||||
sub(/^[^:]*:[^:]*:[^ ]+ /, "", raw_after)
|
||||
msgid = $1
|
||||
if (!msgid) next
|
||||
|
||||
if (MSGIDREQ != "" && msgid != MSGIDREQ) next
|
||||
|
||||
# Init Times
|
||||
if (!(msgid in first_ts) || ts < first_ts[msgid]) first_ts[msgid]=ts
|
||||
if (!(msgid in last_ts) || ts > last_ts[msgid]) last_ts[msgid]=ts
|
||||
|
||||
# Sender extrahieren
|
||||
if (index($0, "<=") == 1 || $2 == "<=") {
|
||||
if (match($0, / from <[^>]+>/, m)) {
|
||||
s=m[0]; gsub(/.* from </, "", s); gsub(/>.*/, "", s)
|
||||
from=trim(s)
|
||||
} else if (match($0, /<= [^ ]+@[^ ]+/, m2)) {
|
||||
split(m2[0], a, " "); from=trim(a[2])
|
||||
} else { from="" }
|
||||
if (from != "") {
|
||||
from_addr[msgid]=from
|
||||
if (FROMREQ != "" && from != FROMREQ) from_mismatch[msgid]=1
|
||||
}
|
||||
}
|
||||
|
||||
# Empfänger extrahieren
|
||||
rcpt=""
|
||||
if (match($0, / for <[^>]+>/, r)) {
|
||||
rcpt=r[0]; gsub(/ for </, "", rcpt); gsub(/>/, "", rcpt); rcpt=trim(rcpt)
|
||||
} else if (match($0, /\*\* [^ ]+@[^ ]+/, r2)) {
|
||||
split(r2[0], b, " "); rcpt=trim(b[2])
|
||||
}
|
||||
if (rcpt != "") add_rcpt(msgid, rcpt)
|
||||
|
||||
# Klassify line types
|
||||
is_delivery = index($0, " => ")>0
|
||||
is_fail = index($0, " ** ")>0
|
||||
is_remote = (index($0, " T=remote_smtp")>0 || index($0, " T=remote_smtp_dkim")>0)
|
||||
|
||||
# Success detection: C="250 ..." oder "250 " in C=...
|
||||
is_success = 0
|
||||
if (is_delivery && is_remote) {
|
||||
if (match($0, /C="[^"]*250[^"]*"/)) is_success=1
|
||||
}
|
||||
|
||||
# Deferred detection
|
||||
# typische Phrasen: "retry time not reached", "temporarily", "Connection timed out", "have been failing"
|
||||
is_defer = 0
|
||||
if (is_remote && (index($0, "retry time not reached")>0 || index($0, "temporar")>0 || index($0, "timed out")>0 || match($0, /all hosts for '\''[^']+'\'' have been failing/))) {
|
||||
is_defer=1
|
||||
}
|
||||
|
||||
# Fail detection (permanent)
|
||||
# Zeilen mit "** rcpt ..." sind i. d. R. permanent; weitere Marker: "Unrouteable address", "rejected", "mailbox full" kann defer sein -> belassen
|
||||
is_perm_fail = 0
|
||||
if (is_fail) {
|
||||
is_perm_fail=1
|
||||
} else if (match($0, /(Unrouteable address|no such user|User unknown|550 )/)) {
|
||||
is_perm_fail=1
|
||||
}
|
||||
|
||||
# Domainfilter (weicher): wir verwerfen die ID nicht, zeigen aber non-matching Zeilen trotzdem mit
|
||||
if (WANTDOM != "") {
|
||||
# keine harte Filterung, nur spätere Auswertung nutzt WANTDOM, falls nötig
|
||||
;
|
||||
}
|
||||
|
||||
# Extra Regex? (nur für Zeilenfilterung, ID bleibt)
|
||||
if (EXTRA != "" && $0 !~ EXTRA) {
|
||||
; # wir speichern die Zeile dennoch im Voll-Log, sonst verliert man Kontext
|
||||
}
|
||||
|
||||
# Counters
|
||||
if (is_success) deliveries[msgid]++
|
||||
if (is_defer) defers[msgid]++
|
||||
if (is_perm_fail) fails[msgid]++
|
||||
|
||||
# „have been failing …" Marker
|
||||
if (match($0, /all hosts for '\''[^']+'\'' have been failing/)) {
|
||||
fail_marker[msgid]=1
|
||||
}
|
||||
|
||||
# Full log sammeln
|
||||
status[msgid]=(status[msgid] ? status[msgid] "\n" : "") ts " | " $0
|
||||
}
|
||||
|
||||
END {
|
||||
# IDs einsammeln & sortieren
|
||||
n=0
|
||||
for (id in first_ts){ ids[++n]=id }
|
||||
for (i=2;i<=n;i++){
|
||||
key=ids[i]; j=i-1
|
||||
while (j>=1 && first_ts[ids[j]] > first_ts[key]) { ids[j+1]=ids[j]; j-- }
|
||||
ids[j+1]=key
|
||||
}
|
||||
|
||||
if (n==0){
|
||||
print "Keine passenden Einträge im gewählten Zeitfenster gefunden."
|
||||
exit 0
|
||||
}
|
||||
|
||||
sep="------------------------------------------------------------------------"
|
||||
for (k=1;k<=n;k++){
|
||||
id=ids[k]
|
||||
|
||||
# FROM Nachfilter
|
||||
if (FROMREQ != "" && from_mismatch[id]==1) {
|
||||
if (from_addr[id] != FROMREQ && from_addr[id] != "") continue
|
||||
}
|
||||
|
||||
d=(id in deliveries ? deliveries[id] : 0)
|
||||
e=(id in defers ? defers[id] : 0)
|
||||
f=(id in fails ? fails[id] : 0)
|
||||
|
||||
# Status-Logik:
|
||||
# - d>0 && f==0 && e==0 -> OK
|
||||
# - d>0 && (f>0 || e>0) -> PARTIAL
|
||||
# - d==0 && f>0 -> FAIL
|
||||
# - d==0 && f==0 && e>0 -> DELAY
|
||||
# - sonst -> UNKNOWN
|
||||
status_txt="UNKNOWN"
|
||||
if (d>0 && f==0 && e==0) status_txt="OK"
|
||||
else if (d>0 && (f>0 || e>0)) status_txt="PARTIAL"
|
||||
else if (d==0 && f>0) status_txt="FAIL"
|
||||
else if (d==0 && f==0 && e>0) status_txt="DELAY"
|
||||
|
||||
# Summary-Zeile
|
||||
# Rcpts sammeln
|
||||
out_rcpts=""
|
||||
if (id in rcpts){
|
||||
for (r in rcpts[id]) {
|
||||
out_rcpts = (out_rcpts=="" ? r : out_rcpts ", " r)
|
||||
}
|
||||
}
|
||||
printf "[%s] ID=%s | First=%s | From=%s | Rcpts=%s | Delivered=%d Deferred=%d Failed=%d\n",
|
||||
status_txt, id, first_ts[id], (from_addr[id] ? from_addr[id] : "(?)"), (out_rcpts=="" ? "(?)" : out_rcpts), d, e, f
|
||||
|
||||
# Marker-Hinweis
|
||||
if (fail_marker[id]) {
|
||||
printf " Note: DELIVERY STALL erkannt: '\''all hosts for domain failing (retry/backoff)'\''\n"
|
||||
}
|
||||
|
||||
# Detailblock
|
||||
print sep
|
||||
printf "ID: %s\n", id
|
||||
printf "First: %s\n", first_ts[id]
|
||||
printf "Last: %s\n", last_ts[id]
|
||||
printf "From: %s\n", (from_addr[id] ? from_addr[id] : "(unbekannt)")
|
||||
printf "Rcpts: %s\n", (out_rcpts=="" ? "(unbekannt)" : out_rcpts)
|
||||
printf "Status: %s (Delivered=%d, Deferred=%d, Failed=%d)\n", status_txt, d, e, f
|
||||
print "Log:"
|
||||
print status[id]
|
||||
print sep
|
||||
}
|
||||
}
|
||||
|
||||
# Built with the help of Claude Code
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
# mx_lookup.sh - MX record lookup functions for exitrace
|
||||
# Maintainer: ljp
|
||||
|
||||
# MX record lookup with A/AAAA resolution
|
||||
show_mx_records() {
|
||||
local domain="$1"
|
||||
|
||||
if ! command -v dig >/dev/null 2>&1; then
|
||||
echo "(Hinweis) dig nicht gefunden, MX-Ausgabe übersprungen." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "== MX für ${domain} =="
|
||||
dig +short MX "$domain" | sort -n || {
|
||||
echo "Fehler: MX-Abfrage für $domain fehlgeschlagen" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "-- A/AAAA der MX:"
|
||||
while read -r mx; do
|
||||
[[ -n "$mx" ]] || continue
|
||||
local host="${mx#* }"
|
||||
printf "%-35s | " "$host"
|
||||
|
||||
# A records
|
||||
local a_records
|
||||
a_records="$(dig +short A "$host" 2>/dev/null | paste -sd, -)"
|
||||
printf "%s" "${a_records:-"-"}"
|
||||
|
||||
printf " | "
|
||||
|
||||
# AAAA records
|
||||
local aaaa_records
|
||||
aaaa_records="$(dig +short AAAA "$host" 2>/dev/null | paste -sd, -)"
|
||||
printf "%s\n" "${aaaa_records:-"-"}"
|
||||
|
||||
done < <(dig +short MX "$domain" | sort -n)
|
||||
|
||||
echo "========================================================================"
|
||||
}
|
||||
|
||||
# Built with the help of Claude Code
|
||||
Reference in New Issue
Block a user