From: Vsevolod Stakhov Date: Wed, 11 Feb 2026 09:25:31 +0000 (+0000) Subject: [Minor] Add colored output and TTY-aware progress to logstats/mapstats X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=3de0f0871ab8c09cc47740d9fa24d9844dcd7e81;p=thirdparty%2Frspamd.git [Minor] Add colored output and TTY-aware progress to logstats/mapstats Gate spinner and ANSI escape codes behind isatty() so piped output is clean. Add ansicolors to logstats (Ham/Spam/Junk labels, symbol names, actions, warnings, summary) and mapstats (map status, match counts, unmatched warnings). --- diff --git a/lualib/lua_log_utils.lua b/lualib/lua_log_utils.lua index 71b8265e9a..03dcdeb3a6 100644 --- a/lualib/lua_log_utils.lua +++ b/lualib/lua_log_utils.lua @@ -19,6 +19,8 @@ local rspamd_util = require "rspamd_util" local exports = {} +local isatty = rspamd_util.isatty() + local decompressor = { bz2 = 'bzip2 -cd', gz = 'gzip -cd', @@ -35,6 +37,9 @@ local spinner_chars = { '/', '-', '\\', '|' } local spinner_update_time = 0 function exports.spinner() + if not isatty then + return + end local now = os.time() if (now - spinner_update_time) < 1 then return @@ -315,15 +320,22 @@ function exports.process_logs(log_file, start_time, end_time, callback, opts) if not h then io.stderr:write(string.format("Cannot open %s: %s\n", path, open_err or 'unknown error')) else - io.stderr:write(string.format("\027[J Parsing log files: [%d/%d] %s\027[G", - idx, #logs, fname)) + if isatty then + io.stderr:write(string.format("\027[J Parsing log files: [%d/%d] %s\027[G", + idx, #logs, fname)) + else + io.stderr:write(string.format(" Parsing log files: [%d/%d] %s\n", + idx, #logs, fname)) + end exports.reset_spinner() exports.spinner() exports.iterate_log(h, start_time, end_time, callback, opts) h:close() end end - io.stderr:write("\027[J\027[G") + if isatty then + io.stderr:write("\027[J\027[G") + end else local h, open_err = exports.open_log_file(log_file) if not h then diff --git a/lualib/rspamadm/logstats.lua b/lualib/rspamadm/logstats.lua index b5749dfff3..981e3ad123 100644 --- a/lualib/rspamadm/logstats.lua +++ b/lualib/rspamadm/logstats.lua @@ -18,6 +18,16 @@ local argparse = require "argparse" local rspamd_regexp = require "rspamd_regexp" local ucl = require "ucl" local log_utils = require "lua_log_utils" +local ansicolors = require "ansicolors" + +local action_colors = { + reject = ansicolors.red, + ['add header'] = ansicolors.yellow, + ['rewrite subject'] = ansicolors.yellow, + ['soft reject'] = ansicolors.magenta, + greylist = ansicolors.cyan, + ['no action'] = ansicolors.green, +} local parser = argparse() :name "rspamadm logstats" @@ -539,13 +549,17 @@ local function handler(args) local jtp = (total_junk ~= 0) and (jh * 100.0 / total_junk) or 0 io.write(string.format( - "%s avg. weight %.3f, hits %d(%.3f%%):\n" .. - " Ham %7.3f%%, %6d/%-6d (%7.3f%%)\n" .. - " Spam %7.3f%%, %6d/%-6d (%7.3f%%)\n" .. - " Junk %7.3f%%, %6d/%-6d (%7.3f%%)\n", - s, r.weight / th, th, (th / total * 100), + "%s avg. weight %.3f, hits %d (%.3f%%):\n" .. + " %s %7.3f%%, %6d/%-6d (%7.3f%%)\n" .. + " %s %7.3f%%, %6d/%-6d (%7.3f%%)\n" .. + " %s %7.3f%%, %6d/%-6d (%7.3f%%)\n", + ansicolors.bright .. s .. ansicolors.reset, + r.weight / th, th, (th / total * 100), + ansicolors.green .. "Ham " .. ansicolors.reset, (hh / th * 100), hh, total_ham, htp, + ansicolors.red .. "Spam" .. ansicolors.reset, (sh / th * 100), sh, total_spam, stp, + ansicolors.yellow .. "Junk" .. ansicolors.reset, (jh / th * 100), jh, total_junk, jtp)) local schp = (total_spam > 0) and (r.spam_change / total_spam * 100.0) or 0 @@ -617,16 +631,19 @@ local function handler(args) if next(alpha_filtered) then io.write(string.format( - "\nWARNING: the following symbols were found but ignored" .. - " due to score < alpha_score (%.2f):\n", diff_alpha)) + "\n%s the following symbols were found but ignored" .. + " due to score < alpha_score (%.2f):\n", + ansicolors.yellow .. "WARNING:" .. ansicolors.reset, diff_alpha)) for sym, count in pairs(alpha_filtered) do - io.write(string.format(" %s: %d hit(s)\n", sym, count)) + io.write(string.format(" %s: %d hit(s)\n", + ansicolors.bright .. sym .. ansicolors.reset, count)) end io.write("Use --alpha-score 0 to include them.\n") end - io.write(string.format("\n=== Summary %s\nMessages scanned: %d", - string.rep('=', 68), total)) + io.write(string.format("\n%s\nMessages scanned: %d", + ansicolors.bright .. "=== Summary " .. string.rep('=', 68) .. ansicolors.reset, + total)) if timeStamp['start'] then io.write(string.format(" [ %s / %s ]\n", timeStamp['start'], timeStamp['end'])) else @@ -639,7 +656,10 @@ local function handler(args) end table.sort(sorted_actions) for _, a in ipairs(sorted_actions) do - io.write(string.format("%11s: %6.2f%%, %d\n", a, 100 * actions[a] / total, actions[a])) + local color = action_colors[a] or ansicolors.white + io.write(string.format("%s: %6.2f%%, %d\n", + color .. string.format("%11s", a) .. ansicolors.reset, + 100 * actions[a] / total, actions[a])) end io.write('\n') if scanTime['min'] then diff --git a/lualib/rspamadm/mapstats.lua b/lualib/rspamadm/mapstats.lua index c6f2950b96..137f80ca42 100644 --- a/lualib/rspamadm/mapstats.lua +++ b/lualib/rspamadm/mapstats.lua @@ -18,6 +18,7 @@ local argparse = require "argparse" local rspamd_regexp = require "rspamd_regexp" local rspamd_ip = require "rspamd_ip" local log_utils = require "lua_log_utils" +local ansicolors = require "ansicolors" local parser = argparse() :name "rspamadm mapstats" @@ -253,7 +254,9 @@ local function handler(args) -- Skip non-file maps if re_non_file_url:match(map_source) then - io.write(string.format("%s: %s [SKIPPED]\n", symbol, map_source)) + io.write(string.format("%s: %s %s\n", + ansicolors.bright .. symbol .. ansicolors.reset, map_source, + ansicolors.yellow .. "[SKIPPED]" .. ansicolors.reset)) goto continue_map end @@ -262,7 +265,9 @@ local function handler(args) local entries = get_map(cfg, file_path) if #entries == 0 then - io.write(string.format("%s: %s [FAILED]\n", symbol, map_source)) + io.write(string.format("%s: %s %s\n", + ansicolors.bright .. symbol .. ansicolors.reset, map_source, + ansicolors.red .. "[FAILED]" .. ansicolors.reset)) goto continue_map end @@ -272,7 +277,9 @@ local function handler(args) entry_count = entry_count + 1 end end - io.write(string.format("%s: %s [OK] - %d entries\n", symbol, map_source, entry_count)) + io.write(string.format("%s: %s %s - %d entries\n", + ansicolors.bright .. symbol .. ansicolors.reset, map_source, + ansicolors.green .. "[OK]" .. ansicolors.reset, entry_count)) table.insert(map[symbol].maps, { source = map_source, @@ -389,7 +396,7 @@ local function handler(args) -- Output results for _, symbol in ipairs(symbols_search) do - io.write(string.format("%s:\n", symbol)) + io.write(string.format("%s:\n", ansicolors.bright .. symbol .. ansicolors.reset)) io.write(string.format(" type=%s\n", map[symbol].type)) for _, map_entry in ipairs(map[symbol].maps) do @@ -408,7 +415,8 @@ local function handler(args) end if entry.count and entry.count > 0 then - io.write(string.format("\t%d", entry.count)) + io.write(string.format("\t%s", + ansicolors.green .. tostring(entry.count) .. ansicolors.reset)) else io.write("\t-") end @@ -427,7 +435,8 @@ local function handler(args) -- Unmatched report if next(unmatched) then - io.write("\nSymbols with unmatched values:\n") + io.write(string.format("\n%s\n", + ansicolors.yellow .. "Symbols with unmatched values:" .. ansicolors.reset)) io.write(string.rep('-', 80) .. '\n') local grouped = {} @@ -451,7 +460,9 @@ local function handler(args) local entries = grouped[symbol] table.sort(entries, function(a, b) return a.count > b.count end) - io.write(string.format("\n%s: %d unmatched value(s)\n", symbol, #entries)) + io.write(string.format("\n%s: %s\n", + ansicolors.bright .. symbol .. ansicolors.reset, + ansicolors.yellow .. string.format("%d unmatched value(s)", #entries) .. ansicolors.reset)) local limit = math.min(#entries, 5) for i = 1, limit do io.write(string.format(" %dx: %s\n", entries[i].count, entries[i].full))