File: //usr/share/rspamd/lualib/rspamadm/fuzzy_stat.lua
local rspamd_util = require "rspamd_util"
local lua_util = require "lua_util"
local opts = {}
local argparse = require "argparse"
local parser = argparse()
:name "rspamadm control fuzzystat"
:description "Shows help for the specified configuration options"
:help_description_margin(32)
parser:flag "--no-ips"
:description "No IPs stats"
parser:flag "--no-keys"
:description "No keys stats"
parser:flag "--short"
:description "Short output mode"
parser:flag "-n --number"
:description "Disable numbers humanization"
parser:option "--sort"
:description "Sort order"
:convert {
checked = "checked",
matched = "matched",
errors = "errors",
name = "name"
}
local function add_data(target, src)
for k, v in pairs(src) do
if type(v) == 'number' then
if target[k] then
target[k] = target[k] + v
else
target[k] = v
end
elseif k == 'ips' then
if not target['ips'] then
target['ips'] = {}
end
-- Iterate over IPs
for ip, st in pairs(v) do
if not target['ips'][ip] then
target['ips'][ip] = {}
end
add_data(target['ips'][ip], st)
end
elseif k == 'flags' then
if not target['flags'] then
target['flags'] = {}
end
-- Iterate over Flags
for flag, st in pairs(v) do
if not target['flags'][flag] then
target['flags'][flag] = {}
end
add_data(target['flags'][flag], st)
end
elseif k == 'keypair' then
if type(v.extensions) == 'table' then
if type(v.extensions.name) == 'string' then
target.name = v.extensions.name
end
if type(v.extensions.email) == 'string' then
target.email = v.extensions.email
end
if type(v.extensions.ratelimit) == 'table' then
if not target.ratelimit then
target.ratelimit = {
cur = {
last = 0,
count = 0
},
}
end
-- Passed as {burst = x, rate = y}
target.ratelimit.limit = v.extensions.ratelimit
end
end
elseif k == 'ratelimit' then
if not target.ratelimit then
target.ratelimit = {
cur = {
last = 0,
count = 0
},
}
end
-- Ratelimit is passed as {cur = count, last = time}
target.ratelimit.cur.count = v.cur + target.ratelimit.cur.count
target.ratelimit.cur.last = math.max(v.last, target.ratelimit.cur.last)
end
end
end
local function print_num(num)
if num then
if opts['n'] or opts['number'] then
return tostring(num)
else
return rspamd_util.humanize_number(num)
end
else
return 'na'
end
end
local function print_stat(st, tabs)
if st['checked'] then
if st.checked_per_hour then
print(string.format('%sChecked: %s (%s per hour in average)', tabs,
print_num(st['checked']), print_num(st['checked_per_hour'])))
else
print(string.format('%sChecked: %s', tabs, print_num(st['checked'])))
end
end
if st['matched'] then
if st.checked and st.checked > 0 and st.checked <= st.matched then
local percentage = st.matched / st.checked * 100.0
if st.matched_per_hour then
print(string.format('%sMatched: %s - %s percent (%s per hour in average)', tabs,
print_num(st['matched']), percentage, print_num(st['matched_per_hour'])))
else
print(string.format('%sMatched: %s - %s percent', tabs, print_num(st['matched']), percentage))
end
else
if st.matched_per_hour then
print(string.format('%sMatched: %s (%s per hour in average)', tabs,
print_num(st['matched']), print_num(st['matched_per_hour'])))
else
print(string.format('%sMatched: %s', tabs, print_num(st['matched'])))
end
end
end
if st['errors'] then
print(string.format('%sErrors: %s', tabs, print_num(st['errors'])))
end
if st['added'] then
print(string.format('%sAdded: %s', tabs, print_num(st['added'])))
end
if st['deleted'] then
print(string.format('%sDeleted: %s', tabs, print_num(st['deleted'])))
end
end
-- Sort by checked
local function sort_hash_table(tbl, sort_opts, key_key)
local res = {}
for k, v in pairs(tbl) do
table.insert(res, { [key_key] = k, data = v })
end
local function sort_order(elt)
local key = 'checked'
local sort_res = 0
if sort_opts['sort'] then
if sort_opts['sort'] == 'matched' then
key = 'matched'
elseif sort_opts['sort'] == 'errors' then
key = 'errors'
elseif sort_opts['sort'] == 'name' then
return elt[key_key]
end
end
if elt.data[key] then
sort_res = elt.data[key]
end
return sort_res
end
table.sort(res, function(a, b)
return sort_order(a) > sort_order(b)
end)
return res
end
local function add_result(dst, src, k)
if type(src) == 'table' then
if type(dst) == 'number' then
-- Convert dst to table
dst = { dst }
elseif type(dst) == 'nil' then
dst = {}
end
for i, v in ipairs(src) do
if dst[i] and k ~= 'fuzzy_stored' then
dst[i] = dst[i] + v
else
dst[i] = v
end
end
else
if type(dst) == 'table' then
if k ~= 'fuzzy_stored' then
dst[1] = dst[1] + src
else
dst[1] = src
end
else
if dst and k ~= 'fuzzy_stored' then
dst = dst + src
else
dst = src
end
end
end
return dst
end
local function print_result(r)
local function num_to_epoch(num)
if num == 1 then
return 'v0.6'
elseif num == 2 then
return 'v0.8'
elseif num == 3 then
return 'v0.9'
elseif num == 4 then
return 'v1.0+'
elseif num == 5 then
return 'v1.7+'
end
return '???'
end
if type(r) == 'table' then
local res = {}
for i, num in ipairs(r) do
res[i] = string.format('(%s: %s)', num_to_epoch(i), print_num(num))
end
return table.concat(res, ', ')
end
return print_num(r)
end
return function(args, res)
local res_ips = {}
local res_databases = {}
local wrk = res['workers']
opts = parser:parse(args)
if wrk then
for _, pr in pairs(wrk) do
-- processes cycle
if pr['data'] then
local id = pr['id']
if id then
local res_db = res_databases[id]
if not res_db then
res_db = {
keys = {}
}
res_databases[id] = res_db
end
-- General stats
for k, v in pairs(pr['data']) do
if k ~= 'keys' and k ~= 'errors_ips' then
res_db[k] = add_result(res_db[k], v, k)
elseif k == 'errors_ips' then
-- Errors ips
if not res_db['errors_ips'] then
res_db['errors_ips'] = {}
end
for ip, nerrors in pairs(v) do
if not res_db['errors_ips'][ip] then
res_db['errors_ips'][ip] = nerrors
else
res_db['errors_ips'][ip] = nerrors + res_db['errors_ips'][ip]
end
end
end
end
if pr['data']['keys'] then
local res_keys = res_db['keys']
if not res_keys then
res_keys = {}
res_db['keys'] = res_keys
end
-- Go through keys in input
for k, elts in pairs(pr['data']['keys']) do
-- keys cycle
if not res_keys[k] then
res_keys[k] = {}
end
add_data(res_keys[k], elts)
if elts['ips'] then
for ip, v in pairs(elts['ips']) do
if not res_ips[ip] then
res_ips[ip] = {}
end
add_data(res_ips[ip], v)
end
end
end
end
end
end
end
end
-- General stats
for db, st in pairs(res_databases) do
print(string.format('Statistics for storage %s', db))
for k, v in pairs(st) do
if k ~= 'keys' and k ~= 'errors_ips' then
print(string.format('%s: %s', k, print_result(v)))
end
end
print('')
local res_keys = st['keys']
if res_keys and not opts['no_keys'] and not opts['short'] then
print('Keys statistics:')
-- Convert into an array to allow sorting
local sorted_keys = sort_hash_table(res_keys, opts, 'key')
for _, key in ipairs(sorted_keys) do
local key_stat = key.data
if key_stat.name then
print(string.format('Key id: %s, name: %s (email: %s)', key.key, key_stat.name,
key_stat.email or 'unknown'))
else
print(string.format('Key id: %s', key.key))
end
print_stat(key_stat, '\t')
if key_stat['ips'] and not opts['no_ips'] then
print('')
print('\tIPs stat:')
local sorted_ips = sort_hash_table(key_stat['ips'], opts, 'ip')
for _, v in ipairs(sorted_ips) do
print(string.format('\t%s', v['ip']))
print_stat(v['data'], '\t\t')
print('')
end
end
if key_stat.flags then
print('')
print('\tFlags stat:')
for flag, v in pairs(key_stat.flags) do
print(string.format('\t[%s]:', flag))
-- Remove irrelevant fields
v.checked = nil
print_stat(v, '\t\t')
print('')
end
end
if key_stat.ratelimit then
print('')
print('\tRatelimit stat:')
print(string.format('\tLimit: %s (%.2f per hour leak rate)',
print_num(key_stat.ratelimit.limit.burst), (key_stat.ratelimit.limit.rate or 0.0) * 3600))
print(string.format('\tCurrent: %s (%s last)',
print_num(key_stat.ratelimit.cur.count), os.date('%c', key_stat.ratelimit.cur.last)))
print('')
end
print('')
end
end
if st['errors_ips'] and not opts['no_ips'] and not opts['short'] then
print('')
print('Errors IPs statistics:')
local ip_stat = st['errors_ips']
local ips = lua_util.keys(ip_stat)
-- Reverse sort by number of errors
table.sort(ips, function(a, b)
return ip_stat[a] > ip_stat[b]
end)
for _, ip in ipairs(ips) do
print(string.format('%s: %s', ip, print_result(ip_stat[ip])))
end
print('')
end
end
if not opts['no_ips'] and not opts['short'] then
print('')
print('IPs statistics:')
local sorted_ips = sort_hash_table(res_ips, opts, 'ip')
for _, v in ipairs(sorted_ips) do
print(string.format('%s', v['ip']))
print_stat(v['data'], '\t')
print('')
end
end
end