diff --git a/kong-0.10.3-0.rockspec b/kong-0.10.3-0.rockspec index 9d88c0811ce..ab6cea3e414 100644 --- a/kong-0.10.3-0.rockspec +++ b/kong-0.10.3-0.rockspec @@ -255,6 +255,8 @@ build = { ["kong.plugins.loggly.handler"] = "kong/plugins/loggly/handler.lua", ["kong.plugins.loggly.schema"] = "kong/plugins/loggly/schema.lua", + ["kong.plugins.datadog.migrations.cassandra"] = "kong/plugins/datadog/migrations/cassandra.lua", + ["kong.plugins.datadog.migrations.postgres"] = "kong/plugins/datadog/migrations/postgres.lua", ["kong.plugins.datadog.handler"] = "kong/plugins/datadog/handler.lua", ["kong.plugins.datadog.schema"] = "kong/plugins/datadog/schema.lua", ["kong.plugins.datadog.statsd_logger"] = "kong/plugins/datadog/statsd_logger.lua", diff --git a/kong/plugins/datadog/handler.lua b/kong/plugins/datadog/handler.lua index ac5c7f14c74..cf4868c3eab 100644 --- a/kong/plugins/datadog/handler.lua +++ b/kong/plugins/datadog/handler.lua @@ -1,76 +1,136 @@ -local BasePlugin = require "kong.plugins.base_plugin" +local BasePlugin = require "kong.plugins.base_plugin" local basic_serializer = require "kong.plugins.log-serializers.basic" -local statsd_logger = require "kong.plugins.datadog.statsd_logger" +local statsd_logger = require "kong.plugins.datadog.statsd_logger" -local DatadogHandler = BasePlugin:extend() +local ngx_log = ngx.log +local ngx_timer_at = ngx.timer.at +local string_gsub = string.gsub +local pairs = pairs +local string_format = string.format +local NGX_ERR = ngx.ERR + + +local DatadogHandler = BasePlugin:extend() DatadogHandler.PRIORITY = 1 -local ngx_timer_at = ngx.timer.at -local string_gsub = string.gsub -local ipairs = ipairs -local gauges = { - request_size = function (api_name, message, logger, tags) - local stat = api_name .. ".request.size" - logger:gauge(stat, message.request.size, 1, tags) +local get_consumer_id = { + consumer_id = function(consumer) + return consumer and string_gsub(consumer.id, "-", "_") end, - response_size = function (api_name, message, logger, tags) - local stat = api_name .. ".response.size" - logger:gauge(stat, message.response.size, 1, tags) + custom_id = function(consumer) + return consumer and consumer.custom_id end, - status_count = function (api_name, message, logger, tags) - local stat = api_name .. ".request.status." .. message.response.status - logger:counter(stat, 1, 1, tags) - end, - latency = function (api_name, message, logger, tags) - local stat = api_name .. ".latency" - logger:gauge(stat, message.latencies.request, 1, tags) + username = function(consumer) + return consumer and consumer.username + end +} + + +local metrics = { + status_count = function (api_name, message, metric_config, logger) + local fmt = string_format("%s.request.status", api_name, + message.response.status) + + logger:send_statsd(string_format("%s.%s", fmt, message.response.status), + 1, logger.stat_types.counter, + metric_config.sample_rate, metric_config.tags) + + logger:send_statsd(string_format("%s.%s", fmt, "total"), 1, + logger.stat_types.counter, + metric_config.sample_rate, metric_config.tags) end, - request_count = function (api_name, message, logger, tags) - local stat = api_name .. ".request.count" - logger:counter(stat, 1, 1, tags) + unique_users = function (api_name, message, metric_config, logger) + local get_consumer_id = get_consumer_id[metric_config.consumer_identifier] + local consumer_id = get_consumer_id(message.consumer) + + if consumer_id then + local stat = string_format("%s.user.uniques", api_name) + + logger:send_statsd(stat, consumer_id, logger.stat_types.set, + nil, metric_config.tags) + end end, - unique_users = function (api_name, message, logger, tags) - if message.authenticated_entity ~= nil and message.authenticated_entity.consumer_id ~= nil then - local stat = api_name .. ".user.uniques" - logger:set(stat, message.authenticated_entity.consumer_id, tags) + request_per_user = function (api_name, message, metric_config, logger) + local get_consumer_id = get_consumer_id[metric_config.consumer_identifier] + local consumer_id = get_consumer_id(message.consumer) + + if consumer_id then + local stat = string_format("%s.user.%s.request.count", api_name, consumer_id) + + logger:send_statsd(stat, 1, logger.stat_types.counter, + metric_config.sample_rate, metric_config.tags) end end, - request_per_user = function (api_name, message, logger, tags) - if message.authenticated_entity ~= nil and message.authenticated_entity.consumer_id ~= nil then - local stat = api_name .. "." .. string_gsub(message.authenticated_entity.consumer_id, "-", "_") .. ".request.count" - logger:counter(stat, 1, 1, tags) + status_count_per_user = function (api_name, message, metric_config, logger) + local get_consumer_id = get_consumer_id[metric_config.consumer_identifier] + local consumer_id = get_consumer_id(message.consumer) + + if consumer_id then + local fmt = string_format("%s.user.%s.request.status", api_name, consumer_id) + + logger:send_statsd(string_format("%s.%s", fmt, message.response.status), + 1, logger.stat_types.counter, + metric_config.sample_rate, metric_config.tags) + + logger:send_statsd(string_format("%s.%s", fmt, "total"), + 1, logger.stat_types.counter, + metric_config.sample_rate, metric_config.tags) end end, - upstream_latency = function (api_name, message, logger, tags) - local stat = api_name .. ".upstream_latency" - logger:gauge(stat, message.latencies.proxy, 1, tags) - end } + local function log(premature, conf, message) if premature then return end + + local api_name = string_gsub(message.api.name, "%.", "_") + local stat_name = { + request_size = api_name .. ".request.size", + response_size = api_name .. ".response.size", + latency = api_name .. ".latency", + upstream_latency = api_name .. ".upstream_latency", + kong_latency = api_name .. ".kong_latency", + request_count = api_name .. ".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, + } + local logger, err = statsd_logger:new(conf) if err then - ngx.log(ngx.ERR, "failed to create Statsd logger: ", err) + ngx_log(NGX_ERR, "failed to create Statsd logger: ", err) return end - - local api_name = string_gsub(message.api.name, "%.", "_") - for _, metric in ipairs(conf.metrics) do - local gauge = gauges[metric] - if gauge then - - gauge(api_name, message, logger, conf.tags[metric]) + + for _, metric_config in pairs(conf.metrics) do + local metric = metrics[metric_config.name] + + if metric then + metric(api_name, message, metric_config, logger) + + else + local stat_name = stat_name[metric_config.name] + local stat_value = stat_value[metric_config.name] + + logger:send_statsd(stat_name, stat_value, + logger.stat_types[metric_config.stat_type], + metric_config.sample_rate, metric_config.tags) end end logger:close_socket() end + function DatadogHandler:new() DatadogHandler.super.new(self, "datadog") end @@ -81,7 +141,7 @@ function DatadogHandler:log(conf) local ok, err = ngx_timer_at(0, log, conf, message) if not ok then - ngx.log(ngx.ERR, "failed to create timer: ", err) + ngx_log(NGX_ERR, "failed to create timer: ", err) end end diff --git a/kong/plugins/datadog/migrations/cassandra.lua b/kong/plugins/datadog/migrations/cassandra.lua new file mode 100644 index 00000000000..50fa1986781 --- /dev/null +++ b/kong/plugins/datadog/migrations/cassandra.lua @@ -0,0 +1,91 @@ +return { + { + name = "2017-06-09-160000_datadog_schema_changes", + up = function(_, _, dao) + + local plugins, err = dao.plugins:find_all { name = "datadog" } + if err then + return err + end + + local default_metrics = { + request_count = { + name = "request_count", + stat_type = "counter", + sample_rate = 1, + }, + latency = { + name = "latency", + stat_type = "gauge", + sample_rate = 1, + }, + request_size = { + name = "request_size", + stat_type = "gauge", + sample_rate = 1, + }, + status_count = { + name = "status_count", + stat_type = "counter", + sample_rate = 1, + }, + response_size = { + name = "response_size", + stat_type = "timer", + }, + unique_users = { + name = "unique_users", + stat_type = "set", + consumer_identifier = "consumer_id", + }, + request_per_user = { + name = "request_per_user", + stat_type = "counter", + sample_rate = 1, + consumer_identifier = "consumer_id", + }, + upstream_latency = { + name = "upstream_latency", + stat_type = "gauge", + sample_rate = 1, + }, + } + + for i = 1, #plugins do + local datadog = plugins[i] + local _, err = dao.plugins:delete(datadog) + if err then + return err + end + + local tags = datadog.config.tags or {} + local new_metrics = {} + if datadog.config.metrics then + for _, metric in ipairs(datadog.config.metrics) do + local new_metric = default_metrics[metric] + if new_metric then + new_metric.tags = tags[metric] + table.insert(new_metrics, new_metric) + end + end + end + + local _, err = dao.plugins:insert { + name = "datadog", + api_id = datadog.api_id, + enabled = datadog.enabled, + config = { + host = datadog.config.host, + port = datadog.config.port, + metrics = new_metrics, + prefix = "kong", + } + } + + if err then + return err + end + end + end + } +} diff --git a/kong/plugins/datadog/migrations/postgres.lua b/kong/plugins/datadog/migrations/postgres.lua new file mode 100644 index 00000000000..34d409d9186 --- /dev/null +++ b/kong/plugins/datadog/migrations/postgres.lua @@ -0,0 +1,93 @@ +return { + { + name = "2017-06-09-160000_datadog_schema_changes", + up = function(_, _, dao) + + local plugins, err = dao.plugins:find_all { name = "datadog" } + if err then + return err + end + + local default_metrics = { + request_count = { + name = "request_count", + stat_type = "counter", + sample_rate = 1, + }, + latency = { + name = "latency", + stat_type = "gauge", + sample_rate = 1, + }, + request_size = { + name = "request_size", + stat_type = "gauge", + sample_rate = 1, + }, + status_count = { + name = "status_count", + stat_type = "counter", + sample_rate = 1, + }, + response_size = { + name = "response_size", + stat_type = "timer", + }, + unique_users = { + name = "unique_users", + stat_type = "set", + consumer_identifier = "consumer_id", + }, + request_per_user = { + name = "request_per_user", + stat_type = "counter", + sample_rate = 1, + consumer_identifier = "consumer_id", + }, + upstream_latency = { + name = "upstream_latency", + stat_type = "gauge", + sample_rate = 1, + }, + } + + for i = 1, #plugins do + local datadog = plugins[i] + local _, err = dao.plugins:delete(datadog) + if err then + return err + end + + local tags = datadog.config.tags or {} + local new_metrics = {} + if datadog.config.metrics then + for _, metric in ipairs(datadog.config.metrics) do + local new_metric = default_metrics[metric] + if new_metric then + new_metric.tags = tags[metric] + table.insert(new_metrics, new_metric) + end + end + end + + local _, err = dao.plugins:insert { + name = "datadog", + api_id = datadog.api_id, + enabled = datadog.enabled, + config = { + host = datadog.config.host, + port = datadog.config.port, + metrics = new_metrics, + prefix = "kong", + } + } + + if err then + return err + end + end + end + }, + down = function() + end, +} diff --git a/kong/plugins/datadog/schema.lua b/kong/plugins/datadog/schema.lua index f2086e1773c..f6db2bdb1c0 100644 --- a/kong/plugins/datadog/schema.lua +++ b/kong/plugins/datadog/schema.lua @@ -1,48 +1,202 @@ -local find = string.find +local find = string.find local pl_utils = require "pl.utils" + local metrics = { - "request_count", - "latency", - "request_size", - "status_count", - "response_size", - "unique_users", - "request_per_user", - "upstream_latency" + ["request_count"] = true, + ["latency"] = true, + ["request_size"] = true, + ["status_count"] = true, + ["response_size"] = true, + ["unique_users"] = true, + ["request_per_user"] = true, + ["upstream_latency"] = true, + ["kong_latency"] = true, + ["status_count_per_user"] = true, } + +local stat_types = { + ["gauge"] = true, + ["timer"] = true, + ["counter"] = true, + ["histogram"] = true, + ["meter"] = true, + ["set"] = true, +} + +local consumer_identifiers = { + ["consumer_id"] = true, + ["custom_id"] = true, + ["username"] = true, +} + +local default_metrics = { + { + name = "request_count", + stat_type = "counter", + sample_rate = 1, + tags = {"app:kong"} + }, + { + name = "latency", + stat_type = "timer", + tags = {"app:kong"} + }, + { + name = "request_size", + stat_type = "timer", + tags = {"app:kong"} + }, + { + name = "status_count", + stat_type = "counter", + sample_rate = 1, + tags = {"app:kong"} + }, + { + name = "response_size", + stat_type = "timer", + tags = {"app:kong"} + }, + { + name = "unique_users", + stat_type = "set", + consumer_identifier = "custom_id", + tags = {"app:kong"} + }, + { + name = "request_per_user", + stat_type = "counter", + sample_rate = 1, + consumer_identifier = "custom_id", + tags = {"app:kong"} + }, + { + name = "upstream_latency", + stat_type = "timer", + tags = {"app:kong"} + }, + { + name = "kong_latency", + stat_type = "timer", + tags = {"app:kong"} + }, + { + name = "status_count_per_user", + stat_type = "counter", + sample_rate = 1, + consumer_identifier = "custom_id", + tags = {"app:kong"} + } +} + + -- entries must have colons to set the key and value apart -local function check_for_value(value) - for i, entry in ipairs(value) do +local function check_tag_value(value) + if value == nil then + return true + end + + for _, entry in ipairs(value) do local ok = find(entry, ":") - if ok then - local _,next = pl_utils.splitv(entry, ':') + if ok then + local _, next = pl_utils.splitv(entry, ':') if not next or #next == 0 then - return false, "key '" .. entry .. "' has no value, " + return nil, "key '" .. entry .. "' has no value" end end end + return true end + +local function check_schema(value) + for _, entry in ipairs(value) do + + if not entry.name or not entry.stat_type then + return false, "name and stat_type must be defined for all stats" + end + + if not metrics[entry.name] then + return false, "unrecognized metric name: " .. entry.name + end + + if not stat_types[entry.stat_type] then + return false, "unrecognized stat_type: " .. entry.stat_type + end + + local tag_ok, tag_error = check_tag_value(entry.tags) + if not tag_ok then + return false, "malformed tags: " .. tag_error + .. ". Tags must be list of key[:value]" + end + + if entry.name == "unique_users" and entry.stat_type ~= "set" then + return false, "unique_users metric only works with stat_type 'set'" + end + + if (entry.stat_type == "counter" or entry.stat_type == "gauge") + and ((not entry.sample_rate) or (entry.sample_rate + and type(entry.sample_rate) ~= "number") + or (entry.sample_rate and entry.sample_rate < 1)) then + + return false, "sample rate must be defined for counters and gauges." + end + + if (entry.name == "status_count_per_user" + or entry.name == "request_per_user" or entry.name == "unique_users") + and not entry.consumer_identifier then + + return false, "consumer_identifier must be defined for metric " .. + entry.name + end + + if (entry.name == "status_count_per_user" + or entry.name == "request_per_user" + or entry.name == "unique_users") + and entry.consumer_identifier then + + if not consumer_identifiers[entry.consumer_identifier] then + + return false, "invalid consumer_identifier for metric '" .. + entry.name .. + "'. Choices are consumer_id, custom_id, and username" + end + end + + if (entry.name == "status_count" + or entry.name == "status_count_per_user" + or entry.name == "request_per_user") + and entry.stat_type ~= "counter" then + + return false, entry.name .. " metric only works with stat_type 'counter'" + end + end + + return true +end + + return { fields = { - host = {required = true, type = "string", default = "localhost"}, - port = {required = true, type = "number", default = 8125}, - metrics = {required = true, type = "array", enum = metrics, default = metrics}, - tags = { - type = "table", - schema = { - fields = { - request_count = {type = "array", default = {}, func = check_for_value}, - latency = {type = "array", default = {}, func = check_for_value}, - request_size = {type = "array", default = {}, func = check_for_value}, - status_count = {type = "array", default = {}, func = check_for_value}, - response_size = {type = "array", default = {}, func = check_for_value}, - unique_users = {type = "array", default = {}, func = check_for_value}, - request_per_user = {type = "array", default = {}, func = check_for_value}, - upstream_latency = {type = "array", default = {}, func = check_for_value} - } - } + host = { + required = true, + type = "string", + default = "localhost", + }, + port = { + required = true, + type = "number", + default = 8125, + }, + metrics = { + required = true, + type = "array", + default = default_metrics, + func = check_schema, + }, + prefix = { + type = "string", + default = "kong", }, - timeout = {type = "number", default = 10000} } } diff --git a/kong/plugins/datadog/statsd_logger.lua b/kong/plugins/datadog/statsd_logger.lua index aa0e8225f91..e5b07a088b6 100644 --- a/kong/plugins/datadog/statsd_logger.lua +++ b/kong/plugins/datadog/statsd_logger.lua @@ -1,94 +1,83 @@ local ngx_socket_udp = ngx.socket.udp -local ngx_log = ngx.log -local table_concat = table.concat -local setmetatable = setmetatable -local NGX_ERR = ngx.ERR -local NGX_DEBUG = ngx.DEBUG +local ngx_log = ngx.log +local table_concat = table.concat +local setmetatable = setmetatable +local NGX_ERR = ngx.ERR +local NGX_DEBUG = ngx.DEBUG +local fmt = string.format +local tostring = tostring + + +local stat_types = { + gauge = "g", + counter = "c", + timer = "ms", + histogram = "h", + meter = "m", + set = "s", +} local statsd_mt = {} statsd_mt.__index = statsd_mt + function statsd_mt:new(conf) - local sock = ngx_socket_udp() - sock:settimeout(conf.timeout) - local ok, err = sock:setpeername(conf.host, conf.port) - if not ok then - return nil, "failed to connect to " .. conf.host .. ":" .. conf.port .. ": " .. err + local sock = ngx_socket_udp() + local _, err = sock:setpeername(conf.host, conf.port) + if err then + return nil, fmt("failed to connect to %s:%s: %s", conf.host, + tostring(conf.port), err) end local statsd = { - host = conf.host, - port = conf.port, - socket = sock, + host = conf.host, + port = conf.port, + prefix = conf.prefix, + socket = sock, + stat_types = stat_types, } - return setmetatable(statsd, statsd_mt) end -function statsd_mt:create_statsd_message(stat, delta, kind, sample_rate, tags) + +local function statsd_message(prefix, stat, delta, kind, sample_rate, tags) local rate = "" local str_tags = "" + if sample_rate and sample_rate ~= 1 then rate = "|@" .. sample_rate end - + if tags and #tags > 0 then str_tags = "|#" .. table_concat(tags, ",") end - local message = { - "kong.", - stat, - ":", - delta, - "|", - kind, - rate, - str_tags - } - return table_concat(message, "") + return fmt("%s.%s:%s|%s%s%s", prefix, stat, + delta, kind, rate, str_tags) end + function statsd_mt:close_socket() local ok, err = self.socket:close() if not ok then - ngx_log(NGX_ERR, "[udp-log] failed to close connection from ", self.host, ":", self.port, ": ", err) + ngx_log(NGX_ERR, "[udp-log] failed to close connection from ", + self.host, ":", self.port, ": ", err) end end + function statsd_mt:send_statsd(stat, delta, kind, sample_rate, tags) - local udp_message = self:create_statsd_message(stat, delta, kind, sample_rate, tags) + local udp_message = statsd_message(self.prefix or "kong", stat, + delta, kind, sample_rate, tags) - ngx_log(NGX_DEBUG, "[udp-log] sending data to statsd server: ", udp_message) + ngx_log(NGX_DEBUG, fmt("Sending data to statsd server: %s", udp_message)) local ok, err = self.socket:send(udp_message) if not ok then - ngx_log(NGX_ERR, "[udp-log] could not send data to ", self.host, ":", self.port, ": ", err) + ngx_log(NGX_ERR, fmt("failed to send data to %s:%s: %s", self.host, + tostring(self.port), err)) end end -function statsd_mt:gauge(stat, value, sample_rate, tags) - return self:send_statsd(stat, value, "g", sample_rate, tags) -end - -function statsd_mt:counter(stat, value, sample_rate, tags) - return self:send_statsd(stat, value, "c", sample_rate, tags) -end - -function statsd_mt:timer(stat, ms, tags) - return self:send_statsd(stat, ms, "ms", nil, tags) -end - -function statsd_mt:histogram(stat, value, tags) - return self:send_statsd(stat, value, "h", nil, tags) -end - -function statsd_mt:meter(stat, value, tags) - return self:send_statsd(stat, value, "m", nil, tags) -end - -function statsd_mt:set(stat, value, tags) - return self:send_statsd(stat, value, "s", nil, tags) -end return statsd_mt diff --git a/spec/03-plugins/08-datadog/01-log_spec.lua b/spec/03-plugins/08-datadog/01-log_spec.lua index e1ff2cd14eb..b2430779d52 100644 --- a/spec/03-plugins/08-datadog/01-log_spec.lua +++ b/spec/03-plugins/08-datadog/01-log_spec.lua @@ -4,29 +4,49 @@ local threads = require "llthreads2.ex" describe("Plugin: datadog (log)", function() local client setup(function() + local consumer1 = assert(helpers.dao.consumers:insert { + username = "foo", + custom_id = "bar" + }) + assert(helpers.dao.keyauth_credentials:insert { + key = "kong", + consumer_id = consumer1.id + }) + local api1 = assert(helpers.dao.apis:insert { - name = "datadog1_com", + name = "dd1", hosts = { "datadog1.com" }, upstream_url = "http://mockbin.com" }) + assert(helpers.dao.plugins:insert { + name = "key-auth", + api_id = api1.id + }) local api2 = assert(helpers.dao.apis:insert { - name = "datadog2_com", + name = "dd2", hosts = { "datadog2.com" }, upstream_url = "http://mockbin.com" }) local api3 = assert(helpers.dao.apis:insert { - name = "datadog3_com", + name = "dd3", hosts = { "datadog3.com" }, upstream_url = "http://mockbin.com" }) - + local api4 = assert(helpers.dao.apis:insert { + name = "dd4", + hosts = { "datadog4.com" }, + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "key-auth", + api_id = api4.id + }) assert(helpers.dao.plugins:insert { name = "datadog", api_id = api1.id, config = { host = "127.0.0.1", - port = 9999, - tags = {} + port = 9999 } }) assert(helpers.dao.plugins:insert { @@ -35,8 +55,18 @@ describe("Plugin: datadog (log)", function() config = { host = "127.0.0.1", port = 9999, - metrics = "request_count,status_count", - tags = {} + metrics = { + { + name = "status_count", + stat_type = "counter", + sample_rate = 1 + }, + { + name = "request_count", + stat_type = "counter", + sample_rate = 1 + } + } } }) assert(helpers.dao.plugins:insert { @@ -45,14 +75,37 @@ describe("Plugin: datadog (log)", function() config = { host = "127.0.0.1", port = 9999, - metrics = "request_count,status_count,latency", - tags = { - request_count = {"T1:V1"}, - status_count = {"T2:V2,T3:V3,T4"}, - latency = {"T2:V2:V3,T4"}, + metrics = { + { + name = "status_count", + stat_type = "counter", + sample_rate = 1, + tags = {"T1:V1"}, + }, + { + name = "request_count", + stat_type = "counter", + sample_rate = 1, + tags = {"T2:V2,T3:V3,T4"}, + }, + { + name = "latency", + stat_type = "gauge", + sample_rate = 1, + tags = {"T2:V2:V3,T4"} + } } } }) + assert(helpers.dao.plugins:insert { + name = "datadog", + api_id = api4.id, + config = { + host = "127.0.0.1", + port = 9999, + prefix = "prefix" + } + }) assert(helpers.start_kong()) client = helpers.proxy_client() @@ -71,7 +124,7 @@ describe("Plugin: datadog (log)", function() server:setoption("reuseaddr", true) server:setsockname("127.0.0.1", 9999) local gauges = {} - for _ = 1, 6 do + for _ = 1, 12 do gauges[#gauges+1] = server:receive() end server:close() @@ -82,7 +135,7 @@ describe("Plugin: datadog (log)", function() local res = assert(client:send { method = "GET", - path = "/status/200", + path = "/status/200/?apikey=kong", headers = { ["Host"] = "datadog1.com" } @@ -91,13 +144,68 @@ describe("Plugin: datadog (log)", function() local ok, gauges = thread:join() assert.True(ok) - assert.equal(6, #gauges) - assert.contains("kong.datadog1_com.request.count:1|c", gauges) - assert.contains("kong.datadog1_com.latency:%d+|g", gauges, true) - assert.contains("kong.datadog1_com.request.size:%d+|g", gauges, true) - assert.contains("kong.datadog1_com.request.status.200:1|c", gauges) - assert.contains("kong.datadog1_com.response.size:%d+|g", gauges, true) - assert.contains("kong.datadog1_com.upstream_latency:%d+|g", gauges, true) + assert.equal(12, #gauges) + assert.contains("kong.dd1.request.count:1|c|#app:kong", gauges) + assert.contains("kong.dd1.latency:%d+|ms|#app:kong", gauges, true) + assert.contains("kong.dd1.request.size:%d+|ms|#app:kong", gauges, true) + assert.contains("kong.dd1.request.status.200:1|c|#app:kong", gauges) + assert.contains("kong.dd1.request.status.total:1|c|#app:kong", gauges) + assert.contains("kong.dd1.response.size:%d+|ms|#app:kong", gauges, true) + assert.contains("kong.dd1.upstream_latency:%d+|ms|#app:kong", gauges, true) + assert.contains("kong.dd1.kong_latency:%d*|ms|#app:kong", gauges, true) + assert.contains("kong.dd1.user.uniques:.*|s|#app:kong", gauges, true) + assert.contains("kong.dd1.user.*.request.count:1|c|#app:kong", gauges, true) + assert.contains("kong.dd1.user.*.request.status.total:1|c|#app:kong", + gauges, true) + assert.contains("kong.dd1.user.*.request.status.200:1|c|#app:kong", + gauges, true) + end) + + it("logs metrics over UDP with custome prefix", function() + local thread = threads.new({ + function() + local socket = require "socket" + local server = assert(socket.udp()) + server:settimeout(1) + server:setoption("reuseaddr", true) + server:setsockname("127.0.0.1", 9999) + local gauges = {} + for _ = 1, 12 do + gauges[#gauges+1] = server:receive() + end + server:close() + return gauges + end + }) + thread:start() + + local res = assert(client:send { + method = "GET", + path = "/status/200/?apikey=kong", + headers = { + ["Host"] = "datadog4.com" + } + }) + assert.res_status(200, res) + + local ok, gauges = thread:join() + assert.True(ok) + assert.equal(12, #gauges) + assert.contains("prefix.dd4.request.count:1|c|#app:kong",gauges) + assert.contains("prefix.dd4.latency:%d+|ms|#app:kong", gauges, true) + assert.contains("prefix.dd4.request.size:%d+|ms|#app:kong", gauges, true) + assert.contains("prefix.dd4.request.status.200:1|c|#app:kong", gauges) + assert.contains("prefix.dd4.request.status.total:1|c|#app:kong", gauges) + assert.contains("prefix.dd4.response.size:%d+|ms|#app:kong", gauges, true) + assert.contains("prefix.dd4.upstream_latency:%d+|ms|#app:kong", gauges, true) + assert.contains("prefix.dd4.kong_latency:%d*|ms|#app:kong", gauges, true) + assert.contains("prefix.dd4.user.uniques:.*|s|#app:kong", gauges, true) + assert.contains("prefix.dd4.user.*.request.count:1|c|#app:kong", + gauges, true) + assert.contains("prefix.dd4.user.*.request.status.total:1|c|#app:kong", + gauges, true) + assert.contains("prefix.dd4.user.*.request.status.200:1|c|#app:kong", + gauges, true) end) it("logs only given metrics", function() @@ -109,7 +217,7 @@ describe("Plugin: datadog (log)", function() server:setoption("reuseaddr", true) server:setsockname("127.0.0.1", 9999) local gauges = {} - for _ = 1, 2 do + for _ = 1, 3 do gauges[#gauges+1] = server:receive() end server:close() @@ -129,9 +237,10 @@ describe("Plugin: datadog (log)", function() local ok, gauges = thread:join() assert.True(ok) - assert.equal(2, #gauges) - assert.contains("kong.datadog2_com.request.count:1|c", gauges) - assert.contains("kong.datadog2_com.request.status.200:1|c", gauges) + assert.equal(3, #gauges) + assert.contains("kong.dd2.request.count:1|c", gauges) + assert.contains("kong.dd2.request.status.200:1|c", gauges) + assert.contains("kong.dd2.request.status.total:1|c", gauges) end) it("logs metrics with tags", function() @@ -143,7 +252,7 @@ describe("Plugin: datadog (log)", function() server:setoption("reuseaddr", true) server:setsockname("127.0.0.1", 9999) local gauges = {} - for _ = 1, 3 do + for _ = 1, 4 do gauges[#gauges+1] = server:receive() end server:close() @@ -163,9 +272,9 @@ describe("Plugin: datadog (log)", function() local ok, gauges = thread:join() assert.True(ok) - assert.equal(3, #gauges) - assert.contains("kong.datadog3_com.request.count:1|c|#T1:V1", gauges) - assert.contains("kong.datadog3_com.request.status.200:1|c|#T2:V2,T3:V3,T4", gauges) - assert.contains("kong.datadog3_com.latency:%d+|g|#T2:V2:V3,T4", gauges, true) + assert.contains("kong.dd3.request.count:1|c|#T2:V2,T3:V3,T4", gauges) + assert.contains("kong.dd3.request.status.200:1|c|#T1:V1", gauges) + assert.contains("kong.dd3.request.status.total:1|c|#T1:V1", gauges) + assert.contains("kong.dd3.latency:%d+|g|#T2:V2:V3,T4", gauges, true) end) end) diff --git a/spec/03-plugins/08-datadog/02-schema_spec.lua b/spec/03-plugins/08-datadog/02-schema_spec.lua new file mode 100644 index 00000000000..98bce6c5f79 --- /dev/null +++ b/spec/03-plugins/08-datadog/02-schema_spec.lua @@ -0,0 +1,140 @@ +local schemas = require "kong.dao.schemas_validation" +local datadog_schema = require "kong.plugins.datadog.schema" +local validate_entity = schemas.validate_entity + +describe("Plugin: datadog (schema)", function() + it("accepts empty config #o", function() + local ok, err = validate_entity({}, datadog_schema) + assert.is_nil(err) + assert.is_true(ok) + end) + it("accepts empty metrics", function() + local metrics_input = {} + local ok, err = validate_entity({ metrics = metrics_input}, datadog_schema) + assert.is_nil(err) + assert.is_true(ok) + end) + it("accepts just one metrics", function() + local metrics_input = { + { + name = "request_count", + stat_type = "counter", + sample_rate = 1, + tags = {"K1:V1"} + } + } + local ok, err = validate_entity({ metrics = metrics_input}, datadog_schema) + assert.is_nil(err) + assert.is_true(ok) + end) + it("rejects if name or stat not defined", function() + local metrics_input = { + { + name = "request_count", + sample_rate = 1 + } + } + local _, err = validate_entity({ metrics = metrics_input}, datadog_schema) + assert.not_nil(err) + assert.equal("name and stat_type must be defined for all stats", err.metrics) + local metrics_input = { + { + stat_type = "counter", + sample_rate = 1 + } + } + _, err = validate_entity({ metrics = metrics_input}, datadog_schema) + assert.not_nil(err) + assert.equal("name and stat_type must be defined for all stats", err.metrics) + end) + it("rejects counters without sample rate", function() + local metrics_input = { + { + name = "request_count", + stat_type = "counter", + } + } + local _, err = validate_entity({ metrics = metrics_input}, datadog_schema) + assert.not_nil(err) + end) + it("rejects invalid metrics name", function() + local metrics_input = { + { + name = "invalid_name", + stat_type = "counter", + } + } + local _, err = validate_entity({ metrics = metrics_input}, datadog_schema) + assert.not_nil(err) + assert.equal("unrecognized metric name: invalid_name", err.metrics) + end) + it("rejects invalid stat type", function() + local metrics_input = { + { + name = "request_count", + stat_type = "invalid_stat", + } + } + local _, err = validate_entity({ metrics = metrics_input}, datadog_schema) + assert.not_nil(err) + assert.equal("unrecognized stat_type: invalid_stat", err.metrics) + end) + it("rejects if customer identifier missing", function() + local metrics_input = { + { + name = "status_count_per_user", + stat_type = "counter", + sample_rate = 1 + } + } + local _, err = validate_entity({ metrics = metrics_input}, datadog_schema) + assert.not_nil(err) + assert.equal("consumer_identifier must be defined for metric status_count_per_user", err.metrics) + end) + it("rejects if metric has wrong stat type", function() + local metrics_input = { + { + name = "unique_users", + stat_type = "counter" + } + } + local _, err = validate_entity({ metrics = metrics_input}, datadog_schema) + assert.not_nil(err) + assert.equal("unique_users metric only works with stat_type 'set'", err.metrics) + metrics_input = { + { + name = "status_count", + stat_type = "set", + sample_rate = 1 + } + } + _, err = validate_entity({ metrics = metrics_input}, datadog_schema) + assert.not_nil(err) + assert.equal("status_count metric only works with stat_type 'counter'", err.metrics) + end) + it("rejects if tags malformed", function() + local metrics_input = { + { + name = "status_count", + stat_type = "counter", + sample_rate = 1, + tags = {"T1:"} + } + } + local _, err = validate_entity({ metrics = metrics_input}, datadog_schema) + assert.not_nil(err) + assert.equal("malformed tags: key 'T1:' has no value. Tags must be list of key[:value]", err.metrics) + end) + it("accept if tags is aempty list", function() + local metrics_input = { + { + name = "status_count", + stat_type = "counter", + sample_rate = 1, + tags = {} + } + } + local _, err = validate_entity({ metrics = metrics_input}, datadog_schema) + assert.is_nil(err) + end) +end)