ntopng/scripts/lua/modules/top_talkers_utils.lua

382 lines
12 KiB
Lua

--
-- (C) 2014-26 - ntop.org
--
local top_talkers_utils = {}
top_talkers_utils.MAX_NUM_ENTRIES = 10
top_talkers_utils.THRESHOLD_LOW = .05
top_talkers_utils.TOP_ENABLED_KEY = "ntopng.prefs.ifid_%i.interface_top_talkers_creation"
local OTHER_KEY = "Other"
local HIDDEN_KEY = "Hidden Hosts"
-- ########################################################
local function getTopEnabledKey(ifid)
return string.format(top_talkers_utils.TOP_ENABLED_KEY, ifid)
end
-- ########################################################
-- Per-direction bounded top: the array part holds at most MAX_NUM_ENTRIES
-- entries sorted by value (descending), 'sum' is the total of all the deltas.
-- Memory is O(MAX_NUM_ENTRIES) per VLAN instead of O(num hosts) and no full sort
-- is needed: most hosts are rejected with a single comparison.
local function newDirectionTop()
return {
n = 0,
sum = 0
}
end
-- ########################################################
local function topInsert(top, value, hostkey, hoststats)
local max_entries = top_talkers_utils.MAX_NUM_ENTRIES
local n = top.n
if n < max_entries then
n = n + 1
top.n = n
elseif value <= top[n].value then
return -- Smaller than the current smallest top entry
end
-- Insertion sort step: shift smaller entries down (the last one is dropped when full)
-- Basically what happens is the following, being the table ordered by the greater to the lowest
-- e.g. : 7 - 5 - 3 - 1
-- this algorithm starts from the end (latest position) and shift on the "right" the values
-- until it finds a greater value where it stops shifting
local i = n
while (i > 1) and (top[i - 1].value < value) do
top[i] = top[i - 1]
i = i - 1
end
top[i] = {
value = value,
hostkey = hostkey,
host = hoststats
}
end
-- ########################################################
local function makeHostRecord(entry)
local hoststats = entry.host
if hoststats == nil then
-- Pseudo entry (e.g. hidden hosts)
return {
address = entry.hostkey,
value = entry.value,
["local"] = "false"
}
end
local hostkey = entry.hostkey
local tskey = hoststats.tskey or hostkey
local name = hoststats.name
local rec = {
address = tskey,
value = entry.value,
["local"] = hoststats.localhost and "true" or "false"
}
if (name ~= nil) and (name ~= tskey) then
rec.label = name
end
if hostkey ~= tskey then
-- storing the IP address is necessary for MAC based keys to create the
-- correct URL
rec.ipaddr = hostkey
end
return rec
end
-- ########################################################
-- Converts a direction top into the list of records saved on the DB:
-- the first MAX_NUM_ENTRIES / 2 entries are always kept, the following ones
-- only if they are at least THRESHOLD_LOW of the total; the rest goes into "Other"
local function finalizeDirection(top, hidden_sum)
if hidden_sum > 0 then
topInsert(top, hidden_sum, HIDDEN_KEY, nil)
end
local total = top.sum
local min_kept = top_talkers_utils.MAX_NUM_ENTRIES / 2
local min_value = total * top_talkers_utils.THRESHOLD_LOW
local list = {}
local kept_sum = 0
for i = 1, top.n do
local entry = top[i]
-- Entries are sorted, once one is discarded all the following ones are too
if (i > min_kept) and (entry.value < min_value) then
break
end
list[i] = makeHostRecord(entry)
kept_sum = kept_sum + entry.value
end
local other = total - kept_sum
if other > 0 then
list[#list + 1] = {
address = OTHER_KEY,
value = other,
["local"] = "false"
}
end
return list
end
-- ########################################################
function top_talkers_utils.areTopEnabled(ifid)
local k = getTopEnabledKey(ifid)
return not (ntop.getPref(k) == "false")
end
-- ########################################################
function top_talkers_utils.disableTop(ifid)
local k = getTopEnabledKey(ifid)
ntop.setPref(k, "false")
end
-- ########################################################
function top_talkers_utils.enableTop(ifid)
local k = getTopEnabledKey(ifid)
ntop.delCache(k)
end
-- ########################################################
-- Computes the top talkers of the last minute (since the previous checkpoint)
-- in a single pass over the hosts, keeping only the top MAX_NUM_ENTRIES per
-- VLAN and direction. Returns the JSON to be saved on the DB.
function top_talkers_utils.makeTopJson(_ifname, ifid)
require "ntop_utils"
if not top_talkers_utils.areTopEnabled(ifid) then
return nil
end
-- Include here otherwise it's a useless waste of memory
local json = require "dkjson"
local getBatchedHostsInfo = interface.getBatchedHostsInfo
local isDeadlineApproaching = ntop.isDeadlineApproaching
local isShuttingDown = ntop.isShuttingDown
local vlans = {}
local next_slot = 0
local num_hosts = 0
local in_time = true
interface.select(_ifname)
-- Iterate the hosts batches directly (no per-host closure/iterator), the
-- deadline is checked once per batch.
-- NOTE: hosts are checkpointed (delta reset) by the C core when fetched
repeat
if isShuttingDown() then
break
end
if isDeadlineApproaching() then
in_time = false
break
end
local batch = getBatchedHostsInfo(next_slot, false --[[ no details ]])
if batch == nil then
break
end
for hostkey, hoststats in pairs(batch.hosts or {}) do
local delta = hoststats.delta
if delta ~= nil then
local vlan_id = hoststats.vlan or 0
local v = vlans[vlan_id]
local sent = delta.sent or 0
local rcvd = delta.rcvd or 0
if v == nil then
v = {
sent = newDirectionTop(),
rcvd = newDirectionTop(),
hidden_sent = 0,
hidden_rcvd = 0
}
vlans[vlan_id] = v
end
if sent > 0 then
local top = v.sent
top.sum = top.sum + sent
if hoststats.hiddenFromTop then
v.hidden_sent = v.hidden_sent + sent
else
topInsert(top, sent, hostkey, hoststats)
end
end
if rcvd > 0 then
local top = v.rcvd
top.sum = top.sum + rcvd
if hoststats.hiddenFromTop then
v.hidden_rcvd = v.hidden_rcvd + rcvd
else
topInsert(top, rcvd, hostkey, hoststats)
end
end
end
num_hosts = num_hosts + 1
end
next_slot = batch.nextSlot
until (next_slot == nil) or (next_slot == 0)
if not in_time then
require "lua_trace"
local deadline = ntop.getDeadline()
local remaining = deadline - os.time()
if num_hosts == 0 then
-- Nothing processed: the script started (almost) at its deadline,
-- i.e. it was waiting in the periodic activities queue
traceError(TRACE_ERROR, TRACE_CONSOLE,
string.format("[%s] Top talkers not computed: the periodic script started too late " ..
"(%d sec before its deadline). Are the periodic activities overloaded?",
_ifname, remaining))
else
traceError(TRACE_ERROR, TRACE_CONSOLE,
string.format("[%s] Cannot complete top talkers generation in 1 minute " ..
"(processed %d hosts, %d sec before the deadline). Is there a huge number of hosts in the system?",
_ifname, num_hosts, remaining))
end
end
local res = {}
for vlan_id, v in pairs(vlans) do
local total = v.sent.sum + v.rcvd.sum
-- Skip VLANs without traffic in the last minute
if total > 0 then
res[#res + 1] = {
hosts = {{
senders = finalizeDirection(v.sent, v.hidden_sent),
receivers = finalizeDirection(v.rcvd, v.hidden_rcvd)
}},
value = total,
address = tostring(vlan_id)
}
end
end
return json.encode({
vlan = res
})
end
-- ########################################################
-- Computes label and url during visualization
function top_talkers_utils.enrichRecordInformation(class_key, rec, show_vlan)
require "ntop_utils"
require "label_utils"
local url = ""
local label = rec.label or rec.address
local url_target = rec.address
if (rec.address ~= "Other") and (rec.address ~= "Hidden Hosts") then
if class_key == "hosts" then
local address = rec.address
if ends(address, "_v4") or ends(address, "_v6") then
rec.visual_addr = visualTsKey(address)
address = string.sub(address, 1, string.len(address) - 3)
if (rec.ipaddr ~= nil) then
url_target = rec.ipaddr
end
end
url_target = url_target .. "&tskey=" .. rec.address
url = ntop.getHttpPrefix() .. '/lua/host_details.lua?always_show_hist=true&host='
-- Use the host alias as label, if set
local alt_name = ip2label(address, 0, 100)
if not isEmptyString(alt_name) and (alt_name ~= address) then
label = alt_name
else
local hinfo = hostkey2hostinfo(address)
if not show_vlan then
hinfo.vlan = 0
end
alt_name = hostinfo2label(hinfo)
if not isEmptyString(alt_name) and (alt_name ~= address) then
label = alt_name
else
label = address
end
end
elseif class_key == "asn" then
url = ntop.getHttpPrefix() .. '/lua/hosts_stats.lua?asn='
elseif class_key == "networks" then
url = ntop.getHttpPrefix() .. '/lua/hosts_stats.lua?network='
local network_name = nil
if rec.address == "-1" then
network_name = i18n("remote_networks")
else
network_name = getFullLocalNetworkName(ntop.getNetworkNameById(tonumber(rec.address)))
end
if not isEmptyString(network_name) then
label = network_name
end
elseif class_key:contains("os") then
url = ntop.getHttpPrefix() .. '/lua/hosts_stats.lua?os='
end
if not isEmptyString(url) then
url = url .. url_target
end
end
-- Update record information
rec.url = url
rec.label = label
end
-- ########################################################
function top_talkers_utils.enrichVlanInformation(vlan_tbl)
local vlan_id = ternary(vlan_tbl.address, tostring(vlan_tbl.address), "0")
vlan_tbl["label"] = vlan_id
vlan_tbl["name"] = vlan_id
vlan_tbl["url"] = ntop.getHttpPrefix() .. '/lua/hosts_stats.lua?vlan=' .. vlan_id
end
-- ########################################################
return top_talkers_utils