2022-10-16 12:44:04 -04:00
2026-07-02 01:54:31 -04:00
--[[
webstats_common.lua - WebStats 统 计 系 统 核 心 模 块
==================================================
本 模 块 提 供 WebStats 系 统 的 核 心 功 能 , 包 括 :
- 数 据 库 连 接 管 理
- 日 志 数 据 处 理 与 存 储
- 定 时 任 务 调 度
- 爬 虫 / 客 户 端 识 别
- IP / URL 统 计
架 构 说 明 :
1. 单 例 模 式 : 通 过 getInstance ( ) 获 取 全 局 唯 一 实 例
2026-07-06 14:12:09 -04:00
2. 共 享 内 存 : webstats_queue 日 志 队 列 , webstats_cache UV / IP / 域 名 等 缓 存
2026-07-02 01:54:31 -04:00
3. SQLite 数 据 库 : 每 个 站 点 独 立 一 个 数 据 库 文 件
2026-07-04 15:36:26 -04:00
4. 定 时 任 务 : 每 0.5 秒 执 行 一 次 日 志 持 久 化
2026-07-02 01:54:31 -04:00
主 要 数 据 流 向 :
2026-07-06 14:12:09 -04:00
webstats_log.lua ( 日 志 采 集 ) -> webstats_queue ( 日 志 队 列 ) -> cron ( ) ( 定 时 处 理 ) -> SQLite ( 持 久 化 )
2026-07-02 01:54:31 -04:00
依 赖 模 块 :
- cjson : JSON 编 解 码
- lsqlite3 : SQLite 数 据 库 操 作
- webstats_config : 全 局 配 置
- webstats_sites : 站 点 配 置
] ]
2022-10-16 12:44:04 -04:00
local setmetatable = setmetatable
2026-07-04 13:16:02 -04:00
local _M = { _VERSION = ' 0.2.5 ' }
2022-10-16 12:44:04 -04:00
local mt = { __index = _M }
2026-07-02 01:54:31 -04:00
-- 依赖模块
2022-10-16 12:44:04 -04:00
local json = require " cjson "
local sqlite3 = require " lsqlite3 "
local config = require " webstats_config "
local sites = require " webstats_sites "
2026-07-02 01:54:31 -04:00
-- 调试模式开关
2022-10-16 12:44:04 -04:00
local debug_mode = true
2026-07-02 01:54:31 -04:00
-- 共享内存队列键名
2022-10-16 12:44:04 -04:00
local total_key = " log_kv_total "
2022-10-17 09:12:42 -04:00
2026-07-02 01:54:31 -04:00
-- 未匹配到站点时使用的默认名称
2022-10-17 09:12:42 -04:00
local unset_server_name = " unset "
2026-07-02 01:54:31 -04:00
-- 日志 ID 最大值,超过后重置
2022-10-17 09:12:42 -04:00
local max_log_id = 99999999999999
2026-07-06 14:12:09 -04:00
-- Nginx 共享内存:队列与缓存分离
local queue = ngx.shared . webstats_queue
local cache = ngx.shared . webstats_cache
2022-10-17 09:12:42 -04:00
2026-07-02 01:54:31 -04:00
-- 当前日期(日)
2022-10-16 12:44:04 -04:00
local day = os.date ( " %d " )
local number_day = tonumber ( day )
2026-07-02 01:54:31 -04:00
-- 数据库列名: day + 日期(如 day01)
local day_column = " day " .. number_day
-- 数据库列名: flow + 日期(如 flow01)
local flow_column = " flow " .. number_day
2022-10-17 09:12:42 -04:00
2026-07-02 02:22:10 -04:00
-- 当前站点的配置(模块级变量,供多个函数共享)
local auto_config = nil
2026-07-02 02:30:04 -04:00
-- 当前日期(用于更新日志文件)
local today = ngx.re . gsub ( ngx.today ( ) , ' - ' , ' ' )
2026-07-02 01:54:31 -04:00
--[[
创 建 模 块 实 例
@ return table 模 块 实 例 对 象
] ]
2022-10-16 12:44:04 -04:00
function _M . new ( self )
local self = {
2026-07-04 13:16:02 -04:00
app_dir = " " , ---
2026-07-02 01:54:31 -04:00
total_key = total_key , -- 共享内存队列键名
params = nil , -- 请求参数
site_config = nil , -- 站点配置
config = nil , -- 全局配置
2022-10-16 12:44:04 -04:00
}
return setmetatable ( self , mt )
end
2026-07-02 01:54:31 -04:00
--[[
获 取 单 例 实 例 ( 懒 加 载 )
首 次 调 用 时 创 建 实 例 并 启 动 定 时 任 务
@ return table 全 局 唯 一 的 模 块 实 例
] ]
2022-10-16 12:44:04 -04:00
function _M . getInstance ( self )
2023-02-11 12:02:19 -05:00
if self.instance == nil then
self.instance = self : new ( )
2022-10-16 12:44:04 -04:00
end
assert ( self.instance ~= nil )
return self.instance
end
2026-07-04 13:16:02 -04:00
function _M . start_cron ( self )
2026-07-04 15:58:34 -04:00
if ngx.worker . id ( ) ~= 0 then
return
end
2026-07-04 13:16:02 -04:00
self : cron ( )
end
function _M . setAppDir ( self , dir )
self.app_dir = dir
end
2022-10-16 12:44:04 -04:00
2026-07-02 01:54:31 -04:00
--[[
初 始 化 SQLite 数 据 库 连 接
@ param input_sn string 站 点 名 称
@ return table | nil SQLite 数 据 库 对 象 , 失 败 返 回 nil
SQLite 优 化 配 置 :
- synchronous = 0 : 关 闭 同 步 写 入 , 提 升 性 能 ( 可 能 丢 失 数 据 )
- cache_size = 8000 : 设 置 页 缓 存 大 小 为 8000 页
- page_size = 32768 : 设 置 页 大 小 为 32 KB
- journal_mode = wal : 使 用 WAL 模 式 , 支 持 并 发 读 写
- journal_size_limit = 20 GB : WAL 文 件 大 小 限 制
] ]
2022-10-16 12:44:04 -04:00
function _M . initDB ( self , input_sn )
2026-07-06 07:10:17 -04:00
local log_dir = self.app_dir .. " /logs "
2022-10-16 12:44:04 -04:00
local path = log_dir .. ' / ' .. input_sn .. " /logs.db "
2026-07-02 00:52:05 -04:00
local db , err = sqlite3.open ( path )
2022-10-17 00:17:37 -04:00
if err then
2026-07-06 07:11:48 -04:00
self : D ( " initDB failed for " .. input_sn .. " : " .. tostring ( err ) )
2022-10-17 00:17:37 -04:00
return nil
end
2022-10-16 12:44:04 -04:00
2022-10-16 23:36:29 -04:00
db : exec ( [[PRAGMA synchronous = 0]] )
2022-10-17 13:20:08 -04:00
db : exec ( [[PRAGMA cache_size = 8000]] )
db : exec ( [[PRAGMA page_size = 32768]] )
2022-10-16 23:36:29 -04:00
db : exec ( [[PRAGMA journal_mode = wal]] )
2023-11-20 10:40:07 -05:00
db : exec ( [[PRAGMA journal_size_limit = 21474836480]] )
2026-07-04 15:56:13 -04:00
db : exec ( [[PRAGMA busy_timeout = 5000]] )
2022-10-16 12:44:04 -04:00
return db
end
2026-07-02 01:54:31 -04:00
--[[
获 取 共 享 内 存 队 列 键 名
@ return string 队 列 键 名
] ]
2022-10-16 12:44:04 -04:00
function _M . getTotalKey ( self )
return self.total_key
end
2026-07-02 01:54:31 -04:00
--[[
JSON 编 码
@ param msg table 待 编 码 的 数 据
@ return string JSON 字 符 串
] ]
2022-11-28 13:58:52 -05:00
function _M . to_json ( self , msg )
return json.encode ( msg )
end
2026-07-02 01:54:31 -04:00
--[[
设 置 配 置 数 据
@ param config table 全 局 配 置
@ param site_config table 站 点 配 置
] ]
function _M . setConfData ( self , config , site_config )
2022-10-16 12:44:04 -04:00
self.config = config
self.site_config = site_config
end
2026-07-02 01:54:31 -04:00
--[[
设 置 请 求 参 数
@ param params table 请 求 参 数
] ]
function _M . setParams ( self , params )
2022-10-16 12:44:04 -04:00
self.params = params
end
2026-07-02 01:54:31 -04:00
--[[
设 置 输 入 站 点 名 称 并 获 取 合 并 后 的 配 置
将 站 点 配 置 与 全 局 配 置 合 并 , 站 点 配 置 优 先 级 更 高
@ param input_sn string 站 点 名 称
@ return table 合 并 后 的 配 置
] ]
2022-10-17 09:12:42 -04:00
function _M . setInputSn ( self , input_sn )
local global_config = config [ " global " ]
if config [ input_sn ] == nil then
auto_config = global_config
else
2023-01-14 00:46:59 -05:00
auto_config = config [ input_sn ]
2026-07-02 01:54:31 -04:00
-- 合并全局配置到站点配置(站点配置优先)
2022-10-17 09:12:42 -04:00
for k , v in pairs ( global_config ) do
if auto_config [ k ] == nil then
auto_config [ k ] = v
end
end
end
return auto_config
end
2026-07-02 01:54:31 -04:00
--[[
获 取 当 前 请 求 的 域 名
@ return string 域 名 , 失 败 返 回 " unknown "
] ]
2022-10-17 09:12:42 -04:00
function _M . get_domain ( self )
2026-07-04 13:16:02 -04:00
local domain = ngx.var . host
2022-11-30 23:43:31 -05:00
if domain == nil then
2022-10-17 09:12:42 -04:00
domain = " unknown "
end
return domain
end
2026-07-02 01:54:31 -04:00
--[[
字 符 串 分 割 函 数
@ param str string | table 待 分 割 的 字 符 串 或 已 分 割 的 表
@ param reps string 分 隔 符
@ return table 分 割 后 的 数 组
注 意 : 如 果 输 入 已 经 是 table 类 型 , 直 接 返 回 原 表 ( 用 于 处 理 反 向 代 理 传 递 的 数 据 )
] ]
2022-10-16 12:44:04 -04:00
function _M . split ( self , str , reps )
2023-11-17 13:40:03 -05:00
if " table " == type ( str ) then
2023-11-17 13:36:18 -05:00
return str
end
2026-07-02 01:54:31 -04:00
local arr = { }
local pattern = " [^ " .. reps .. " ]+ "
for w in string.gmatch ( str , pattern ) do
arr [ # arr + 1 ] = w
end
2022-10-16 12:44:04 -04:00
return arr
end
2026-07-02 01:54:31 -04:00
--[[
获 取 数 组 长 度
@ param arr table 数 组
@ return number 数 组 长 度 , 空 或 nil 返 回 0
] ]
2022-10-17 09:12:42 -04:00
function _M . arrlen ( self , arr )
if not arr then return 0 end
2026-07-02 01:54:31 -04:00
return # arr
2022-10-17 09:12:42 -04:00
end
2026-07-02 01:54:31 -04:00
--[[
验 证 IPv4 地 址 格 式
@ param client_ip string IP 地 址
@ return boolean 是 否 为 有 效 的 IPv4 地 址
] ]
2022-10-17 09:12:42 -04:00
function _M . is_ipaddr ( self , client_ip )
2026-07-02 01:54:31 -04:00
if not client_ip then return false end
local parts = self : split ( client_ip , ' . ' )
if # parts ~= 4 then return false end
for i = 1 , 4 do
local ipv = tonumber ( parts [ i ] )
if not ipv or ipv < 0 or ipv > 255 then return false end
2022-10-17 09:12:42 -04:00
end
return true
end
2026-07-02 01:54:31 -04:00
--[[
根 据 域 名 / 服 务 器 名 获 取 站 点 名 称 ( 带 缓 存 )
匹 配 规 则 :
1. 精 确 匹 配 站 点 名 称
2. 精 确 匹 配 站 点 域 名 列 表
3. 通 配 符 匹 配 ( 支 持 * 通 配 符 )
@ param input_sn string 输 入 的 域 名 或 服 务 器 名
@ return string 匹 配 到 的 站 点 名 称 , 未 匹 配 返 回 " unset "
缓 存 策 略 : 匹 配 结 果 缓 存 24 小 时 ( 86400 秒 )
] ]
2022-10-17 09:12:42 -04:00
function _M . get_sn ( self , input_sn )
2026-07-02 01:54:31 -04:00
-- 优先从缓存获取
2022-10-17 09:12:42 -04:00
local dst_name = cache : get ( input_sn )
if dst_name then return dst_name end
2026-07-02 01:54:31 -04:00
-- 遍历所有站点进行匹配
for _ , v in ipairs ( sites ) do
-- 精确匹配站点名称
2022-10-17 09:12:42 -04:00
if input_sn == v [ " name " ] then
cache : set ( input_sn , v [ ' name ' ] , 86400 )
return v [ " name " ]
end
2026-07-02 01:54:31 -04:00
-- 遍历站点的域名列表
for _ , dst_domain in ipairs ( v [ ' domains ' ] ) do
-- 精确匹配域名
2022-10-17 09:12:42 -04:00
if input_sn == dst_domain then
cache : set ( input_sn , v [ ' name ' ] , 86400 )
return v [ ' name ' ]
2026-07-02 01:54:31 -04:00
-- 通配符匹配(如 *.example.com)
elseif string.find ( dst_domain , " * " , 1 , true ) then
local pattern = " ^ " .. string.gsub ( dst_domain , " * " , " .* " ) .. " $ "
if ngx.re . match ( input_sn , pattern , " ijo " ) then
cache : set ( input_sn , v [ ' name ' ] , 86400 )
return v [ ' name ' ]
2022-10-17 09:12:42 -04:00
end
end
end
end
2026-07-06 14:12:09 -04:00
-- 未匹配站点不写入缓存,避免扫描流量占满 webstats_cache
2022-10-17 09:12:42 -04:00
return unset_server_name
end
2026-07-02 01:54:31 -04:00
--[[
获 取 存 储 键 ( 按 小 时 分 组 )
格 式 : YYYYMMDDHH ( 如 2026070213 )
@ return string 存 储 键
] ]
2022-10-17 09:12:42 -04:00
function _M . get_store_key ( self )
return os.date ( " %Y%m%d%H " , ngx.time ( ) )
end
2026-07-02 01:54:31 -04:00
--[[
根 据 指 定 时 间 获 取 存 储 键
@ param htime number 时 间 戳
@ return string 存 储 键
] ]
2022-10-18 05:41:29 -04:00
function _M . get_store_key_with_time ( self , htime )
return os.date ( " %Y%m%d%H " , htime )
end
2026-07-02 01:54:31 -04:00
--[[
获 取 响 应 体 大 小
@ return number 响 应 体 字 节 数
] ]
2022-10-17 09:12:42 -04:00
function _M . get_length ( self )
2026-07-02 01:54:31 -04:00
local clen = ngx.var . body_bytes_sent
2022-10-17 09:12:42 -04:00
if clen == nil then clen = 0 end
return tonumber ( clen )
end
2026-07-02 01:54:31 -04:00
--[[
获 取 日 志 唯 一 ID ( 自 动 递 增 )
使 用 共 享 内 存 实 现 跨 Worker 进 程 的 ID 递 增
@ param input_sn string 站 点 名 称
@ return number 日 志 ID
溢 出 处 理 : ID 达 到 max_log_id 后 重 置 为 1
] ]
2022-10-17 09:12:42 -04:00
function _M . get_last_id ( self , input_sn )
local last_insert_id_key = input_sn .. " _last_id "
2026-07-02 01:54:31 -04:00
-- 原子递增,初始值为 0
2022-10-17 09:12:42 -04:00
local new_id , err = cache : incr ( last_insert_id_key , 1 , 0 )
2026-07-02 01:54:31 -04:00
-- 溢出检查
2022-10-17 09:12:42 -04:00
if new_id >= max_log_id then
cache : set ( last_insert_id_key , 1 )
new_id = cache : get ( last_insert_id_key )
end
return new_id
end
2026-07-02 01:54:31 -04:00
--[[
获 取 HTTP 请 求 原 始 数 据 ( 请 求 头 + 请 求 体 )
仅 对 非 GET 请 求 记 录 请 求 体
@ return string JSON 格 式 的 请 求 数 据
] ]
2022-10-17 09:12:42 -04:00
function _M . get_http_origin ( self )
local data = " "
2026-07-04 13:16:02 -04:00
local headers = {
user_agent = ngx.var . http_user_agent ,
referer = ngx.var . http_referer ,
host = ngx.var . host ,
x_forwarded_for = ngx.var . http_x_forwarded_for
}
2023-03-09 01:56:43 -05:00
2026-07-04 13:16:02 -04:00
local req_method = ngx.var . request_method
2026-07-02 00:52:05 -04:00
if req_method ~= ' GET ' then
2023-01-06 14:00:55 -05:00
data = ngx.var . request_body
2026-07-04 13:16:02 -04:00
if data and " string " == type ( data ) then
2022-10-17 09:12:42 -04:00
headers [ " payload " ] = data
end
end
return json.encode ( headers )
end
2026-07-02 01:54:31 -04:00
--[[
定 时 任 务 前 置 准 备 ( 初 始 化 统 计 记 录 )
为 当 前 小 时 和 下 一 小 时 预 创 建 统 计 记 录 , 避 免 UPDATE 时 记 录 不 存 在
处 理 流 程 :
1. 获 取 锁 防 止 并 发 执 行
2. 遍 历 所 有 站 点
3. 为 每 个 站 点 初 始 化 数 据 库 连 接
4. 在 事 务 中 预 创 建 统 计 记 录 ( request_stat , client_stat , spider_stat )
5. 提 交 事 务 并 关 闭 数 据 库
@ return boolean 是 否 成 功
] ]
2023-02-11 09:16:08 -05:00
function _M . cronPre ( self )
2023-11-17 15:32:35 -05:00
self : lock_working ( ' cron_init_stat ' )
2026-07-04 10:43:20 -04:00
local ok , err = pcall ( function ( )
-- 获取当前小时和下一小时的存储键
local time_key = self : get_store_key ( )
local time_key_next = self : get_store_key_with_time ( ngx.time ( ) + 3600 )
-- 需要预创建的统计表
local wc_stat = { ' request_stat ' , ' client_stat ' , ' spider_stat ' }
2023-02-11 09:16:08 -05:00
2026-07-04 10:43:20 -04:00
-- 遍历所有站点
for _ , site_v in ipairs ( sites ) do
local input_sn = site_v [ " name " ]
2023-02-11 09:16:08 -05:00
2026-07-06 07:24:36 -04:00
if not self : is_migrating ( input_sn ) then
-- 安全地初始化数据库连接
local db_ok , db = pcall ( function ( ) return self : initDB ( input_sn ) end )
if not db_ok or not db then
self : D ( " cronPre initDB failed for " .. tostring ( input_sn ) .. " : " .. tostring ( db ) )
return false
end
2026-07-04 10:43:20 -04:00
2026-07-06 07:24:36 -04:00
-- 开启事务
db : exec ( [[BEGIN TRANSACTION]] )
2026-07-04 10:43:20 -04:00
2026-07-06 07:24:36 -04:00
local success = true
-- 预创建当前小时和下一小时的统计记录
for _ , ws_v in ipairs ( wc_stat ) do
if not self : _update_stat_pre ( db , ws_v , time_key ) then
success = false
break
end
if not self : _update_stat_pre ( db , ws_v , time_key_next ) then
success = false
break
end
2026-07-04 10:43:20 -04:00
end
2023-02-11 09:16:08 -05:00
2026-07-06 07:24:36 -04:00
-- 提交或回滚事务
if success then
pcall ( function ( ) db : execute ( [[COMMIT]] ) end )
else
pcall ( function ( ) db : exec ( [[ROLLBACK]] ) end )
end
2023-03-05 05:34:35 -05:00
2026-07-06 07:24:36 -04:00
-- 安全关闭数据库
pcall ( function ( ) db : close ( ) end )
2026-07-02 01:54:31 -04:00
2026-07-06 07:24:36 -04:00
if not success then
return false
end
2026-07-04 10:43:20 -04:00
end
2023-03-05 05:34:35 -05:00
end
2026-07-04 10:43:20 -04:00
return true
end )
2023-03-05 05:34:35 -05:00
2023-11-17 15:32:35 -05:00
self : unlock_working ( ' cron_init_stat ' )
2026-07-04 10:43:20 -04:00
if not ok then
self : D ( " cronPre error: " .. tostring ( err ) )
return false
end
return err
2023-02-11 09:16:08 -05:00
end
2026-07-02 01:54:31 -04:00
--[[
定 时 任 务 调 度 器
2026-07-04 15:36:39 -04:00
启 动 一 个 每 0.5 秒 执 行 一 次 的 定 时 器 , 负 责 :
2026-07-02 01:54:31 -04:00
1. 从 共 享 内 存 队 列 中 读 取 日 志 数 据
2. 将 日 志 写 入 SQLite 数 据 库
3. 统 计 请 求 、 客 户 端 、 爬 虫 数 据
4. 更 新 IP 和 URI 统 计
5. 清 理 过 期 日 志
执 行 流 程 :
1. 检 查 队 列 是 否 有 数 据
2. 获 取 锁 防 止 并 发 执 行
3. 初 始 化 所 有 站 点 的 数 据 库 连 接
4. 批 量 处 理 队 列 中 的 日 志 数 据
5. 更 新 统 计 信 息
6. 清 理 过 期 日 志
7. 提 交 事 务 并 关 闭 连 接
] ]
2022-10-16 12:44:04 -04:00
function _M . cron ( self )
2026-07-02 01:54:31 -04:00
--[[
定 时 任 务 核 心 处 理 函 数
@ param premature boolean 是 否 为 定 时 器 提 前 触 发
] ]
2022-10-16 12:44:04 -04:00
local timer_every_get_data = function ( premature )
2023-02-12 14:05:26 -05:00
2026-07-06 14:12:09 -04:00
local ok , llen = pcall ( function ( ) return queue : llen ( total_key ) end )
if not ok then
local queue_capacity = queue : capacity ( )
local queue_free = queue : free_space ( )
local queue_used = queue_capacity > 0 and ( 100 - math.floor ( queue_free / queue_capacity * 100 ) ) or 0
local cache_capacity = cache : capacity ( )
local cache_free = cache : free_space ( )
local cache_used = cache_capacity > 0 and ( 100 - math.floor ( cache_free / cache_capacity * 100 ) ) or 0
self : D ( " timer_every_get_data shared memory for " .. total_key .. " : " .. tostring ( llen )
.. " , queue_used= " .. tostring ( queue_used ) .. " %, queue_free= " .. tostring ( queue_free )
.. " , cache_used= " .. tostring ( cache_used ) .. " %, cache_free= " .. tostring ( cache_free ) )
return true
end
2022-10-18 05:41:29 -04:00
if llen == 0 then
2022-10-17 12:56:31 -04:00
return true
end
2022-10-16 23:36:29 -04:00
2023-03-12 13:33:07 -04:00
local cron_key = ' cron_every_1s '
if self : is_working ( cron_key ) then
return true
end
2026-07-02 00:52:05 -04:00
local ready_ok = self : cronPre ( )
if not ready_ok then
return true
end
2022-10-17 13:08:05 -04:00
2026-07-02 01:15:12 -04:00
self : lock_working ( cron_key )
2026-07-02 01:02:31 -04:00
2022-10-16 23:36:29 -04:00
local dbs = { }
2022-10-17 14:13:07 -04:00
local stmts = { }
2022-10-18 05:41:29 -04:00
local stat_fields = { }
2022-10-20 00:38:54 -04:00
local ip_stats = { }
local url_stats = { }
2026-07-02 01:15:12 -04:00
local rollback_sites = { }
2022-10-18 05:41:29 -04:00
local time_key = self : get_store_key ( )
2023-02-11 10:25:29 -05:00
2026-07-04 10:43:20 -04:00
local function cleanup_and_unlock ( )
for _ , site_v in ipairs ( sites ) do
local input_sn = site_v [ " name " ]
local stmt = stmts [ input_sn ]
if stmt then
pcall ( function ( ) stmt : finalize ( ) end )
end
local db = dbs [ input_sn ]
2026-07-02 01:15:12 -04:00
if db and db : isopen ( ) then
2026-07-04 10:43:20 -04:00
pcall ( function ( ) db : close ( ) end )
2026-07-02 01:15:12 -04:00
end
2022-10-17 01:06:34 -04:00
end
2026-07-04 10:43:20 -04:00
self : unlock_working ( cron_key )
2022-10-17 01:06:34 -04:00
end
2022-10-16 23:36:29 -04:00
2026-07-04 10:43:20 -04:00
local ok , err = pcall ( function ( )
2026-07-06 07:24:36 -04:00
if self : is_working ( ' cron_init_stat ' ) then
return
end
2026-07-04 10:43:20 -04:00
for _ , site_v in ipairs ( sites ) do
local input_sn = site_v [ " name " ]
if self : is_migrating ( input_sn ) then
2026-07-06 07:24:36 -04:00
-- 该站点迁移中,跳过;其它站点继续写入
else
local db_ok , db = pcall ( function ( ) return self : initDB ( input_sn ) end )
if not db_ok or not db then
self : D ( " initDB failed for " .. input_sn .. " : " .. tostring ( db ) )
return
end
2026-07-02 01:15:12 -04:00
2026-07-06 07:24:36 -04:00
stat_fields [ input_sn ] = { }
2022-10-18 05:41:29 -04:00
2026-07-06 07:24:36 -04:00
dbs [ input_sn ] = db
self : clean_stats ( db , input_sn )
2023-11-21 08:59:24 -05:00
2026-07-06 07:24:36 -04:00
local stmt_ok , stmt = pcall ( function ( )
return db : prepare [ [ INSERT INTO web_logs (
time , ip , scheme , domain , server_name , method , status_code , uri , body_length ,
referer , user_agent , protocol , request_time , is_spider , request_headers , ip_list , client_port )
VALUES ( : time , : ip , : scheme , : domain , : server_name , : method , : status_code , : uri ,
: body_length , : referer , : user_agent , : protocol , : request_time , : is_spider ,
: request_headers , : ip_list , : client_port ) ] ]
end )
if stmt_ok and stmt then
stmts [ input_sn ] = stmt
db : exec ( [[BEGIN TRANSACTION]] )
else
if db and db : isopen ( ) then
db : close ( )
end
dbs [ input_sn ] = nil
return
2022-10-18 05:41:29 -04:00
end
2022-10-20 00:38:54 -04:00
end
2026-07-04 10:43:20 -04:00
end
2022-10-20 00:38:54 -04:00
2026-07-04 10:43:20 -04:00
for i = 1 , llen do
2026-07-06 14:12:09 -04:00
local data = queue : lpop ( total_key )
2026-07-04 10:43:20 -04:00
if not data then
break
2022-10-20 00:38:54 -04:00
end
2026-07-04 10:43:20 -04:00
local decode_ok , info = pcall ( function ( ) return json.decode ( data ) end )
if not decode_ok then
self : D ( " json decode failed: " .. tostring ( info ) )
2026-07-06 14:12:09 -04:00
queue : rpush ( total_key , data )
2026-07-04 10:43:20 -04:00
return
end
2022-10-20 00:38:54 -04:00
2026-07-04 10:43:20 -04:00
local input_sn = info [ ' server_name ' ]
2026-07-06 07:24:36 -04:00
if self : is_migrating ( input_sn ) then
2026-07-06 14:12:09 -04:00
queue : rpush ( total_key , data )
2026-07-06 07:24:36 -04:00
else
local db = dbs [ input_sn ]
local stmt = stmts [ input_sn ]
if not db or not stmt then
2026-07-06 14:12:09 -04:00
queue : rpush ( total_key , data )
2026-07-04 10:43:20 -04:00
else
2026-07-06 07:24:36 -04:00
local insert_ok = self : store_logs_line ( db , stmt , input_sn , info )
if not insert_ok then
self : D ( " store_logs_line failed for " .. input_sn .. " : " .. tostring ( insert_ok ) )
2026-07-06 14:12:09 -04:00
queue : rpush ( total_key , data )
2026-07-06 07:24:36 -04:00
rollback_sites [ input_sn ] = true
else
local log_kv = info [ " log_kv " ]
local excluded = log_kv [ ' excluded ' ]
local stat_tmp_fields = info [ ' stat_fields ' ]
local stat_fields_is = stat_fields [ input_sn ]
for stf_k , stf_v in pairs ( stat_tmp_fields ) do
if excluded then
if stf_k == " spider_stat_fields " or stf_k == " client_stat_fields " then
break
end
end
local stf_is = stat_fields_is [ stf_k ]
if not stf_is then
stf_is = { }
stat_fields_is [ stf_k ] = stf_is
end
for sv_k , sv_v in pairs ( stf_v ) do
stf_is [ sv_k ] = ( stf_is [ sv_k ] or 0 ) + sv_v
end
end
if not excluded then
local ip = log_kv [ ' ip ' ]
local body_length = log_kv [ " body_length " ]
local ip_stats_sn = ip_stats [ input_sn ]
if not ip_stats_sn then
ip_stats_sn = { }
ip_stats [ input_sn ] = ip_stats_sn
end
local ip_stat = ip_stats_sn [ ip ]
if not ip_stat then
ip_stats_sn [ ip ] = { ip_num = 1 , body_length = body_length }
else
ip_stat.ip_num = ip_stat.ip_num + 1
ip_stat.body_length = ip_stat.body_length + body_length
end
local url_stats_sn = url_stats [ input_sn ]
if not url_stats_sn then
url_stats_sn = { }
url_stats [ input_sn ] = url_stats_sn
end
local request_uri = log_kv [ " request_uri " ]
local request_uri_md5 = ngx.md5 ( request_uri )
local url_stat = url_stats_sn [ request_uri_md5 ]
if not url_stat then
url_stats_sn [ request_uri_md5 ] = { url_num = 1 , uri = request_uri , body_length = body_length }
else
url_stat.url_num = url_stat.url_num + 1
url_stat.body_length = url_stat.body_length + body_length
end
end
end
2026-07-04 10:43:20 -04:00
end
end
2022-10-17 14:54:31 -04:00
end
2026-07-04 10:43:20 -04:00
local save_day = config [ ' global ' ] [ " save_day " ]
local now_date = os.date ( " *t " )
local save_date_timestamp = os.time { year = now_date.year , month = now_date.month , day = now_date.day - save_day , hour = 0 }
local delete_sql = " DELETE FROM web_logs WHERE time< " .. tostring ( save_date_timestamp )
2022-10-18 05:41:29 -04:00
2026-07-04 10:43:20 -04:00
for _ , site_v in ipairs ( sites ) do
local input_sn = site_v [ " name " ]
local stmt = stmts [ input_sn ]
if stmt then
pcall ( function ( ) stmt : finalize ( ) end )
end
local stat_fields_is = stat_fields [ input_sn ]
local db = dbs [ input_sn ]
if db then
if rollback_sites [ input_sn ] then
pcall ( function ( ) db : exec ( [[ROLLBACK]] ) end )
else
for sti_k , sti_v in pairs ( stat_fields_is ) do
local vkk = " "
for sv_k , sv_v in pairs ( sti_v ) do
vkk = vkk .. sv_k .. " = " .. sv_k .. " + " .. sv_v .. " , "
end
if vkk ~= " " then
vkk = string.sub ( vkk , 1 , string.len ( vkk ) - 1 )
if sti_k == ' request_stat_fields ' then
self : update_stat ( db , " request_stat " , time_key , vkk )
elseif sti_k == ' client_stat_fields ' then
self : update_stat ( db , " client_stat " , time_key , vkk )
elseif sti_k == ' spider_stat_fields ' then
self : update_stat ( db , " spider_stat " , time_key , vkk )
end
2026-07-02 01:15:12 -04:00
end
2026-07-02 00:52:05 -04:00
end
2022-10-18 05:41:29 -04:00
2026-07-04 10:43:20 -04:00
local local_ip_stats = ip_stats [ input_sn ]
if local_ip_stats then
for ip_addr , ip_val in pairs ( local_ip_stats ) do
self : update_statistics_ip ( db , ip_addr , ip_val.ip_num , ip_val.body_length )
end
2026-07-02 01:15:12 -04:00
end
2022-10-18 05:41:29 -04:00
2026-07-04 10:43:20 -04:00
local local_url_stats = url_stats [ input_sn ]
if local_url_stats then
for url_md5 , url_val in pairs ( local_url_stats ) do
self : update_statistics_uri ( db , url_val.uri , url_md5 , url_val.url_num , url_val.body_length )
end
2026-07-02 01:15:12 -04:00
end
2022-10-18 05:41:29 -04:00
2026-07-04 10:43:20 -04:00
db : exec ( delete_sql )
pcall ( function ( ) db : execute ( [[COMMIT]] ) end )
end
end
2026-07-02 01:15:12 -04:00
2026-07-04 10:43:20 -04:00
if db and db : isopen ( ) then
pcall ( function ( ) db : close ( ) end )
2026-07-02 01:15:12 -04:00
end
2022-10-17 00:17:37 -04:00
end
2026-07-04 10:43:20 -04:00
end )
2022-10-17 00:17:37 -04:00
2026-07-04 10:43:20 -04:00
if not ok then
self : D ( " cron process error: " .. tostring ( err ) )
cleanup_and_unlock ( )
return true
2022-10-17 14:54:31 -04:00
end
2022-10-16 14:45:18 -04:00
2022-10-16 14:04:01 -04:00
self : unlock_working ( cron_key )
2022-10-16 12:44:04 -04:00
end
2023-11-21 06:08:39 -05:00
function timer_every_get_data_try ( )
2026-07-04 10:56:49 -04:00
local presult , err = pcall ( function ( ) timer_every_get_data ( ) end )
2023-11-21 06:08:39 -05:00
if not presult then
2026-07-04 10:56:49 -04:00
self : D ( " debug cron error on : " .. tostring ( err ) )
2023-11-21 06:08:39 -05:00
return true
end
2026-07-04 10:56:49 -04:00
end
function timer_every_get_data_try_debug ( )
2026-07-04 11:02:54 -04:00
ngx.update_time ( )
2026-07-04 10:56:49 -04:00
local start_time = ngx.now ( )
2026-07-06 14:12:09 -04:00
local start_llen = queue : llen ( total_key )
2026-07-04 10:56:49 -04:00
local presult , err = pcall ( function ( ) timer_every_get_data ( ) end )
2026-07-04 11:02:54 -04:00
ngx.update_time ( )
2026-07-04 10:56:49 -04:00
local end_time = ngx.now ( )
2026-07-06 14:12:09 -04:00
local end_llen = queue : llen ( total_key )
2026-07-04 10:56:49 -04:00
local duration = ( end_time - start_time ) * 1000
2026-07-04 11:02:54 -04:00
local processed = start_llen - end_llen
2026-07-04 10:56:49 -04:00
if not presult then
self : D ( " debug cron error on : " .. tostring ( err ) )
return true
end
if start_llen > 0 then
2026-07-04 11:02:54 -04:00
self : D ( string.format ( " cron process: took %.2fms, start=%d, end=%d, processed=%d " ,
duration , start_llen , end_llen , processed ) )
2026-07-04 10:56:49 -04:00
end
2023-11-21 06:08:39 -05:00
end
2026-07-04 15:36:26 -04:00
ngx.timer . every ( 0.5 , timer_every_get_data_try )
2022-10-16 12:44:04 -04:00
end
function _M . store_logs_line ( self , db , stmt , input_sn , info )
local logline = info [ ' log_kv ' ]
2026-07-02 00:52:05 -04:00
2025-08-22 12:53:47 -04:00
local excluded = logline [ " excluded " ]
2026-07-02 00:52:05 -04:00
if excluded then
return true
end
2025-08-22 12:53:47 -04:00
2026-07-02 00:52:05 -04:00
local user_agent = logline [ " user_agent " ]
2025-08-22 12:53:47 -04:00
if " table " == type ( user_agent ) then
user_agent = self : to_json ( user_agent )
end
2025-08-22 12:53:58 -04:00
2026-07-02 00:52:05 -04:00
stmt : bind_names {
time = logline [ " time " ] ,
ip = logline [ " ip " ] ,
2026-07-05 03:28:56 -04:00
scheme = logline [ " scheme " ] ,
2026-07-02 00:52:05 -04:00
domain = logline [ " domain " ] ,
server_name = logline [ " server_name " ] ,
method = logline [ " method " ] ,
status_code = logline [ " status_code " ] ,
uri = logline [ " request_uri " ] ,
body_length = logline [ " body_length " ] ,
referer = logline [ " referer " ] ,
user_agent = user_agent ,
protocol = logline [ " protocol " ] ,
request_time = logline [ " request_time " ] ,
is_spider = logline [ " is_spider " ] ,
request_headers = logline [ " request_headers " ] ,
ip_list = logline [ " ip_list " ] ,
client_port = logline [ " client_port " ] ,
}
2022-10-16 12:44:04 -04:00
2026-07-02 00:52:05 -04:00
local res = stmt : step ( )
if tostring ( res ) == " 5 " then
2022-10-17 13:48:18 -04:00
stmt : reset ( )
2026-07-02 00:52:05 -04:00
return false
2022-10-16 12:44:04 -04:00
end
2026-07-02 00:52:05 -04:00
stmt : reset ( )
2022-10-16 12:44:04 -04:00
return true
end
2022-10-17 09:12:42 -04:00
function _M . statistics_ipc ( self , input_sn , ip )
-- 判断IP是否重复的时间限定范围是请求的当前时间+24小时
local ipc = 0
local ip_token = input_sn .. ' _ ' .. ip
if not cache : get ( ip_token ) then
ipc = 1
cache : set ( ip_token , 1 , self : get_end_time ( ) )
end
return ipc
end
function _M . statistics_request ( self , ip , is_spider , body_length )
local pvc = 0
local uvc = 0
2024-05-24 23:31:56 -04:00
if not is_spider and ngx.status == 200 and body_length > 0 then
2026-07-04 13:16:02 -04:00
local ua = ngx.var . http_user_agent or ' '
ua = string.lower ( ua )
2022-10-17 09:12:42 -04:00
2024-05-24 23:31:56 -04:00
pvc = 1
2026-07-04 13:16:02 -04:00
if ua and ua ~= ' ' then
2024-05-24 23:31:56 -04:00
local today = ngx.today ( )
local uv_token = ngx.md5 ( ip .. ua .. today )
if not cache : get ( uv_token ) then
uvc = 1
cache : set ( uv_token , 1 , self : get_end_time ( ) )
end
end
end
return pvc , uvc
end
function _M . statistics_request_old ( self , ip , is_spider , body_length )
local pvc = 0
local uvc = 0
2026-07-04 13:16:02 -04:00
local method = ngx.var . request_method
2022-10-17 09:12:42 -04:00
if not is_spider and method == ' GET ' and ngx.status == 200 and body_length > 512 then
2026-07-04 13:16:02 -04:00
local ua = ngx.var . http_user_agent or ' '
ua = string.lower ( ua )
local content_type = ngx.var . content_type
if content_type and string.find ( content_type , ' text/html ' , 1 , true ) then
pvc = 1
if ua and string.find ( ua , ' mozilla ' ) then
local today = ngx.today ( )
local uv_token = ngx.md5 ( ip .. ua .. today )
if not cache : get ( uv_token ) then
uvc = 1
cache : set ( uv_token , 1 , self : get_end_time ( ) )
2022-10-17 09:12:42 -04:00
end
end
end
end
return pvc , uvc
end
--------------------- db start ---------------------------
2022-10-18 05:41:29 -04:00
2022-10-20 00:38:54 -04:00
function _M . update_statistics_uri ( self , db , uri , uri_md5 , day_num , body_length )
local open_statistics_uri = config [ ' global ' ] [ " statistics_uri " ]
if not open_statistics_uri then return true end
2026-07-02 01:15:12 -04:00
local insert_stmt = db : prepare ( " INSERT INTO uri_stat(uri_md5, uri) SELECT :uri_md5, :uri WHERE NOT EXISTS (SELECT uri_md5 FROM uri_stat WHERE uri_md5 = :uri_md5); " )
2026-07-02 00:52:05 -04:00
insert_stmt : bind_names { uri_md5 = uri_md5 , uri = uri }
insert_stmt : step ( )
2026-07-02 01:15:12 -04:00
insert_stmt : finalize ( )
local update_sql = " UPDATE uri_stat SET " .. day_column .. " = " .. day_column .. " + " .. day_num .. " , " .. flow_column .. " = " .. flow_column .. " + " .. body_length .. " WHERE uri_md5= \" " .. uri_md5 .. " \" ; "
db : exec ( update_sql )
2022-10-20 00:38:54 -04:00
return true
end
2022-10-16 12:44:04 -04:00
function _M . statistics_uri ( self , db , uri , uri_md5 , body_length )
-- count the number of URI requests and traffic
local open_statistics_uri = config [ ' global ' ] [ " statistics_uri " ]
if not open_statistics_uri then return true end
local stat_sql = nil
stat_sql = " INSERT INTO uri_stat(uri_md5,uri) SELECT \" " .. uri_md5 .. " \" , \" " .. uri .. " \" WHERE NOT EXISTS (SELECT uri_md5 FROM uri_stat WHERE uri_md5= \" " .. uri_md5 .. " \" ); "
local res , err = db : exec ( stat_sql )
2022-10-17 11:29:57 -04:00
stat_sql = " UPDATE uri_stat SET " .. day_column .. " = " .. day_column .. " +1, " .. flow_column .. " = " .. flow_column .. " + " .. body_length .. " WHERE uri_md5= \" " .. uri_md5 .. " \" ; "
2022-10-16 12:44:04 -04:00
local res , err = db : exec ( stat_sql )
return true
end
2022-10-20 00:38:54 -04:00
2026-07-02 00:52:05 -04:00
function _M . update_statistics_ip ( self , db , ip , day_num , body_length )
2022-10-20 00:38:54 -04:00
local open_statistics_ip = config [ ' global ' ] [ " statistics_ip " ]
if not open_statistics_ip then return true end
2026-07-02 01:15:12 -04:00
local insert_stmt = db : prepare ( " INSERT INTO ip_stat(ip) SELECT :ip WHERE NOT EXISTS (SELECT ip FROM ip_stat WHERE ip = :ip); " )
2026-07-02 00:52:05 -04:00
insert_stmt : bind_names { ip = ip }
insert_stmt : step ( )
2026-07-02 01:15:12 -04:00
insert_stmt : finalize ( )
local update_sql = " UPDATE ip_stat SET " .. day_column .. " = " .. day_column .. " + " .. day_num .. " , " .. flow_column .. " = " .. flow_column .. " + " .. body_length .. " WHERE ip= \" " .. ip .. " \" ; "
db : exec ( update_sql )
2022-10-20 00:38:54 -04:00
return true
end
2022-10-16 12:44:04 -04:00
function _M . statistics_ip ( self , db , ip , body_length )
2026-07-02 00:52:05 -04:00
return self : update_statistics_ip ( db , ip , 1 , body_length )
2022-10-16 12:44:04 -04:00
end
2022-10-18 05:41:29 -04:00
function _M . _update_stat_pre ( self , db , stat_table , key )
2023-01-14 02:14:51 -05:00
local local_sql = string.format ( " INSERT INTO %s(time) SELECT :time WHERE NOT EXISTS(SELECT time FROM %s WHERE time=:time); " , stat_table , stat_table )
local update_stat_stmt = db : prepare ( local_sql )
2023-03-05 05:34:35 -05:00
if update_stat_stmt then
update_stat_stmt : bind_names { time = key }
update_stat_stmt : step ( )
update_stat_stmt : finalize ( )
return true
end
return false
2022-10-18 05:41:29 -04:00
end
function _M . update_stat_quick ( self , db , stat_table , key , columns )
if not columns then return end
local update_sql = " UPDATE " .. stat_table .. " SET " .. columns .. " WHERE time= " .. key
return db : exec ( update_sql )
end
2022-10-17 09:12:42 -04:00
2022-10-18 05:41:29 -04:00
function _M . update_stat ( self , db , stat_table , key , columns )
2022-10-16 12:44:04 -04:00
if not columns then return end
2026-07-02 00:52:05 -04:00
2026-07-02 01:15:12 -04:00
local insert_stmt = db : prepare ( " INSERT INTO " .. stat_table .. " (time) SELECT :time WHERE NOT EXISTS(SELECT time FROM " .. stat_table .. " WHERE time=:time); " )
2026-07-02 00:52:05 -04:00
insert_stmt : bind_names { time = key }
insert_stmt : step ( )
2026-07-02 01:15:12 -04:00
insert_stmt : finalize ( )
2026-07-02 00:52:05 -04:00
local update_sql = " UPDATE " .. stat_table .. " SET " .. columns .. " WHERE time= " .. key
2022-10-17 09:12:42 -04:00
return db : exec ( update_sql )
2022-10-16 12:44:04 -04:00
end
2022-10-17 09:12:42 -04:00
--------------------- db end ---------------------------
2022-10-16 12:44:04 -04:00
function _M . D ( self , msg )
if not debug_mode then return true end
2026-07-04 13:16:02 -04:00
local fp = io.open ( self.app_dir .. ' /debug.log ' , ' ab ' )
2022-10-16 12:44:04 -04:00
if fp == nil then
return nil
end
local localtime = os.date ( " %Y-%m-%d %H:%M:%S " )
2026-07-04 13:16:02 -04:00
fp : write ( localtime .. " : " .. tostring ( msg ) .. " \n " )
2022-10-16 12:44:04 -04:00
fp : flush ( )
fp : close ( )
return true
end
2022-10-17 01:44:35 -04:00
function _M . is_migrating ( self , input_sn )
2026-07-06 07:03:27 -04:00
local fp = io.open ( self.app_dir .. " /migrating " , " rb " )
if fp then
fp : close ( )
return true
end
local fp = io.open ( self.app_dir .. " /logs/ " .. input_sn .. " /migrating " , " rb " )
if fp then
fp : close ( )
return true
end
2022-10-16 12:44:04 -04:00
return false
end
2022-10-16 14:04:01 -04:00
function _M . is_working ( self , sign )
local work_status = cache : get ( sign .. " _working " )
2022-10-16 12:44:04 -04:00
if work_status ~= nil and work_status == true then
return true
end
return false
end
2022-10-16 14:04:01 -04:00
function _M . lock_working ( self , sign )
local working_key = sign .. " _working "
2026-07-04 10:43:20 -04:00
cache : set ( working_key , true , 10 )
2022-10-16 12:44:04 -04:00
end
2022-10-16 14:04:01 -04:00
function _M . unlock_working ( self , sign )
local working_key = sign .. " _working "
2022-10-16 12:44:04 -04:00
cache : set ( working_key , false )
end
function _M . write_file ( self , filename , body , mode )
local fp = io.open ( filename , mode )
if fp == nil then
return nil
end
fp : write ( body )
fp : flush ( )
fp : close ( )
return true
end
function _M . read_file_body ( self , filename )
local fp = io.open ( filename , ' rb ' )
if not fp then
return nil
end
2026-07-02 00:52:05 -04:00
local fbody = fp : read ( " *a " )
2022-10-16 12:44:04 -04:00
fp : close ( )
if fbody == ' ' then
return nil
end
return fbody
end
function _M . load_update_day ( self , input_sn )
2026-07-04 13:16:02 -04:00
local _file = self.app_dir .. " /logs/ " .. input_sn .. " /update_day.log "
2022-10-16 12:44:04 -04:00
return self : read_file_body ( _file )
end
function _M . write_update_day ( self , input_sn )
2026-07-04 13:16:02 -04:00
local _file = self.app_dir .. " /logs/ " .. input_sn .. " /update_day.log "
2022-10-16 12:44:04 -04:00
return self : write_file ( _file , today , " w " )
end
2022-10-17 11:29:57 -04:00
function _M . clean_stats ( self , db , input_sn )
-- 清空 uri,ip 汇总的信息[昨日]
local update_day = self : load_update_day ( input_sn )
if not update_day or update_day ~= today then
local update_sql = " UPDATE uri_stat SET " .. day_column .. " =0, " .. flow_column .. " =0 "
db : exec ( update_sql )
update_sql = " UPDATE ip_stat SET " .. day_column .. " =0, " .. flow_column .. " =0 "
db : exec ( update_sql )
self : write_update_day ( input_sn )
end
end
2022-10-16 12:44:04 -04:00
function _M . lpop ( self )
2026-07-06 14:12:09 -04:00
return queue : lpop ( total_key )
2022-10-16 12:44:04 -04:00
end
function _M . rpop ( self )
2026-07-06 14:12:09 -04:00
return queue : rpop ( total_key )
2022-10-16 12:44:04 -04:00
end
2022-10-17 09:12:42 -04:00
function _M . get_update_field ( self , field , value )
return field .. " = " .. field .. " + " .. tostring ( value )
end
function _M . get_request_time ( self )
2026-07-04 13:16:02 -04:00
local request_time = math.floor ( tonumber ( ngx.var . request_time or 0 ) * 1000 )
2022-10-17 09:12:42 -04:00
if request_time == 0 then request_time = 1 end
return request_time
end
function _M . get_end_time ( self )
local s_time = ngx.time ( )
local n_date = os.date ( " *t " , s_time + 86400 )
n_date.hour = 0
n_date.min = 0
n_date.sec = 0
local d_time = ngx.time ( n_date )
return d_time - s_time
end
2026-07-02 00:52:05 -04:00
local spider_table = {
[ " baidu " ] = 1 ,
[ " bing " ] = 2 ,
[ " qh360 " ] = 3 ,
[ " google " ] = 4 ,
[ " bytes " ] = 5 ,
[ " sogou " ] = 6 ,
[ " youdao " ] = 7 ,
[ " soso " ] = 8 ,
[ " dnspod " ] = 9 ,
[ " yandex " ] = 10 ,
[ " yisou " ] = 11 ,
[ " other " ] = 12 ,
[ " mpcrawler " ] = 13 ,
[ " yahoo " ] = 14 ,
[ " duckduckgo " ] = 15
}
local spider_keywords = {
[ " baidu " ] = " baidu " ,
[ " bytes " ] = " bytes " ,
[ " 360spider " ] = " qh360 " ,
[ " sogou " ] = " sogou " ,
[ " soso " ] = " soso " ,
[ " google " ] = " google " ,
[ " bingbot " ] = " bing " ,
[ " youdao " ] = " youdao " ,
[ " dnspod " ] = " dnspod " ,
[ " yandex " ] = " yandex " ,
[ " yisou " ] = " yisou " ,
[ " mpcrawler " ] = " mpcrawler " ,
[ " yahoo " ] = " yahoo " ,
[ " slurp " ] = " other " ,
[ " duckduckgo " ] = " other " ,
[ " semrush " ] = " other " ,
[ " spider " ] = " other " ,
[ " bot " ] = " other " ,
[ " crawler " ] = " other "
}
2022-10-17 09:12:42 -04:00
function _M . match_spider ( self , ua )
local is_spider = false
local spider_name = " "
2026-07-02 00:52:05 -04:00
if not ua then
return false , " " , 0
end
local lower_ua = string.lower ( ua )
for kw , name in pairs ( spider_keywords ) do
if string.find ( lower_ua , kw , 1 , true ) then
is_spider = true
spider_name = name
break
2022-10-17 09:12:42 -04:00
end
end
2026-07-02 00:52:05 -04:00
if is_spider then
2022-10-17 09:12:42 -04:00
return is_spider , spider_name , spider_table [ spider_name ]
end
2026-07-02 00:52:05 -04:00
2022-10-17 09:12:42 -04:00
return false , " " , 0
end
2026-07-02 00:52:05 -04:00
local clients_map = {
[ " android " ] = " android " ,
[ " iphone " ] = " iphone " ,
[ " ipod " ] = " iphone " ,
[ " ipad " ] = " iphone " ,
[ " firefox " ] = " firefox " ,
[ " msie " ] = " msie " ,
[ " trident " ] = " msie " ,
[ " 360se " ] = " qh360 " ,
[ " 360ee " ] = " qh360 " ,
[ " 360browser " ] = " qh360 " ,
[ " qihoo " ] = " qh360 " ,
[ " theworld " ] = " theworld " ,
[ " tencenttraveler " ] = " tt " ,
[ " maxthon " ] = " maxthon " ,
[ " opera " ] = " opera " ,
[ " qqbrowser " ] = " qq " ,
[ " ucweb " ] = " uc " ,
[ " ubrowser " ] = " uc " ,
[ " safari " ] = " safari " ,
[ " chrome " ] = " chrome " ,
[ " metasr " ] = " metasr " ,
[ " 2345explorer " ] = " pc2345 " ,
[ " edge " ] = " edg " ,
[ " edg " ] = " edg " ,
[ " windows " ] = " windows " ,
[ " linux " ] = " linux " ,
[ " macintosh " ] = " mac " ,
[ " mobile " ] = " mobile "
}
local mobile_keywords = { " mobile " , " android " , " iphone " , " ipod " , " ipad " }
local pc_browser_keywords = {
[ " 360se " ] = " qh360 " ,
[ " 360ee " ] = " qh360 " ,
[ " 360browser " ] = " qh360 " ,
[ " qihoo " ] = " qh360 " ,
[ " theworld " ] = " theworld " ,
[ " tencenttraveler " ] = " tt " ,
[ " maxthon " ] = " maxthon " ,
[ " opera " ] = " opera " ,
[ " qqbrowser " ] = " qq " ,
[ " ucweb " ] = " uc " ,
[ " ubrowser " ] = " uc " ,
[ " metasr " ] = " metasr " ,
[ " 2345explorer " ] = " pc2345 " ,
[ " edge " ] = " edg " ,
[ " firefox " ] = " firefox " ,
[ " chrome " ] = " chrome " ,
[ " safari " ] = " safari " ,
[ " msie " ] = " msie " ,
[ " trident " ] = " msie "
}
local os_keywords = { " windows " , " linux " , " macintosh " }
local machine_keywords = { " apachebench " , " curl " , " headlesschrome " , " wget " , " spider " , " crawler " , " scrapy " , " zgrab " , " python " , " java " }
local function _match_client_table ( self , ua )
local client_stat_fields = { }
2022-10-17 09:12:42 -04:00
if not ua then
return client_stat_fields
end
2026-07-02 00:52:05 -04:00
if " table " == type ( ua ) then
ua = self : to_json ( ua )
end
local lower_ua = string.lower ( ua )
local is_mobile = false
for _ , kw in ipairs ( mobile_keywords ) do
if string.find ( lower_ua , kw , 1 , true ) then
is_mobile = true
client_stat_fields [ " mobile " ] = 1
if kw ~= " mobile " then
client_stat_fields [ clients_map [ kw ] ] = 1
2022-10-17 09:12:42 -04:00
end
2026-07-02 00:52:05 -04:00
break
2022-10-17 09:12:42 -04:00
end
end
2022-10-18 05:41:29 -04:00
2026-07-02 00:52:05 -04:00
if not is_mobile then
2022-10-18 05:41:29 -04:00
local cls_pc = nil
2026-07-02 00:52:05 -04:00
for kw , name in pairs ( pc_browser_keywords ) do
if string.find ( lower_ua , kw , 1 , true ) then
cls_pc = name
break
2022-10-18 05:41:29 -04:00
end
end
2026-07-02 00:52:05 -04:00
2022-10-18 05:41:29 -04:00
if cls_pc then
2026-07-02 00:52:05 -04:00
client_stat_fields [ cls_pc ] = 1
2022-10-18 05:41:29 -04:00
else
2026-07-02 00:52:05 -04:00
local is_machine = false
for _ , kw in ipairs ( machine_keywords ) do
if string.find ( lower_ua , kw , 1 , true ) then
is_machine = true
break
end
end
if is_machine then
2022-10-18 05:41:29 -04:00
client_stat_fields [ " machine " ] = 1
else
client_stat_fields [ " other " ] = 1
end
end
2026-07-02 00:52:05 -04:00
for _ , kw in ipairs ( os_keywords ) do
if string.find ( lower_ua , kw , 1 , true ) then
client_stat_fields [ clients_map [ kw ] ] = 1
break
end
2022-10-18 05:41:29 -04:00
end
end
2026-07-02 00:52:05 -04:00
if string.find ( lower_ua , " micromessenger " , 1 , true ) then
2022-10-18 05:41:29 -04:00
client_stat_fields [ " weixin " ] = 1
end
2026-07-02 00:52:05 -04:00
2022-10-18 05:41:29 -04:00
return client_stat_fields
end
2026-07-02 00:52:05 -04:00
function _M . match_client_arr ( self , ua )
return _match_client_table ( self , ua )
end
function _M . match_client ( self , ua )
local client_stat_fields = _match_client_table ( self , ua )
local fields_str = " "
for k , v in pairs ( client_stat_fields ) do
fields_str = fields_str .. " , " .. self : get_update_field ( k , v )
end
if fields_str ~= " " then
fields_str = string.sub ( fields_str , 2 )
end
return fields_str
end
2022-10-17 09:12:42 -04:00
function _M . get_client_ip ( self )
local client_ip = " unknown "
2026-07-02 02:22:10 -04:00
if auto_config and auto_config [ ' cdn ' ] == true then
for _ , v in ipairs ( auto_config [ ' cdn_headers ' ] or { } ) do
2026-07-04 13:16:02 -04:00
local header_var = " http_ " .. string.gsub ( string.lower ( v ) , " - " , " _ " )
local ip_list = ngx.var [ header_var ]
if ip_list ~= nil and ip_list ~= " " then
2026-07-02 02:22:10 -04:00
client_ip = self : split ( ip_list , ' , ' ) [ 1 ]
break
2022-10-17 09:12:42 -04:00
end
end
end
if type ( client_ip ) == ' table ' then client_ip = " " end
if client_ip ~= " unknown " and ngx.re . match ( client_ip , " ^([a-fA-F0-9]*): " ) then
return client_ip
end
if not ngx.re . match ( client_ip , " \\ d+ \\ . \\ d+ \\ . \\ d+ \\ . \\ d+ " ) == nil or not self : is_ipaddr ( client_ip ) then
client_ip = ngx.var . remote_addr
if client_ip == nil then
client_ip = " unknown "
end
end
return client_ip
end
2022-10-16 12:44:04 -04:00
return _M