Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core) add X-Kong-Upstream-Status header #3263

Closed
wants to merge 2 commits into from

Conversation

hbagdi
Copy link
Member

@hbagdi hbagdi commented Feb 26, 2018

While Kong returns the same status code as
that of the upstream when it proxies a request,
it is useful to record the real status code
returned by the upstream.

This can be used for monitoring Kong
itself if it is returning the same response
as the upstream.

Summary

  • Add an upstream status header to proxied requests

@hbagdi
Copy link
Member Author

hbagdi commented Feb 26, 2018

cc @maguec

@maguec
Copy link

maguec commented Feb 26, 2018

+1
This will be needed for cloud as well as is very useful for troubleshooting on-site

@@ -41,6 +41,7 @@ return {
HOST_OVERRIDE = "X-Host-Override",
PROXY_LATENCY = "X-Kong-Proxy-Latency",
UPSTREAM_LATENCY = "X-Kong-Upstream-Latency",
UPSTREAM_STATUS = "X-Kong-Upstream-Status",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you fix the indent?

@@ -523,6 +523,7 @@ return {
local now = get_now()
ctx.KONG_WAITING_TIME = now - ctx.KONG_ACCESS_ENDED_AT -- time spent waiting for a response from upstream
ctx.KONG_HEADER_FILTER_STARTED_AT = now
header[constants.HEADERS.UPSTREAM_STATUS] = ngx.status
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hbagdi @thibaultcha I might be wrong here, I remember ngx.status indicates the current request's response status, the accurate upstream status should come from a nginx variable: "ngx.var.upstream_status", see http://nginx.org/en/docs/http/ngx_http_upstream_module.html#var_upstream_status

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This header should be nested under the feature flag test for server_tokens (see below). Ideally, another flag for "debugging tokens" could be added at some point, but "server_tokens" is the one we use today in accordance with NGINX.

Copy link
Member

@thibaultcha thibaultcha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will you provide a test case better describing the scenario you are trying to over with this? I know you will, just making sure :)

Thanks

@@ -523,6 +523,7 @@ return {
local now = get_now()
ctx.KONG_WAITING_TIME = now - ctx.KONG_ACCESS_ENDED_AT -- time spent waiting for a response from upstream
ctx.KONG_HEADER_FILTER_STARTED_AT = now
header[constants.HEADERS.UPSTREAM_STATUS] = ngx.status
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This header should be nested under the feature flag test for server_tokens (see below). Ideally, another flag for "debugging tokens" could be added at some point, but "server_tokens" is the one we use today in accordance with NGINX.

@thibaultcha
Copy link
Member

FYI, you can create custom plugins for the tests in the spec/fixtures folder - and update/write a plugin to highlight the use-case for this header.

guanlan
guanlan previously approved these changes Feb 27, 2018
@guanlan
Copy link
Member

guanlan commented Feb 27, 2018

LGTM

@hbagdi
Copy link
Member Author

hbagdi commented Feb 27, 2018

@guanlan This is work in progress, need to add a test case when there is a responses mismatch between Kong's response and upstream's response.
Will notify once everything is ready and passes tests cases.

@thibaultcha thibaultcha dismissed guanlan’s stale review February 27, 2018 19:11

not read (besides we are in feature freeze)

@hbagdi hbagdi force-pushed the feat/upstream-status-header branch 3 times, most recently from 8d9caf7 to 8cb77bd Compare February 28, 2018 01:20
@hbagdi
Copy link
Member Author

hbagdi commented Feb 28, 2018

@guanlan @thibaultcha Please take another look over this. Thank you.

@hbagdi
Copy link
Member Author

hbagdi commented Mar 13, 2018

@thibaultcha ping

if ctx.KONG_PROXIED then
local now = get_now()
ctx.KONG_WAITING_TIME = now - ctx.KONG_ACCESS_ENDED_AT -- time spent waiting for a response from upstream
ctx.KONG_HEADER_FILTER_STARTED_AT = now
end

if singletons.configuration.server_tokens then
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is tricky. Upon re-reading the description for both server_tokens and latency_tokens, this new header does not fall under either category. After giving it some thoughts, I have been thinking of changing how we specify those headers... bear with me.

As we strive towards a simpler configuration file (with less directive, but no less flexibility, and more intuitiveness), we can follow a similar approach as #3147, and provide a new way to configure such headers served by Kong, like so:

tokens = [server|latency|debug|off] custom ...

A few examples of which headers are served with each setting:

tokens = server
  Via: ... (or Server: ... upon non-proxying) - same behavior as server_tokens = on

tokens = latency
  X-Kong-Proxy-Latency: ...
  X-Kong-Upstream-Latency: ... (same behavior as latency_tokens = on

token = server, latency
  same as both previous examples

tokens = debug
  All previous headers plus:
  X-Kong-Upstream-Status
  X-Kong-Matched-API (already present in the router when the Kong-Debug request header is set)

tokens = X-Kong-Upstream-Latency
  Only send the upstream latency header

tokens = off
  No header will be injected by Kong core

I know this is more work than this PR originally intended to be, but I hope you can see where I am coming from here. We want to provide a configuration file that makes sense and is intuitive.

@thibaultcha thibaultcha added pr/status/do not merge (to discuss) pr/changes requested Changes were requested to this PR by a maintainer. Please address them and ping back once done. and removed pr/do not merge labels Mar 13, 2018
@thibaultcha
Copy link
Member

Moved to #3300 from what I can tell? If so, will you please close this and delete the branch? Thanks!

@hbagdi
Copy link
Member Author

hbagdi commented Mar 20, 2018

@thibaultcha I was planning to put in X-Kong-Proxy-Status as a separate commit and #3300 is just changing the configuration. I'll rebase this and update once #3300 goes through.

@hbagdi hbagdi force-pushed the feat/upstream-status-header branch from 8cb77bd to 3a8e601 Compare April 20, 2018 22:56
@hbagdi hbagdi changed the base branch from master to next April 21, 2018 15:04
@hbagdi hbagdi force-pushed the feat/upstream-status-header branch from 3a8e601 to ac628c7 Compare April 21, 2018 15:18
@hbagdi hbagdi added pr/please review and removed pr/changes requested Changes were requested to this PR by a maintainer. Please address them and ping back once done. labels Apr 21, 2018
@hbagdi
Copy link
Member Author

hbagdi commented Apr 21, 2018

@thibaultcha I've reworked this PR to use the work we did in #3300.


if singletons.configuration.headers[constants.HEADERS.UPSTREAM_STATUS] then
header[constants.HEADERS.UPSTREAM_STATUS] = var.upstream_status
end
Copy link
Member

@thibaultcha thibaultcha Apr 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$upstream_status can contain a comma-separated list of upstream status codes if several upstreams were selected. I see no test case covering this, so I am wondering if it is an oversight?

Regardless, my understanding of this header is that it would return the status code of the last upstream request (others are tracked by the healthchecks and transparent to the client). Thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: note the 2 spaces before = as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In any case, this is an oversight.

I agree that this header should return the last upstream status and multiple upstream retries for the same request should happen transparently.

I'm assuming that you agree with the above implementation and will work on it and ping you back.

@hbagdi hbagdi force-pushed the feat/upstream-status-header branch 2 times, most recently from 73e5394 to eb15304 Compare April 27, 2018 18:54
@hbagdi hbagdi force-pushed the feat/upstream-status-header branch from eb15304 to 41df399 Compare April 28, 2018 03:05

local upstream_header = constants.HEADERS.UPSTREAM_STATUS
if singletons.configuration.headers[upstream_header] then
header[upstream_header] = string.match(var.upstream_status, "(%S+)$")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, string.match is part of the NYI (Not Yet Implemented) LuaJIT API (meaning it will not JIT compile, and thus prevent the creation of a trace in this chunk), which is a problem for such hot code paths. Introducing non-JITable code paths in the proxy path has given us performance regressions in the past, and we should be mindful about them.

Instead, you may use the PCRE API offered by ngx.re, or any other trick you may think of to extract this value in plain Lua (the former is probably healthier).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If anyone is interested in viewing the entire list of NYI methods. As I was curious myself.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it is in our CONTRUBITING.md file, we expect all contributors to have read this list already :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to be a little less forward about where I found it 😏

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out. I never gave NYI LuaJIT methods much thought before. Will be more careful next time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hbagdi also keep in mind that without the "jo" flag, ngx.re.match can be (much!) slower than string.match.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hishamhm Thank you for pointing this out!

local route1 = bp.routes:insert {
protocols = { "http" },
service = service,
paths = { "/foo" },
Copy link
Member

@thibaultcha thibaultcha Apr 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Minor) one piece of advice here: these tests tend to grow quite fast and using paths such as /foo etc quickly makes them unmanageable (several hundreds of lines down in the same files, you might not remember what the /foo path maps to).

I would advise the use of /status/200 or /status/500 (or any other more explicit name you may think of) instead. Not really a blocker, but if you care enough to change them, it'd be appreciated.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point and have personally experienced this problem in a couple of test files myself. I'll update this to use more descriptive names.

}
})
assert.res_status(200, res)
assert.equal('200', res.headers[constants.HEADERS.UPSTREAM_STATUS])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: let's avoid single quotes altogether when possible - if we ever enforce this in our linter we'd have to fix a ton of places.

