From 312bd8594f3fc8d5b60adc011b2357b208899395 Mon Sep 17 00:00:00 2001 From: Christoph Date: Tue, 7 Jul 2026 23:15:30 +0200 Subject: [PATCH] Enhance dovecot-logreport script: add progress display, improve sudo handling for unreadable log files, and update usage instructions --- dovecot-logreport.sh | 122 +++++++++++++++++++++++++++++-------------- 1 file changed, 83 insertions(+), 39 deletions(-) diff --git a/dovecot-logreport.sh b/dovecot-logreport.sh index 30ff640..9fa33c4 100755 --- a/dovecot-logreport.sh +++ b/dovecot-logreport.sh @@ -26,7 +26,7 @@ set -euo pipefail # * --oneline: 1 Zeile TSV (kein Leerzeilentrenner) # * --stats: Statistik statt Events (inkl. Matrix/Toplisten) # - Live-Modus: -# * --follow: tail -F (gz-Dateien werden dafür übersprungen) +# * --follow: tail -F, sudo nur falls nötig (gz-Dateien werden dafür übersprungen) # # Anforderungen/Annahmen: # - Timestamp steht als erstes Feld in ISO-Format (z.B. 2026-01-17T12:34:56+01:00) @@ -53,6 +53,10 @@ FOLLOW=0 ALL=0 ONELINE=0 STATS=0 +SHOW_PROGRESS=0 +if [[ -t 2 ]]; then + SHOW_PROGRESS=1 +fi # Service-/Event-Filter (OR-verknüpft, wenn mehrere gesetzt sind) ONLY_LOGIN=0 @@ -111,7 +115,7 @@ INPUT OPTIONS You can combine --all and --file to add extra files. --follow - Follow the active logfile(s) (tail -F). + Follow the active logfile(s) (tail -F; sudo only if needed). .gz files cannot be followed and will be skipped in follow mode. TIME FILTERS (ISO8601 recommended) @@ -336,6 +340,24 @@ if [[ $FOLLOW -eq 1 ]]; then [[ ${#FOLLOW_FILES[@]} -gt 0 ]] || die "--follow requested, but only .gz files selected. Add an active log with --file." fi +NEED_SUDO=0 +declare -a UNREADABLE_FILES=() +for f in "${LOGFILES[@]}"; do + if ! { : < "$f"; } 2>/dev/null; then + NEED_SUDO=1 + UNREADABLE_FILES+=("$f") + fi +done + +if [[ $NEED_SUDO -eq 1 ]]; then + { + echo "Using sudo because these selected log file(s) cannot be opened for reading by $(id -un):" + echo "Current groups: $(id -nG)" + printf " %s\n" "${UNREADABLE_FILES[@]}" + } >&2 + sudo -v || die "sudo authentication failed; cannot read Dovecot log files." +fi + # ------------------------------------------------------------------- # POSIX-AWK Parser # - kompatibel zu mawk/nawk/busybox awk @@ -388,7 +410,7 @@ function extract_type(line, arr, t) { # Tries to extract a user/mailbox from common patterns. function extract_user(line, arr, u) { if (match(line, /user=<[^>]+>/, arr)) { - return substr(arr[0], 7, length(arr[0])-7-1) + return substr(arr[0], 7, length(arr[0])-7) } if (match(line, /: (imap|pop3|lmtp)\([^)]*\)/, arr)) { @@ -468,42 +490,49 @@ function type_regex_match(type, typere) { return 0 } -# ---- STATS helper: sort and print top N ---- -function print_top_sorted(title, arr, n, k,i,j,keys,tmp) { - print title - i=0 - for (k in arr) { i++; keys[i]=k } +# ---- progress spinner on stderr ---- +function progress_tick( now, ch) { + if (show_progress != 1) return - for (i=1; i<=length(keys); i++) { - for (j=i+1; j<=length(keys); j++) { - if (arr[keys[j]] > arr[keys[i]]) { - tmp=keys[i]; keys[i]=keys[j]; keys[j]=tmp - } - } - } + now = systime() + if (PROGRESS_LAST != "" && now - PROGRESS_LAST < 2) return - for (i=1; i<=length(keys) && i<=n; i++) { - printf " %7d %s\n", arr[keys[i]], keys[i] - } - print "" + PROGRESS_LAST = now + PROGRESS_IDX = (PROGRESS_IDX % 4) + 1 + ch = substr("|/-\\", PROGRESS_IDX, 1) + printf "\r%s", ch > "/dev/stderr" + fflush("/dev/stderr") + PROGRESS_DRAWN = 1 } -# ---- STATS helper: sort and print top N for matrix-like keys ---- -function print_matrix(title, arr, n, k,i,j,keys,tmp) { - print title - i=0 - for (k in arr) { i++; keys[i]=k } +function progress_clear() { + if (show_progress != 1 || PROGRESS_DRAWN != 1) return + printf "\r \r" > "/dev/stderr" + fflush("/dev/stderr") + PROGRESS_DRAWN = 0 +} - for (i=1; i<=length(keys); i++) { - for (j=i+1; j<=length(keys); j++) { - if (arr[keys[j]] > arr[keys[i]]) { - tmp=keys[i]; keys[i]=keys[j]; keys[j]=tmp - } +# ---- STATS helper: keep only Top N while scanning the array ---- +function print_top_sorted(title, arr, n, k,i,pos,count,topk,topv) { + print title + + count=0 + for (k in arr) { + pos=1 + while (pos<=count && arr[k] <= topv[pos]) pos++ + if (pos > n) continue + + if (count < n) count++ + for (i=count; i>pos; i--) { + topk[i]=topk[i-1] + topv[i]=topv[i-1] } + topk[pos]=k + topv[pos]=arr[k] } - for (i=1; i<=length(keys) && i<=n; i++) { - printf " %7d %s\n", arr[keys[i]], keys[i] + for (i=1; i<=count; i++) { + printf " %7d %s\n", topv[i], topk[i] } print "" } @@ -524,10 +553,14 @@ BEGIN { onlypop3 = ENVIRON["ONLY_POP3"] + 0 typere = ENVIRON["TYPE_RE"] + show_progress = ENVIRON["SHOW_PROGRESS"] + 0 total=0 + progress_tick() } { + progress_tick() + line=$0 if (line !~ /dovecot:/) next @@ -592,10 +625,12 @@ BEGIN { # Eventausgabe if (oneline == 1) { + progress_clear() printf "%s\t%s\t%s\t%s\t%s\t%s\n", ts, type, res, ip, user, msg next } + progress_clear() print "time: " ts print "type: " type print "client-ip: " ip @@ -606,6 +641,7 @@ BEGIN { } END { + progress_clear() if (stats != 1) exit print "Events: " total @@ -626,7 +662,7 @@ END { print_top_sorted("Top users (overall, Top 10)", c_user, 10) print_top_sorted("Top users (fail only, Top 10)", c_user_fail, 10) - print_matrix("Matrix result@type (Top 30)", c_res_type, 30) + print_top_sorted("Matrix result@type (Top 30)", c_res_type, 30) print_top_sorted("Fail combos IP -> user (Top 20)", c_fail_ip_user, 20) } @@ -635,16 +671,25 @@ END { # ------------------------------------------------------------------- # run_pipeline(): # Liest die ausgewählten Files (inkl. .gz über zcat -f) und pipe't -# in awk. In --follow Mode: tail -F auf nicht-gz Dateien. +# in awk. sudo wird nur verwendet, wenn mindestens eine Logdatei nicht +# direkt lesbar ist. In --follow Mode: tail -F auf nicht-gz Dateien. # ------------------------------------------------------------------- +run_read() { + if [[ $NEED_SUDO -eq 1 ]]; then + sudo "$@" + else + "$@" + fi +} + run_pipeline() { local f if [[ $FOLLOW -eq 1 ]]; then - tail -F "${FOLLOW_FILES[@]}" | \ + run_read tail -F "${FOLLOW_FILES[@]}" | \ env SINCE="$SINCE" UNTIL="$UNTIL" ADDR_RE="$ADDR_RE" EMAIL_NEEDLE="$EMAIL_NEEDLE" TYPE_RE="$TYPE_RE" \ ONLY_LOGIN="$ONLY_LOGIN" ONLY_LMTP="$ONLY_LMTP" ONLY_IMAP="$ONLY_IMAP" ONLY_POP3="$ONLY_POP3" \ - STATUS_FILTER="$STATUS_FILTER" ONELINE="$ONELINE" STATS="$STATS" \ + STATUS_FILTER="$STATUS_FILTER" ONELINE="$ONELINE" STATS="$STATS" SHOW_PROGRESS="$SHOW_PROGRESS" \ awk "$AWK_SCRIPT" return fi @@ -652,16 +697,15 @@ run_pipeline() { { for f in "${LOGFILES[@]}"; do if [[ "$f" =~ \.gz$ ]]; then - zcat -f -- "$f" 2>/dev/null || true + run_read zcat -f -- "$f" || true else - cat -- "$f" 2>/dev/null || true + run_read cat -- "$f" || true fi done } | env SINCE="$SINCE" UNTIL="$UNTIL" ADDR_RE="$ADDR_RE" EMAIL_NEEDLE="$EMAIL_NEEDLE" TYPE_RE="$TYPE_RE" \ ONLY_LOGIN="$ONLY_LOGIN" ONLY_LMTP="$ONLY_LMTP" ONLY_IMAP="$ONLY_IMAP" ONLY_POP3="$ONLY_POP3" \ - STATUS_FILTER="$STATUS_FILTER" ONELINE="$ONELINE" STATS="$STATS" \ + STATUS_FILTER="$STATUS_FILTER" ONELINE="$ONELINE" STATS="$STATS" SHOW_PROGRESS="$SHOW_PROGRESS" \ awk "$AWK_SCRIPT" } run_pipeline -