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

httpgrpc: correct handling of non-loggable errors #421

Merged
merged 4 commits into from
Oct 31, 2023

Conversation

duricanikolic
Copy link
Contributor

@duricanikolic duricanikolic commented Oct 27, 2023

What this PR does:
This PR allows correct handling of non-loggable errors.

First of all, it introduces middleware.DoNotLogError type, representing the errors that should not be logged. Additionally, it introduces httpgrpc.DoNotLogErrorHeaderKey, an HTTP header key with value X-DoNotLogError. In case of an erroneous HTTPResponse, the presence of this header means that the corresponding error should not be logged.

When httpgrpc's Server.Handle() receives a response from the HTTP server, an HTTPResponse object is created. HTTPResponses with a status 5xx are translated into gRPC errors, by using the httpgrpc package. Before this PR, all these errors were loggable. This PR allows Server.Handle() to return errors that should not be logged. More precisely, the presence of a header with key httpgrpc.DoNotLogHeader and any value in an erroneous HTTPResponse indicates to Servier.Handle() to wrap the gRPC error (created by the httpgrpc package) with a httpgrpc.DoNotLogError.

One more change introduced by this PR concerns httpgrpc.HTTPResponseFromError(). Namely, before this PR, errors built by httpgrpc package and wrapped with another error, were not recognized by httpgrpc.HTTPResponseFromError(). This PR fixes that behavior.

Which issue(s) this PR fixes:

Part of grafana/mimir#6008.

Checklist

  • Tests updated
  • CHANGELOG.md updated - the order of entries should be [CHANGE], [FEATURE], [ENHANCEMENT], [BUGFIX]

Copy link
Member

@pstibrany pstibrany left a comment

Choose a reason for hiding this comment

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

looks good overall, left some minor comments.

@@ -234,3 +239,8 @@ func fromHeader(hs http.Header) []*httpgrpc.Header {
}
return result
}

func containsDoNotLogErrorKey(hs http.Header) bool {
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 too shallow. I understand it's that way for testing, but this functionality can be tested together with the rest of server handler too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have removed this method, as well as the corresponding test.

Comment on lines 155 to 187
func TestContainsDoNotLogErrorKey(t *testing.T) {
testCases := map[string]struct {
header http.Header
expectedOutcome bool
}{
"if headers do not contain X-DoNotLogError, return false": {
header: http.Header{
"X-First": []string{"a", "b", "c"},
"X-Second": []string{"1", "2"},
},
expectedOutcome: false,
},
"if headers contain X-DoNotLogError with a value, return true": {
header: http.Header{
"X-First": []string{"a", "b", "c"},
"X-DoNotLogError": []string{"true"},
},
expectedOutcome: true,
},
"if headers contain X-DoNotLogError without a value, return true": {
header: http.Header{
"X-First": []string{"a", "b", "c"},
"X-DoNotLogError": nil,
},
expectedOutcome: true,
},
}
for testName, testData := range testCases {
t.Run(testName, func(t *testing.T) {
require.Equal(t, testData.expectedOutcome, containsDoNotLogErrorKey(testData.header))
})
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Honestly, this test seems bit too much for testing what's essentially a simple map key-check. I'd suggest removing this test, and including checks for DoNotLog error into already existing tests that handle 500 errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have applied the required fix.

return nil, httpgrpc.ErrorFromHTTPResponse(resp)
err := httpgrpc.ErrorFromHTTPResponse(resp)
if containsDoNotLogErrorKey(header) {
return nil, middleware.DoNotLogError{Err: err}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return nil, middleware.DoNotLogError{Err: err}
err = middleware.DoNotLogError{Err: err}

Comment on lines 28 to 30
var (
DoNotLogErrorHeader = http.CanonicalHeaderKey("X-DoNotLogError")
)
Copy link
Member

Choose a reason for hiding this comment

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

I think this belongs to httpgrpc package, where server is using it. grpc_logging.go has nothing to do with HTTP.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have moved this header to httpgrpc/server.go.

// If the given error, or any error from its tree are a status.Status,
// that status.Status and the outcome true are returned.
// Otherwise, nil and the outcome false are returned.
// This implementation differs from status.FromError() because the
Copy link
Contributor

Choose a reason for hiding this comment

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

It does check that. The implementation is nearly the same as this method:

https://github.com/grafana/mimir/blob/main/vendor/google.golang.org/grpc/status/status.go#L113

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@56quarters you linked the grpc’s status, and not gogo’s status.

Copy link
Contributor

Choose a reason for hiding this comment

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

OK, but that's the one we should be using? That's the one that the circuit breakers in Mimir use to extract gRPC error codes. The gogo behavior sounds like something we absolutely should avoid.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @56quarters,
Curently gogo is used both in dskit and in mimir. It is used even for the generation of Go structs out of .proto sources (e.g., here).
I agree that we should replace usages of gogo's status package with grpc's status package, but this should be done in a separate PR, because that change would be very big.

WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Update: apparently we already have an issue for removing the deprecated gogo dependency. But this would be a breaking change, plus we don't know which package to use instead. The grpc package has some big performance issues, that's why the gogo project was created.

That's the one that the circuit breakers in Mimir use to extract gRPC error codes

This is not a problem, since gRPC errors created by both grpc and gogo can be recognized by each other.

Signed-off-by: Yuri Nikolic <durica.nikolic@grafana.com>
Signed-off-by: Yuri Nikolic <durica.nikolic@grafana.com>
Copy link
Member

@pstibrany pstibrany left a comment

Choose a reason for hiding this comment

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

lgtm overall, but would like to see a better test for httpgrpc.Server.

httpgrpc/server/server.go Show resolved Hide resolved
httpgrpc/server/server.go Show resolved Hide resolved
httpgrpc/server/server_test.go Outdated Show resolved Hide resolved
Signed-off-by: Yuri Nikolic <durica.nikolic@grafana.com>
Copy link
Member

@pstibrany pstibrany left a comment

Choose a reason for hiding this comment

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

Thank you.

return nil, httpgrpc.ErrorFromHTTPResponse(resp)
err := httpgrpc.ErrorFromHTTPResponse(resp)
if _, ok := header[DoNotLogErrorHeaderKey]; ok {
recorder.Header().Del(DoNotLogErrorHeaderKey)
Copy link
Member

Choose a reason for hiding this comment

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

We don't need to remove header here, only headers in resp are important, and fromHeader is covering that already.

Signed-off-by: Yuri Nikolic <durica.nikolic@grafana.com>
@duricanikolic duricanikolic merged commit 52f4e8d into main Oct 31, 2023
3 checks passed
@duricanikolic duricanikolic deleted the yuri/do-not-log-error branch October 31, 2023 13:28
ying-jeanne pushed a commit that referenced this pull request Nov 2, 2023
* httpgrpc: correct handling of non-loggable errors

Signed-off-by: Yuri Nikolic <durica.nikolic@grafana.com>

* Fixing review findings

Signed-off-by: Yuri Nikolic <durica.nikolic@grafana.com>

* Fixing review findings 2

Signed-off-by: Yuri Nikolic <durica.nikolic@grafana.com>

* Fixing review findings 3

Signed-off-by: Yuri Nikolic <durica.nikolic@grafana.com>

---------

Signed-off-by: Yuri Nikolic <durica.nikolic@grafana.com>
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.

None yet

3 participants