Files
2026-07-13 12:32:21 +08:00

344 lines
9.8 KiB
Lua

local get_request = require "resty.core.base".get_request
local ngx = ngx
local type = type
local pcall = pcall
local select = select
local ipairs = ipairs
local assert = assert
local ngx_log = ngx.log
local ngx_WARN = ngx.WARN
local ngx_get_phase = ngx.get_phase
local _M = {}
local NON_FUNCTION_HOOKS = {
--[[
[group_name] = {
[hook_name] = <function>,
...
},
...
--]]
}
local ALWAYS_ENABLED_GROUPS = {}
local function should_execute_original_func(group_name)
if ALWAYS_ENABLED_GROUPS[group_name] then
return
end
local phase = ngx_get_phase()
if phase == "init" or phase == "init_worker" then
return true
end
local dynamic_hook = ngx.ctx.dynamic_hook
if not dynamic_hook then
return true
end
local enabled_groups = dynamic_hook.enabled_groups
if not enabled_groups[group_name] then
return true
end
end
local function execute_hook_vararg(hook, hook_type, group_name, ...)
if not hook then
return
end
local ok, err = pcall(hook, ...)
if not ok then
ngx_log(ngx_WARN, "failed to run ", hook_type, " hook of ", group_name, ": ", err)
end
end
local function execute_hooks_vararg(hooks, hook_type, group_name, ...)
if not hooks then
return
end
for _, hook in ipairs(hooks) do
execute_hook_vararg(hook, hook_type, group_name, ...)
end
end
local function execute_after_hooks_vararg(handlers, group_name, ...)
execute_hooks_vararg(handlers.afters, "after", group_name, ...)
return ...
end
local function wrap_function_vararg(group_name, original_func, handlers)
return function (...)
if should_execute_original_func(group_name) then
return original_func(...)
end
execute_hooks_vararg(handlers.befores, "before", group_name, ...)
return execute_after_hooks_vararg(handlers, group_name, original_func(...))
end
end
local function execute_hook(hook, hook_type, group_name, a1, a2, a3, a4, a5, a6, a7, a8)
if not hook then
return
end
local ok, err = pcall(hook, a1, a2, a3, a4, a5, a6, a7, a8)
if not ok then
ngx_log(ngx_WARN, "failed to run ", hook_type, " hook of ", group_name, ": ", err)
end
end
local function execute_hooks(hooks, hook_type, group_name, a1, a2, a3, a4, a5, a6, a7, a8)
if not hooks then
return
end
for _, hook in ipairs(hooks) do
execute_hook(hook, hook_type, group_name, a1, a2, a3, a4, a5, a6, a7, a8)
end
end
local function execute_original_func(max_args, original_func, a1, a2, a3, a4, a5, a6, a7, a8)
if max_args == 0 then
return original_func()
elseif max_args == 1 then
return original_func(a1)
elseif max_args == 2 then
return original_func(a1, a2)
elseif max_args == 3 then
return original_func(a1, a2, a3)
elseif max_args == 4 then
return original_func(a1, a2, a3, a4)
elseif max_args == 5 then
return original_func(a1, a2, a3, a4, a5)
elseif max_args == 6 then
return original_func(a1, a2, a3, a4, a5, a6)
elseif max_args == 7 then
return original_func(a1, a2, a3, a4, a5, a6, a7)
else
return original_func(a1, a2, a3, a4, a5, a6, a7, a8)
end
end
local function wrap_function(max_args, group_name, original_func, handlers)
return function(a1, a2, a3, a4, a5, a6, a7, a8)
local r1, r2, r3, r4, r5, r6, r7, r8
if should_execute_original_func(group_name) then
r1, r2, r3, r4, r5, r6, r7, r8 = execute_original_func(max_args, original_func, a1, a2, a3, a4, a5, a6, a7, a8)
else
execute_hooks(handlers.befores, "before", group_name, a1, a2, a3, a4, a5, a6, a7, a8)
r1, r2, r3, r4, r5, r6, r7, r8 = execute_original_func(max_args, original_func, a1, a2, a3, a4, a5, a6, a7, a8)
execute_hooks(handlers.afters, "after", group_name, r1, r2, r3, r4, r5, r6, r7, r8)
end
return r1, r2, r3, r4, r5, r6, r7, r8
end
end
--- Hooks (patches) a function
-- Hooks "before" and "after" handlers to a function. The function is patched
-- to execute the handlers when it is called. The `parent` and `function_key`
-- parameters are used to identify and patch the function to be hooked.
--
-- @function dynamic_hook:hook_function
-- @tparam string group_name The name of the hook group
-- @tparam table parent The table containing the function to be hooked
-- @tparam string function_key The key (in the `parent` table) of the function
-- to be hooked
-- @tparam number max_args The maximum number of arguments the function accepts
-- @tparam table handlers A table containing the `before` and `after` handlers.
-- The table may contain the keys listed below:
-- * table `befores` array of handlers to execute before the function
-- * table `afters` array of handlers to execute before the function
--
-- @usage
-- -- Define a "before" handler to be executed before the _G.print function
-- dynamic_hook.hook_function("my_group", _G, "print", "varargs", {
-- befores = { before_handler },
-- })
function _M.hook_function(group_name, parent, function_key, max_args, handlers)
assert(type(group_name) == "string", "group_name must be a string")
assert(type(parent) == "table", "parent must be a table")
assert(type(function_key) == "string", "function_key must be a string")
local is_varargs = max_args == "varargs"
if not is_varargs then
assert(type(max_args) == "number", 'max_args must be a number or "varargs"')
assert(max_args >= 0 and max_args <= 8, 'max_args must be >= 0 and <= 8, or "varargs"')
end
local original_func = parent[function_key]
assert(type(original_func) == "function", "parent[" .. function_key .. "] must be a function")
if is_varargs then
parent[function_key] = wrap_function_vararg(group_name, original_func, handlers)
else
parent[function_key] = wrap_function(max_args, group_name, original_func, handlers)
end
end
--- Registers a new hook
-- The hook handler function is executed when `run_hook` is called with the
-- same `group_name` and `hook_name`.
--
-- @function dynamic_hook:hook
-- @tparam string group_name The name of the hook group
-- @tparam string hook_name The name of the hook
-- @tparam table handler The hook function
function _M.hook(group_name, hook_name, handler)
assert(type(group_name) == "string", "group_name must be a string")
assert(type(hook_name) == "string", "hook_name must be a string")
assert(type(handler) == "function", "handler must be a function")
local hooks = NON_FUNCTION_HOOKS[group_name]
if not hooks then
hooks = {}
NON_FUNCTION_HOOKS[group_name] = hooks
end
hooks[hook_name] = handler
end
--- Checks if a hook group is enabled.
-- If a group is enabled, its hooks can be executed when `run_hook` is called
-- with the corresponding `group_name` and `hook_name` parameters.
--
-- @function dynamic_hook:is_group_enabled
-- @tparam string group_name The name of the hook group
-- @treturn boolean `true` if the group is enabled, `false` otherwise
function _M.is_group_enabled(group_name)
assert(type(group_name) == "string", "group_name must be a string")
if ALWAYS_ENABLED_GROUPS[group_name] then
return true
end
if not get_request() then
return false
end
local dynamic_hook = ngx.ctx.dynamic_hook
if not dynamic_hook then
return false
end
local enabled_groups = dynamic_hook.enabled_groups
if not enabled_groups[group_name] then
return false
end
return true
end
--- Runs a hook
-- Runs the hook registered for the given `group_name` and `hook_name` (if the
-- group is enabled).
--
-- @function dynamic_hook:run_hook
-- @tparam string group_name The name of the hook group
-- @tparam string hook_name The name of the hook
-- @tparam any `a1, a2, ..., a8` Arguments passed to the hook function
-- @tparam any ... Additional arguments passed to the hook function
-- @usage
-- -- Run the "my_hook" hook of the "my_group" group
-- dynamic_hook.run_hook("my_group", "my_hook", arg1, arg2)
function _M.run_hook(group_name, hook_name, a1, a2, a3, a4, a5, a6, a7, a8, ...)
assert(type(group_name) == "string", "group_name must be a string")
assert(type(hook_name) == "string", "hook_name must be a string")
if not _M.is_group_enabled(group_name) then
return
end
local hooks = NON_FUNCTION_HOOKS[group_name]
if not hooks then
return
end
local handler = hooks[hook_name]
if not handler then
return
end
local argc = select("#", ...)
local ok, err
if argc == 0 then
ok, err = pcall(handler, a1, a2, a3, a4, a5, a6, a7, a8)
else
ok, err = pcall(handler, a1, a2, a3, a4, a5, a6, a7, a8, ...)
end
if not ok then
ngx_log(ngx_WARN, "failed to run dynamic hook ", group_name, ".", hook_name, ": ", err)
end
end
--- Enables a hook group for the current request
--
-- @function dynamic_hook:enable_on_this_request
-- @tparam string group_name The name of the hook group to enable
-- @tparam table (optional) ngx_ctx The Nginx context object
function _M.enable_on_this_request(group_name, ngx_ctx)
assert(type(group_name) == "string", "group_name must be a string")
ngx_ctx = ngx_ctx or ngx.ctx
if ngx_ctx.dynamic_hook then
ngx_ctx.dynamic_hook.enabled_groups[group_name] = true
else
ngx_ctx.dynamic_hook = {
enabled_groups = {
[group_name] = true
},
}
end
end
--- Enables a hook group for all requests
--
-- @function dynamic_hook:enable_by_default
-- @tparam string group_name The name of the hook group to enable
function _M.enable_by_default(group_name)
assert(type(group_name) == "string", "group_name must be a string")
ALWAYS_ENABLED_GROUPS[group_name] = true
end
--- Disables a hook group that was enabled with `enable_by_default`
--
-- @function dynamic_hook:disable_by_default
-- @tparam string group_name The name of the hook group to disable
function _M.disable_by_default(group_name)
assert(type(group_name) == "string", "group_name must be a string")
ALWAYS_ENABLED_GROUPS[group_name] = nil
end
return _M