This is an automated email from the ASF dual-hosted git repository. membphis pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push: new 240942e feature: implemented `request-id` plugin to uniquely track requests in APISIX (#2026) 240942e is described below commit 240942e9416ef0cd771e8cd6f2f7336553b45de2 Author: Nirojan Selvanathan <sshn...@gmail.com> AuthorDate: Wed Aug 12 17:09:39 2020 +0200 feature: implemented `request-id` plugin to uniquely track requests in APISIX (#2026) fix #2022 --- apisix/plugins/request-id.lua | 68 ++++++++ conf/config.yaml | 1 + doc/plugins/request-id.md | 90 ++++++++++ t/admin/plugins.t | 2 +- t/debug/debug-mode.t | 1 + t/plugin/request-id.t | 376 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 537 insertions(+), 1 deletion(-) diff --git a/apisix/plugins/request-id.lua b/apisix/plugins/request-id.lua new file mode 100644 index 0000000..0de07fa --- /dev/null +++ b/apisix/plugins/request-id.lua @@ -0,0 +1,68 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You under the Apache License, Version 2.0 +-- (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +local core = require("apisix.core") +local plugin_name = "request-id" +local ngx = ngx +local uuid = require("resty.jit-uuid") + +local schema = { + type = "object", + properties = { + header_name = {type = "string", default = "X-Request-Id"}, + include_in_response = {type = "boolean", default = true} + } +} + + +local _M = { + version = 0.1, + priority = 11010, + name = plugin_name, + schema = schema, +} + + +function _M.check_schema(conf) + return core.schema.check(schema, conf) +end + + +function _M.rewrite(conf, ctx) + local headers = ngx.req.get_headers() + local uuid_val = uuid() + if not headers[conf.header_name] then + core.request.set_header(conf.header_name, uuid_val) + end + + if conf.include_in_response then + ctx.x_request_id = uuid_val + end +end + + +function _M.header_filter(conf, ctx) + if not conf.include_in_response then + return + end + + local headers = ngx.resp.get_headers() + if not headers[conf.header_name] then + core.response.set_header(conf.header_name, ctx.x_request_id) + end +end + +return _M diff --git a/conf/config.yaml b/conf/config.yaml index aa0361f..047f775 100644 --- a/conf/config.yaml +++ b/conf/config.yaml @@ -178,6 +178,7 @@ plugins: # plugin list - request-validation - proxy-cache - proxy-mirror + - request-id stream_plugins: - mqtt-proxy diff --git a/doc/plugins/request-id.md b/doc/plugins/request-id.md new file mode 100644 index 0000000..0857e0e --- /dev/null +++ b/doc/plugins/request-id.md @@ -0,0 +1,90 @@ +<!-- +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +--> + +[Chinese](../zh-cn/plugins/request-validation.md) + +# Summary +- [**Name**](#name) +- [**Attributes**](#attributes) +- [**How To Enable**](#how-to-enable) +- [**Test Plugin**](#test-plugin) +- [**Disable Plugin**](#disable-plugin) +- [**Examples**](#examples) + + +## Name + +`request-id` plugin adds a unique ID (UUID) to each request proxied through APISIX. This plugin can be used to track an +API request. The plugin will not add a request id if the `header_name` is already present in the request. + +## Attributes + +|Name |Requirement |Description| +|--------- |-------- |-----------| +| header_name |optional |Request ID header name (default: X-Request-Id)| +| include_in_response |optional |Option to include the unique request ID in the response header (default: true)| + +## How To Enable + +Create a route and enable the request-id plugin on the route: + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/5 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/get", + "plugins": { + "request-id": { + "include_in_response": true + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:8080": 1 + } + } +} +``` + +## Test Plugin + +```shell +$ curl -i http://127.0.0.1:9080/hello +HTTP/1.1 200 OK +``` + +## Disable Plugin + +Remove the corresponding json configuration in the plugin configuration to disable the `request-validation`. +APISIX plugins are hot-reloaded, therefore no need to restart APISIX. + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/5 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/get", + "plugins": { + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:8080": 1 + } + } +} +``` diff --git a/t/admin/plugins.t b/t/admin/plugins.t index 9d96556..d750390 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -30,7 +30,7 @@ __DATA__ --- request GET /apisix/admin/plugins/list --- response_body_like eval -qr/\["fault-injection","serverless-pre-function","batch-requests","cors","ip-restriction","uri-blocker","request-validation","openid-connect","wolf-rbac","basic-auth","jwt-auth","key-auth","consumer-restriction","authz-keycloak","proxy-mirror","proxy-cache","proxy-rewrite","limit-conn","limit-count","limit-req","node-status","redirect","response-rewrite","grpc-transcode","prometheus","echo","http-logger","tcp-logger","kafka-logger","syslog","udp-logger","zipkin","skywalking","serverless- [...] +qr/\["request-id","fault-injection","serverless-pre-function","batch-requests","cors","ip-restriction","uri-blocker","request-validation","openid-connect","wolf-rbac","basic-auth","jwt-auth","key-auth","consumer-restriction","authz-keycloak","proxy-mirror","proxy-cache","proxy-rewrite","limit-conn","limit-count","limit-req","node-status","redirect","response-rewrite","grpc-transcode","prometheus","echo","http-logger","tcp-logger","kafka-logger","syslog","udp-logger","zipkin","skywalking" [...] --- no_error_log [error] diff --git a/t/debug/debug-mode.t b/t/debug/debug-mode.t index 0b81bad..9f4f1a9 100644 --- a/t/debug/debug-mode.t +++ b/t/debug/debug-mode.t @@ -53,6 +53,7 @@ done --- grep_error_log eval qr/loaded plugin and sort by priority: [-\d]+ name: [\w-]+/ --- grep_error_log_out +loaded plugin and sort by priority: 11010 name: request-id loaded plugin and sort by priority: 11000 name: fault-injection loaded plugin and sort by priority: 10000 name: serverless-pre-function loaded plugin and sort by priority: 4010 name: batch-requests diff --git a/t/plugin/request-id.t b/t/plugin/request-id.t new file mode 100644 index 0000000..3fef3d8 --- /dev/null +++ b/t/plugin/request-id.t @@ -0,0 +1,376 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +use t::APISIX 'no_plan'; + +log_level('debug'); +repeat_each(1); +no_long_string(); +no_root_location(); +run_tests; + +__DATA__ + +=== TEST 1: sanity +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.request-id") + local ok, err = plugin.check_schema({}) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- request +GET /t +--- response_body +done +--- no_error_log +[error] + + + +=== TEST 2: wrong type +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.request-id") + local ok, err = plugin.check_schema({include_in_response = "bad_type"}) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- request +GET /t +--- response_body +property "include_in_response" validation failed: wrong type: expected boolean, got string +done +--- no_error_log +[error] + + + +=== TEST 3: add plugin with include_in_response true (default true) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "request-id": { + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/opentracing" + }]], + [[{ + "node": { + "value": { + "plugins": { + "request-id": { + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/opentracing" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 4: check for request id in response header (default header name) +--- config + location /t { + content_by_lua_block { + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/opentracing" + local res, err = httpc:request_uri(uri, + { + method = "GET", + headers = { + ["Content-Type"] = "application/json", + } + }) + + if res.headers["X-Request-Id"] then + ngx.say("request header present") + else + ngx.say("failed") + end + } + } +--- request +GET /t +--- response_body +request header present +--- no_error_log +[error] + + + +=== TEST 5: check for unique id +--- config + location /t { + content_by_lua_block { + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/opentracing" + local res1, err1 = httpc:request_uri(uri, + { + method = "GET", + headers = { + ["Content-Type"] = "application/json", + } + } + ) + local res2, err2 = httpc:request_uri(uri, + { + method = "GET", + headers = { + ["Content-Type"] = "application/json", + } + } + ) + + -- ngx.say("res1: ", res1.headers["X-Request-Id"]) + -- ngx.say("res2: ", res2.headers["X-Request-Id"]) + if res1.headers["X-Request-Id"] == res2.headers["X-Request-Id"] then + ngx.say("ids not unique") + else + ngx.say("true") + end + } + } +--- request +GET /t +--- response_body +true +--- no_error_log +[error] + + + +=== TEST 6: add plugin with custom header name +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "request-id": { + "header_name": "Custom-Header-Name" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/opentracing" + }]], + [[{ + "node": { + "value": { + "plugins": { + "request-id": { + "header_name": "Custom-Header-Name", + "include_in_response": true + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/opentracing" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 7: check for request id in response header (custom header name) +--- config + location /t { + content_by_lua_block { + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/opentracing" + local res, err = httpc:request_uri(uri, + { + method = "GET", + headers = { + ["Content-Type"] = "application/json", + } + }) + + if res.headers["Custom-Header-Name"] then + ngx.say("request header present") + else + ngx.say("failed") + end + } + } +--- request +GET /t +--- response_body +request header present +--- no_error_log +[error] + + + +=== TEST 8: add plugin with include_in_response false (default true) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "request-id": { + "include_in_response": false + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/opentracing" + }]], + [[{ + "node": { + "value": { + "plugins": { + "request-id": { + "include_in_response": false + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/opentracing" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 9: check for request id is not present in the response header +--- config + location /t { + content_by_lua_block { + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/opentracing" + local res, err = httpc:request_uri(uri, + { + method = "GET", + headers = { + ["Content-Type"] = "application/json", + } + }) + + if not res.headers["X-Request-Id"] then + ngx.say("request header not present") + else + ngx.say("failed") + end + } + } +--- request +GET /t +--- response_body +request header not present +--- no_error_log +[error]