#!/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) 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); 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