-
Notifications
You must be signed in to change notification settings - Fork 116
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(router): template expressions for access logs #1545
base: main
Are you sure you want to change the base?
feat(router): template expressions for access logs #1545
Conversation
Squashed commits: [1823f4836] fix: refactor logging and fix exprlang dereferencing [87ca8d1] Added template logging
A note on adding
However, it seems like the body is not part of the expression context, when I was looking at adding it to the context I noticed it was of type As such, because of this + because the core specification was about adding the "error" to the context and allowing template expressions, I did not focus on this, and would like to discuss this more before continuing on deciding to add |
// and as the method receivers are all pointers (*reportError) | ||
// the Error() function wont get called unless its of type *reportError | ||
switch result := val.(type) { | ||
case reportError: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As the comment mentions, depending on the expression exprlang
will return reportError
instead of the pointer version *reportError
, which means that Error() wont end up being called
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't follow. reportError
is an internal error from our router and has nothing to do with expr
. Could you eloborate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's say the expression in the yml is expression: "request.error"
exprlang will evaluate the expression to *core.reportError
Which works
However if we do the expression expression: "request.error ?? request.url"
, exprlang will evaluate the expression to core.reportError
(not a pointer)
The problem is that because the method uses a pointer receiver
func (e *reportError) Error() string {
the second expression does not call Error, thus will print out a memory address instead. Thus I cast it to *core.reportError
This could happen to any other similar errors (could happen both ways, depending on if the receiver is a pointer or not), however I only noticed this happening with core.reportError
for now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now I understand. You were referring to the returned error of the expression. Why do you believe it is important toi align this. The value should not matter because we will pass it straight to the logger which accepts any.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even though the logger accepts any, the final log message ends up becoming a memory location. This becomes a problem when a customer expects the error message to be printed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd expect that whatever the user returns in an expression that zap is able to log it and that the pointer is dereferenced automatically before printing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From my testing so far no, but let me double check a bit more and get back to you.
@@ -0,0 +1,32 @@ | |||
package requestlogger |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The other function that tests the logger lies in this package requestlogger
, but creating of the requestContext which is needed for evaluating expressions is in the core
package, and is not exported.
Which means if I was to write my test cases the same way here in the subgraph_logger_test.go
file, I will need to export the buildRequestContext
function (and related struct), which seemed like a bad idea for this one test.
But it also made me wonder if that code should be in a separate (maybe "inner-core" package, or this package merged with core since its quite small?). I ended up writing a unit test directly access_log_field_handler_test
in the core package.
// and as the method receivers are all pointers (*reportError) | ||
// the Error() function wont get called unless its of type *reportError | ||
switch result := val.(type) { | ||
case reportError: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't follow. reportError
is an internal error from our router and has nothing to do with expr
. Could you eloborate?
router/core/graphql_prehandler.go
Outdated
@@ -217,7 +217,7 @@ func (h *PreHandler) Handler(next http.Handler) http.Handler { | |||
|
|||
executionOptions, traceOptions, err := h.parseRequestOptions(r, clientInfo, requestLogger) | |||
if err != nil { | |||
requestContext.error = err | |||
AssignErrors(requestContext, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of writing a utility function to assign the error, you could implement ResolveAnyExpression
similar as withResolveStringExpression
on the requestContext
. This would follow the pattern and the error can be set right before we pass the expressionContext to the template engine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree on the ResolveAnyExpression
, however I want to clarify about assigning errors in the ResolveAnyExpression
function.
My assumption was that whenever an error occurred that the error should be available as part of the expressionContext. So if anyone did
reqContext.expressionContext.errror
at any time they wont get null (and would be the same requestContext.error
).
With this change they would get null, which could be a mental model mismatch,
But from what you are saying is that they should not access the expressionContext this way so its ok, instead they should access it via any Resolve*Expression
method, for which we will set the error to the expressionContext before the resolving step?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct. Thanks for the explanation.
That's correct. I'd suggest to finish it up to the point we discussed it. We can add this later. The customer wanted to log the GraphQL operation in case of an error. |
b0e8ed0
to
c6b6df6
Compare
router/core/context.go
Outdated
func (c *requestContext) ResolveStringExpression(expression *vm.Program) (string, error) { | ||
c.setErrorContext() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We now set the expressionContext error when accessing through these functions
router/core/context.go
Outdated
currContextRequest := c.expressionContext.Request | ||
if err != nil { | ||
// We don't really want to do a deep clone since we only know that error only can get modified | ||
copyContext := expr.Context{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to create a new expr.Context
? it's not clear from your comment.
router/core/context.go
Outdated
currContextRequest := c.expressionContext.Request | ||
if err != nil { | ||
// We don't really want to do a deep clone since we only know that error only can get modified | ||
copyContext := expr.Context{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ran into a problem where when an error occurs in the subgraph, it's not assigned to the requestContext.error
at the point of being called.
Thus any expressions like request.error?? 'no-error'
returned 'no-error'
. Observing the same pattern that was used in the predefined context fields for request_error
which passes an error directly, I used a similar approach / workaround. Creating a copy of the expressionContext (non deep) and assigning it to resolve the expression.
@@ -316,3 +316,14 @@ func writeOperationError(r *http.Request, w http.ResponseWriter, requestLogger * | |||
writeRequestErrors(r, w, http.StatusInternalServerError, graphqlerrors.RequestErrorsFromError(errInternalServer), requestLogger) | |||
} | |||
} | |||
|
|||
type ExprWrapError struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is to circumvent this problem described here:
#1545 (comment)
While testing further I noticed that there can be other error types, so it was safer to wrap the error in a type we can control, so we have a deterministic error type outputted from the exprlang
expression.
(I didn't come across anything atm, but if there is a better existing type to use, we can reuse that)
router/core/context.go
Outdated
if c.error == nil { | ||
return | ||
} | ||
c.expressionContext.Request.Error = &ExprWrapError{c.error} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refer https://github.com/wundergraph/cosmo/pull/1545/files#r1936122019 and #1545 (comment) for why we wrap
logger *zap.Logger, | ||
attributes []config.CustomAttribute, | ||
exprAttributes []requestlogger.ExpressionAttribute, | ||
passedErr error, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous type was "any" because it took inputs from panics and errors, I went ahead and split it into two as we want to maintain the error type in the ExpressionContext
, but if we are ok to make it even more generic just for the panic, we can merge the fields again.
continue | ||
} | ||
|
||
err := exprlocal.ValidateAnyExpression(exprString) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validate against nil and a few other types, we can add / remove types later as necessary
Motivation and Context
The goal is to extend the router to allow defining custom access log attributes by template expressions.
In order to do this the PR adds an "expression" field which can be set to process any log messages based on these exprlang expressions
However on top of this, the PR also goes ahead and adds the request
error
to theexpressionContext
.This means that the user could now use expressions for access logs
In Progress / Remaining Work atm
Adding more of an "integration test" (right now I've written a unit test) for testing the expressions
docs
Checklist