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
+83 -39
View File
@@ -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