-- 日志记录模块 -- 记录IP频率限制的详细日志信息 local _M = {} -- 日志级别 _M.LOG_DEBUG = "DEBUG" _M.LOG_INFO = "INFO" _M.LOG_WARN = "WARN" _M.LOG_ERROR = "ERROR" -- 从公共配置加载日志配置 local common_config = require "config.common_config" local config = common_config.log_config -- 日志文件句柄缓存 config.log_file_handle = nil -- 日志级别映射 local log_levels = { DEBUG = ngx.DEBUG, INFO = ngx.INFO, WARN = ngx.WARN, ERROR = ngx.ERR } -- 检查日志级别是否应该输出 local function should_log(level) if not config.enabled then return false end local current_level = log_levels[config.log_level] or ngx.INFO local msg_level = log_levels[level] or ngx.INFO return msg_level >= current_level end -- 清理字符串,防止日志注入 local function sanitize_string(str) if not str then return "" end -- 去除换行符、回车符等特殊字符 str = tostring(str) str = str:gsub("[\r\n\t]", " ") str = str:gsub("[%z\1-\31]", "") -- 限制长度 if #str > 500 then str = str:sub(1, 500) .. "..." end return str end -- 获取当前时间戳 local function get_timestamp() return ngx.time() end -- 格式化日志消息 local function format_message(level, message, extra_info) local timestamp = get_timestamp() local date_str = ngx.http_time(timestamp) local log_msg = string.format("%s %s [%s] %s", "[IP_RATE_LIMIT]", date_str, level, message ) if extra_info and next(extra_info) then local extras = {} for k, v in pairs(extra_info) do table.insert(extras, string.format("%s=%s", k, sanitize_string(v))) end log_msg = log_msg .. " | " .. table.concat(extras, " ") end -- 添加换行符 log_msg = log_msg .. "\n" return log_msg end -- 打开日志文件 local function open_log_file() if config.log_path == "" then return nil end -- 如果已经有打开的文件句柄,直接返回 if config.log_file_handle then return config.log_file_handle end -- 尝试打开文件(追加模式) local file, err = io.open(config.log_path, "a") if not file then ngx.log(ngx.ERR, "[IP_RATE_LIMIT] Failed to open log file: ", config.log_path, " error: ", err) return nil end config.log_file_handle = file return file end -- 关闭日志文件 local function close_log_file() if config.log_file_handle then config.log_file_handle:flush() config.log_file_handle:close() config.log_file_handle = nil end end -- 写入日志 local function write_log(log_msg, ngx_level) -- 如果配置了日志文件路径,写入文件 if config.log_path ~= "" then local file = open_log_file() if file then file:write(log_msg) file:flush() end end -- 同时写入Nginx error.log(用于调试和备份) ngx.log(ngx_level, log_msg) end -- 记录日志 local function log(level, message, extra_info) if not should_log(level) then return end local log_msg = format_message(level, message, extra_info) local ngx_level = log_levels[level] or ngx.INFO write_log(log_msg, ngx_level) end -- ============================================ -- 公共API -- ============================================ -- 记录放行日志 function _M.log_allowed(client_ip, request_count, time_window) if not config.log_allowed then return end log(_M.LOG_DEBUG, "Request allowed", { ip = client_ip, count = tostring(request_count), window = tostring(time_window) .. "s", action = "ALLOWED" }) end -- 记录限流日志 function _M.log_rate_limited(client_ip, request_count, max_requests, time_window) log(_M.LOG_WARN, "Rate limit exceeded", { ip = client_ip, count = tostring(request_count), max = tostring(max_requests), window = tostring(time_window) .. "s", action = "RATE_LIMITED", status = "429" }) end -- 记录封禁日志 function _M.log_blocked(client_ip, request_count, max_requests, time_window) log(_M.LOG_WARN, "Access blocked", { ip = client_ip, count = tostring(request_count), max = tostring(max_requests), window = tostring(time_window) .. "s", action = "BLOCKED", status = "403" }) end -- 记录请求详情 function _M.log_request_details(client_ip, uri, method, user_agent) if not config.log_request_details then return end log(_M.LOG_DEBUG, "Request details", { ip = client_ip, uri = sanitize_string(uri), method = method, ua = sanitize_string(user_agent or "") }) end -- 记录错误日志 function _M.log_error(message, error_info) log(_M.LOG_ERROR, message, error_info) end -- 记录警告日志 function _M.log_warn(message, warn_info) log(_M.LOG_WARN, message, warn_info) end -- 记录信息日志 function _M.log_info(message, info) log(_M.LOG_INFO, message, info) end -- 刷新日志(确保所有日志已写入) function _M.flush() if config.log_file_handle then config.log_file_handle:flush() end end -- 清理资源(在worker退出时调用) function _M.cleanup() close_log_file() end return _M