-
Notifications
You must be signed in to change notification settings - Fork 202
feat(server): log unsafe errors #1258
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
Conversation
70f3040
to
0aed48b
Compare
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 side. @klausi ? @Kingdutch ?
src/Entity/Server.php
Outdated
if (empty($result->errors)) { | ||
return; | ||
} | ||
|
||
$unsafeErrors = array_filter($result->errors, function ($e) { | ||
return !($e instanceof ClientAware) || !$e->isClientSafe(); | ||
}); | ||
$isPreviousErrorLogged = FALSE; | ||
foreach ($unsafeErrors as $error) { | ||
if ($error->getPrevious() instanceof \Throwable) { | ||
_drupal_log_error(ErrorUtil::decodeException($error->getPrevious())); | ||
$isPreviousErrorLogged = TRUE; | ||
} | ||
} | ||
|
||
if ($unsafeErrors) { |
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 like to move the filter into the foreach. This means we don't have an N
+ M
(N
being all, M
being the server errors) but only an N
loop. It can also slightly improve readability :)
The type of $result->errors
is always an array. So if it's empty the foreach loop is just skipped, there's no need for an additional empty check.
if (empty($result->errors)) { | |
return; | |
} | |
$unsafeErrors = array_filter($result->errors, function ($e) { | |
return !($e instanceof ClientAware) || !$e->isClientSafe(); | |
}); | |
$isPreviousErrorLogged = FALSE; | |
foreach ($unsafeErrors as $error) { | |
if ($error->getPrevious() instanceof \Throwable) { | |
_drupal_log_error(ErrorUtil::decodeException($error->getPrevious())); | |
$isPreviousErrorLogged = TRUE; | |
} | |
} | |
if ($unsafeErrors) { | |
$hasServerErrors = FALSE; | |
$hasLoggedPrevious = FALSE; | |
foreach ($result->errors as $error) { | |
// Don't log errors intended for clients, only log those that | |
// a client would not be able to solve, they'd require work from | |
// a server developer. | |
if ($e instanceof ClientAware && $e->isClientSafe()) { | |
continue; | |
} | |
$hasServerErrors = TRUE; | |
// Log the error that cause the error we caught. This makes the error | |
// logs more useful because GraphQL usually wraps the original error. | |
if ($error->getPrevious() instanceof \Throwable) { | |
_drupal_log_error(ErrorUtil::decodeException($error->getPrevious())); | |
$hasLoggedPrevious = TRUE; | |
} | |
} | |
if ($hasServerErrors) { |
We could even replace the if ($hasServerErrors) {
with a guard clause, but I'm not sure that'd necessarily improve readability.
if (!$hasServerErrors) {
return;
}
// \Drupal::logger here
I'm wondering if trying to json_encode
$result->errors
will cause us any grief in testing. See sebastianbergmann/phpunit#4371 (comment) for details, which previously led to f9c573a
src/Entity/Server.php
Outdated
\Drupal::logger('graphql')->error( | ||
"There were errors during a GraphQL execution.\n{see_previous}\nDebug:\n<pre>\n{debug}\n</pre>", | ||
[ | ||
'see_previous' => $isPreviousErrorLogged ? 'See the previous log messages for the error details.' : '', |
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'm personally never a fan of the "see the previous log messages" because on busy sites they're hardly ever next to each other. Would there be a good way to get those errors all into a single error log message?
On systems that send alerts based on the amount of errors this can also easily trip monitoring for a single request, which may not be desirable (e.g. if a single request caused the same error to happen multple times).
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.
Thanks, makes sense to me! @Kingdutch has some good points, please fix those.
src/Entity/Server.php
Outdated
$isPreviousErrorLogged = FALSE; | ||
foreach ($unsafeErrors as $error) { | ||
if ($error->getPrevious() instanceof \Throwable) { | ||
_drupal_log_error(ErrorUtil::decodeException($error->getPrevious())); |
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.
are we sure we want to use _drupal_log_error()? It also sets an HTML response, which does not make sense to me in a graphql API context (when displaying errors is enabled)? But then again it contains a lot of useful code for decoding the error, so this should be fine.
@Kingdutch @klausi I did the changes.
I don't know what to use as a replacement. I chose
This error makes no sense to me. Also it appears only with PHP 7. Could it be that it is caused by the recent PHPStan update? |
Looks like a phpstan bug, I will try to resolve that! |
PHPStan fixed in #1261 , merged & running the tests again. |
Tested this with our project setup and it works for me! It would be cool to have a test case for this, it should be viable since we are also testing full query requests in ResultTest for example. Not sure how easy it is to check the logs in a test or set a mock log service for the assertion. Now thinking about it I'm not sure that the Server entity is the correct place to do this logging. The executor seems more like the best fitting place? I see doExecuteUncached() there that is probably the right place. What do you think? |
@klausi thanks! I implemented both suggestions. @Kingdutch for some reason I cannot re-request a review from you. |
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.
Wow, outstanding work @Leksat !!! Something like this is exactly what I was thinking of, great job.
Will merge this now.
$this->installEntitySchema('graphql_server'); | ||
$this->installEntitySchema('configurable_language'); | ||
$this->installConfig(['language']); | ||
$this->installEntitySchema('menu_link_content'); |
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 don't even want to know why we need this :-D But totally fine, thanks!
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.
On the query run there was an error logged. Something like "DB error: menu_link_content table does not exist". I also did not want to know why it happens 😅 So I just installed menu_link_content
schema.
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.
Do we know in what test this was? This could indicate a hidden dependency that I don't think should exist. So in theory this could even be flagging a breaking change.
(It's also not a module I'd expect should be added for all tests, so I'm not sure if GraphQLTestBase
is the way to go).
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.
It was happening in the new test I created.
} | ||
|
||
if ($hasUnsafeErrors) { | ||
\Drupal::logger('graphql')->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.
We should inject the logger, but does not matter much in this class. Can be done in a follow-up.
}); | ||
$this->mockResolver('Query', 'takesIntArgument'); | ||
|
||
$this->container->get('logger.factory')->addLogger($this); |
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.
Nice trick, this is really convenient. I need to remember that.
Yeah I'm not officially a maintainer, just someone who uses GraphQL and has opinions :D |
* 8.x-4.x: test(dataprovider): Rename dataprovider functions to not be accidentally tested (drupal-graphql#1266) fix(EntityLoad): Return NULL on NULL entity IDs when composing entity load (drupal-graphql#1174) fix(entity_reference): Return emtpy arrays instead of NULL (drupal-graphql#1265) chore(voyager): JS dependency updates with yarn audit chore(explorer): JS dependency updates with yarn audit test(coder): Update Coder to 8.3.14 (drupal-graphql#1264) test(phpstan): Enable PHPStan on PHP 8 by disabling PHP opcache (drupal-graphql#1262) feat(server): Log unsafe server errors for better error tracing (drupal-graphql#1258) test(phpstan): Ignore PHPStan warning directly in code with a comment (drupal-graphql#1263) test(phpstan): Ignore a false unreachable statement error (drupal-graphql#1261) test(phpstan): Update PHPStan and dependencies (drupal-graphql#1257) test(php): Add PHP 8.1 for testing (drupal-graphql#1256) test(core): Update Drupal core to 9.3 for testing (drupal-graphql#1255)
Closes #1195