8000 [Debug] generic ErrorHandler by nicolas-grekas · Pull Request #10921 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Debug] generic ErrorHandler #10921

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

Merged
merged 2 commits into from
Jun 16, 2014

Conversation

nicolas-grekas
Copy link
Member
Q A
Bug fix? no
New feature? yes
BC breaks? minor, see updated tests
Deprecations? yes
Tests pass? yes
Fixed tickets none
License MIT
Doc PR none

The proposed goal of this PR is to build a class that can serve as a foundation for a standard error handler, shared with other projects and not only used by the FrameworkBundle.

This is a merge of my previous work on the subject (https://github.com/nicolas-grekas/Patchwork/blob/master/core/logger/class/Patchwork/PHP/InDepthErrorHandler.php) and recent improvements of error handling in Symfony's Debug\ErrorHandler.

ErrorHandler is introduced, which provides five bit fields that control how errors are handled:

  • thrownErrors: errors thrown as ContextErrorException
  • loggedErrors: logged errors, when not @-silenced
  • scopedErrors: errors thrown or logged with their local context
  • tracedErrors: errors logged with their trace, only once for repeated errors
  • screamedErrors: never @-silenced errors

Each error level can be logged by a dedicated PSR-3 logger object.
Screaming only applies to logging.
Throwing takes precedence over logging.
Uncaught exceptions are logged as E_ERROR.
E_DEPRECATED and E_USER_DEPRECATED levels never throw.
E_RECOVERABLE_ERROR and E_USER_ERROR levels always throw.
Non catchable errors that can be detected at shutdown time are logged when the scream bit field allows so.
As errors have a performance cost, repeated errors are all logged, so that the developer
can see them and weight them as more important to fix than others of the same level.

  • build a more generic ErrorHandler
  • update service/listeners definitions to take advantage of the new interface of ErrorHandler
  • add phpdocs
  • add tests

@gnugat
Copy link
Contributor
gnugat commented May 18, 2014

Wow! You've putted a lot of effort to document your work, which is great!

@nicolas-grekas
Copy link
Member Author

I just updated ErrorHandler to take advantage of UniversalErrorHandler.
Tests pass, with some slight updates/BC breaks. Please tell me if this is affordable or not.
Fatal error handling is still in ErrorHandler and needs "universalization", but at least for other kind of errors, this hopefully demonstrates the "Universal" prefix.
Convincing?

@wouterj
Copy link
Member
wouterj commented May 18, 2014

Tests pass, with some slight updates/BC breaks. Please tell me if the is affordable or not.

As long as you stick to http://symfony.com/bc all is fine :)

$stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10);
}

self::$loggers['deprecation']->warning($message, array('type' => self::TYPE_DEPRECATION, 'stack' => $stack));
Copy link
Member Author

Choose a reason for hiding this comment

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

Removing this line is the most severe BC break that this patch does: the deprecation logger is now going to receive a more standardized context as second argument (type, file, line, trace, level). So the change is about renaming stack to trace and having type's value set to E_DEPRECATED or E_USER_DEPRECATED, not self::TYPE_DEPRECATION anymore.
To me, this special const was a hack, so removing it is a good thing (c).

@mvrhov
Copy link
mvrhov commented May 19, 2014

Just came close to the functionality provided by tracy and I'll switch back both the dev and prod error handlers.

@nicolas-grekas
Copy link
Member Author

Uncaught exceptions, OOM and other fatal errors are now handled thanks to #10941 and updates in UniversalErrorHandler. I'm quite ok with the way things a "universalized" now, but that needs reviews. Feedback welcomed.

I'll now work on reconfiguring Symfony for taking advantage of UniversalErrorHandler (last commit is a WIP).

But I have a question: I'm not comfortable with the 3 monolog channels Symfony currently has (emergency, deprecation, scream). I'd prefer having only one for php-errors. But then, should I preserve BC, ie map the old channels somewhere?

The UniversalErrorHandler is currently able to have one different logger/channel per error level, just to cope with Symfony's way of logging errors. But I'm not sure this is a best practice, so I'd prefer removing this possibility, if that's a Good Thing. Is it?

ping @Tobion @Seldaek ?

@stof
Copy link
Member
stof commented May 20, 2014

@nicolas-grekas using a separate channel for deprecations was the way found to integrate deprecations in the profiler AFAIK.

@Seldaek
Copy link
Member
Seldaek commented May 20, 2014

IMO emergency makes no sense because that could be on the main app channel or something with a log level set to EMERGENCY. deprecation it's good to keep it separated, and scream I am not quite sure what it is, errors that have an @ but scream was enabled or ..?

@nicolas-grekas
Copy link
Member Author

@stof it maybe was the intention, but if I read the code correctly, channel information is lost during data collection, so the code relies on a side effect, based on $log['context']['type'] === ErrorHandler::TYPE_DEPRECATION. A loose test that works but doesn't require any specific channel.
In short, the deprecation channel is not used at all.
@Seldaek you guessed, scream is for ignoring @.
From your comments, I suggest having only one channel, so that if someone wants to send php errors somewhere, they can be isolated, being an emergency, a deprecation or any other, silenced or not.

