I strongly recommend we create a class DrupalException extends Exception, which all other core / module exception classes can extend. This is consistent with a large number of other wrapper functions in core, and has the following possibilities:
* Automatic exception logging - use register_shutdown_function
* Globally consistent stack traces - getTraceAsString data plus the exception message can go into the watchdog row
* Global option for also displaying the stack track to the screen when encountered (for developing)
* Intelligently determine whether to log to watchdog or (if the DB is down) using trigger_error
* Adding additional data to the exception object, such as the values of the function inputs
This really plays to the strengths of the exception system we can now use in core, thanks to PHP5.
In our own CMS, we've had immense success with something similar. We also divided up exceptions into "critical" ones, which almost always bubble all the way up and reflect the DB / file system being hosed and avoid trying to do anything further, and "Normal" exceptions which often means the rest of the page can render normally (for example, without a sidebar widget).
I am willing to provide a patch, but some indication of interest / some implementation suggestions would be appreciated.
Comment | File | Size | Author |
---|---|---|---|
#61 | drupalexception_added-522746-61.patch | 8.72 KB | kwinters |
#54 | drupalexception_added-522746-54.patch | 8.23 KB | kwinters |
#51 | drupalexception_added-522746-51.patch | 8.38 KB | kwinters |
#48 | drupalexception_added-522746-48.patch | 8.43 KB | kwinters |
#46 | drupalexception_added-522746-46.patch | 8.46 KB | kwinters |
Comments
Comment #1
kwinters CreditAttribution: kwinters commentedNot sure what was going on with the title.
Comment #2
jrchamp CreditAttribution: jrchamp commentedSubscribing.
Comment #3
Damien Tournoud CreditAttribution: Damien Tournoud commentedWe already have exception logging in core.
We don't log the stack trace right now. We could do that.
There have been some discussion about that, see for example #296574: Provide debug facility for general and testing. Part of the work could probably be handled by contrib (for example by the Devel module).
We do log to the watchdog, but we don't have "intelligent" fallback right now.
I'm not sure I see the benefit of this.
Comment #4
kwinters CreditAttribution: kwinters commentedFrom what I was able to tell by experiment and poking around in the code, exception logging in core is only applicable when the exception bubbles up all the way. The exception should be logged even if it is caught and the overall process continues unabated.
At admin/settings/logging you can choose whether or not to display PHP errors, and I'm thinking that it would be consistent to place the option on that same form. Overall I'm thinking that a hook might still be an appropriate way to handle the actual output, but that deserves more thought.
When you do a watchdog call, you pass in the variables that you want to log in an array, and the message in a translatable string. That's more or less what I was thinking of here, passing in the variables array in the new DrupalException() call.
Hrm, one of the main reasons we chose to log on shutdown was because (as the exception bubbled up) we could catch, add data, and then throw in a variety of our core rendering functions because the stack trace won't contain info about variables not passed in as arguments (but the global $user id can still be relevant). This doesn't really play into the traditional watchdog variables methodology, because these extra bits of data aren't available at the time of the message creation and aren't in the same scope, but are still useful. That should still be possible to work around.
Comment #5
Damien Tournoud CreditAttribution: Damien Tournoud commentedIt makes very little sense to log exceptions that are caught by a try/catch block.
Making sure that exceptions, as call to watchdog(), are translatable, is a nice idea. But exceptions are *not* a replacement for watchdog() calls. After all, we do have the watchdog() function for that, already.
Comment #6
kwinters CreditAttribution: kwinters commentedI disagree about not logging caught exceptions.
Unless someone is using the exception system as a message-passing tool (which is bad for all sorts of reasons), an exception represents a *problem* during the overall rendering of the page / running of a script / etc. If it's a fatal problem, we display an error page. If it's minor like my sidebar widget example, then we don't need to scrap the whole execution so we catch the exception, but we still want to let a site admin know that the sidebar widget is broken. Making that process automatic removes a burden from the developers, and also ensures consistency.
The alternative is to force developers to both throw an exception and (immediately prior to that) record a watchdog message. Or, any time you write a try / catch you will have to put a watchdog call in the catch. That doesn't seem like a reasonable response to some developers mis-using the exception system overall.
If there is no problem going on, the developer shouldn't throw an exception and instead should (optionally, usually notice level) make a watchdog call and (mandatory) leave the function gracefully.
I'm all for making caught exceptions and uncaught log at different levels, however. Then it puts the option squarely in the hands of the site admin.
In any case, I think we should at least hurry and get some stubs in place and get the current exceptions converted. Matters like when / where to log the data don't affect the need to have a standard exception class, and that standard can be extended later as needed.
Comment #7
RobLoachSubscribe.... I'd love to have some nice debugging on multiple tests.
Comment #8
CorniI CreditAttribution: CorniI commentedsubscribe...
Comment #9
kwinters CreditAttribution: kwinters commentedPlan for initial version, which I hope to have in patch form by next Monday:
* Create the class, modify all existing core Exception classes to extend it
* Change all throw new Exception lines to throw new DrupalException
* Interface with Watchdog in a sensible way
* Ability to append data to the exception and then pass it back up the tree
* Have stack traces available in some form
* Treat caught and uncaught differently, according to some settings (specifically, treat everything as caught until it hits the top level then change a flag)
* If DB is not available for watchdog message, trigger_error instead (may do it in a different patch)
* Possibly have more developer-friendly output via a patch to the Devel module (including an option to output potentially sensitive info)
edit: reminder to self -- depending on how the logging works out, there may need to be a function call to temporarily suppress exception logging
Comment #10
kwinters CreditAttribution: kwinters commentedI have a basic version in place, patch attached. As is, it would be an improvement to get the class in place, and we can further extend it later. Further documentation, etc. to come.
Current version notes:
* Class created
* Now thrown / extended as needed in the rest of the code
* Exception messages are fully translatable, including the string replacement from array logic
* By default, logs at WATCHDOG_NOTICE level if caught, WATCHDOG_ERROR if uncaught
* Method for catch -> add data -> throw pattern, which helps with debugging
* Modified dblog_event to also display all variable data, which is often redundant but in this case allows variables not used in the message to be seen through the admin
Now that exception messages are translatable, there are a number of places that should be modified to use it. I modified the one place in core that needed to change.
Also, I didn't change anything in the simpletests because I'm not sure if they should be normal exceptions or DrupalExceptions.
Some remaining items that I haven't really found an optimal way to handle yet:
* Full stack trace for development - will probably add to devel module instead of doing any logging in core, since it's a lot of data
* Full data logging when database is unavailable (will currently just log the PHP error from uncaught exception handler)
Some usage examples are below.
Comment #11
kwinters CreditAttribution: kwinters commentedDidn't realize that the patch ended up with a .txt extension... fixing for testing system.
Comment #12
kwinters CreditAttribution: kwinters commentedAnd the patch...
Comment #13
kwinters CreditAttribution: kwinters commentedWhen your IDE fails you...
Comment #14
kwinters CreditAttribution: kwinters commentedTerm saves the day.
Comment #16
chx CreditAttribution: chx commented+ $message = t($message, $exception->getVariables());
t() does not take variables for the message that's not how it works.
+ function setLink($link) {
+ $this->link = $link;
+ }
OOP bloat and in bootstrap to begin with! Begone.
Comment #17
Gábor HojtsyAgreed with chx, this use of t() means that whatever is extending DrupalException would need to be known to the translation template extractor, which is not a controllable way to have strings translated. t() should not be used this way.
Comment #18
kwinters CreditAttribution: kwinters commentedAdded suggestions from chx / Gabor.
I left $shouldLog protected with a setter because the setter does something other than just chance the var (registers shutdown func). The watchdog member vars are now public, and the setters / getters are gone.
The t() is also gone, but I think that it's still worth adding back eventually. The t() function is used in that way in a dozen or so places in core already, so it's not inconsistent. Even if nobody ever translates an exception message (which has somewhat limited utility anyway, since the code is English), the t() still does the variable text replacement so you don't always have to look up the exception in the watchdog report.
I see two possible solutions for translating exception messages (if it's valuable to do so):
* Change the code that looks for t() and provides a list of translatable strings to also look for this - new *Exception('*
* Chance the usage to new DrupalException( t('The message !var', array('!var' => 'value')))
I don't really like the second option because the logging won't be as good (you no longer have the variables separated out), and it's not as transparent to the developers.
Comment #20
jrchamp CreditAttribution: jrchamp commentedThat's odd, the patch applies for me.
Comment #21
Damien Tournoud CreditAttribution: Damien Tournoud commentedI still don't believe logging a caught exception makes sense. You could do that yourself (in your code) if you want to. Anyway, if you really want to do that, you should avoid registering a shutdown function for each exception, as this would be very inefficient. I suggest the following (pseudo code):
Comment #23
yched CreditAttribution: yched commentedNote that Field API form validation relies on caught exceptions. See field_attach_validate() / field_attach_form_validate().
A field validation error in a form translates to error messages, while the same error during a programmatic save raises exceptions.
Comment #24
kwinters CreditAttribution: kwinters commentedDamien, in practice there should be very few exceptions thrown during a given run, but I'll see how a static array works out next time I'm digging around in there.
yched, in this version you would throw new DrupalException('message', $vars_or_code, FALSE) to avoid logging those user errors.
My general preference is to err on the side of excessive NOTICE-level logging rather than accidentally failing to report something that was important, but if the general consensus is do default to not-logged then I'm OK with that. Any other opinions?
Comment #25
kwinters CreditAttribution: kwinters commentedchx, reconsidering the setters:
The general reason that people use the usually-bloated getter / setter pattern is the same reason that core uses wrappers for PHP functions; if we find an issue with a wrapper-less function later (often enough UTF-8 issues), then it's an enormous pain to fix because we have to fix 100 places in contrib instead of just one place in core.
I don't think UTF-8 specifically will be an issue, but I also don't believe we can safely say that some Exception class down the line won't want to extend a setter to something more than just $this->var = $var.
Lesser of two evils?
Comment #26
kwinters CreditAttribution: kwinters commentedCross-post, in the event that potx needs modifications: #533296: Exceptions and potx
Comment #27
jtsnow CreditAttribution: jtsnow commentedI like this idea. Subscribing.
Comment #28
jrchamp CreditAttribution: jrchamp commentedRerolled, even though I'm not sure what's going on with the test bot. Also, makes some minor coding standards changes. There was a new Exception() (already in core) that was not being thrown. It seems like it should be, so this patch contains that change.
Comment #29
jrchamp CreditAttribution: jrchamp commentedSetting status so test bot can give it a spin.
Comment #31
jrchamp CreditAttribution: jrchamp commentedThis one has DamZ's "single shutdown function" code rolled in. I wish the test bot would explain why it's not happy.
Comment #32
Crell CreditAttribution: Crell commentedSubscribe.
I'm undecided at the moment here. Need time to think about it when I'm not at work. :-)
Comment #33
chx CreditAttribution: chx commentedabout setters, when and if we have a problem we wil fix that even if it is a bit painful. I do not consider it a valid argument that "sometime in the future we might need ... so let's bloat core".
Comment #35
jrchamp CreditAttribution: jrchamp commentedRerolled against head.
Comment #37
kwinters CreditAttribution: kwinters commentedLast week I talked at a fair length with Crell in IRC, and here are some notes now that I've had some time to digest it some.
Most places that do a catch are going to catch Exception anyway (or DrupalException then Exception) because they also have to worry about PDO Exceptions and / or DB exceptions (Crell expressed a desire to keep the DBTNG layer loosely coupled with Drupal core, which may result in the DB layer not using the DrupalException class at all). So, we will probably need a utility function drupal_log_exception($ex) that accepts either a DrupalException or Exception. Then, most of the time the developer won't have to care what kind of Exception it is.
With the DrupalException close to the current form, this helper function will just set logging to TRUE if it's a DrupalException, or do a watchdog call if it's a plain Exception (passing in the Exception message text).
It is impossible to translate all Exceptions via potx, because potx won't have access to the source of any libraries getting compiled in to PHP (which usually throw PDOExceptions). However, my vote is that we still pass the Exception $messages through t() when we log them (and / or during display???) so that it is possible to do the translation manually if desired. I'm still undecided which is better: throw new DrupalException(t('message !var', array('!var' => 'val'))) and just taking $message or having DrupalException take the same args as watchdog, but I'm leaning towards the watchdog syntax even though it will require some potx regex changes.
Comment #38
jrchamp CreditAttribution: jrchamp commentedFatal error fixed. Missed one of the DrupalException::$loggedExceptions.
Comment #39
drewish CreditAttribution: drewish commentedsubscribing... i was banging my head against some of these issues with file.inc: #348993: Use exceptions to report file.inc failures
Comment #40
kwinters CreditAttribution: kwinters commentedThe other issue brings up some important considerations:
"The exceptions seem to need to end up two places, on the form via form_set_error() and logs via watchdog(). watchdog() wants the untranslated string and arguments and form_set_error() needs a form element to blame so I added a DrupalException class that can handle this bit."
The current versions on this issue don't report form errors that well, I'll roll it in.
In the other version, the DrupalException calls parent with the translated string as the message. I'll have to put some more thought into that one, not sure where it will come into play.
New patch coming soon, I'd love to get this in before the cut-off date.
Comment #41
kwinters CreditAttribution: kwinters commentedSummary so far (this is of course my opinion, so chime in if you disagree on anything):
This is a developer experience issue. Since D7 is PHP5 only, we can replace the drupal_set_message then return (and pray) workflow with a thrown exception. A core exception system could handle logging, displaying messages to the user, and form validation error messages.
The old way also runs into major problems when an error occurs deep down the code tree and you need to detect / handle the situation gracefully, usually because some function in the middle doesn't return an appropriate result on error.
Minimally we need to have the following in core for this to work well:
* Some functions for gracefully logging and handling PDOExceptions and other errors from external libraries
* A core DrupalException class to hold the additional data that Drupal needs on errors (form source, watchdog data, etc.)
* Sensible translation scheme
Additional wishlist items:
* Ability to pull a stack trace for a given logged exception and / or display it to the screen
* More powerful logging tools and logic
* If the database has gone away, log to apache error log (this is a definite candidate for a different issue)
* Easy to get all the needed data into
And items of contention:
* How much code is appropriate to put into common / bootstrap
* What should be handled by the Devel module (or similar contrib code)
* By default, should a caught exception log? This determines whether a developer needs to explicitly log or suppress a caught exception (they will often need to do one or the other).
* Should we pass variable strings to t()? This allows PDOExceptions to be manually translated, but potx will need a patch to its regex.
* Related to the above: new DrupalException(t('my message')) or have DrupalException handle it transparently?
* OOP - when a painful bug fix later can be avoided by creating a public function now, prefer future-proofing or un-bloated core?
* Are shutdown functions acceptable from a performance standpoint? In this case they will only be registered when an exception is going to be logged.
Future, not critical for code freeze:
* More behavior settings for exceptions / logging
Comment #42
jrchamp CreditAttribution: jrchamp commentedThe nice thing is that only one shutdown function needs to be registered. As far as execution time, the performance should be acceptable. By default a caught exception probably should not log, because the audience to which Drupal caters is varied and designed to be accessible to nontechnical individuals. Rather, if a static similar to the one for keeping track of exceptions which have occurred could be set by contrib modules like devel (or even a system configuration option) to enable exception logging by default.
Comment #43
kwinters CreditAttribution: kwinters commentedMy line of thinking was: if the default case is to log, less experienced developers won't really even have to think about it in more cases. But it's pretty much a toss-up in my book, I can get behind either approach.
I did plan on having a setting at some point, specifically a threshold to log to the database. If the uncaught and caught exceptions are logged at different levels, it's easy to ignore what you want on the production environment.
However, the default still determines what the actual code will have to look like. The developers will either have to explicitly prevent recording some messages or explicitly log some other set, and the code will accommodate one or the other. I'm pretty sure they're mutually exclusive.
Comment #45
kwinters CreditAttribution: kwinters commentedNotes from IRC:
* Going to scrap automatic logging of caught exceptions for now, add a setting to enable it (or devel module hook or something) later.
* PHP Magic Methods for getters / setters eliminates the need for actual ones entirely, since we can make the change silently later if it's needed (thanks cwgordon7).
* Some interest in the ability to catch, add data, and throw to add context to an exception. I think I'll leave that in, since it shouldn't add much of a code or memory footprint by itself. May still split it off into a different issue.
Comment #46
kwinters CreditAttribution: kwinters commentedAttached is a basic patch, with the sole intent of getting the API established ASAP. I fully intend to get simpletests, etc. in place afterward.
Sample usage:
For later:
* Simpletests
* Documentation
* Getting the API used throughout core (some catch blocks may need be modified, and almost all custom Exception classes, form validation)
* potx expression changes
* Better output to the screen, including more consistent use of t()
* Stack traces
* Log differently when the DB isn't available
* _drupal_decode_exception should pass variables up the tree, so it can be translated without logging the translated version to the DB
Comment #47
Bèr Kessels CreditAttribution: Bèr Kessels commentedReviewing on DrupalconPars codesprint.
First problem encountered:
In the current situation, the code in
function _drupal_exception_handler($exception)
will log the exception in watchdog twice, when the exception is't catched.Once by the DrupalException shutdown function, and once by PHP itself, passing the error up to watchdog again.
And if it is caught, it still logs twice by itself.
Another minor thing: there is a debug var_dump left in the patch that should be removed.
Comment #48
kwinters CreditAttribution: kwinters commentedThanks for the review!
I removed the var_dump.
The double-logging is because _drupal_decode_exception needs a rewrite for a number of reasons (as noted in comment 46), but that's not an API change so I'm going to do it in a separate patch.
I'm not seeing a double-log when it is caught, using code in my sample above.
Comment #49
jrchamp CreditAttribution: jrchamp commented+1
We'll have to make sure to eliminate the double-logging later when the _drupal_decode_exception gets rewritten. For now, a little extra watchdog information doesn't seem like a bad thing.
Comment #50
cwgordon7 CreditAttribution: cwgordon7 commentedMostly minor code style problems:
- Stray whitespace change near
drupal_render_cache_set()
as well as near@see drupal_render_cache_set()
, should be removed.-
} catch (Exception $e) {
- blocks like this should be separated onto their own lines, so it's more like. This is found multiple places in your code - it would be awesome if you could just go through and add the extra line break where necessary?
-
When the params are provided in that format, $code will be set to 0.
— dead comment?-
//NOTE: Could potentially log in a different way.
— needs a leading space, plus maybe different language; something like// Note: we could potentially log here in a different way.
-
* Serialized variables array from watchdog row
— missing a space at the start, and a period at the end.-
- only one blank line is necessary.
I'm also not entirely sure why watchdog_exception() is necessary - is it to log an exception without killing the PHP request? Seems a bit weird / hard to understand - maybe it could use some additional documentation to clarify?
Other than that though, this is looking really good! I'd be happy to give this another review (and hopefully rtbc it!) when the above feedback is addressed. Thanks!
Comment #51
kwinters CreditAttribution: kwinters commentedThanks for the review!
The
drupal_render_cache_set()
items were trailing whitespace removed by my IDE. I can remove them from the patch if you want, but I believe we're supposed to remove trailing whitespace anyway so it should still be an improvement.} catch (Exception $e) {
- There was one added in my patch, and two that already existed in the database module. I removed the one from the patch, but I'm hesitant to remove the other one yet. I opened a separate issue: #576248: [policy] Coding Standards Update for PHP5I cleaned up the DrupalException constructor comment, it had leftovers from a previous patch that no longer applied.
NOTE comment format changed and additional details added.
Serialized variables array from watchdog row
comment fixed as above.Regarding the double blank lines in the dblog admin file: I previously made it consistent with what was in there before, but now I set it to be a single blank line throughout the file.
The function watchdog_exception is needed because external libraries, etc. will never throw a DrupalException, and this function makes it so the developer doesn't need to care if all they want to do is log. I added a better explanation to the function header, which was somewhat difficult in 80 characters and one line.
Comment #52
mattyoung CreditAttribution: mattyoung commentedsubscribe
Comment #53
cwgordon7 CreditAttribution: cwgordon7 commentedYes, please remove the stray whitespace changes. Also, I noticed that watchdog_exception() is missing doxygen documentation for all but the first parameter? Fix those up and this should be good to go. :)
Comment #54
kwinters CreditAttribution: kwinters commentedCommon.inc whitespace changes removed, documented remaining watchdog_exception params.
Comment #55
cwgordon7 CreditAttribution: cwgordon7 commentedLooks awesome now, and no explosions when applied, so rtbc. :)
Comment #56
Damien Tournoud CreditAttribution: Damien Tournoud commentedI don't get this code. I don't understand the use case for logging some exceptions and not some others. The only way you can apparently log an exception is to explicitely mark it as ->shouldLog(), so there is no way for an external code to specifically log a given exception for debug purposes. I also don't understand the need for logging caught exceptions.
More importantly, most of the methods of the DrupalException class are not documented. Marking as needs work based on that.
Comment #57
kwinters CreditAttribution: kwinters commentedIt is impossible to make all Exceptions extend DrupalException, because PDOExceptions, etc. will be thrown from outside code. Therefore, we need a helper method to log non-Drupal Exceptions, and since that method also accepts DrupalExceptions, you should only ever need to call watchdog_exception() in a catch block other than a few special cases. Module code should rarely call shouldLog().
Logging a caught exception is entirely up to the developer. I intend to use it in the Block module later, because a failed block render should log (it's an error and needs to be fixed) but doesn't need to be fatal.
Per comment #46 there are a number of things left to do, and the initial patch just has the goal of determining the API so that its scope is achievable. However, I can add method comments to the patch later today if it's going to hold up the initial commit.
Comment #58
Damien Tournoud CreditAttribution: Damien Tournoud commentedWhat is the difference between:
and
Comment #59
kwinters CreditAttribution: kwinters commentedThat's not really a good example, because it doesn't reflect the normal use case. That is the throw-er setting the exception to log, which really shouldn't be done (at least in normal cases, there may be some outliers). Whether or not to log should be up to the function doing the catch.
Here is a more typical example:
Probably 95% of the watchdog_exception appearances in the code will look like that.
Comment #60
Crell CreditAttribution: Crell commentedNo need to loop. $this->variables += $vars already does what you're looking for there, but faster.
The parent constructor should be called at the start of the constructor unless there's a specific reason not to.
Honestly I'm still not entirely convinced here, but don't have time to really dig into it right now.
Comment #61
kwinters CreditAttribution: kwinters commentedMade the two changes from Crell and added more documentation for DamZ.
Is it the whole data adding / shouldLog / shutdown function workflow the part you guys are leery about, or is there some other aspect? I like it because it allows developers to add context to a log entry, for example that the Exception was thrown during the render of block 123, which has been incredibly helpful in our old custom CMS. If you have a different suggestion for chaining exceptions, I'm all ears.
Comment #62
kwinters CreditAttribution: kwinters commentedSummary of the issue so far:
The goal is to make Exception handling in Drupal more developer-friendly and functional. This means creating sensible usage patterns that Drupal developers will be familiar with already (similar to watchdog) and gracefully handling caught and uncaught exceptions with minimal developer action.
To this end, we've created the DrupalException class and some support functions for logging. Also new is the ability to append additional variables to a DrupalException as it bubbles up the execution stack, so that the Block module can record the block ID for context and easier debugging.
The current patch goal is just to get the API nailed down before the October 15th deadline. There are many things that will have to be done after (see below) but in the interest of keeping this patch reviewable and workable that can / will be done after the initial commit. To test / review, use the patch in #61.
Basic use cases:
More elaborate example with addVariables:
For later:
* Rewriting _drupal_decode_exception and the Exception handler function (currently logs twice and the code isn't separated out well)
* _drupal_decode_exception should pass variables up the tree, so it can be translated without logging the translated version to the DB
* Simpletests
* Documentation
* Getting the API used throughout core (some catch blocks may need be modified, and almost all custom Exception classes, form validation)
* potx expression changes
* Better output to the screen, including more consistent use of t()
* Stack traces in conjunction with Devel module
* Log differently when the DB isn't available
Comment #63
kwinters CreditAttribution: kwinters commentedtagging
Comment #64
sunLooks like many are not sold on this idea yet, so removing that second tag.
Comment #66
kwinters CreditAttribution: kwinters commentedForked minimalist version to #601020: Exception Handler Logging and Improvements so we can get something in for D7.
Comment #67
valthebaldIs it still relevant for 8.x-dev, with adoption of Symfony's ExceptionListener?
Comment #68
Crell CreditAttribution: Crell commentedI don't think so. It's not really viable in a component-based architecture.