Enhance dovecot-logreport script: add progress display, improve sudo handling for unreadable log files, and update usage instructions

This commit is contained in:
2026-07-07 23:15:30 +02:00
parent e23a51af10
commit 312bd8594f
+86 -42
View File
@@ -26,7 +26,7 @@ set -euo pipefail
# * --oneline: 1 Zeile TSV (kein Leerzeilentrenner) # * --oneline: 1 Zeile TSV (kein Leerzeilentrenner)
# * --stats: Statistik statt Events (inkl. Matrix/Toplisten) # * --stats: Statistik statt Events (inkl. Matrix/Toplisten)
# - Live-Modus: # - 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: # Anforderungen/Annahmen:
# - Timestamp steht als erstes Feld in ISO-Format (z.B. 2026-01-17T12:34:56+01:00) # - 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 ALL=0
ONELINE=0 ONELINE=0
STATS=0 STATS=0
SHOW_PROGRESS=0
if [[ -t 2 ]]; then
SHOW_PROGRESS=1
fi
# Service-/Event-Filter (OR-verknüpft, wenn mehrere gesetzt sind) # Service-/Event-Filter (OR-verknüpft, wenn mehrere gesetzt sind)
ONLY_LOGIN=0 ONLY_LOGIN=0
@@ -111,7 +115,7 @@ INPUT OPTIONS
You can combine --all and --file to add extra files. You can combine --all and --file to add extra files.
--follow --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. .gz files cannot be followed and will be skipped in follow mode.
TIME FILTERS (ISO8601 recommended) 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." [[ ${#FOLLOW_FILES[@]} -gt 0 ]] || die "--follow requested, but only .gz files selected. Add an active log with --file."
fi 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 # POSIX-AWK Parser
# - kompatibel zu mawk/nawk/busybox awk # - 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. # Tries to extract a user/mailbox from common patterns.
function extract_user(line, arr, u) { function extract_user(line, arr, u) {
if (match(line, /user=<[^>]+>/, arr)) { 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)) { if (match(line, /: (imap|pop3|lmtp)\([^)]*\)/, arr)) {
@@ -468,42 +490,49 @@ function type_regex_match(type, typere) {
return 0 return 0
} }
# ---- STATS helper: sort and print top N ---- # ---- progress spinner on stderr ----
function print_top_sorted(title, arr, n, k,i,j,keys,tmp) { function progress_tick( now, ch) {
if (show_progress != 1) return
now = systime()
if (PROGRESS_LAST != "" && now - PROGRESS_LAST < 2) return
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
}
function progress_clear() {
if (show_progress != 1 || PROGRESS_DRAWN != 1) return
printf "\r \r" > "/dev/stderr"
fflush("/dev/stderr")
PROGRESS_DRAWN = 0
}
# ---- 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 print title
i=0
for (k in arr) { i++; keys[i]=k }
for (i=1; i<=length(keys); i++) { count=0
for (j=i+1; j<=length(keys); j++) { for (k in arr) {
if (arr[keys[j]] > arr[keys[i]]) { pos=1
tmp=keys[i]; keys[i]=keys[j]; keys[j]=tmp 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++) { for (i=1; i<=count; i++) {
printf " %7d %s\n", arr[keys[i]], keys[i] printf " %7d %s\n", topv[i], topk[i]
}
print ""
}
# ---- 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 }
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
}
}
}
for (i=1; i<=length(keys) && i<=n; i++) {
printf " %7d %s\n", arr[keys[i]], keys[i]
} }
print "" print ""
} }
@@ -524,10 +553,14 @@ BEGIN {
onlypop3 = ENVIRON["ONLY_POP3"] + 0 onlypop3 = ENVIRON["ONLY_POP3"] + 0
typere = ENVIRON["TYPE_RE"] typere = ENVIRON["TYPE_RE"]
show_progress = ENVIRON["SHOW_PROGRESS"] + 0
total=0 total=0
progress_tick()
} }
{ {
progress_tick()
line=$0 line=$0
if (line !~ /dovecot:/) next if (line !~ /dovecot:/) next
@@ -592,10 +625,12 @@ BEGIN {
# Eventausgabe # Eventausgabe
if (oneline == 1) { if (oneline == 1) {
progress_clear()
printf "%s\t%s\t%s\t%s\t%s\t%s\n", ts, type, res, ip, user, msg printf "%s\t%s\t%s\t%s\t%s\t%s\n", ts, type, res, ip, user, msg
next next
} }
progress_clear()
print "time: " ts print "time: " ts
print "type: " type print "type: " type
print "client-ip: " ip print "client-ip: " ip
@@ -606,6 +641,7 @@ BEGIN {
} }
END { END {
progress_clear()
if (stats != 1) exit if (stats != 1) exit
print "Events: " total print "Events: " total
@@ -626,7 +662,7 @@ END {
print_top_sorted("Top users (overall, Top 10)", c_user, 10) 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_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) print_top_sorted("Fail combos IP -> user (Top 20)", c_fail_ip_user, 20)
} }
@@ -635,16 +671,25 @@ END {
# ------------------------------------------------------------------- # -------------------------------------------------------------------
# run_pipeline(): # run_pipeline():
# Liest die ausgewählten Files (inkl. .gz über zcat -f) und pipe't # 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() { run_pipeline() {
local f local f
if [[ $FOLLOW -eq 1 ]]; then 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" \ 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" \ 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" awk "$AWK_SCRIPT"
return return
fi fi
@@ -652,16 +697,15 @@ run_pipeline() {
{ {
for f in "${LOGFILES[@]}"; do for f in "${LOGFILES[@]}"; do
if [[ "$f" =~ \.gz$ ]]; then if [[ "$f" =~ \.gz$ ]]; then
zcat -f -- "$f" 2>/dev/null || true run_read zcat -f -- "$f" || true
else else
cat -- "$f" 2>/dev/null || true run_read cat -- "$f" || true
fi fi
done done
} | env SINCE="$SINCE" UNTIL="$UNTIL" ADDR_RE="$ADDR_RE" EMAIL_NEEDLE="$EMAIL_NEEDLE" TYPE_RE="$TYPE_RE" \ } | 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" \ 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" awk "$AWK_SCRIPT"
} }
run_pipeline run_pipeline