@lyrixx
Copy link
Member
lyrixx commented May 20, 2014

I'd prefer having only one for php-errors. But then, should I preserve BC, ie map the old channels somewhere?

Having N logger has a cost. So i'm 👎 to have a logger per php-errors by default. But indeed, if it's configurable (in a PHP way) it could be perfect.

@nicolas-grekas nicolas-grekas changed the title [WIP] [Debug] UniversalErrorHandler [RFR] [Debug] UniversalErrorHandler May 21, 2014
@nicolas-grekas
Copy link
8000 Member Author

I pushed a new iteration, the PR is ready for review, please comment.

Thanks for your feedback @lyrixx, with this PR, you should be able to plug e.g. Sentry by service configuration, no need to statically ErrorHandler::setLogger() directly anymore:
I plugged the new error handler in symfony by adding a single php channel, some XML and FrameworkExtension updates, with more flexible options (I believe).

@nicolas-grekas nicolas-grekas changed the title [RFR] [Debug] UniversalErrorHandler [RFR] [Debug] generic ErrorHandler May 22, 2014
@@ -11,6 +11,7 @@

namespace Symfony\Bundle\FrameworkBundle\DependencyInjection;

use Psr\Log\LogLevel;
Copy link
Member

Choose a reason for hiding this comment

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

currently, FrameworkBundle does not depend on psr/log in its composer.json. but this makes it required now (while the optional logger in some classes does not). You need to update the composer.json to require psr/log in the standalone bundle (the fullstack package already depends on psr/log)

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

@nicolas-grekas
Copy link
Member Author

I just discovered that something is missing since a long time in the configuration of the prod environment for error handling: the ErrorsLoggersListener does a ErrorHandler::setLogger(), but ErrorHandler::register() is never called, so no logging can ever happen, except the one done by the native PHP handler.

This led me to two points:

  • I think we should always call ErrorHandler::register(), and use set_error_handler()'s second argument to catch only what we want to catch (thus no perf impact in prod).
  • Does Symfony have recommended settings for log_errors and error_log ini settings? Because IMHO it could be a good idea to put native php logs in app/logs/, if the settings are not configured.

Also, I updated the behavior of the proposed error handler for E_RECOVERABLE_ERROR and E_USER_ERROR levels: these levels are special in that they are considered fatal by the native PHP error handler, even when @-silenced. Thus, the only viable option for a user error handler is to always throw an exception for these two types, no matter the error_reporting() level nor anything else.
This strategy ensures that code paths are correct, but also that the application can continue if it decides to handle the exception.

This special behavior is an other argument for always registering the error handler, even and especially in the prod env.

@nicolas-grekas nicolas-grekas changed the title [RFR] [Debug] generic ErrorHandler [Debug] generic ErrorHandler Jun 4, 2014
@nicolas-grekas
Copy link
Member Author

This PR is ready for review/merge IMHO.
Any 👍 from deciders/mergers?

*
* @deprecated since 2.6, to be removed in 3.0.
*/
private $displayErrors = 0x1FFF;
Copy link
Member

Choose a reason for hiding this comment

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

a private property does not need deprecated. It can be removed directly as there is no BC concern for private stuff

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 prefer keeping the @deprecated notice because that will make easier to spot where code is to be removed when moving to 3.0.
I can't remove this private right now since it's used in a deprecated method.

@nicolas-grekas
Copy link
Member Author

I just merged AbstractExceptionHandler back into ExceptionHandler.

@@ -130,7 +132,7 @@ public function handle(\Exception $exception)
* @see sendPhpResponse
* @see createResponse
*/
private function failSafeHandle(\Exception $exception)
protected function failSafeHandle(\Exception $exception)
Copy link
Member

Choose a reason for hiding this comment

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

why are you changing the visibility ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch, I missed telling about that: I think that we should open more custom use of the ExceptionHandler. The failSafeHandle() is the right hooking point. It's a more lightweight version of the previous AbstractExceptionHandler.

Copy link
Member

Choose a reason for hiding this comment

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

sendPhpResponse() and createResponse() are public, so there is already a way to change the way this method works.

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 just removed the private failSafeHandle completely and replaced it by a call to sendPhpResponse().
Still ok for you?

@stof
Copy link
Member
stof commented Jun 16, 2014

except for the visibility change, I'm 👍

@romainneutron
Copy link
Contributor

I'm 👍 on the ErrorHandler changes, I'm pretty confident that changes in the framework glue have been carefully reviewed by @stof

@romainneutron
Copy link
Contributor

Good to me 👍

@nicolas-grekas nicolas-grekas merged commit 1701447 into symfony:master Jun 16, 2014
nicolas-grekas added a commit that referenced this pull request Jun 16, 2014
This PR was merged into the 2.6-dev branch.

