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

Request ID Logging #1087

Merged
merged 10 commits into from
Mar 21, 2021
Merged

Request ID Logging #1087

merged 10 commits into from
Mar 21, 2021

Conversation

NickMeves
Copy link
Member

@NickMeves NickMeves commented Mar 8, 2021

Supports logging of a client's X-Request-ID

Description

Track a RequestID throughout the lifespan of a request in the RequestScope. If a X-Request-Id is set in the client request headers, we use that. Otherwise we make a random UUID.

The RequestID is used in auth & request logging, Error pages & added to upstream HTTP requests (if it wasn't originally present & we generated our own UUID).

Motivation and Context

Increase logging and tracing of requests throughout the application to ease debugging.

How Has This Been Tested?

Unit Tests

Checklist:

  • My change requires a change to the documentation or CHANGELOG.
  • I have updated the documentation/CHANGELOG accordingly.
  • I have created a feature (non-master) branch for my PR.

@NickMeves
Copy link
Member Author

When we use our request builder for requests to provider endpoints, should we inject the X-Request-Id header with our Context/RequestScope RequestID?

If yes -- we can probably make it automatic if a context were added?

(This PR does add it to HTTP requests to the upstream where it didn't already exist and we made our own random UUID, that is up for discussion too).

@NickMeves NickMeves linked an issue Mar 8, 2021 that may be closed by this pull request
@NickMeves NickMeves force-pushed the request-id-logging branch 2 times, most recently from 72b1e76 to 0229e18 Compare March 8, 2021 05:16
@JoelSpeed JoelSpeed added this to In Progress (Pull Requests) in Release v7.1.0 via automation Mar 11, 2021
pkg/logger/logger.go Outdated Show resolved Hide resolved
@NickMeves NickMeves force-pushed the request-id-logging branch 4 times, most recently from aaf1870 to e96c232 Compare March 13, 2021 01:38
@NickMeves
Copy link
Member Author

When we use our request builder for requests to provider endpoints, should we inject the X-Request-Id header with our Context/RequestScope RequestID?

If yes -- we can probably make it automatic if a context were added?

(This PR does add it to HTTP requests to the upstream where it didn't already exist and we made our own random UUID, that is up for discussion too).

Let's not do this for now. I've also removed injecting request ID into upstream headers for the time being.

This is better handled with a future tracing PR.

@NickMeves NickMeves marked this pull request as ready for review March 13, 2021 01:40
@NickMeves NickMeves requested a review from a team as a code owner March 13, 2021 01:40
docs/docs/configuration/overview.md Show resolved Hide resolved
pkg/apis/middleware/scope.go Show resolved Hide resolved
pkg/apis/options/logging.go Outdated Show resolved Hide resolved
pkg/app/pagewriter/error.html Show resolved Hide resolved
pkg/app/pagewriter/error_page.go Show resolved Hide resolved
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

const requestID = "11111111-2222-4333-8444-555555555555"
Copy link
Member

Choose a reason for hiding this comment

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

Same as before, can we rename?

Suggested change
const requestID = "11111111-2222-4333-8444-555555555555"
const testRequestID = "11111111-2222-4333-8444-555555555555"

@@ -48,6 +52,11 @@ var _ = Describe("Scope Suite", func() {
Expect(scope).ToNot(BeNil())
Expect(scope.ReverseProxy).To(BeFalse())
})

It("sets the RequestID using the request", func() {
Copy link
Member

Choose a reason for hiding this comment

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

Do we have a test when the request ID isn't already set?

Copy link
Member Author

Choose a reason for hiding this comment

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

Added tests for no + custom request ID headers

XForwardedProto = "X-Forwarded-Proto"
XForwardedHost = "X-Forwarded-Host"
XForwardedURI = "X-Forwarded-Uri"
XRequestID = "X-Request-Id"
Copy link
Member

Choose a reason for hiding this comment

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

Oh I see why this is here now, I'm a bit conflicted. Do we import the other consts into other packages?

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'm trying to remember why in this package I had util_test as a separate package when I made this suite for the other header helpers 🤔

I think that's the main reason I made these public, let me play around with getting the test suite to share a package and get these private.

Copy link
Member Author

@NickMeves NickMeves Mar 13, 2021

Choose a reason for hiding this comment

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

Maybe my thinking was since this is request utils these are useful constants other packages could use to match up with our getters? I dunno, now I'm rationalizing for a past version of myself I don't remember 🤣

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh - I left a comment for myself why:

// TestRequestUtilSuite and related tests are in a *_test package
// to prevent circular imports with the `logger` package which uses
// this functionality
func TestRequestUtilSuite(t *testing.T) {

Copy link
Member Author

Choose a reason for hiding this comment

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

Leave it public? Or make private and duplicate in the util_test package?

Copy link
Member

Choose a reason for hiding this comment

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

Happy to leave it public, will likely come in useful somewhere else anyways

XRequestID = "X-Request-Id"
)

var requestIDHeader = XRequestID
Copy link
Member

Choose a reason for hiding this comment

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

Does this get overriden when the config is loaded? Not a huge fan of this 🤔

How many times is this actually used in the code? I see it's used in tests a lot but this should actually only be in the scope middleware right? Maybe logging? Perhaps we want to pass in the configured value every time?

I guess this is kind of like how things are done in the logger package right, configuring the default logger? Perhaps lets add a comment explaining why this is done this way and leave 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.

So GetRequestID is a little overloaded 😅

func GetRequestID(req *http.Request) string {
	scope := middlewareapi.GetRequestScope(req)
	if scope != nil {
		return scope.RequestID
	}
	rid := req.Header.Get(requestIDHeader)
	if rid != "" {
		return rid
	}
	return uuid.New().String()
}

The first time it is called for a request, it is in the Scope initialization itself, so it falls through to the 2nd or 3rd case and sets the scope (only time our configured requestIDHeader is used). Every other time in the lifecycle, scope is set so it is pulling from that first block.

I guess we are technically memoizing on the middlewareapi.Scope for how we consume it in practice.

Copy link
Member

Choose a reason for hiding this comment

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

Should we separate this out, so we have two different things.

  1. In the request scope middleware, we add or generate the request ID.
  2. We have a helper here to read it from the scope

This way we can pass the request ID header as a parameter when we construct the middleware scope, will likely be cleaner than the current implementation right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Question for you since you are more familiar with the ordering of everything:

Since GetRequestID is used in logging/error pages, I opted to merge the two cases (rather than GetRequestID always assuming a scope was already initialized) in case we had a scenario where we errored or attemtped to log before the scope was initialied.

That's the benefit of keeping aligned. But Is that a thing? Otherwise you are right, could be split to be cleaner.

Copy link
Member

Choose a reason for hiding this comment

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

The scope middleware should be the first thing in the request chain, so, unless we do any logging that would use the request ID as part of the middleware scope, then we are safe

})

AfterEach(func() {
util.SetRequestIDHeader(util.XRequestID)
Copy link
Member

Choose a reason for hiding this comment

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

The unset logic is to have an empty value right? This should do the same? (This would then look similar to below and allow us to change the default under the hood)

Suggested change
util.SetRequestIDHeader(util.XRequestID)
util.SetRequestIDHeader("")

Though I see you have explicit tests below so maybe this can be ignored

@@ -13,7 +13,7 @@ func NewScope(reverseProxy bool) alice.Constructor {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
scope := &middlewareapi.RequestScope{
ReverseProxy: reverseProxy,
RequestID: requestutil.GetRequestID(req),
RequestID: requestutil.GenRequestID(req),
Copy link
Member

Choose a reason for hiding this comment

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

I had intended for the generation logic to be in this package, that way the request ID header that is configured doesn't need to be set anywhere as a package level var. Everything else can rely just on the request scope

Copy link
Member Author

Choose a reason for hiding this comment

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

So I'm torn for 2 reasons:

1.) The actual business logic really matches everything else that is happening in requestutils.
2.) If the requestutils.GetRequestID we consume throughout the application is truly only RequestScope aware, in tests we'd need to setup a full scope setting middleware in all their setup. At the moment it's easy to just splice in the extra header for non-middleware related test suites.

The original intent was to memoize the random UUID case, and I guess things got wonky because we jumped across boundaries to RequestScope to perform the semi-memoization (since it was the first caller setting it on itself). In a RequestScope free world, I would memoize this in the GetRequestID on its own Context key.

Do we want to do that? Or maybe still memoize in the RequestScope, but not set it on RequestScope initialization, but instead actually memoize (set the stored value on the first real call to 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.

Ah - I follow, the concern is mostly around the management of the custom request ID header -- missed that aspect in the previous discussion 🤔

This way we can pass the request ID header as a parameter when we construct the middleware scope, will likely be cleaner than the current implementation right?

Copy link
Member Author

@NickMeves NickMeves Mar 15, 2021

Choose a reason for hiding this comment

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

Eh - just realized concern (2) isn't that big of a deal, we have this at our fingertips to setup tests without the full middleware handler: req = middlewareapi.AddRequestScope(req, scope)

Copy link
Member

Choose a reason for hiding this comment

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

We have created a pattern where we add the request scope in all our tests anyway, so ensuring this always gets created with request ID at the beginning as well shouldn't add too much extra bloat right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I think it isn't too bad. I was initially thinking of the handler route where we spun up the whole middleware for all handler related tests. Our non-middleware test suites didn't have that easy injection point. But I can use req = middlewareapi.AddRequestScope(req, scope) super easily 👍

I think my plan of attack is to completely remove request ID logic from requestutils and have the logic be owned by RequestScope

Copy link
Member Author

Choose a reason for hiding this comment

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

Alrighty - Moved everything over to RequestScope management in the last commit and ditched the package variables & requestutils management of the ID.

pkg/app/pagewriter/error_page.go Show resolved Hide resolved
Comment on lines 10 to 11
// HTML sanitization policy creation done once for lifespan of the program
var sanitizer = bluemonday.StrictPolicy()
Copy link
Member

Choose a reason for hiding this comment

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

Hmm yes that's a good point, we should definitely be sanitizing anything we log, maybe leave that for a future PR as you've suggested though

pkg/app/pagewriter/sign_in_page_test.go Show resolved Hide resolved
XForwardedProto = "X-Forwarded-Proto"
XForwardedHost = "X-Forwarded-Host"
XForwardedURI = "X-Forwarded-Uri"
XRequestID = "X-Request-Id"
Copy link
Member

Choose a reason for hiding this comment

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

Happy to leave it public, will likely come in useful somewhere else anyways

Copy link
Member

@JoelSpeed JoelSpeed left a comment

Choose a reason for hiding this comment

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

Very nearly there, I think the only real thing for me is that sanitzer vs html escape question

pkg/app/pagewriter/error_page.go Show resolved Hide resolved

body, err := ioutil.ReadAll(recorder.Result().Body)
Expect(err).ToNot(HaveOccurred())
Expect(string(body)).To(Equal("Forbidden An extra message: with more context. /prefix/ 403 /redirect Custom Footer Text v0.0.0-test"))
Expect(string(body)).To(Equal("Forbidden You do not have permission to access this resource. /prefix/ 403 /redirect Custom Footer Text v0.0.0-test"))
Copy link
Member

Choose a reason for hiding this comment

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

HTML escape would probably be easier to understand wouldn't it? Then you at least see that there was something there rather than an empty string? Thinking from a UX perspective here

pkg/middleware/scope.go Show resolved Hide resolved
@NickMeves
Copy link
Member Author

Apparently we got HTML escaping out of the box 😄

I left in a test case to assert that we indeed still escaping in the future.

Copy link
Member

@JoelSpeed JoelSpeed left a comment

Choose a reason for hiding this comment

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

LGTM, thanks @NickMeves

@JoelSpeed JoelSpeed merged commit c1267bb into master Mar 21, 2021
Release v7.1.0 automation moved this from In Progress (Pull Requests) to Done Mar 21, 2021
@JoelSpeed JoelSpeed deleted the request-id-logging branch March 21, 2021 18:27
acobaugh pushed a commit to PennState/oauth2-proxy that referenced this pull request Jun 24, 2021
* Add RequestID to the RequestScope

* Expose RequestID to auth & request loggers

* Use the RequestID in templated HTML pages

* Allow customizing the RequestID header

* Document new Request ID support

* Add more cases to scope/requestID tests

* Split Get vs Generate RequestID funtionality

* Add {{.RequestID}} to the request logger tests

* Move RequestID management to RequestScope

* Use HTML escape instead of sanitization for Request ID rendering
k-jell pushed a commit to liquidinvestigations/oauth2-proxy that referenced this pull request Apr 6, 2022
* Add RequestID to the RequestScope

* Expose RequestID to auth & request loggers

* Use the RequestID in templated HTML pages

* Allow customizing the RequestID header

* Document new Request ID support

* Add more cases to scope/requestID tests

* Split Get vs Generate RequestID funtionality

* Add {{.RequestID}} to the request logger tests

* Move RequestID management to RequestScope

* Use HTML escape instead of sanitization for Request ID rendering
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
No open projects
Development

Successfully merging this pull request may close these issues.

Support X-Request-ID in Access Logs
2 participants