diff --git a/kong-0.7.0-0.rockspec b/kong-0.7.0-0.rockspec index 1cc3c6ca9aed..cc64bfe6a946 100644 --- a/kong-0.7.0-0.rockspec +++ b/kong-0.7.0-0.rockspec @@ -219,6 +219,9 @@ build = { ["kong.plugins.acl.api"] = "kong/plugins/acl/api.lua", ["kong.plugins.acl.daos"] = "kong/plugins/acl/daos.lua", + ["kong.plugins.correlation-id.handler"] = "kong/plugins/correlation-id/handler.lua", + ["kong.plugins.correlation-id.schema"] = "kong/plugins/correlation-id/schema.lua", + ["kong.api.app"] = "kong/api/app.lua", ["kong.api.crud_helpers"] = "kong/api/crud_helpers.lua", ["kong.api.route_helpers"] = "kong/api/route_helpers.lua", diff --git a/kong/constants.lua b/kong/constants.lua index 4ff64180bd38..fbb2ff6d3712 100644 --- a/kong/constants.lua +++ b/kong/constants.lua @@ -14,7 +14,7 @@ return { NGINX_CONFIG = "nginx.conf" }, PLUGINS_AVAILABLE = { - "ssl", "jwt", "acl", "cors", "oauth2", "tcp-log", "udp-log", "file-log", + "ssl", "jwt", "acl", "correlation-id", "cors", "oauth2", "tcp-log", "udp-log", "file-log", "http-log", "key-auth", "hmac-auth", "basic-auth", "ip-restriction", "mashape-analytics", "request-transformer", "response-transformer", "request-size-limiting", "rate-limiting", "response-ratelimiting", "syslog", diff --git a/kong/plugins/correlation-id/handler.lua b/kong/plugins/correlation-id/handler.lua new file mode 100644 index 000000000000..e4ac667ae893 --- /dev/null +++ b/kong/plugins/correlation-id/handler.lua @@ -0,0 +1,60 @@ +-- Copyright (C) Mashape, Inc. + +local BasePlugin = require "kong.plugins.base_plugin" +local uuid = require "lua_uuid" +local req_set_header = ngx.req.set_header +local req_get_headers = ngx.req.get_headers + +local CorrelationIdHandler = BasePlugin:extend() + +local worker_uuid +local worker_counter + +local generators = setmetatable({ + ["uuid"] = function() + return uuid() + end, + ["uuid#counter"] = function() + worker_counter = worker_counter + 1 + return worker_uuid.."#"..worker_counter + end, +}, { __index = function(self, generator) + ngx.log(ngx.ERR, "Invalid generator: "..generator) +end +}) + +function CorrelationIdHandler:new() + CorrelationIdHandler.super.new(self, "correlation-id") +end + +function CorrelationIdHandler:init_worker() + CorrelationIdHandler.super.init_worker(self) + worker_uuid = uuid() + worker_counter = 0 +end + +function CorrelationIdHandler:access(conf) + CorrelationIdHandler.super.access(self) + + -- Set header for upstream + local header_value = req_get_headers()[conf.header_name] + if not header_value then + -- Generate the header value + header_value = generators[conf.generator]() + req_set_header(conf.header_name, header_value) + end + + if conf.echo_downstream then + -- For later use, to echo it back downstream + ngx.ctx.correlationid_header_value = header_value + end +end + +function CorrelationIdHandler:header_filter(conf) + CorrelationIdHandler.super.header_filter(self) + if conf.echo_downstream then + ngx.header[conf.header_name] = ngx.ctx.correlationid_header_value + end +end + +return CorrelationIdHandler diff --git a/kong/plugins/correlation-id/schema.lua b/kong/plugins/correlation-id/schema.lua new file mode 100644 index 000000000000..6f9782f6a753 --- /dev/null +++ b/kong/plugins/correlation-id/schema.lua @@ -0,0 +1,17 @@ +return { + fields = { + header_name = { + type = "string", + default = "Kong-Request-ID" + }, + generator = { + type = "string", + default = "uuid#counter", + enum = {"uuid", "uuid#counter"} + }, + echo_downstream = { + type = "boolean", + default = false + } + } +} diff --git a/spec/plugins/correlation-id/access_spec.lua b/spec/plugins/correlation-id/access_spec.lua new file mode 100644 index 000000000000..906e572d9f01 --- /dev/null +++ b/spec/plugins/correlation-id/access_spec.lua @@ -0,0 +1,91 @@ +local spec_helper = require "spec.spec_helpers" +local http_client = require "kong.tools.http_client" +local json = require "cjson" + +local STUB_GET_URL = spec_helper.STUB_GET_URL +local UUID_PATTERN = "%x%x%x%x%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%x%x%x%x%x%x%x%x" +local UUID_COUNTER_PATTERN = UUID_PATTERN.."#%d" +local DEFAULT_HEADER_NAME = "Kong-Request-ID" + +describe("Correlation ID Plugin", function() + + setup(function() + spec_helper.prepare_db() + spec_helper.insert_fixtures { + api = { + {request_host = "correlation1.com", upstream_url = "http://mockbin.com"}, + {request_host = "correlation2.com", upstream_url = "http://mockbin.com"}, + {request_host = "correlation3.com", upstream_url = "http://mockbin.com"}, + {request_host = "correlation4.com", upstream_url = "http://mockbin.com"} + }, + plugin = { + {name = "correlation-id", config = {echo_downstream = true}, __api = 1}, + {name = "correlation-id", config = {header_name = "Foo-Bar-Id", echo_downstream = true}, __api = 2}, + {name = "correlation-id", config = {generator = "uuid", echo_downstream = true}, __api = 3}, + {name = "correlation-id", config = {}, __api = 4}, + } + } + spec_helper.start_kong() + end) + + teardown(function() + spec_helper.stop_kong() + end) + + local function test_with(host, header, pattern) + local _, status1, headers1 = http_client.get(STUB_GET_URL, nil, {host = host}) + assert.equal(200, status1) + local correlation_id1 = headers1[header:lower()] + assert.are.matches(pattern, correlation_id1) + + local _, status2, headers2 = http_client.get(STUB_GET_URL, nil, {host = host}) + assert.equal(200, status2) + local correlation_id2 = headers2[header:lower()] + assert.are.matches(pattern, correlation_id2) + + assert.are_not_equals(correlation_id1, correlation_id2) + + -- TODO kong_TEST.yml's worker_processes has to be 1 for the below to work. + --[[ + if pattern == UUID_COUNTER_PATTERN then + local uuid1 = correlation_id1:sub(0, -3) + local uuid2 = correlation_id2:sub(0, -3) + assert.equals(uuid1, uuid2) + + local counter1 = correlation_id1:sub(-1) + local counter2 = correlation_id2:sub(-1) + assert.True(counter1 + 1 == counter2) + end + --]] + end + + it("should increment the counter", function() + test_with("correlation1.com", DEFAULT_HEADER_NAME, UUID_COUNTER_PATTERN) + end) + + it("should use the header in the configuration", function() + test_with("correlation2.com", "Foo-Bar-Id", UUID_COUNTER_PATTERN) + end) + + it("should generate a unique UUID for every request using default header", function() + test_with("correlation3.com", DEFAULT_HEADER_NAME, UUID_PATTERN) + end) + + it("should honour the existing header", function() + local existing_correlation_id = "foo" + local response, status = http_client.get( + STUB_GET_URL, + nil, + {host = "correlation1.com", [DEFAULT_HEADER_NAME] = existing_correlation_id}) + assert.equal(200, status) + local correlation_id = json.decode(response).headers[DEFAULT_HEADER_NAME:lower()] + assert.equals(existing_correlation_id, correlation_id) + end) + + it("should not echo back the correlation header", function() + local _, status, headers = http_client.get(STUB_GET_URL, nil, {host = "correlation4.com"}) + assert.equal(200, status) + local correlation_id = headers[DEFAULT_HEADER_NAME:lower()] + assert.falsy(correlation_id) + end) +end)