Скрипт-счетчик уникальных устройств и IP для HAProxy с учетом geo и доменов
- Считаем количество уникальных устройств (
x-device) и уникальных пар IP+география (ip-geo) внутри окна 10 минут. - Работаем только для разрешённых доменов: api1.rts.ru, api1.tld.ru, api1.center.ru, service1.lines.com. Для других — счетчики не увеличиваются.
- Исключаем некоторые эндпоинты и хосты (
webhooks,/3.0/fb/api/,hotels,/3.0/voice,rst.api1.ff.ru). - Разделяем обработку на два режима: обычный и “fly” для определённых стран (например, RU, CN, KZ и др. — см.
ignore_countries). - В “fly” режиме отдельное хранение данных и счетчики.
- Для каждой пары (IP+geo) ведёт ассоциативный массив устройств с меткой времени и локалью; для каждого устройства — аналогичным образом ассоциативный массив IP+geo.
- Если количество уникальных ключей в одной таблице превышает 25 000, таблица для этого сегмента очищается.
- Для каждого сегмента считает: всего уникальных устройств, устройств с ru-локалью, устройств с не-ru-локалью; аналогично для уникальных IP.
- Все счетчики пишутся в переменные HAProxy
txn:set_varдля дальнейшей обработки или логирования. - При нехватке обязательных данных (ip, geo, x-device) — все счетчики = 0.
Назначение: собирать в разрезе домена, географии и локали информацию о количестве уникальных устройств и IP за последние 10 минут, разделяя обычные и “fly” страны, для целей мониторинга, анализа аномалий и противодействия фроду.
lua-load count_uniq_xdevice_and_ip.lua
#count_uniq_x-device_ip
http-request lua.count_uniq_xdevice_and_ip if { req.hdr(x-device) -m found }
local ttl = 600
local MAX_KEYS_PER_TABLE = 25000
local ignore_countries = {
CN=true, TH=true, AE=true, KG=true, KZ=true,
UZ=true, TJ=true, AM=true, TR=true, TM=true,
AZ=true, RU=true, BY=true
}
local allowed_domains = {
["api1.rts.ru"]=true,
["api1.tld.ru"]=true,
["api1.center.ru"]=true,
["service1.lines.com"]=true
}
local function is_non_empty(val)
return val and val:match("^%s*$") == nil
end
local function is_allowed_host(host)
if not host then return false end
host = host:lower()
for dom in pairs(allowed_domains) do
if host:sub(-#dom) == dom then
return true
end
end
return false
end
local function is_excluded(host, path)
if not host then host = "" end
if not path then path = "" end
host, path = host:lower(), path:lower()
return (
path:find("webhooks", 1, true) or
path:sub(1, #"/3.0/fb/api/") == "/3.0/fb/api/" or
path:find("hotels", 1, true) or
path:sub(1, #"/3.0/voice") == "/3.0/voice" or
host == "rst.api1.ff.ru"
)
end
core.register_init(function()
devices_by_ipgeo = {}
ips_by_device = {}
fly_devices_by_ipgeo = {}
fly_ips_by_device = {}
end)
local function table_size(tbl)
local count = 0
for _ in pairs(tbl) do count = count + 1 end
return count
end
core.register_action("count_uniq_xdevice_and_ip", {"http-req"}, function(txn)
local xff = txn.sf:req_hdr("X-Real-IP")
local ip = txn.sf:req_hdr("X-Real-IP")
local geo = (txn.sf:req_hdr("ip-geo") or ""):upper()
local device = txn.sf:req_hdr("x-device")
local host = txn.sf:req_hdr("host")
local path = txn.sf:path()
local xlocale = txn.sf:req_hdr("x-locale") or ""
local xlocale_lower = xlocale:lower()
local ipgeo = ip .. "_" .. geo
local now = os.time()
txn:set_var("req.xff_chain", xff or "")
local all_vars = {
"uniq_xdevice_count", "uniq_ip_count", "uniq_xdevice_count_fly", "uniq_ip_count_fly",
"uniq_xdevice_count_not_lan_ru", "uniq_ip_count_not_lan_ru",
"uniq_xdevice_count_fly_not_lan_ru", "uniq_ip_count_fly_not_lan_ru",
"uniq_xdevice_count_lan_ru", "uniq_ip_count_lan_ru",
"uniq_xdevice_count_fly_lan_ru", "uniq_ip_count_fly_lan_ru",
"dev_table_size", "ip_table_size"
}
if not is_non_empty(ip) or not is_non_empty(geo) or not is_non_empty(device) then
for _,v in ipairs(all_vars) do txn:set_var("req."..v, 0) end
return
end
if not is_allowed_host(host) then
for _,v in ipairs(all_vars) do txn:set_var("req."..v, 0) end
return
end
if is_excluded(host, path) then
for _,v in ipairs(all_vars) do txn:set_var("req."..v, 0) end
return
end
local mode = ignore_countries[geo] and "fly" or "common"
-- --- devices_by_ipgeo ---
local devtbl
if mode == "fly" then
fly_devices_by_ipgeo[ipgeo] = fly_devices_by_ipgeo[ipgeo] or {}
devtbl = fly_devices_by_ipgeo[ipgeo]
else
devices_by_ipgeo[ipgeo] = devices_by_ipgeo[ipgeo] or {}
devtbl = devices_by_ipgeo[ipgeo]
end
for d, info in pairs(devtbl) do
local ts = (type(info)=="table" and info[1]) or info
if now - ts > ttl then
devtbl[d] = nil
end
end
if table_size(devtbl) >= MAX_KEYS_PER_TABLE then
devtbl = {}
if mode == "fly" then fly_devices_by_ipgeo[ipgeo] = devtbl else devices_by_ipgeo[ipgeo] = devtbl end
end
devtbl[device] = {now, xlocale_lower}
txn:set_var("req.dev_table_size", table_size(devtbl))
-- --- ips_by_device ---
local iptbl
if mode == "fly" then
fly_ips_by_device[device] = fly_ips_by_device[device] or {}
iptbl = fly_ips_by_device[device]
else
ips_by_device[device] = ips_by_device[device] or {}
iptbl = ips_by_device[device]
end
for ipg, info in pairs(iptbl) do
local ts = (type(info)=="table" and info[1]) or info
if now - ts > ttl then
iptbl[ipg] = nil
end
end
if table_size(iptbl) >= MAX_KEYS_PER_TABLE then
iptbl = {}
if mode == "fly" then fly_ips_by_device[device] = iptbl else ips_by_device[device] = iptbl end
end
iptbl[ipgeo] = {now, xlocale_lower}
txn:set_var("req.ip_table_size", table_size(iptbl))
-- ---- UNIQUE COUNTS: device ----
local count_all = 0
local count_lan_ru = 0
local count_not_lan_ru = 0
for _, info in pairs(devtbl) do
local ts, loc = info[1], info[2]
if now - ts <= ttl then
count_all = count_all + 1
if loc == "ru" then count_lan_ru = count_lan_ru + 1
elseif loc ~= "" then count_not_lan_ru = count_not_lan_ru + 1 end
end
end
-- ---- UNIQUE COUNTS: ip ----
local countip_all = 0
local countip_lan_ru = 0
local countip_not_lan_ru = 0
for _, info in pairs(iptbl) do
local ts, loc = info[1], info[2]
if now - ts <= ttl then
countip_all = countip_all + 1
if loc == "ru" then countip_lan_ru = countip_lan_ru + 1
elseif loc ~= "" then countip_not_lan_ru = countip_not_lan_ru + 1 end
end
end
if mode == "fly" then
txn:set_var("req.uniq_xdevice_count_fly", count_all)
txn:set_var("req.uniq_xdevice_count_fly_lan_ru", count_lan_ru)
txn:set_var("req.uniq_xdevice_count_fly_not_lan_ru", count_not_lan_ru)
txn:set_var("req.uniq_ip_count_fly", countip_all)
txn:set_var("req.uniq_ip_count_fly_lan_ru", countip_lan_ru)
txn:set_var("req.uniq_ip_count_fly_not_lan_ru", countip_not_lan_ru)
txn:set_var("req.uniq_xdevice_count", 0)
txn:set_var("req.uniq_xdevice_count_lan_ru", 0)
txn:set_var("req.uniq_xdevice_count_not_lan_ru", 0)
txn:set_var("req.uniq_ip_count", 0)
txn:set_var("req.uniq_ip_count_lan_ru", 0)
txn:set_var("req.uniq_ip_count_not_lan_ru", 0)
else
txn:set_var("req.uniq_xdevice_count", count_all)
txn:set_var("req.uniq_xdevice_count_lan_ru", count_lan_ru)
txn:set_var("req.uniq_xdevice_count_not_lan_ru", count_not_lan_ru)
txn:set_var("req.uniq_ip_count", countip_all)
txn:set_var("req.uniq_ip_count_lan_ru", countip_lan_ru)
txn:set_var("req.uniq_ip_count_not_lan_ru", countip_not_lan_ru)
txn:set_var("req.uniq_xdevice_count_fly", 0)
txn:set_var("req.uniq_xdevice_count_fly_lan_ru", 0)
txn:set_var("req.uniq_xdevice_count_fly_not_lan_ru", 0)
txn:set_var("req.uniq_ip_count_fly", 0)
txn:set_var("req.uniq_ip_count_fly_lan_ru", 0)
txn:set_var("req.uniq_ip_count_fly_not_lan_ru", 0)
end
end)