Discussion
----------

[Debug] generic ErrorHandler

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | minor, see updated tests
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | none
| License       | MIT
| Doc PR        | none

The proposed goal of this PR is to build a class that can serve as a foundation for a standard error handler, shared with other projects and not only used by the FrameworkBundle.

This is a merge of my previous work on the subject (https://github.com/nicolas-grekas/Patchwork-Error-Logger/blob/master/class/Patchwork/PHP/InDepthErrorHandler.php) and recent improvements of error handling in Symfony's Debug\ErrorHandler.

ExceptionHandler has a new AbstractExceptionHandler base class that factors out the handling of fatal errors casted to exceptions.

ErrorHandler is introduced, which provides five bit fields that control how errors are handled:

- thrownErrors: errors thrown as ContextErrorException
- loggedErrors: logged errors, when not @-silenced
- scopedErrors: errors thrown or logged with their local context
- tracedErrors: errors logged with their trace, only once for repeated errors
- screamedErrors: never @-silenced errors

Each error level can be logged by a dedicated PSR-3 logger object.
Screaming only applies to logging.
Throwing takes precedence over logging.
Uncaught exceptions are logged as E_ERROR.
E_DEPRECATED and E_USER_DEPRECATED levels never throw.
E_RECOVERABLE_ERROR and E_USER_ERROR levels always throw.
Non catchable errors that can be detected at shutdown time are logged when the scream bit field allows so.
As errors have a performance cost, repeated errors are all logged, so that the developer
can see them and weight them as more important to fix than others of the same level.

- [x] build a more generic ErrorHandler
- [x] update service/listeners definitions to take advantage of the new interface of ErrorHandler
- [x] add phpdocs
- [x] add tests

Commits
-------

1701447 [Debug] update FrameworkBundle and HttpKernel for new ErrorHandler
839e9ac [Debug] generalized ErrorHandler
@romainneutron
Copy link
Contributor

First merge 👏

@nicolas-grekas nicolas-grekas deleted the universal-error-handler branch June 16, 2014 14:05
@nicolas-grekas
Copy link
Member Author

It's your turn now @romainneutron ! Let's help @fabpot scale with Symfony :)

@benbor
Copy link
benbor commented Oct 18, 2018

Hi @nicolas-grekas
I tried to throw an exception even for E_USER_DEPRECATED errors, but couldn't do it.

ErrorHandler::register()->throwAt(E_ALL, true);

I started to debug ErrorHandler and found this PR. I see you made the following changes

E_DEPRECATED and E_USER_DEPRECATED levels never throw.

But I really don't understand why? Could you please explain this point, or get me some post about it?
Big thanks :)

@nicolas-grekas
Copy link
Member Author
nicolas-grekas commented Oct 18, 2018

Because a deprecation should never break the execution flow: they are just notices you can ignore - and you should log if you don't want to.

@benbor
Copy link
benbor commented Oct 18, 2018

Thank you, @nicolas-grekas for your faster reaction.
But I have an idea, I want to break the execution flow for not production environment. I want to force clear code. If I make PR which extends the behavior and will throw Error even for E_DEPRECATED it will be useful?

public function throwAt($levels, $replace = false, $ignoreDeprecated = true)
{
    $prev = $this->thrownErrors;
    $this->thrownErrors = ($levels | E_RECOVERABLE_ERROR | E_USER_ERROR);
    if ($ignoreDeprecated) {
        $this->thrownErrors &= ~E_USER_DEPRECATED & ~E_DEPRECATED; 
    }
    //...
} 

I will not change behavior by default, but allow to use the wondeful feature of see that errors while development.
WDYT?
Thanks

@nicolas-grekas
Copy link
Member Author
nicolas-grekas commented Oct 18, 2018

I don't think that's the appropriate place to do so.
If you want to enforce your devs to fix deprecations, you should use the phpunit-bridge in your CI instead.

*/
private function reRegister($prev)
{
if ($prev !== $this->thrownErrors | $this->loggedErrors) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry to be so late, but isn't there an operator precedence bug here? https://3v4l.org/UmbJk

Copy link
Member Author

Choose a reason for hiding this comment

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

Looks like so! Please send a fix, branch 4.4

Copy link
Contributor

Choose a reason for hiding this comment

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

Submitted #47100

nicolas-grekas added a commit that referenced this pull request Jul 28, 2022
…avier)

This PR was squashed before being merged into the 4.4 branch.

Discussion
----------

[Debug][ErrorHandler] fix operator precedence

| Q             | A
| ------------- | ---
| Branch?       | 4.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | Fix #10921 (comment)
| License       | MIT

But I'm struggling to come up with a *failing* test scenario :/ if the bug is only causing evaluating the condition as truthy instead of false when `$prev` is already equal to `$this->thrownErrors | $this->loggedErrors`, I guess it will just re-register the same, without real visible effect?

Commits
-------

7f68914 [Debug][ErrorHandler] fix operator precedence
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

0