471 lines
14 KiB
Lua
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
|