if client then
client:close()
end
helpers.stop_kong(nil, nil, true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using true in arguments to helpers.stop_kong should be for debugging purposes only. This is a leftover probably? There are other places in this file.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was used to avoid truncating tables between tests runs in this file since stop_kong will truncate them by default. Should it still be avoided?

service = service,
paths = { "/bar" },
}
assert(route2)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Lua idiom we follow is to wrap the :insert() call within an assert(). The new DAO is built for this purpose in mind, since the second return value will be a string, thus producing the equivalent of assert(nil, err_from_insert). This is a typical use of assert and errors with Lua. In the current case, the assert would fail but no errors would be printed explaining why. It's also shorter and more eleguant.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for explaining this, I'll keep in mind next time. Updated this in the most recent push.

@hbagdi hbagdi force-pushed the feat/upstream-status-header branch 2 times, most recently from 17690b2 to 736a801 Compare May 1, 2018 00:34
local constants = require "kong.constants"

local function setup_db()
ngx.log(ngx.ERR, "before aeach called" )
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove logging from the tests (noisy and we spent time removing a bunch by the past already)

port = helpers.mock_upstream_port,
protocol = helpers.mock_upstream_protocol,
}
assert(service)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like this assert still isn't in the right place?

local matches, err = ngx.re.match(var.upstream_status,
"[0-9]+$", "oj")
if err then
log(ERR, "failed to set upstream status header", err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This message is wrongly formatted. It is also somewhat obscure. It should be better formatted and cold be clarified, like so:

log(ERR, "failed to set ", upstream_status_header, " header: ", err)

(Notice the more appropriate name for the upstream_header variable as well...)


local upstream_header = constants.HEADERS.UPSTREAM_STATUS
if singletons.configuration.headers[upstream_header] then
local matches, err = ngx.re.match(var.upstream_status,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be cached at the top level chunk of this file according to style and this file's conventions:

re_match = ngx.re.match

X-Kong-Upstream-Status header returns the status
code returned by the upstream API.
This header is disabled by default and can be enabled
by adding it to `headers` config property in kong.conf.
@hbagdi hbagdi force-pushed the feat/upstream-status-header branch from 3966db9 to 141c076 Compare May 2, 2018 15:18

local function setup_db()
local bp = helpers.get_db_utils()
assert(helpers.dao:run_migrations())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, the get_db_utils() utility will run the migrations for you :) I am proceeding with the merge at this point and will fix it locally, but for next time!


local upstream_status_header = constants.HEADERS.UPSTREAM_STATUS
if singletons.configuration.headers[upstream_status_header] then
local matches, err = re_match(var.upstream_status,
Copy link
Member

@thibaultcha thibaultcha May 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It just struck me that we are missing a case here that would result in a guaranteed timeout when the request was short-circuited by a plugin (e.g. authentication) because the variable would be an empty string, and there would be no match, and we'd be indexing a nil value below. We should either nest this below the above if proxied branch, or program defensively against such a case. I will be adding a regression test and making that change in the merge.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A tweaked version of this came to my mind if the code would insert this header for not proxied requests as well but seems like my understanding was wrong. Proxied request means that a request actually hits the upstream. Thank you for catching this!

thibaultcha pushed a commit that referenced this pull request May 3, 2018
If enabled, the `X-Kong-Upstream-Status` header is injected by Kong in
the response for the client. It contains the status code returned by the
upstream response.

This header is disabled by default, and can be enabled by adding it to
the `headers` configuration property.

From #3263

Signed-off-by: Thibault Charbonnier <thibaultcha@me.com>
@thibaultcha
Copy link
Member

Manually merged with the discussed tweaks. Thanks!

@thibaultcha thibaultcha closed this May 3, 2018
@thibaultcha thibaultcha deleted the feat/upstream-status-header branch May 3, 2018 00:25
hbagdi added a commit that referenced this pull request May 3, 2018
…atus header

The hotfix commit 75e071b (#3419) changes the configuration property from
`headers` to `enabled_headers`.
The commit did not update a conditional statement introduced in
60c335e (#3263). This commit corrects the check from `headers` to
`enabled_headers`.

This was caught by the test suite and hence no regression test
has been added.
thibaultcha pushed a commit that referenced this pull request May 3, 2018
The hotfix commit 75e071b (#3419) changes the configuration property from
`headers` to `enabled_headers`.

The commit did not update a conditional statement introduced in
60c335e (#3263). This commit corrects the check from `headers` to
`enabled_headers`.

This was caught by the test suite and hence no regression test
has been added.

From #3433
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants