- 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>
178 lines
5.5 KiB
Awk
Executable File
178 lines
5.5 KiB
Awk
Executable File
#!/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 |