aboutsummaryrefslogtreecommitdiffstats
path: root/lualib/rspamadm/fuzzy_stat.lua
blob: 011f9d37cb9c3e57cf338a1b8712872c96d33e51 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
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 = {}
          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 = {}
      end
      -- Ratelimit is passed as {cur = count, last = time}
      target.ratelimit.cur = v
    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), os.date('%c', key_stat.ratelimit.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