From c535036bce9a6c2a29dad8662665ee8d2d07b6ab Mon Sep 17 00:00:00 2001 From: Dhruv Sarawagi Date: Thu, 26 Aug 2021 14:02:07 +0530 Subject: [PATCH 01/12] fix: perf fixes --- kong/plugins/advanced-router/handler.lua | 64 ++++++++--------- kong/plugins/advanced-router/io.lua | 88 +++++++++++------------- kong/plugins/advanced-router/schema.lua | 39 +++++++++-- kong/plugins/advanced-router/utils.lua | 66 ++++++------------ 4 files changed, 128 insertions(+), 129 deletions(-) diff --git a/kong/plugins/advanced-router/handler.lua b/kong/plugins/advanced-router/handler.lua index d1610ff..328e660 100644 --- a/kong/plugins/advanced-router/handler.lua +++ b/kong/plugins/advanced-router/handler.lua @@ -2,31 +2,18 @@ local inspect = require("inspect") local cjson_safe = require "cjson.safe" local url = require "socket.url" local date = require "date" -local pl_tablex = require "pl.tablex" +local pl_utils = require "pl.utils" local get_io_data = require("kong.plugins.advanced-router.io").get_io_data local extract = require("kong.plugins.advanced-router.utils").extract -local replaceStringEnvVariables = require("kong.plugins.advanced-router.utils").replaceStringEnvVariables -local build_url = require("kong.plugins.advanced-router.utils").build_url +local interpolate_string_env = require("kong.plugins.advanced-router.utils").interpolate_string_env local AdvancedRouterHandler = {} - -AdvancedRouterHandler.PRIORITY = tonumber((os.getenv("PRIORITY_ADVANCED_ROUTER"))) +AdvancedRouterHandler.PRIORITY = tonumber(os.getenv("PRIORITY_ADVANCED_ROUTER")) AdvancedRouterHandler.VERSION = "1.0.0" local boolean_functions = {} -local kong_proxy_port - -function get_poxy_port() - local http_listener = pl_tablex.filter(kong.configuration.proxy_listeners, function(x) - if not x.ssl then - return true - end - end) - return http_listener[1]['port'] -end - function get_current_timestamp_utc() return date.diff(date(true), date(1970, 1, 1)):spanseconds() end @@ -40,14 +27,16 @@ function extract_from_io_response(key) end function generate_boolean_function(proposition) + if proposition['condition'] == 'default' then return assert(loadstring("return " .. "\"" .. proposition["upstream_url"] .. "\"")) end - local upstream_url = build_url(proposition["upstream_url"], kong_proxy_port) + local upstream_url = proposition["upstream_url"] return assert(loadstring("if " .. proposition["condition"] .. "then return " .. "\"" .. upstream_url .. "\"" .. " end")) end function get_upstream_url(conf) + -- TODO - Done - added log: check if this is happening for all req in load if boolean_functions[conf.route_id] == nil then boolean_functions[conf.route_id] = {} local propositions_json, err1 = cjson_safe.decode(conf.propositions_json) @@ -69,27 +58,20 @@ function get_upstream_url(conf) break end end - return value + return interpolate_string_env(value) end function set_upstream(upstream_url) local parsed_url = url.parse(upstream_url) - local scheme = parsed_url['scheme'] or 'http' - local host = parsed_url['host'] - local path = parsed_url['path'] - local port = tonumber(parsed_url['port']) or 80 - kong.service.request.set_scheme(scheme) - kong.log.debug("Upstream URL::" .. inspect(upstream_url)) - kong.log.debug("Parsed Upstream URL::" .. inspect(parsed_url)) - kong.service.set_target(host, port) + kong.service.request.set_scheme(parsed_url['scheme'] or 'http') + kong.service.set_target(parsed_url['host'], tonumber(parsed_url['port']) or 80) if path then - kong.service.request.set_path(path) + kong.service.request.set_path(parsed_url['path']) end - end function AdvancedRouterHandler:access(conf) - local io_data, err = get_io_data(pl_tablex.merge(conf, { kong_proxy_port = kong_proxy_port }, true)) + local io_data, err = get_io_data(conf) if err then kong.log.err("Error in getting io data" .. inspect(err)) return kong.response.exit(500, { error = "Error in getting io data" .. inspect(err) }) @@ -100,12 +82,32 @@ function AdvancedRouterHandler:access(conf) if not upstream_url then return kong.response.exit(500, "Not able to resolve upstream in advanced router") end - upstream_url = replaceStringEnvVariables(upstream_url, io_data) set_upstream(upstream_url) end function AdvancedRouterHandler:init_worker() - kong_proxy_port = get_poxy_port() + kong.worker_events.register( + function(data) + if type(data) ~= "string" then + return + end + + local key_parts = pl_utils.split(data, ":") + if key_parts[1] ~= "plugins" or key_parts[2] ~= "advanced-router" then + return + end + local route_id = key_parts[3] + kong.log.info("Invalidating boolean functions of route :: " .. route_id) + if boolean_functions[route_id] ~= nil then + boolean_functions[route_id] = nil + end + kong.log.info("Invalidating io_request_template of route :: " .. route_id) + kong.cache:invalidate('io_request_template' .. route_id, false) + + end, + "mlcache", + "mlcache:invalidations:kong_core_db_cache" + ) end return AdvancedRouterHandler diff --git a/kong/plugins/advanced-router/io.lua b/kong/plugins/advanced-router/io.lua index 1060c7b..ff7f2ba 100644 --- a/kong/plugins/advanced-router/io.lua +++ b/kong/plugins/advanced-router/io.lua @@ -1,21 +1,20 @@ local http = require "resty.http" local cjson_safe = require "cjson.safe" local inspect = require "inspect" +local pl_utils = require "pl.utils" +local pl_tablex = require "pl.tablex" -local generate_signature_hash = require("kong.plugins.advanced-router.utils").generate_signature_hash local extract = require "kong.plugins.advanced-router.utils".extract -local belongs = require "kong.plugins.advanced-router.utils".belongs -local replaceStringEnvVariables = require "kong.plugins.advanced-router.utils".replaceStringEnvVariables -local build_url = require("kong.plugins.advanced-router.utils").build_url +local interpolate_string_env = require "kong.plugins.advanced-router.utils".interpolate_string_env local _M = {} local function get_http_client(conf) local client = http.new() client:set_timeouts( - conf["http_connect_timeout"], - conf["http_read_timeout"], - conf["http_send_timeout"] + conf["http_connect_timeout"], + conf["http_read_timeout"], + conf["http_send_timeout"] ) return client end @@ -24,16 +23,21 @@ function extract_from_request(object, key) local value if object == 'headers' then value = extract(string.lower(key), kong.request.get_headers()) - return value else value = extract(key, kong.request.get_query()) - return value end return value end +function get_io_request_template(conf) + return cjson_safe.decode(conf.io_request_template), nil, 120 +end + function extract_io_data_from_request(conf) - local io_request_template = cjson_safe.decode(conf.io_request_template) + -- TODO - Done: cache this for each route + local cache_key = 'io_request_template:' .. conf.route_id + local io_request_template = kong.cache:get(cache_key, {}, get_io_request_template, conf) + local io_req = { headers = {}, query = {}, @@ -42,19 +46,15 @@ function extract_io_data_from_request(conf) local req_parts = { "headers", "query", "body" } -- This loop parses the io_request_template and populates each field from request data - for _, part in ipairs(req_parts) do - if io_request_template[part] then - for key, value in pairs(io_request_template[part]) do - local i = string.find(value, "%.") - if i then - local from_part = value:sub(1, i - 1) - if belongs(part, req_parts) then - io_req[part][key] = extract_from_request(from_part, value:sub(i + 1)) - else - io_req[part][key] = value - end + for _, req_part in ipairs(req_parts) do + if io_request_template[req_part] then + -- Ex: req_part = headers, key = a, value = headers.a + for key, value in pairs(io_request_template[req_part]) do + local first_part, second_part = pl_utils.splitv(value, '%.') + if second_part and pl_tablex.find(req_parts, first_part) then + io_req[req_part][key] = extract_from_request(first_part, second_part) else - io_req[part][key] = value + io_req[req_part][key] = value end end end @@ -62,26 +62,17 @@ function extract_io_data_from_request(conf) return io_req end -function get_cache_key(data) - local json_string = cjson_safe.encode(data) - local hash = generate_signature_hash(json_string) - return hash -end - function get_io_data_from_remote(request_data, conf) - kong.log.debug("Making io call::" .. inspect(request_data)) local client = get_http_client(conf) - request_data.headers["Content-Type"] = "application/json" local res, err1 = client:request_uri( - request_data.io_url, - { - method = request_data.io_http_method, - headers = request_data.headers, - body = cjson_safe.encode(request_data.body), - query = request_data.query - } + request_data.io_url, + { + method = request_data.io_http_method, + headers = pl_tablex.merge(request_data.headers, { ['Content-Type'] = 'application/json' }), + body = cjson_safe.encode(request_data.body), + query = request_data.query + } ) - kong.log.debug("IO call error::" .. inspect(err1)) if not res or err1 then return nil, err1 end @@ -92,22 +83,18 @@ function get_io_data_from_remote(request_data, conf) if not bodyJson then return nil, err2 end - kong.log.debug("Data from I/O::" .. inspect(bodyJson)) local cacheTTL if conf.cache_io_response then cacheTTL = res.headers[conf.cache_ttl_header] or conf["default_edge_ttl_sec"] else cacheTTL = -1 end - return bodyJson, nil, tonumber(cacheTTL) end function get_io_data(request_data, conf) - local cache_key = get_cache_key(request_data) - kong.log.debug("cache_key::" .. cache_key) - -- TODO: Check all options parameters - -- ttl values are overridden if callback returns ttl as third return value + local cache_key = request_data['cache_key'] + -- These values are default ttl values if they are not returned from callback function local options = { ttl = 60, neg_ttl = 60 @@ -122,9 +109,18 @@ end function create_io_request(conf) local io_request = extract_io_data_from_request(conf) - kong.log.debug("conf" .. inspect(conf)) - io_request["io_url"] = build_url(replaceStringEnvVariables(conf.io_url), conf.kong_proxy_port) + -- TODO - Done - cached inside function: not p0 cache this and check perfa + local req_part, key = pl_utils.splitv(conf.cache_identifier, "%.") + + local cache_identifier = extract_from_request(req_part, key) + if not cache_identifier then + return kong.response.exit(400, { error = "Cache identifier not found in request:: " .. conf.cache_identifier }) + end + + io_request["cache_key"] = conf.io_http_method .. ':' .. conf.io_url .. ':' .. extract_from_request(req_part, key) + io_request["io_url"] = interpolate_string_env(conf.io_url) io_request["io_http_method"] = conf.io_http_method + return io_request end diff --git a/kong/plugins/advanced-router/schema.lua b/kong/plugins/advanced-router/schema.lua index df5aaf1..718ed5f 100644 --- a/kong/plugins/advanced-router/schema.lua +++ b/kong/plugins/advanced-router/schema.lua @@ -1,8 +1,9 @@ local typedefs = require "kong.db.schema.typedefs" local json_safe = require "cjson.safe" local url = require "socket.url" - -local belongs = require "kong.plugins.advanced-router.utils".belongs +local pl_utils = require "pl.utils" +local pl_tablex = require "pl.tablex" +local inspect = require "inspect" local function json_validator(config_string) local decoded, err = json_safe.decode(config_string) @@ -18,6 +19,7 @@ local function validate_propositions_json(config_string) -- This functions validates the port and protocols of the upstream urls in the propositions_json local result, err, propositions_json = json_validator(config_string) if not result then + kong.log.err(kong.log.err("Invalid JSON in propositions_json" .. inspect(err))) return nil, err end local valid_schemes = { 'http', 'https' } @@ -25,19 +27,38 @@ local function validate_propositions_json(config_string) local upstream_url = v['upstream_url'] local parsed_url = url.parse(upstream_url) local scheme = parsed_url['scheme'] or 'http' - if not belongs(scheme, valid_schemes) then - return nil, "Invalid protocol: " .. scheme .. " for url: " .. upstream_url + if not pl_tablex.find(valid_schemes, scheme) then + kong.log.err(kong.log.err("Invalid protocol: " .. scheme .. " for url: " .. upstream_url)) + return nil, kong.log.err("Invalid protocol: " .. scheme .. " for url: " .. upstream_url) end if parsed_url['port'] and not tonumber(parsed_url['port']) then + kong.log.err("Invalid port: " .. parsed_url['port'] .. " for url: " .. upstream_url) return nil, "Invalid port: " .. parsed_url['port'] .. " for url: " .. upstream_url end end return true end +function validate_cache_identifier(cache_identifier) + local req_part, key = pl_utils.splitv(cache_identifier, "%.") + local common_err_msg = "Cache identifier should consist of two non empty strings joined by . (dot)" + if not pl_tablex.find({ "headers", "query" }, req_part) then + local err = "Invalid first string in cache identifier: " .. cache_identifier .. ". " .. common_err_msg .. " and first string should pe either headers or query" + kong.log.err(err) + return nil, err + end + + if not key then + local err = "Invalid second string in cache identifier: " .. cache_identifier .. ". " .. common_err_msg .. " and first string should pe either headers or query" + kong.log.err(err) + return nil, err + end + return true +end + local function schema_validator(conf) - return validate_propositions_json(conf.propositions_json) and json_validator(conf.io_request_template) + return validate_propositions_json(conf.propositions_json) and json_validator(conf.io_request_template) and validate_cache_identifier(conf.cache_identifier) end return { @@ -73,6 +94,12 @@ return { default = true } }, + { + cache_identifier = { + type = "string", + required = true + } + }, { io_http_method = { type = "string", @@ -91,7 +118,7 @@ return { type = "number", required = true } - } + }, }, custom_validator = schema_validator } diff --git a/kong/plugins/advanced-router/utils.lua b/kong/plugins/advanced-router/utils.lua index 930cd48..d1720ba 100644 --- a/kong/plugins/advanced-router/utils.lua +++ b/kong/plugins/advanced-router/utils.lua @@ -1,30 +1,29 @@ local pl_utils = require "pl.utils" -local sha256 = require "resty.sha256" -local encode_base64 = ngx.encode_base64 local inspect = require "inspect" -local url = require "socket.url" local _M = {} -function _M.replaceStringEnvVariables(s, data) - return - string.gsub( - s, - "%%[A-Za-z_%.]+%%", - function(str) - - local variable = string.sub(str, 2, string.len(str) - 1) - kong.log.debug("string=" .. variable) - if data then - local value_from_data = _M.extract(variable, data) - if value_from_data then - return value_from_data - end +function interpolate_string_env_cb(s) + local ttl = 300 + return string.gsub( + s, + "%%[A-Za-z_%.]+%%", + function(str) + local variable = string.sub(str, 2, string.len(str) - 1) + return os.getenv(variable) end - kong.log.debug("from env=" .. inspect(os.getenv(variable))) - return os.getenv(variable) - end - ) + ), nil, ttl +end + + +-- TODO - Done: remove interpolate from io_data and cache the result +function _M.interpolate_string_env(s) + local result, err = kong.cache:get('interpolate:' .. s, {}, interpolate_string_env_cb, s) + if err then + kong.log.err("Error in interpolating string::" .. inspect(s)) + return kong.response.exit(500, { error = "Something went wrong" }) + end + return result end function _M.extract(key, data) @@ -40,29 +39,4 @@ function _M.extract(key, data) end end -function _M.generate_signature_hash(s) - local digest = sha256:new() - digest:update(s) - local hash = encode_base64(digest:final()) - return hash -end - -function _M.belongs(val, tbl) - for _, v in pairs(tbl) do - if v == val then - return true - end - end - return false -end - -function _M.build_url(upstream_url, kong_proxy_port) - local parsed_url = url.parse(upstream_url); - if not parsed_url["host"] then - parsed_url["host"] = "localhost" - parsed_url["port"] = kong_proxy_port - end - return url.build(parsed_url) -end - return _M From 47eca676fa131172eb0e01760c1fda51d4eb96de Mon Sep 17 00:00:00 2001 From: Dhruv Sarawagi Date: Thu, 26 Aug 2021 14:13:19 +0530 Subject: [PATCH 02/12] fix: add timeouts in schema --- kong/plugins/advanced-router/io.lua | 2 +- kong/plugins/advanced-router/schema.lua | 32 ++++++++++++++++++------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/kong/plugins/advanced-router/io.lua b/kong/plugins/advanced-router/io.lua index ff7f2ba..d99c5e1 100644 --- a/kong/plugins/advanced-router/io.lua +++ b/kong/plugins/advanced-router/io.lua @@ -85,7 +85,7 @@ function get_io_data_from_remote(request_data, conf) end local cacheTTL if conf.cache_io_response then - cacheTTL = res.headers[conf.cache_ttl_header] or conf["default_edge_ttl_sec"] + cacheTTL = res.headers[conf.cache_ttl_header] or conf["default_cache_ttl_sec"] else cacheTTL = -1 end diff --git a/kong/plugins/advanced-router/schema.lua b/kong/plugins/advanced-router/schema.lua index 718ed5f..4470834 100644 --- a/kong/plugins/advanced-router/schema.lua +++ b/kong/plugins/advanced-router/schema.lua @@ -82,12 +82,35 @@ return { default = "http://io-call%placeholder%/round" } }, + { + io_http_method = { + type = "string", + default = "GET", + one_of = { "GET", "POST" } + } + }, { io_request_template = { type = "string", default = "{\n \"headers\": {\n \"a\":\"headers.x\"\n },\n \"query\": {\n \"b\": \"query.y\"\n },\n \"body\": {\n \"c\": \"query.z\",\n \"d\": \"hardcoded\"\n }\n}" } }, + { + http_connect_timeout = { + type = "number", + required = true + } + },{ + http_send_timeout = { + type = "number", + required = true + } + },{ + http_read_timeout = { + type = "number", + required = true + } + }, { cache_io_response = { type = "boolean", @@ -100,13 +123,6 @@ return { required = true } }, - { - io_http_method = { - type = "string", - default = "GET", - one_of = { "GET", "POST" } - } - }, { cache_ttl_header = { type = "string", @@ -114,7 +130,7 @@ return { } }, { - default_edge_ttl_sec = { + default_cache_ttl_sec = { type = "number", required = true } From e6c5883b6fe9cb03fae4a8e5db66480f6beb29ba Mon Sep 17 00:00:00 2001 From: Dhruv Sarawagi Date: Thu, 26 Aug 2021 14:26:42 +0530 Subject: [PATCH 03/12] fix: add default timeouts in schema --- kong/plugins/advanced-router/schema.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kong/plugins/advanced-router/schema.lua b/kong/plugins/advanced-router/schema.lua index 4470834..a2ca088 100644 --- a/kong/plugins/advanced-router/schema.lua +++ b/kong/plugins/advanced-router/schema.lua @@ -98,17 +98,17 @@ return { { http_connect_timeout = { type = "number", - required = true + default = 5000 } },{ http_send_timeout = { type = "number", - required = true + default = 5000 } },{ - http_read_timeout = { + http_send_timeout = { type = "number", - required = true + default = 5000 } }, { From fa53e0365e45e0d44879bc221c340de3c72a408d Mon Sep 17 00:00:00 2001 From: Dhruv Sarawagi Date: Thu, 26 Aug 2021 14:49:16 +0530 Subject: [PATCH 04/12] refactor: format code --- kong/plugins/advanced-router/handler.lua | 40 ++++++++++++------------ kong/plugins/advanced-router/io.lua | 20 ++++++------ kong/plugins/advanced-router/schema.lua | 4 +-- kong/plugins/advanced-router/utils.lua | 12 +++---- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/kong/plugins/advanced-router/handler.lua b/kong/plugins/advanced-router/handler.lua index 328e660..0e2e683 100644 --- a/kong/plugins/advanced-router/handler.lua +++ b/kong/plugins/advanced-router/handler.lua @@ -87,26 +87,26 @@ end function AdvancedRouterHandler:init_worker() kong.worker_events.register( - function(data) - if type(data) ~= "string" then - return - end - - local key_parts = pl_utils.split(data, ":") - if key_parts[1] ~= "plugins" or key_parts[2] ~= "advanced-router" then - return - end - local route_id = key_parts[3] - kong.log.info("Invalidating boolean functions of route :: " .. route_id) - if boolean_functions[route_id] ~= nil then - boolean_functions[route_id] = nil - end - kong.log.info("Invalidating io_request_template of route :: " .. route_id) - kong.cache:invalidate('io_request_template' .. route_id, false) - - end, - "mlcache", - "mlcache:invalidations:kong_core_db_cache" + function(data) + if type(data) ~= "string" then + return + end + + local key_parts = pl_utils.split(data, ":") + if key_parts[1] ~= "plugins" or key_parts[2] ~= "advanced-router" then + return + end + local route_id = key_parts[3] + kong.log.info("Invalidating boolean functions of route :: " .. route_id) + if boolean_functions[route_id] ~= nil then + boolean_functions[route_id] = nil + end + kong.log.info("Invalidating io_request_template of route :: " .. route_id) + kong.cache:invalidate('io_request_template' .. route_id, false) + + end, + "mlcache", + "mlcache:invalidations:kong_core_db_cache" ) end diff --git a/kong/plugins/advanced-router/io.lua b/kong/plugins/advanced-router/io.lua index d99c5e1..bc485e0 100644 --- a/kong/plugins/advanced-router/io.lua +++ b/kong/plugins/advanced-router/io.lua @@ -12,9 +12,9 @@ local _M = {} local function get_http_client(conf) local client = http.new() client:set_timeouts( - conf["http_connect_timeout"], - conf["http_read_timeout"], - conf["http_send_timeout"] + conf["http_connect_timeout"], + conf["http_read_timeout"], + conf["http_send_timeout"] ) return client end @@ -65,13 +65,13 @@ end function get_io_data_from_remote(request_data, conf) local client = get_http_client(conf) local res, err1 = client:request_uri( - request_data.io_url, - { - method = request_data.io_http_method, - headers = pl_tablex.merge(request_data.headers, { ['Content-Type'] = 'application/json' }), - body = cjson_safe.encode(request_data.body), - query = request_data.query - } + request_data.io_url, + { + method = request_data.io_http_method, + headers = pl_tablex.merge(request_data.headers, { ['Content-Type'] = 'application/json' }), + body = cjson_safe.encode(request_data.body), + query = request_data.query + } ) if not res or err1 then return nil, err1 diff --git a/kong/plugins/advanced-router/schema.lua b/kong/plugins/advanced-router/schema.lua index a2ca088..ec92ff4 100644 --- a/kong/plugins/advanced-router/schema.lua +++ b/kong/plugins/advanced-router/schema.lua @@ -100,12 +100,12 @@ return { type = "number", default = 5000 } - },{ + }, { http_send_timeout = { type = "number", default = 5000 } - },{ + }, { http_send_timeout = { type = "number", default = 5000 diff --git a/kong/plugins/advanced-router/utils.lua b/kong/plugins/advanced-router/utils.lua index d1720ba..c5c9d20 100644 --- a/kong/plugins/advanced-router/utils.lua +++ b/kong/plugins/advanced-router/utils.lua @@ -6,12 +6,12 @@ local _M = {} function interpolate_string_env_cb(s) local ttl = 300 return string.gsub( - s, - "%%[A-Za-z_%.]+%%", - function(str) - local variable = string.sub(str, 2, string.len(str) - 1) - return os.getenv(variable) - end + s, + "%%[A-Za-z_%.]+%%", + function(str) + local variable = string.sub(str, 2, string.len(str) - 1) + return os.getenv(variable) + end ), nil, ttl end From acd4d4efd4b16e2d1f2aaaa92fa11aa65a0ff319 Mon Sep 17 00:00:00 2001 From: Dhruv Sarawagi Date: Thu, 26 Aug 2021 14:51:38 +0530 Subject: [PATCH 05/12] chore: remove done todos --- kong/plugins/advanced-router/handler.lua | 1 - kong/plugins/advanced-router/io.lua | 2 -- kong/plugins/advanced-router/utils.lua | 1 - 3 files changed, 4 deletions(-) diff --git a/kong/plugins/advanced-router/handler.lua b/kong/plugins/advanced-router/handler.lua index 0e2e683..8e804d5 100644 --- a/kong/plugins/advanced-router/handler.lua +++ b/kong/plugins/advanced-router/handler.lua @@ -36,7 +36,6 @@ function generate_boolean_function(proposition) end function get_upstream_url(conf) - -- TODO - Done - added log: check if this is happening for all req in load if boolean_functions[conf.route_id] == nil then boolean_functions[conf.route_id] = {} local propositions_json, err1 = cjson_safe.decode(conf.propositions_json) diff --git a/kong/plugins/advanced-router/io.lua b/kong/plugins/advanced-router/io.lua index bc485e0..4229c7b 100644 --- a/kong/plugins/advanced-router/io.lua +++ b/kong/plugins/advanced-router/io.lua @@ -34,7 +34,6 @@ function get_io_request_template(conf) end function extract_io_data_from_request(conf) - -- TODO - Done: cache this for each route local cache_key = 'io_request_template:' .. conf.route_id local io_request_template = kong.cache:get(cache_key, {}, get_io_request_template, conf) @@ -109,7 +108,6 @@ end function create_io_request(conf) local io_request = extract_io_data_from_request(conf) - -- TODO - Done - cached inside function: not p0 cache this and check perfa local req_part, key = pl_utils.splitv(conf.cache_identifier, "%.") local cache_identifier = extract_from_request(req_part, key) diff --git a/kong/plugins/advanced-router/utils.lua b/kong/plugins/advanced-router/utils.lua index c5c9d20..2f583a0 100644 --- a/kong/plugins/advanced-router/utils.lua +++ b/kong/plugins/advanced-router/utils.lua @@ -16,7 +16,6 @@ function interpolate_string_env_cb(s) end --- TODO - Done: remove interpolate from io_data and cache the result function _M.interpolate_string_env(s) local result, err = kong.cache:get('interpolate:' .. s, {}, interpolate_string_env_cb, s) if err then From c37c1f29d4cf58931170cdeb3cc9cd906376b1d8 Mon Sep 17 00:00:00 2001 From: Dhruv Sarawagi Date: Thu, 26 Aug 2021 15:37:35 +0530 Subject: [PATCH 06/12] fix: schema http_read_timeout fix --- kong/plugins/advanced-router/schema.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kong/plugins/advanced-router/schema.lua b/kong/plugins/advanced-router/schema.lua index ec92ff4..3f4e4e7 100644 --- a/kong/plugins/advanced-router/schema.lua +++ b/kong/plugins/advanced-router/schema.lua @@ -106,7 +106,7 @@ return { default = 5000 } }, { - http_send_timeout = { + http_read_timeout = { type = "number", default = 5000 } From dfea6352e6e4e80e695c3ad4753429d9c9fc5f8f Mon Sep 17 00:00:00 2001 From: Dhruv Sarawagi Date: Fri, 27 Aug 2021 12:58:44 +0530 Subject: [PATCH 07/12] fix: max spiky cpu --- kong/plugins/advanced-router/handler.lua | 46 ++++++++++++------------ kong/plugins/advanced-router/io.lua | 40 ++++++++++++--------- kong/plugins/advanced-router/schema.lua | 7 ++++ kong/plugins/advanced-router/utils.lua | 2 +- 4 files changed, 55 insertions(+), 40 deletions(-) diff --git a/kong/plugins/advanced-router/handler.lua b/kong/plugins/advanced-router/handler.lua index 8e804d5..0eaa364 100644 --- a/kong/plugins/advanced-router/handler.lua +++ b/kong/plugins/advanced-router/handler.lua @@ -1,9 +1,11 @@ -local inspect = require("inspect") +local inspect = require "inspect" local cjson_safe = require "cjson.safe" local url = require "socket.url" local date = require "date" local pl_utils = require "pl.utils" +local kong = kong + local get_io_data = require("kong.plugins.advanced-router.io").get_io_data local extract = require("kong.plugins.advanced-router.utils").extract local interpolate_string_env = require("kong.plugins.advanced-router.utils").interpolate_string_env @@ -23,7 +25,7 @@ function get_timestamp_utc(date_string) end function extract_from_io_response(key) - return extract(key, kong.ctx.plugin.io_data) + return kong.ctx.plugin.io_data[key] end function generate_boolean_function(proposition) @@ -86,26 +88,26 @@ end function AdvancedRouterHandler:init_worker() kong.worker_events.register( - function(data) - if type(data) ~= "string" then - return - end - - local key_parts = pl_utils.split(data, ":") - if key_parts[1] ~= "plugins" or key_parts[2] ~= "advanced-router" then - return - end - local route_id = key_parts[3] - kong.log.info("Invalidating boolean functions of route :: " .. route_id) - if boolean_functions[route_id] ~= nil then - boolean_functions[route_id] = nil - end - kong.log.info("Invalidating io_request_template of route :: " .. route_id) - kong.cache:invalidate('io_request_template' .. route_id, false) - - end, - "mlcache", - "mlcache:invalidations:kong_core_db_cache" + function(data) + if type(data) ~= "string" then + return + end + + local key_parts = pl_utils.split(data, ":") + if key_parts[1] ~= "plugins" or key_parts[2] ~= "advanced-router" then + return + end + local route_id = key_parts[3] + kong.log.info("Invalidating boolean functions of route :: " .. route_id) + if boolean_functions[route_id] ~= nil then + boolean_functions[route_id] = nil + end + kong.log.info("Invalidating io_request_template of route :: " .. route_id) + kong.cache:invalidate('io_request_template' .. route_id, false) + + end, + "mlcache", + "mlcache:invalidations:kong_core_db_cache" ) end diff --git a/kong/plugins/advanced-router/io.lua b/kong/plugins/advanced-router/io.lua index 4229c7b..90a76ab 100644 --- a/kong/plugins/advanced-router/io.lua +++ b/kong/plugins/advanced-router/io.lua @@ -1,20 +1,21 @@ local http = require "resty.http" local cjson_safe = require "cjson.safe" -local inspect = require "inspect" local pl_utils = require "pl.utils" local pl_tablex = require "pl.tablex" -local extract = require "kong.plugins.advanced-router.utils".extract +local kong = kong + local interpolate_string_env = require "kong.plugins.advanced-router.utils".interpolate_string_env +local extract = require "kong.plugins.advanced-router.utils".extract local _M = {} local function get_http_client(conf) local client = http.new() client:set_timeouts( - conf["http_connect_timeout"], - conf["http_read_timeout"], - conf["http_send_timeout"] + conf["http_connect_timeout"], + conf["http_read_timeout"], + conf["http_send_timeout"] ) return client end @@ -22,15 +23,15 @@ end function extract_from_request(object, key) local value if object == 'headers' then - value = extract(string.lower(key), kong.request.get_headers()) + value = kong.request.get_header(string.lower(key)) else - value = extract(key, kong.request.get_query()) + value = kong.request.get_query_arg(key) end return value end function get_io_request_template(conf) - return cjson_safe.decode(conf.io_request_template), nil, 120 + return cjson_safe.decode(conf.io_request_template), nil, 3600 end function extract_io_data_from_request(conf) @@ -64,13 +65,13 @@ end function get_io_data_from_remote(request_data, conf) local client = get_http_client(conf) local res, err1 = client:request_uri( - request_data.io_url, - { - method = request_data.io_http_method, - headers = pl_tablex.merge(request_data.headers, { ['Content-Type'] = 'application/json' }), - body = cjson_safe.encode(request_data.body), - query = request_data.query - } + request_data.io_url, + { + method = request_data.io_http_method, + headers = pl_tablex.merge(request_data.headers, { ['Content-Type'] = 'application/json' }), + body = cjson_safe.encode(request_data.body), + query = request_data.query + } ) if not res or err1 then return nil, err1 @@ -88,7 +89,12 @@ function get_io_data_from_remote(request_data, conf) else cacheTTL = -1 end - return bodyJson, nil, tonumber(cacheTTL) + + local result = {} + for _, v in ipairs(conf.variables) do + result[v] = extract(v, bodyJson) + end + return result, nil, tonumber(cacheTTL) end function get_io_data(request_data, conf) @@ -115,7 +121,7 @@ function create_io_request(conf) return kong.response.exit(400, { error = "Cache identifier not found in request:: " .. conf.cache_identifier }) end - io_request["cache_key"] = conf.io_http_method .. ':' .. conf.io_url .. ':' .. extract_from_request(req_part, key) + io_request["cache_key"] = conf.io_http_method .. ':' .. conf.io_url .. ':' .. cache_identifier io_request["io_url"] = interpolate_string_env(conf.io_url) io_request["io_http_method"] = conf.io_http_method diff --git a/kong/plugins/advanced-router/schema.lua b/kong/plugins/advanced-router/schema.lua index 3f4e4e7..9437293 100644 --- a/kong/plugins/advanced-router/schema.lua +++ b/kong/plugins/advanced-router/schema.lua @@ -76,6 +76,13 @@ return { default = "[\n {\n \"condition\":\"extract_from_io_response('a') == 'x' and extract_from_io_response('b') == 'y'\",\n \"upstream_url\":\"alpha.com\"\n },\n {\n \"condition\":\"extract_from_io_response('a') == z or extract_from_io_response('b') == 'z'\",\n \"upstream_url\":\"beta.com\"\n },\n {\n \"condition\":\"default\",\n \"upstream_url\":\"default.com\"\n }\n]" } }, + { + variables = { + type = "array", + elements = { type = "string" }, + required = true + } + }, { io_url = { type = "string", diff --git a/kong/plugins/advanced-router/utils.lua b/kong/plugins/advanced-router/utils.lua index 2f583a0..a296a96 100644 --- a/kong/plugins/advanced-router/utils.lua +++ b/kong/plugins/advanced-router/utils.lua @@ -4,7 +4,7 @@ local inspect = require "inspect" local _M = {} function interpolate_string_env_cb(s) - local ttl = 300 + local ttl = 36000 return string.gsub( s, "%%[A-Za-z_%.]+%%", From 390556e0bc74b5f042d0754216d122ac5753f941 Mon Sep 17 00:00:00 2001 From: Dhruv Sarawagi Date: Fri, 27 Aug 2021 13:00:50 +0530 Subject: [PATCH 08/12] refactor: formatting --- kong/plugins/advanced-router/handler.lua | 40 ++++++++++++------------ kong/plugins/advanced-router/io.lua | 20 ++++++------ kong/plugins/advanced-router/utils.lua | 1 - 3 files changed, 30 insertions(+), 31 deletions(-) diff --git a/kong/plugins/advanced-router/handler.lua b/kong/plugins/advanced-router/handler.lua index 0eaa364..3c791a9 100644 --- a/kong/plugins/advanced-router/handler.lua +++ b/kong/plugins/advanced-router/handler.lua @@ -88,26 +88,26 @@ end function AdvancedRouterHandler:init_worker() kong.worker_events.register( - function(data) - if type(data) ~= "string" then - return - end - - local key_parts = pl_utils.split(data, ":") - if key_parts[1] ~= "plugins" or key_parts[2] ~= "advanced-router" then - return - end - local route_id = key_parts[3] - kong.log.info("Invalidating boolean functions of route :: " .. route_id) - if boolean_functions[route_id] ~= nil then - boolean_functions[route_id] = nil - end - kong.log.info("Invalidating io_request_template of route :: " .. route_id) - kong.cache:invalidate('io_request_template' .. route_id, false) - - end, - "mlcache", - "mlcache:invalidations:kong_core_db_cache" + function(data) + if type(data) ~= "string" then + return + end + + local key_parts = pl_utils.split(data, ":") + if key_parts[1] ~= "plugins" or key_parts[2] ~= "advanced-router" then + return + end + local route_id = key_parts[3] + kong.log.info("Invalidating boolean functions of route :: " .. route_id) + if boolean_functions[route_id] ~= nil then + boolean_functions[route_id] = nil + end + kong.log.info("Invalidating io_request_template of route :: " .. route_id) + kong.cache:invalidate('io_request_template' .. route_id, false) + + end, + "mlcache", + "mlcache:invalidations:kong_core_db_cache" ) end diff --git a/kong/plugins/advanced-router/io.lua b/kong/plugins/advanced-router/io.lua index 90a76ab..c2cabb2 100644 --- a/kong/plugins/advanced-router/io.lua +++ b/kong/plugins/advanced-router/io.lua @@ -13,9 +13,9 @@ local _M = {} local function get_http_client(conf) local client = http.new() client:set_timeouts( - conf["http_connect_timeout"], - conf["http_read_timeout"], - conf["http_send_timeout"] + conf["http_connect_timeout"], + conf["http_read_timeout"], + conf["http_send_timeout"] ) return client end @@ -65,13 +65,13 @@ end function get_io_data_from_remote(request_data, conf) local client = get_http_client(conf) local res, err1 = client:request_uri( - request_data.io_url, - { - method = request_data.io_http_method, - headers = pl_tablex.merge(request_data.headers, { ['Content-Type'] = 'application/json' }), - body = cjson_safe.encode(request_data.body), - query = request_data.query - } + request_data.io_url, + { + method = request_data.io_http_method, + headers = pl_tablex.merge(request_data.headers, { ['Content-Type'] = 'application/json' }), + body = cjson_safe.encode(request_data.body), + query = request_data.query + } ) if not res or err1 then return nil, err1 diff --git a/kong/plugins/advanced-router/utils.lua b/kong/plugins/advanced-router/utils.lua index a296a96..ec2220f 100644 --- a/kong/plugins/advanced-router/utils.lua +++ b/kong/plugins/advanced-router/utils.lua @@ -15,7 +15,6 @@ function interpolate_string_env_cb(s) ), nil, ttl end - function _M.interpolate_string_env(s) local result, err = kong.cache:get('interpolate:' .. s, {}, interpolate_string_env_cb, s) if err then From c9d30b91d01c3dcf389416e3e44969fab6684340 Mon Sep 17 00:00:00 2001 From: Dhruv Sarawagi Date: Fri, 27 Aug 2021 18:41:44 +0530 Subject: [PATCH 09/12] fix: test cases and fixes --- README.md | 6 +- kong/plugins/advanced-router/handler.lua | 2 +- kong/plugins/advanced-router/io.lua | 2 +- spec/advanced-router/01-access_spec.lua | 73 +++-- spec/output-handlers/custom_format.lua | 325 +++++++++++++++++++++++ 5 files changed, 388 insertions(+), 20 deletions(-) create mode 100644 spec/output-handlers/custom_format.lua diff --git a/README.md b/README.md index 27a352d..7819e8b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -#Kong-advanced-router +# Kong-advanced-router -##Description +## Description Routing a request to a particular service based on the response of an I/O call @@ -12,7 +12,7 @@ For making the I/O call, all parameters of the call like URL, Method, body etc. I/O URL can be interpolated using env variables and upstream URLS can also be interpolated using I/O data. -##Implementation +## Implementation diff --git a/kong/plugins/advanced-router/handler.lua b/kong/plugins/advanced-router/handler.lua index 3c791a9..22fd7dd 100644 --- a/kong/plugins/advanced-router/handler.lua +++ b/kong/plugins/advanced-router/handler.lua @@ -66,7 +66,7 @@ function set_upstream(upstream_url) local parsed_url = url.parse(upstream_url) kong.service.request.set_scheme(parsed_url['scheme'] or 'http') kong.service.set_target(parsed_url['host'], tonumber(parsed_url['port']) or 80) - if path then + if parsed_url['path'] then kong.service.request.set_path(parsed_url['path']) end end diff --git a/kong/plugins/advanced-router/io.lua b/kong/plugins/advanced-router/io.lua index c2cabb2..66823e3 100644 --- a/kong/plugins/advanced-router/io.lua +++ b/kong/plugins/advanced-router/io.lua @@ -68,7 +68,7 @@ function get_io_data_from_remote(request_data, conf) request_data.io_url, { method = request_data.io_http_method, - headers = pl_tablex.merge(request_data.headers, { ['Content-Type'] = 'application/json' }), + headers = pl_tablex.merge(request_data.headers, { ['Content-Type'] = 'application/json' }, true), body = cjson_safe.encode(request_data.body), query = request_data.query } diff --git a/spec/advanced-router/01-access_spec.lua b/spec/advanced-router/01-access_spec.lua index f0e1739..294c360 100644 --- a/spec/advanced-router/01-access_spec.lua +++ b/spec/advanced-router/01-access_spec.lua @@ -4,7 +4,7 @@ local inspect = require "inspect" for _, strategy in helpers.each_strategy() do - describe("advanced router plugin I/O data from headers [#" .. strategy .. "]", function() + describe("advanced router plugin [#" .. strategy .. "]", function() local KONG_IO_CALL_HOST_ENV = "-io-call" local KONG_SERVICE_ONE_HOST_ENV = "-one" @@ -52,7 +52,7 @@ for _, strategy in helpers.each_strategy() do port = 15555 } - function setup_db(propositions_json, io_request_template, service_io_call_host) + function setup_db(propositions_json, io_request_template, service_io_call_host, variables, cache_identifier) local test_service = assert(bp.services:insert( { protocol = "http", @@ -80,7 +80,13 @@ for _, strategy in helpers.each_strategy() do cache_io_response = true, io_http_method = "GET", cache_ttl_header = "edge_ttl", - default_edge_ttl_sec = 10 + variables = variables, + http_connect_timeout = 2000, + http_send_timeout = 2000, + http_read_timeout = 2000, + cache_identifier = cache_identifier, + default_cache_ttl_sec = 10 + }, route = main_route }) @@ -141,7 +147,7 @@ for _, strategy in helpers.each_strategy() do ['route'] = "query.route1" } } - setup_db(propositions_json, io_request_template, service_io_call_host) + setup_db(propositions_json, io_request_template, service_io_call_host, { "data.service_host", "data.route" }, "headers.route") end) teardown(function() @@ -156,13 +162,13 @@ for _, strategy in helpers.each_strategy() do end) it("Should send data in query parameters correctly #template_query", function() - local req_data = { query = { service_host = '-one', route = '/one' } } + local req_data = { query = { service_host = '-one', route = '/one' }, headers = { route = '/one' } } local expected_resp = { host = service_one_host, uri = service_one_route, scheme = 'http' } get_and_assert_upstream(req_data, expected_resp) end) it("Should send data in body correctly #template_body", function() - local req_data = { query = { service_host1 = '-one', route1 = '/one', method = 'POST' } } + local req_data = { query = { service_host1 = '-one', route1 = '/one', method = 'POST' }, headers = { route = '/one' } } local expected_resp = { host = service_one_host, uri = service_one_route, scheme = 'http' } get_and_assert_upstream(req_data, expected_resp) end) @@ -184,7 +190,7 @@ for _, strategy in helpers.each_strategy() do ['roundstarttime'] = "headers.roundstarttime" } } - setup_db(propositions_json, io_request_template, service_io_call_host) + setup_db(propositions_json, io_request_template, service_io_call_host, { "data.service_host", "data.route" }, "headers.route") end) teardown(function() @@ -227,7 +233,7 @@ for _, strategy in helpers.each_strategy() do ['roundstarttime'] = "headers.roundstarttime" } } - setup_db(propositions_json, io_request_template, service_io_call_host) + setup_db(propositions_json, io_request_template, service_io_call_host, { "data.service_host", "data.route", "data.roundstarttime" }, "headers.roundstarttime") end) teardown(function() @@ -235,20 +241,20 @@ for _, strategy in helpers.each_strategy() do db:truncate() end) - it("Should match first condition #service_one1", function() + it("Should match first condition #time_service_one", function() local req_data = { headers = { service_host = '-one', route = '/one', roundstarttime = "2925-10-17T11:15:14.000Z" } } local expected_resp = { host = service_one_host, uri = service_one_route, scheme = 'http' } get_and_assert_upstream(req_data, expected_resp) end) - it("Should match second condition #service_two2", function() + it("Should match second condition #time_service_two", function() local req_data = { headers = { service_host = '-two', route = '/two', roundstarttime = "1925-10-17T11:15:14.000Z" } } local expected_resp = { host = service_two_host, uri = service_two_route, scheme = 'http' } get_and_assert_upstream(req_data, expected_resp) end) - it("Should match default condition #service_default1", function() - local req_data = { headers = { service_host = '-two', route = '/one', roundstarttime = "1925-10-17T11:15:14.000Z" } } + it("Should match default condition #time_service_default", function() + local req_data = { headers = { service_host = '-two', route = '/one', roundstarttime = "1925-10-16T11:15:14.000Z" } } local expected_resp = { host = service_default_host, uri = service_default_route, scheme = 'http' } get_and_assert_upstream(req_data, expected_resp) end) @@ -270,7 +276,7 @@ for _, strategy in helpers.each_strategy() do ['roundstarttime'] = "headers.roundstarttime" } } - setup_db(propositions_json, io_request_template, service_io_call_host_variable) + setup_db(propositions_json, io_request_template, service_io_call_host_variable, { "data.service_host", "data.route" }, "headers.route") end) teardown(function() @@ -290,14 +296,51 @@ for _, strategy in helpers.each_strategy() do get_and_assert_upstream(req_data, expected_resp) end) - it("Should interpolate upstream_url of second condition from I/O data #UpIO", function() + end) + + describe("Should cache io_data using cache identifier correctly #cache_identifier", function() + + lazy_setup(function() + local propositions_json = { + { condition = "extract_from_io_response('data.service_host') == '-one' and extract_from_io_response('data.route') == '/one'", upstream_url = "http://" .. service_one_host .. service_one_route }, + { condition = "extract_from_io_response('data.service_host') == '-two' and extract_from_io_response('data.route') == '/two'", upstream_url = "http://" .. service_two_host .. service_two_route }, + { condition = "default", upstream_url = "http://" .. service_default_host .. service_default_route }, + } + + local io_request_template = { + headers = { + ['service_host'] = "headers.service_host", + ['route'] = "headers.route", + ['roundstarttime'] = "headers.roundstarttime" + } + } + setup_db(propositions_json, io_request_template, service_io_call_host, { "data.service_host", "data.route" }, "headers.route") + end) + + teardown(function() + helpers.stop_kong() + db:truncate() + end) + + it("Should call remote on first call #cache_identifier_one", function() + local req_data = { headers = { service_host = '-one', route = '/one' } } + local expected_resp = { host = service_one_host, uri = service_one_route, scheme = 'http' } + get_and_assert_upstream(req_data, expected_resp) + end) + + it("Should use cached data on second call #cache_identifier_two", function() + local req_data = { headers = { service_host = '-two', route = '/one' } } + local expected_resp = { host = service_one_host, uri = service_one_route, scheme = 'http' } + get_and_assert_upstream(req_data, expected_resp) + end) + + it("Should call remote when different cache_identifier value is sent in request #cache_identifier_three", function() local req_data = { headers = { service_host = '-two', route = '/two' } } local expected_resp = { host = service_two_host, uri = service_two_route, scheme = 'http' } get_and_assert_upstream(req_data, expected_resp) end) end) - end) break end diff --git a/spec/output-handlers/custom_format.lua b/spec/output-handlers/custom_format.lua new file mode 100644 index 0000000..63cf27f --- /dev/null +++ b/spec/output-handlers/custom_format.lua @@ -0,0 +1,325 @@ +local pretty = require 'pl.pretty' +local term = require 'term' + +local colors + +local isatty = io.type(io.stdout) == 'file' and term.isatty(io.stdout) + +local isWindows = package.config:sub(1,1) == '\\' + +if isWindows and not os.getenv("ANSICON") then + colors = setmetatable({}, {__index = function() return function(s) return s end end}) + isatty = false +else + colors = require 'term.colors' +end + +local clreol = "\27[K" +local cursorup = isatty and "\27[1A" or "" + +return function(options) + local busted = require 'busted' + local handler = require 'busted.outputHandlers.base'() + + local repeatSuiteString = '\nRepeating all tests (run %u of %u) . . .\n\n' + local randomizeString = colors.yellow('Note: Randomizing test order with a seed of %u.\n') + local suiteStartString = colors.green ('=======') .. ' Running tests from scanned files.\n' + local globalSetup = colors.green ('-------') .. ' Global test environment setup.\n' + local fileStartString = colors.green ('-------') .. ' Running tests from ' .. colors.cyan('%s') .. ' :\n' + local runningString = colors.green (' RUN') .. ' %s' + local successString = colors.green (' OK') .. clreol .. ' %s' + local skippedString = colors.yellow ('SKIP') .. clreol .. ' %s' + + local failureStartString = colors.red (' __________' .. clreol .. '\n FAIL') .. ' %s' + local failureString = colors.red ('__________') + + local errorStartString = colors.magenta(' __________' .. clreol .. '\n ERR') .. ' %s' + local errorString = colors.magenta('__________') + local errorAltEndString = colors.magenta(' __________') .. '\n\n' + + local fileEndString = colors.green ('-------') .. ' %u %s from %s (%.2f ms total)\n\n' + local globalTeardown = colors.green ('-------') .. ' Global test environment teardown.\n' + local suiteEndString = colors.green ('=======') .. ' %u %s from %u test %s ran. (%.2f ms total)\n' + local successStatus = colors.green ('PASSED ') .. ' %u %s.\n\n' + + local summaryStrings = { + skipped = { + header = 'SKIPPED %u %s, listed below:\n', + test = colors.yellow ('SKIPPED') .. ' %s\n', + footer = ' %u SKIPPED %s\n', + }, + + failure = { + header = ' FAILED %u %s, listed below:\n', + test = colors.red (' FAILED') .. ' %s\n', + footer = ' %u FAILED %s\n', + }, + + error = { + header = ' ERROR %u %s, listed below:\n', + test = colors.magenta(' ERROR') .. ' %s\n', + footer = ' %u %s\n', + }, + } + + local fileCount = 0 + local fileTestCount = 0 + local testCount = 0 + local successCount = 0 + local skippedCount = 0 + local failureCount = 0 + local errorCount = 0 + + local pendingDescription = function(pending) + local string = '' + + if type(pending.message) == 'string' then + string = string .. pending.message .. '\n' + elseif pending.message ~= nil then + string = string .. pretty.write(pending.message) .. '\n' + end + + return string + end + + local failureDescription = function(failure) + local string = failure.randomseed and ('Random seed: ' .. failure.randomseed .. '\n') or '' + if type(failure.message) == 'string' then + string = string .. failure.message + elseif failure.message == nil then + string = string .. 'Nil error' + else + string = string .. pretty.write(failure.message) + end + + string = '\n' .. string .. '\n' + + if options.verbose and failure.trace and failure.trace.traceback then + string = string .. failure.trace.traceback .. '\n' + end + + return string + end + + local getFileLine = function(element) + local fileline = '' + if element.trace and element.trace.source then + fileline = colors.cyan(element.trace.source:gsub("^@", "")) .. ':' .. + colors.cyan(element.trace.currentline) .. ': ' + end + return fileline + end + + local getTestList = function(status, count, list, getDescription) + local string = '' + local header = summaryStrings[status].header + if count > 0 and header then + local tests = (count == 1 and 'test' or 'tests') + local errors = (count == 1 and 'error' or 'errors') + string = header:format(count, status == 'error' and errors or tests) + + local testString = summaryStrings[status].test + if testString then + for _, t in ipairs(list) do + local fullname = getFileLine(t.element) .. colors.bright(t.name) + string = string .. testString:format(fullname) + if options.deferPrint then + string = string .. getDescription(t) + end + end + end + end + return string + end + + local getSummary = function(status, count) + local string = '' + local footer = summaryStrings[status].footer + if count > 0 and footer then + local tests = (count == 1 and 'TEST' or 'TESTS') + local errors = (count == 1 and 'ERROR' or 'ERRORS') + string = footer:format(count, status == 'error' and errors or tests) + end + return string + end + + local getSummaryString = function() + local tests = (successCount == 1 and 'test' or 'tests') + local string = successStatus:format(successCount, tests) + + string = string .. getTestList('skipped', skippedCount, handler.pendings, pendingDescription) + string = string .. getTestList('failure', failureCount, handler.failures, failureDescription) + string = string .. getTestList('error', errorCount, handler.errors, failureDescription) + + string = string .. ((skippedCount + failureCount + errorCount) > 0 and '\n' or '') + string = string .. getSummary('skipped', skippedCount) + string = string .. getSummary('failure', failureCount) + string = string .. getSummary('error', errorCount) + + return string + end + + local getTestName = function(element) + local out = {} + for text, hashtag in handler.getFullName(element):gmatch("([^#]*)(#?[%w_-]*)") do + table.insert(out, colors.bright(text)) + table.insert(out, colors.bright(colors.cyan(hashtag))) + end + return table.concat(out) + end + + local getFullName = function(element) + return getFileLine(element) .. getTestName(element) + end + + local clock = function(ms) + if ms < 1000 then + return colors.cyan(("%7.2f"):format(ms)) + elseif ms < 10000 then + return colors.yellow(("%7.2f"):format(ms)) + else + return colors.bright(colors.red(("%7.2f"):format(ms))) + end + end + + handler.suiteReset = function() + fileCount = 0 + fileTestCount = 0 + testCount = 0 + successCount = 0 + skippedCount = 0 + failureCount = 0 + errorCount = 0 + + return nil, true + end + + handler.suiteStart = function(suite, count, total, randomseed) + if total > 1 then + io.write(repeatSuiteString:format(count, total)) + end + if randomseed then + io.write(randomizeString:format(randomseed)) + end + io.write(suiteStartString) + io.write(globalSetup) + io.flush() + + return nil, true + end + + handler.suiteEnd = function(suite, count, total) + local elapsedTime_ms = suite.duration * 1000 + local tests = (testCount == 1 and 'test' or 'tests') + local files = (fileCount == 1 and 'file' or 'files') + io.write(globalTeardown) + io.write(suiteEndString:format(testCount, tests, fileCount, files, elapsedTime_ms)) + io.write(getSummaryString()) + io.flush() + + return nil, true + end + + handler.fileStart = function(file) + fileTestCount = 0 + + io.write(fileStartString:format(file.name)) + io.flush() + return nil, true + end + + handler.fileEnd = function(file) + local elapsedTime_ms = file.duration * 1000 + local tests = (fileTestCount == 1 and 'test' or 'tests') + fileCount = fileCount + 1 + io.write(fileEndString:format(fileTestCount, tests, file.name, elapsedTime_ms)) + io.flush() + return nil, true + end + + handler.testStart = function(element, parent) + if isatty then + local successName = colors.cyan(element.trace.currentline) .. ': '.. getTestName(element) + local str = '....... ' .. runningString:format(successName) .. '\n' + io.write(str) + io.flush() + end + + return nil, true + end + + handler.testEnd = function(element, parent, status, debug) + local elapsedTime_ms = element.duration * 1000 + local string + + fileTestCount = fileTestCount + 1 + testCount = testCount + 1 + local successName = colors.cyan(element.trace.currentline) .. ': '.. getTestName(element) + if status == 'success' then + io.write(cursorup) + successCount = successCount + 1 + string = clock(elapsedTime_ms) .. ' ' .. successString:format(successName) .. '\n' + elseif status == 'pending' then + io.write(cursorup) + skippedCount = skippedCount + 1 + string = ' ' .. skippedString:format(successName) .. '\n' + elseif status == 'failure' then + failureCount = failureCount + 1 + string = clock(elapsedTime_ms) .. ' ' .. failureString .. '\n\n' + elseif status == 'error' then + errorCount = errorCount + 1 + string = clock(elapsedTime_ms) .. ' ' .. errorString .. '\n\n' + end + + io.write(string) + io.flush() + + return nil, true + end + + handler.testFailure = function(element, parent, message, debug) + if not options.deferPrint then + io.write(failureStartString:format(getFullName(element))) + io.write(failureDescription(handler.failures[#handler.failures])) + io.flush() + end + return nil, true + end + + handler.testError = function(element, parent, message, debug) + if not options.deferPrint then + io.write(errorStartString:format(getFullName(element))) + io.write(failureDescription(handler.errors[#handler.errors])) + io.flush() + end + return nil, true + end + + handler.error = function(element, parent, message, debug) + if element.descriptor ~= 'it' then + if not options.deferPrint then + io.write(errorStartString:format(getFullName(element))) + io.write(failureDescription(handler.errors[#handler.errors])) + io.write(errorAltEndString) + io.flush() + end + errorCount = errorCount + 1 + end + + return nil, true + end + + busted.subscribe({ 'suite', 'reset' }, handler.suiteReset) + busted.subscribe({ 'suite', 'start' }, handler.suiteStart) + busted.subscribe({ 'suite', 'end' }, handler.suiteEnd) + busted.subscribe({ 'file', 'start' }, handler.fileStart) + busted.subscribe({ 'file', 'end' }, handler.fileEnd) + busted.subscribe({ 'test', 'start' }, handler.testStart, { predicate = handler.cancelOnPending }) + busted.subscribe({ 'test', 'end' }, handler.testEnd, { predicate = handler.cancelOnPending }) + busted.subscribe({ 'failure', 'it' }, handler.testFailure) + busted.subscribe({ 'error', 'it' }, handler.testError) + busted.subscribe({ 'failure' }, handler.error) + busted.subscribe({ 'error' }, handler.error) + + return handler +end \ No newline at end of file From 1e9d60e900a89fba25574864dcf205be2203760d Mon Sep 17 00:00:00 2001 From: Dhruv Sarawagi Date: Wed, 1 Sep 2021 16:56:13 +0530 Subject: [PATCH 10/12] doc-wip --- README.md | 114 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 81 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 7819e8b..67405c4 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,98 @@ # Kong-advanced-router -## Description - -Routing a request to a particular service based on the response of an I/O call - -Ex: Routing `/myteam` request to Team service before round lock and to PC service after roundlock by fetching round data using `/round` api of tour service before sending the request to upstream. - -A set of propositions can be defined based on the I/O call response data which are evaluated for each request and routed accordingly. - -For making the I/O call, all parameters of the call like URL, Method, body etc. can be defined in the config and can ge generated dynamically for each request. Results of the I/O calls are cached preventing round trips for same request. - -I/O URL can be interpolated using env variables and upstream URLS can also be interpolated using I/O data. - -## Implementation - +## Overview + +`kong-advanced-router` is a kong plugin that provides functionality to route a request to a particular upstream from a set of predefined upstreams based on the response of an I/O call. + +## Usecase + +Suppose we want to proxy a request to fetch the orders of a user. We want to proxy the request to order service A if the user's status is 1, proxy to order service B if the status is 2 and to order service C otherwise. This plugin can be used to fetch the user details before proxying the request. The upstream service will be set as order service A, B, or C based on the response of this call. + +## How it works + +1. The plugin uses the `io_url`, `io_http_method`, `io_request_template` parameters from the config to make an I/O call +2. It caches the response based on the `cache_ttl_header` header from the I/O response if `cache_io_response` is set to true in the config. +3. It evaluates the response against a list of conditions provided in `propositions_json`. +4. It then sets the upstream target and path using the `upstream_url` of the condition that evaluates to true or to the default values if all conditions evaluate to false. +5. The plugin interpolates the `upstream_url` and the `io_url` with environment variables before using them. + +## Example + +```lua + config = { + io_url = "http://user_service/user" , + io_http_method = "GET", + io_request_template = "{\"body\":{\"id\":\"headers.user_id\"}}", + http_connect_timeout = 2000, + http_send_timeout = 2000, + http_read_timeout = 2000, + cache_io_response = true, + cache_ttl_header = "edge_ttl", + cache_identifier = "headers.user_id", + default_cache_ttl_sec = 10, + propositions_json = "[ + { + \"condition\": \"extract_from_io_response('data.status') == 1\", + \"upstream_url\": \"http://order_service_a/orders\" + }, + { + \"condition\": \"extract_from_io_response('data.status') == 2\", + \"upstream_url\": \"http://order_service_b/orders\" + }, + { + \"condition\": \"default\", + \"upstream_url\": \"http://order_service_c/orders\" + } + ]", + variables = {"data.status"}, +} +``` +For the above config applied on route `/orders`. Suppose we make the below request +```shell +curl --location --request GET 'localhost:8000/orders' \ +--header 'user_id: 1' +``` -Propositions Json +The plugin first makes the below I/O call. +```shell +curl --location --request GET 'http://user_service/user' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "id" : 1 +}' +``` +Suppose the response received is ```json -[ - { - "condition": "get_timestamp_utc(extract_from_io_response('round.RoundStartTime')) > get_current_timestamp_utc()", - "upstream_url": "http://pc%TEAM_SUFFIX%.dream11%VPC_SUFFIX%.local:5000/myteam" - }, - { - "condition": "default", - "upstream_url": "http://team%TEAM_SUFFIX%.dream11%VPC_SUFFIX%.local:5000/myteam" - } -] +{ + "data": { + "status": 2, + "name": "foo", + "city": "bar" + } +} ``` -I/O request Template +The plugin caches only the keys from the response which are defined in `variables` in the config. + +So the data cached is ```json { - "body": { - "roundId": "headers.roundId" - } + "data.status": 2 } ``` -I/O Url -```json -http://tour%TEAM_SUFFIX%.dream11%VPC_SUFFIX%.local/round +Now this data is used to evaluate the conditions given in `propositions_json`. `extract_from_io_response` is an abstraction that is used to extract values from the response. In this case, the second condition evaluates to true i.e. +```lua +extract_from_io_response('data.status') == 2 ``` -Cache ttl header: `d11-edge-ttl` +Hence, the upstream url is set as `http://order_service_b/orders` + +### Other abstractions that can be used in propositions_json + +The plugin converts the conditions in `propositions_json` into lua functions using the `loadstring` function and keeps them in a global table against the route_id. It uses these functions for subsequent requests. + From 50ce0516385d6720f0eadf952eb8a847e4a4c3ae Mon Sep 17 00:00:00 2001 From: Dhruv Sarawagi Date: Sun, 12 Sep 2021 22:28:09 +0530 Subject: [PATCH 11/12] doc: complete documentation --- README.md | 59 +++++++++++++++++--- docs/index.md | 146 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 198 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 67405c4..a7cf160 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,63 @@ +[![Continuous Integration](https://github.com/dream11/kong-advanced-router/actions/workflows/ci.yml/badge.svg)](https://github.com/dream11/kong-advanced-router/actions/workflows/ci.yml) +![License](https://img.shields.io/badge/license-MIT-green.svg) + # Kong-advanced-router ## Overview -`kong-advanced-router` is a kong plugin that provides functionality to route a request to a particular upstream from a set of predefined upstreams based on the response of an I/O call. +`kong-advanced-router` is a kong plugin that provides functionality to route a request to a particular URL from a set of predefined URLs based on the response of an intermediate I/O call. ## Usecase Suppose we want to proxy a request to fetch the orders of a user. We want to proxy the request to order service A if the user's status is 1, proxy to order service B if the status is 2 and to order service C otherwise. This plugin can be used to fetch the user details before proxying the request. The upstream service will be set as order service A, B, or C based on the response of this call. +## Installation + +### [luarocks](https://luarocks.org/modules/dream11/kong-advanced-router) +```bash +luarocks install kong-advanced-router +``` + +You will also need to enable this plugin by adding it to the list of enabled plugins using `KONG_PLUGINS` environment variable or the `plugins` key in `kong.conf` + + export KONG_PLUGINS=advanced-router + +OR + + plugins=advanced-router + +### source +Clone this repo and run: +``` +luarocks make +``` + +### Parameters + +| Key | Default | Type | Required | Description | +| --- | --- | --- | --- | --- | +| io_url | | string | true | URL of the I/O call | +| io_http_method | GET | string | false | Http Method (GET, POST) of the I/O call | +| io_request_template | | string | true | Template of the I/O call in JSON. Must be a valid json string | +| http_connect_timeout | 5000 | number | false | Connect timeout (ms) of the I/O call | +| http_send_timeout | 5000 | number | false | Send timeout (ms) of the I/O call | +| http_read_timeout | 5000 | number | false | Read timeout (ms) of the I/O call | +| cache_io_response | true | boolean | false | Should the I/O response be cached | +| cache_ttl_header | | string | true | Header from the I/O response that will be used to set the ttl of the cached response | +| cache_identifier | | string | true | Key from the request which uniquely identifies the request. This is used to create the key against which the response is cached | +| default_cache_ttl_sec | | number | true | This ttl is used if `cache_ttl_header` in the I/O response is null | +| propositions_json | | string | true | The conditions that are used to set the upstream url. Must be a valid json string | +| variables | | array of strings | true | The list of all the keys that are passed to `extract_from_io_response` in `propositions_json` | + ## How it works -1. The plugin uses the `io_url`, `io_http_method`, `io_request_template` parameters from the config to make an I/O call +1. The plugin uses the `io_url`, `io_http_method`, `io_request_template` parameters from the config to make the I/O call 2. It caches the response based on the `cache_ttl_header` header from the I/O response if `cache_io_response` is set to true in the config. 3. It evaluates the response against a list of conditions provided in `propositions_json`. 4. It then sets the upstream target and path using the `upstream_url` of the condition that evaluates to true or to the default values if all conditions evaluate to false. 5. The plugin interpolates the `upstream_url` and the `io_url` with environment variables before using them. -## Example +## Usage ```lua config = { @@ -85,14 +126,20 @@ So the data cached is } ``` -Now this data is used to evaluate the conditions given in `propositions_json`. `extract_from_io_response` is an abstraction that is used to extract values from the response. In this case, the second condition evaluates to true i.e. +Now this data is used to evaluate the conditions given in `propositions_json`. `extract_from_io_response` is an abstraction that is used to extract values from the I/O call response. In this case, the second condition evaluates to true i.e. ```lua extract_from_io_response('data.status') == 2 ``` Hence, the upstream url is set as `http://order_service_b/orders` -### Other abstractions that can be used in propositions_json +### Other abstractions that can be used in the condition part of propositions_json + +The below functions can be used to write conditions in propositions_json + +1. `extract_from_io_response(key)` - Returns the value of the provided key from the I/O call response body. Nested keys can be passed by concatenating with dot (.). Eg - `data.status` + +2. `get_timestamp_utc(datestring)` - Returns the UTC timestamp of a datestring. It internally uses the [Tieske/date](https://github.com/Tieske/date) module. +3. `get_current_timestamp_utc` - Returns the current timestamp in UTC. -The plugin converts the conditions in `propositions_json` into lua functions using the `loadstring` function and keeps them in a global table against the route_id. It uses these functions for subsequent requests. diff --git a/docs/index.md b/docs/index.md index bfa03e8..a7cf160 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1 +1,145 @@ -#Kong-advanced-router +[![Continuous Integration](https://github.com/dream11/kong-advanced-router/actions/workflows/ci.yml/badge.svg)](https://github.com/dream11/kong-advanced-router/actions/workflows/ci.yml) +![License](https://img.shields.io/badge/license-MIT-green.svg) + +# Kong-advanced-router + +## Overview + +`kong-advanced-router` is a kong plugin that provides functionality to route a request to a particular URL from a set of predefined URLs based on the response of an intermediate I/O call. + +## Usecase + +Suppose we want to proxy a request to fetch the orders of a user. We want to proxy the request to order service A if the user's status is 1, proxy to order service B if the status is 2 and to order service C otherwise. This plugin can be used to fetch the user details before proxying the request. The upstream service will be set as order service A, B, or C based on the response of this call. + +## Installation + +### [luarocks](https://luarocks.org/modules/dream11/kong-advanced-router) +```bash +luarocks install kong-advanced-router +``` + +You will also need to enable this plugin by adding it to the list of enabled plugins using `KONG_PLUGINS` environment variable or the `plugins` key in `kong.conf` + + export KONG_PLUGINS=advanced-router + +OR + + plugins=advanced-router + +### source +Clone this repo and run: +``` +luarocks make +``` + +### Parameters + +| Key | Default | Type | Required | Description | +| --- | --- | --- | --- | --- | +| io_url | | string | true | URL of the I/O call | +| io_http_method | GET | string | false | Http Method (GET, POST) of the I/O call | +| io_request_template | | string | true | Template of the I/O call in JSON. Must be a valid json string | +| http_connect_timeout | 5000 | number | false | Connect timeout (ms) of the I/O call | +| http_send_timeout | 5000 | number | false | Send timeout (ms) of the I/O call | +| http_read_timeout | 5000 | number | false | Read timeout (ms) of the I/O call | +| cache_io_response | true | boolean | false | Should the I/O response be cached | +| cache_ttl_header | | string | true | Header from the I/O response that will be used to set the ttl of the cached response | +| cache_identifier | | string | true | Key from the request which uniquely identifies the request. This is used to create the key against which the response is cached | +| default_cache_ttl_sec | | number | true | This ttl is used if `cache_ttl_header` in the I/O response is null | +| propositions_json | | string | true | The conditions that are used to set the upstream url. Must be a valid json string | +| variables | | array of strings | true | The list of all the keys that are passed to `extract_from_io_response` in `propositions_json` | + +## How it works + +1. The plugin uses the `io_url`, `io_http_method`, `io_request_template` parameters from the config to make the I/O call +2. It caches the response based on the `cache_ttl_header` header from the I/O response if `cache_io_response` is set to true in the config. +3. It evaluates the response against a list of conditions provided in `propositions_json`. +4. It then sets the upstream target and path using the `upstream_url` of the condition that evaluates to true or to the default values if all conditions evaluate to false. +5. The plugin interpolates the `upstream_url` and the `io_url` with environment variables before using them. + +## Usage + +```lua + config = { + io_url = "http://user_service/user" , + io_http_method = "GET", + io_request_template = "{\"body\":{\"id\":\"headers.user_id\"}}", + http_connect_timeout = 2000, + http_send_timeout = 2000, + http_read_timeout = 2000, + cache_io_response = true, + cache_ttl_header = "edge_ttl", + cache_identifier = "headers.user_id", + default_cache_ttl_sec = 10, + propositions_json = "[ + { + \"condition\": \"extract_from_io_response('data.status') == 1\", + \"upstream_url\": \"http://order_service_a/orders\" + }, + { + \"condition\": \"extract_from_io_response('data.status') == 2\", + \"upstream_url\": \"http://order_service_b/orders\" + }, + { + \"condition\": \"default\", + \"upstream_url\": \"http://order_service_c/orders\" + } + ]", + variables = {"data.status"}, +} +``` + +For the above config applied on route `/orders`. Suppose we make the below request + +```shell +curl --location --request GET 'localhost:8000/orders' \ +--header 'user_id: 1' +``` + +The plugin first makes the below I/O call. +```shell +curl --location --request GET 'http://user_service/user' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "id" : 1 +}' +``` +Suppose the response received is + +```json +{ + "data": { + "status": 2, + "name": "foo", + "city": "bar" + } +} +``` + +The plugin caches only the keys from the response which are defined in `variables` in the config. + +So the data cached is + +```json +{ + "data.status": 2 +} +``` + +Now this data is used to evaluate the conditions given in `propositions_json`. `extract_from_io_response` is an abstraction that is used to extract values from the I/O call response. In this case, the second condition evaluates to true i.e. +```lua +extract_from_io_response('data.status') == 2 +``` +Hence, the upstream url is set as `http://order_service_b/orders` + +### Other abstractions that can be used in the condition part of propositions_json + +The below functions can be used to write conditions in propositions_json + +1. `extract_from_io_response(key)` - Returns the value of the provided key from the I/O call response body. Nested keys can be passed by concatenating with dot (.). Eg - `data.status` + +2. `get_timestamp_utc(datestring)` - Returns the UTC timestamp of a datestring. It internally uses the [Tieske/date](https://github.com/Tieske/date) module. +3. `get_current_timestamp_utc` - Returns the current timestamp in UTC. + + + From 8da92ce2052497040e0d553af8d0afaa219a2372 Mon Sep 17 00:00:00 2001 From: Dhruv Sarawagi Date: Sun, 12 Sep 2021 22:54:48 +0530 Subject: [PATCH 12/12] chore: delete custom output handlers --- spec/output-handlers/custom_format.lua | 325 ------------------------- 1 file changed, 325 deletions(-) delete mode 100644 spec/output-handlers/custom_format.lua diff --git a/spec/output-handlers/custom_format.lua b/spec/output-handlers/custom_format.lua deleted file mode 100644 index 63cf27f..0000000 --- a/spec/output-handlers/custom_format.lua +++ /dev/null @@ -1,325 +0,0 @@ -local pretty = require 'pl.pretty' -local term = require 'term' - -local colors - -local isatty = io.type(io.stdout) == 'file' and term.isatty(io.stdout) - -local isWindows = package.config:sub(1,1) == '\\' - -if isWindows and not os.getenv("ANSICON") then - colors = setmetatable({}, {__index = function() return function(s) return s end end}) - isatty = false -else - colors = require 'term.colors' -end - -local clreol = "\27[K" -local cursorup = isatty and "\27[1A" or "" - -return function(options) - local busted = require 'busted' - local handler = require 'busted.outputHandlers.base'() - - local repeatSuiteString = '\nRepeating all tests (run %u of %u) . . .\n\n' - local randomizeString = colors.yellow('Note: Randomizing test order with a seed of %u.\n') - local suiteStartString = colors.green ('=======') .. ' Running tests from scanned files.\n' - local globalSetup = colors.green ('-------') .. ' Global test environment setup.\n' - local fileStartString = colors.green ('-------') .. ' Running tests from ' .. colors.cyan('%s') .. ' :\n' - local runningString = colors.green (' RUN') .. ' %s' - local successString = colors.green (' OK') .. clreol .. ' %s' - local skippedString = colors.yellow ('SKIP') .. clreol .. ' %s' - - local failureStartString = colors.red (' __________' .. clreol .. '\n FAIL') .. ' %s' - local failureString = colors.red ('__________') - - local errorStartString = colors.magenta(' __________' .. clreol .. '\n ERR') .. ' %s' - local errorString = colors.magenta('__________') - local errorAltEndString = colors.magenta(' __________') .. '\n\n' - - local fileEndString = colors.green ('-------') .. ' %u %s from %s (%.2f ms total)\n\n' - local globalTeardown = colors.green ('-------') .. ' Global test environment teardown.\n' - local suiteEndString = colors.green ('=======') .. ' %u %s from %u test %s ran. (%.2f ms total)\n' - local successStatus = colors.green ('PASSED ') .. ' %u %s.\n\n' - - local summaryStrings = { - skipped = { - header = 'SKIPPED %u %s, listed below:\n', - test = colors.yellow ('SKIPPED') .. ' %s\n', - footer = ' %u SKIPPED %s\n', - }, - - failure = { - header = ' FAILED %u %s, listed below:\n', - test = colors.red (' FAILED') .. ' %s\n', - footer = ' %u FAILED %s\n', - }, - - error = { - header = ' ERROR %u %s, listed below:\n', - test = colors.magenta(' ERROR') .. ' %s\n', - footer = ' %u %s\n', - }, - } - - local fileCount = 0 - local fileTestCount = 0 - local testCount = 0 - local successCount = 0 - local skippedCount = 0 - local failureCount = 0 - local errorCount = 0 - - local pendingDescription = function(pending) - local string = '' - - if type(pending.message) == 'string' then - string = string .. pending.message .. '\n' - elseif pending.message ~= nil then - string = string .. pretty.write(pending.message) .. '\n' - end - - return string - end - - local failureDescription = function(failure) - local string = failure.randomseed and ('Random seed: ' .. failure.randomseed .. '\n') or '' - if type(failure.message) == 'string' then - string = string .. failure.message - elseif failure.message == nil then - string = string .. 'Nil error' - else - string = string .. pretty.write(failure.message) - end - - string = '\n' .. string .. '\n' - - if options.verbose and failure.trace and failure.trace.traceback then - string = string .. failure.trace.traceback .. '\n' - end - - return string - end - - local getFileLine = function(element) - local fileline = '' - if element.trace and element.trace.source then - fileline = colors.cyan(element.trace.source:gsub("^@", "")) .. ':' .. - colors.cyan(element.trace.currentline) .. ': ' - end - return fileline - end - - local getTestList = function(status, count, list, getDescription) - local string = '' - local header = summaryStrings[status].header - if count > 0 and header then - local tests = (count == 1 and 'test' or 'tests') - local errors = (count == 1 and 'error' or 'errors') - string = header:format(count, status == 'error' and errors or tests) - - local testString = summaryStrings[status].test - if testString then - for _, t in ipairs(list) do - local fullname = getFileLine(t.element) .. colors.bright(t.name) - string = string .. testString:format(fullname) - if options.deferPrint then - string = string .. getDescription(t) - end - end - end - end - return string - end - - local getSummary = function(status, count) - local string = '' - local footer = summaryStrings[status].footer - if count > 0 and footer then - local tests = (count == 1 and 'TEST' or 'TESTS') - local errors = (count == 1 and 'ERROR' or 'ERRORS') - string = footer:format(count, status == 'error' and errors or tests) - end - return string - end - - local getSummaryString = function() - local tests = (successCount == 1 and 'test' or 'tests') - local string = successStatus:format(successCount, tests) - - string = string .. getTestList('skipped', skippedCount, handler.pendings, pendingDescription) - string = string .. getTestList('failure', failureCount, handler.failures, failureDescription) - string = string .. getTestList('error', errorCount, handler.errors, failureDescription) - - string = string .. ((skippedCount + failureCount + errorCount) > 0 and '\n' or '') - string = string .. getSummary('skipped', skippedCount) - string = string .. getSummary('failure', failureCount) - string = string .. getSummary('error', errorCount) - - return string - end - - local getTestName = function(element) - local out = {} - for text, hashtag in handler.getFullName(element):gmatch("([^#]*)(#?[%w_-]*)") do - table.insert(out, colors.bright(text)) - table.insert(out, colors.bright(colors.cyan(hashtag))) - end - return table.concat(out) - end - - local getFullName = function(element) - return getFileLine(element) .. getTestName(element) - end - - local clock = function(ms) - if ms < 1000 then - return colors.cyan(("%7.2f"):format(ms)) - elseif ms < 10000 then - return colors.yellow(("%7.2f"):format(ms)) - else - return colors.bright(colors.red(("%7.2f"):format(ms))) - end - end - - handler.suiteReset = function() - fileCount = 0 - fileTestCount = 0 - testCount = 0 - successCount = 0 - skippedCount = 0 - failureCount = 0 - errorCount = 0 - - return nil, true - end - - handler.suiteStart = function(suite, count, total, randomseed) - if total > 1 then - io.write(repeatSuiteString:format(count, total)) - end - if randomseed then - io.write(randomizeString:format(randomseed)) - end - io.write(suiteStartString) - io.write(globalSetup) - io.flush() - - return nil, true - end - - handler.suiteEnd = function(suite, count, total) - local elapsedTime_ms = suite.duration * 1000 - local tests = (testCount == 1 and 'test' or 'tests') - local files = (fileCount == 1 and 'file' or 'files') - io.write(globalTeardown) - io.write(suiteEndString:format(testCount, tests, fileCount, files, elapsedTime_ms)) - io.write(getSummaryString()) - io.flush() - - return nil, true - end - - handler.fileStart = function(file) - fileTestCount = 0 - - io.write(fileStartString:format(file.name)) - io.flush() - return nil, true - end - - handler.fileEnd = function(file) - local elapsedTime_ms = file.duration * 1000 - local tests = (fileTestCount == 1 and 'test' or 'tests') - fileCount = fileCount + 1 - io.write(fileEndString:format(fileTestCount, tests, file.name, elapsedTime_ms)) - io.flush() - return nil, true - end - - handler.testStart = function(element, parent) - if isatty then - local successName = colors.cyan(element.trace.currentline) .. ': '.. getTestName(element) - local str = '....... ' .. runningString:format(successName) .. '\n' - io.write(str) - io.flush() - end - - return nil, true - end - - handler.testEnd = function(element, parent, status, debug) - local elapsedTime_ms = element.duration * 1000 - local string - - fileTestCount = fileTestCount + 1 - testCount = testCount + 1 - local successName = colors.cyan(element.trace.currentline) .. ': '.. getTestName(element) - if status == 'success' then - io.write(cursorup) - successCount = successCount + 1 - string = clock(elapsedTime_ms) .. ' ' .. successString:format(successName) .. '\n' - elseif status == 'pending' then - io.write(cursorup) - skippedCount = skippedCount + 1 - string = ' ' .. skippedString:format(successName) .. '\n' - elseif status == 'failure' then - failureCount = failureCount + 1 - string = clock(elapsedTime_ms) .. ' ' .. failureString .. '\n\n' - elseif status == 'error' then - errorCount = errorCount + 1 - string = clock(elapsedTime_ms) .. ' ' .. errorString .. '\n\n' - end - - io.write(string) - io.flush() - - return nil, true - end - - handler.testFailure = function(element, parent, message, debug) - if not options.deferPrint then - io.write(failureStartString:format(getFullName(element))) - io.write(failureDescription(handler.failures[#handler.failures])) - io.flush() - end - return nil, true - end - - handler.testError = function(element, parent, message, debug) - if not options.deferPrint then - io.write(errorStartString:format(getFullName(element))) - io.write(failureDescription(handler.errors[#handler.errors])) - io.flush() - end - return nil, true - end - - handler.error = function(element, parent, message, debug) - if element.descriptor ~= 'it' then - if not options.deferPrint then - io.write(errorStartString:format(getFullName(element))) - io.write(failureDescription(handler.errors[#handler.errors])) - io.write(errorAltEndString) - io.flush() - end - errorCount = errorCount + 1 - end - - return nil, true - end - - busted.subscribe({ 'suite', 'reset' }, handler.suiteReset) - busted.subscribe({ 'suite', 'start' }, handler.suiteStart) - busted.subscribe({ 'suite', 'end' }, handler.suiteEnd) - busted.subscribe({ 'file', 'start' }, handler.fileStart) - busted.subscribe({ 'file', 'end' }, handler.fileEnd) - busted.subscribe({ 'test', 'start' }, handler.testStart, { predicate = handler.cancelOnPending }) - busted.subscribe({ 'test', 'end' }, handler.testEnd, { predicate = handler.cancelOnPending }) - busted.subscribe({ 'failure', 'it' }, handler.testFailure) - busted.subscribe({ 'error', 'it' }, handler.testError) - busted.subscribe({ 'failure' }, handler.error) - busted.subscribe({ 'error' }, handler.error) - - return handler -end \ No newline at end of file