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

471 lines
14 KiB
Lua

local Queue = require "kong.tools.queue"
local constants = require "kong.plugins.statsd.constants"
local statsd_logger = require "kong.plugins.statsd.statsd_logger"
local ws = require "kong.workspaces"
local ngx = ngx
local kong = kong
local ngx_time = ngx.time
local re_gsub = ngx.re.gsub
local pairs = pairs
local ipairs = ipairs
local string_format = string.format
local match = ngx.re.match
local ipairs = ipairs
local tonumber = tonumber
local knode = kong and kong.node or require "kong.pdk.node".new()
local null = ngx.null
local START_RANGE_IDX = 1
local END_RANGE_IDX = 2
local result_cache = setmetatable({}, { __mode = "k" })
local range_cache = setmetatable({}, { __mode = "k" })
local _M = {}
local function get_cache_value(cache, cache_key)
local cache_value = cache[cache_key]
if not cache_value then
cache_value = {}
cache[cache_key] = cache_value
end
return cache_value
end
local function extract_range(status_code_list, range)
local start_code, end_code
local ranges = get_cache_value(range_cache, status_code_list)
-- If range isn't in the cache, extract and put it in
if not ranges[range] then
local range_result, err = match(range, constants.REGEX_SPLIT_STATUS_CODES_BY_DASH, "oj")
if err then
kong.log.err(err)
return
end
ranges[range] = { range_result[START_RANGE_IDX], range_result[END_RANGE_IDX] }
end
start_code = ranges[range][START_RANGE_IDX]
end_code = ranges[range][END_RANGE_IDX]
return start_code, end_code
end
-- Returns true if a given status code is within status code ranges
local function is_in_range(status_code_list, status_code)
-- If there is no configuration then pass all response codes
if not status_code_list then
return true
end
local result_list = get_cache_value(result_cache, status_code_list)
local result = result_list[status_code]
-- If result is found in a cache then return results instantly
if result ~= nil then
return result
end
for _, range in ipairs(status_code_list) do
-- Get status code range splitting by "-" character
local start_code, end_code = extract_range(status_code_list, range)
-- Checks if there is both interval numbers
if start_code and end_code then
-- If HTTP response code is in the range return true
if status_code >= tonumber(start_code) and status_code <= tonumber(end_code) then
-- Storing results in a cache
result_list[status_code] = true
return true
end
end
end
-- Return false if there are no match for a given status code ranges and store it in cache
result_list[status_code] = false
return false
end
local worker_id
local hostname = re_gsub(knode.get_hostname(), [[\.]], "_", "oj")
-- downsample timestamp
local shdict_metrics_last_sent = 0
local SHDICT_METRICS_SEND_THRESHOLD = 60
local get_consumer_id = {
consumer_id = function(consumer)
return consumer and consumer.id
end,
custom_id = function(consumer)
return consumer and consumer.custom_id
end,
username = function(consumer)
return consumer and consumer.username
end
}
local get_service_id = {
service_id = function(service)
return service and service.id
end,
service_name = function(service)
return service and service.name
end,
service_host = function(service)
return service and service.host
end,
service_name_or_host = function(service)
return service and (service.name ~= null and
service.name or service.host)
end
}
local get_workspace_id = {
workspace_id = function()
return ws.get_workspace_id()
end,
workspace_name = function()
return ws.get_workspace_name()
end
}
local metrics = {
unique_users = function (scope_name, message, metric_config, logger, conf, tags)
if conf.tag_style then
-- skip unique_users in tag mode
return
end
local get_consumer_id = get_consumer_id[metric_config.consumer_identifier or conf.consumer_identifier_default]
local consumer_id = get_consumer_id(message.consumer)
if consumer_id then
local stat = string_format("%s.user.uniques", scope_name)
logger:send_statsd(stat, consumer_id, logger.stat_types.set)
end
end,
request_per_user = function (scope_name, message, metric_config, logger, conf, tags)
if conf.tag_style then
-- skip request_per_user in tag mode
return
end
local get_consumer_id = get_consumer_id[metric_config.consumer_identifier or conf.consumer_identifier_default]
local consumer_id = get_consumer_id(message.consumer)
if consumer_id then
local stat = string_format("%s.user.%s.request.count", scope_name, consumer_id)
logger:send_statsd(stat, 1, logger.stat_types.counter,
metric_config.sample_rate)
end
end,
status_count = function (scope_name, message, metric_config, logger, conf, tags)
if conf.tag_style then
-- skip status_count in tag mode
return
end
logger:send_statsd(string_format("%s.status.%s", scope_name, message.response.status),
1, logger.stat_types.counter, metric_config.sample_rate)
end,
status_count_per_user = function (scope_name, message, metric_config, logger, conf, tags)
if conf.tag_style then
-- skip status_count_per_user in tag mode
return
end
local get_consumer_id = get_consumer_id[metric_config.consumer_identifier or conf.consumer_identifier_default]
local consumer_id = get_consumer_id(message.consumer)
if consumer_id then
logger:send_statsd(string_format("%s.user.%s.status.%s", scope_name,
consumer_id, message.response.status),
1, logger.stat_types.counter,
metric_config.sample_rate)
end
end,
status_count_per_workspace = function (scope_name, message, metric_config, logger, conf, tags)
if conf.tag_style then
-- skip status_count_per_workspace in tag mode
return
end
local get_workspace_id = get_workspace_id[metric_config.workspace_identifier or conf.workspace_identifier_default]
local workspace_id = get_workspace_id()
if workspace_id then
logger:send_statsd(string_format("%s.workspace.%s.status.%s", scope_name,
workspace_id, message.response.status),
1, logger.stat_types.counter,
metric_config.sample_rate)
end
end,
status_count_per_user_per_route = function (_, message, metric_config, logger, conf, tags)
if conf.tag_style then
-- skip status_count_per_user_per_route in tag mode
return
end
local get_consumer_id = get_consumer_id[metric_config.consumer_identifier or conf.consumer_identifier_default]
local consumer_id = get_consumer_id(message.consumer)
if not consumer_id then
return
end
local route = message.route
if route.id then
logger:send_statsd(string_format("route.%s.user.%s.status.%s", route.id,
consumer_id, message.response.status),
1, logger.stat_types.counter,
metric_config.sample_rate)
end
end,
}
-- add shdict metrics
if ngx.config.ngx_lua_version >= 10011 then
metrics.shdict_usage = function (_, message, metric_config, logger, conf, tags)
-- we don't need this for every request, send every 1 minute
-- also only one worker needs to send this because it's shared
if worker_id ~= 0 then
return
end
local now = ngx_time()
if shdict_metrics_last_sent + SHDICT_METRICS_SEND_THRESHOLD < now then
shdict_metrics_last_sent = now
for shdict_name, shdict in pairs(ngx.shared) do
if conf.tag_style then
local tags = {
["node"] = hostname,
["shdict"] = shdict_name,
}
logger:send_statsd(string_format("shdict.free_space"),
shdict:free_space(), logger.stat_types.gauge,
metric_config.sample_rate, tags, conf.tag_style)
logger:send_statsd(string_format("shdict.capacity"),
shdict:capacity(), logger.stat_types.gauge,
metric_config.sample_rate, tags, conf.tag_style)
else
if conf.hostname_in_prefix then
logger:send_statsd(string_format("shdict.%s.free_space", shdict_name),
shdict:free_space(), logger.stat_types.gauge,
metric_config.sample_rate)
logger:send_statsd(string_format("shdict.%s.capacity", shdict_name),
shdict:capacity(), logger.stat_types.gauge,
metric_config.sample_rate)
else
logger:send_statsd(string_format("node.%s.shdict.%s.free_space",
hostname, shdict_name),
shdict:free_space(), logger.stat_types.gauge,
metric_config.sample_rate)
logger:send_statsd(string_format("node.%s.shdict.%s.capacity",
hostname, shdict_name),
shdict:capacity(), logger.stat_types.gauge,
metric_config.sample_rate)
end
end
end
end
end
end
local function get_scope_name(conf, message, service_identifier)
local api = message.api
local service = message.service
local scope_name
if service then
scope_name = "service."
-- don't fail on ce schema where service_identifier is not defined
if not service_identifier then
service_identifier = "service_name_or_host"
end
local service_name = get_service_id[service_identifier](service)
if not service_name or service_name == null then
scope_name = scope_name .. "unnamed"
else
scope_name = scope_name .. re_gsub(service_name, [[\.]], "_", "oj")
end
elseif api then
scope_name = "api."
if not api or api == null then
scope_name = scope_name .. "unnamed"
else
scope_name = scope_name .. re_gsub(api.name, [[\.]], "_", "oj")
end
else
-- TODO: this follows the pattern used by
-- https://github.com/Kong/kong/pull/2702 (which prevents an error from
-- being thrown and avoids confusing reports as per our metrics keys), but
-- as it stands, hides traffic from monitoring tools when the plugin is
-- configured globally. In fact, this basically disables this plugin when
-- it is configured to run globally, or per-consumer without an
-- API/Route/Service.
-- Changes in statsd-advanced: we still log these requests, but into a namespace of
-- "global.unmatched".
-- And we don't send upstream_latency and metrics with consumer or route
scope_name = "global.unmatched"
end
return scope_name
end
local function get_tags(conf, message, metric_config)
local tags = {}
local get_workspace_id = get_workspace_id[metric_config.workspace_identifier or conf.workspace_identifier_default]
local workspace_id = get_workspace_id()
local get_service_id = get_service_id[metric_config.service_identifier or conf.service_identifier_default]
local service_id = get_service_id(message.service)
local get_consumer_id = get_consumer_id[metric_config.consumer_identifier or conf.consumer_identifier_default]
local consumer_id = get_consumer_id(message.consumer)
if service_id then
tags["service"] = service_id
else
-- do not record any stats if the service is not present
return
end
local route_name
if message and message.route then
route_name = message.route.name or message.route.id
tags["route"] = route_name
end
if workspace_id then
tags["workspace"] = workspace_id
end
if workspace_id then
tags["consumer"] = consumer_id
end
if hostname then
tags["node"] = hostname
end
if message and message.response and message.response.status then
tags["status"] = message.response.status
end
return tags
end
local function send_entries_to_upstream_server(conf, messages)
local logger, err = statsd_logger:new(conf)
if err then
kong.log.err("failed to create Statsd logger: ", err)
return false, err
end
for _, message in ipairs(messages) do
local stat_name = {
request_size = "request.size",
response_size = "response.size",
latency = "latency",
upstream_latency = "upstream_latency",
kong_latency = "kong_latency",
request_count = "request.count",
}
local stat_value = {
request_size = message.request.size,
response_size = message.response.size,
latency = message.latencies.request,
upstream_latency = message.latencies.proxy,
kong_latency = message.latencies.kong,
request_count = 1,
}
for _, metric_config in pairs(conf.metrics) do
local metric_config_name = metric_config.name
local metric = metrics[metric_config_name]
local name
local tags
if conf.tag_style then
tags = get_tags(conf, message, metric_config)
else
name = get_scope_name(conf, message, metric_config.service_identifier or conf.service_identifier_default)
end
if metric then
metric(name, message, metric_config, logger, conf, tags)
else
local stat_name = stat_name[metric_config_name]
local stat_value = stat_value[metric_config_name]
if stat_value ~= nil and stat_value ~= -1 then
if conf.tag_style then
logger:send_statsd(stat_name, stat_value,
logger.stat_types[metric_config.stat_type],
metric_config.sample_rate, tags, conf.tag_style)
else
logger:send_statsd(name .. "." .. stat_name, stat_value,
logger.stat_types[metric_config.stat_type],
metric_config.sample_rate)
end
end
end
end
end
logger:close_socket()
return true
end
function _M.execute(conf)
if not is_in_range(conf.allow_status_codes, ngx.status) then
return
end
kong.log.debug("Status code is within given status code ranges")
if not worker_id then
worker_id = ngx.worker.id() or -1
end
conf._prefix = conf.prefix
if conf.hostname_in_prefix and conf.tag_style == nil then
conf._prefix = conf._prefix .. ".node." .. hostname
end
local message = kong.log.serialize({ngx = ngx, kong = kong, })
message.cache_metrics = ngx.ctx.cache_metrics
local ok, err = Queue.enqueue(
Queue.get_plugin_params("statsd", conf),
send_entries_to_upstream_server,
conf,
message
)
if not ok then
kong.log.err("Failed to enqueue log entry to StatsD server: ", err)
end
end
-- only for test
_M.is_in_range = is_in_range
return _M