4

I am working on a PHP project where I am catching exceptions and logging the errors using Monolog and returning a user-friendly page as a response.

The project is currently in its baby phases, so I am just logging the errors to a file using Monolog's StreamHandler class in an app directory outside of the public's reach, as I progress I realize this may fail if there's an IO error of some sort and so I will also be logging to a database (possible ElasticSearch) and sending critical errors via email to the admin.

As I am using the StreamHandler, I can see that it throws an exception if it fails to open the file.

Now, how should I be handling this case of exception and how should I log it if the logging mechanism itself fails?

I can have the exception be handled by another logger which sends an email on such critical situations, but again, how do I handle the exception being thrown by the mailer?

I assume the page would be filled with too many try-catch blocks with loggers spread out throughout the page which would look downright ugly.

Is there an elegant, clean solution that doesn't involve too many nested try-catch blocks that are being used in large scale projects? (Unpopular opinions are also welcome)

Here is some code for reference:

try
{
    $routes = require_once(__DIR__.'/Routes.php');

    $router = new RouteFactory($routes, $request, \Skletter\View\ErrorPages::class);
    $router->buildPaths('Skletter\Controller\\', 'Skletter\View\\');

    $app = new Application($injector);
    $app->run($request, $router);
}
catch (InjectionException | InvalidErrorPage | NoHandlerSpecifiedException $e)
{
    $log = new Logger('Resolution');
    try
    {
        $log->pushHandler(new StreamHandler(__DIR__ . '/../app/logs/error.log', Logger::CRITICAL));
        $log->addCritical($e->getMessage(),
            array(
                'Stack Trace' => $e->getTraceAsString()
            ));
    }
    catch (Exception $e)
    {
        echo "No access to log file: ". $e->getMessage();
        // Should I handle this exception by pushing to db or emailing?
        // Can possibly introduce another nested try-catch block 
    }
    finally
    {
        /**
         * @var \Skletter\View\ErrorPageView $errorPage
         */
        $errorPage = $injector->make(\Skletter\View\ErrorPages::class);
        $errorPage->internalError($request)->send();
    }
}
twodee
  • 606
  • 5
  • 24

5 Answers5

4

Logging of exceptions and notification about them are two tasks which must be solved globally for the whole project. They shouldn't be solved with help try-catch blocks, because as a usual thing try-catch should be used to try to resolve the concrete local located problems which made an exception (for example, modify data or try to repeat execution) or to do actions to restore an application state. Logging and notification about exceptions are tasks which should be solved with a global exception handler. PHP has a native mechanism to configure an exception handler with set_exception_handler function. For example:

function handle_exception(Exception $exception)
{
    //do something, for example, store an exception to log file
}

set_exception_handler('handle_exception');

After configuring handler, all thrown exception will be handled with handle_exception() function. For example:

function handle_exception(Exception $exception)
{
    echo $exception->getMessage();
}

set_exception_handler('handle_exception');
// some code
throw Exception('Some error was happened');

Also, you can always disable the current exception handler with help restore_exception_handler function.

In your case, you can create a simple exception handler class which will contain logging methods and notification methods and implement a mechanism to handle exceptions which will select a necessary method. For example:

class ExceptionHandler
{    
    /**
     * Store an exception into a log file         
     * @param Exception $exception the exception that'll be sent
     */
    protected function storeToLog(Exception $exception)
    {}

    /**
     * Send an exception to the email address
     * @param Exception $exception the exception that'll be sent
     */
    protected function sendToEmail(Exception $exception)
    {}

    /**
     * Do some other actions with an exception
     * @param Exception $exception the exception that'll be handled
     */
    protected function doSomething(Exception $exception)
    {}

    /**
     * Handle an exception
     * @param Exception $exception the exception that'll be handled
     */
    public function handle(Exception $exception)
    {
        try {
            // try to store the exception to log file
            $this->storeToLog($exception);
        } catch (Exception $exception) {
            try {
                // if the exception wasn't stored to log file 
                // then try to send the exception via email
                $this->sendToEmail($exception);
            } catch (Exception $exception) {
                // if the exception wasn't stored to log file 
                // and wasn't send via email 
                // then try to do something else
                $this->doSomething($exception);
            }
        }

    }
}

After it, you can register this handler

$handler = new ExceptionHandler();
set_exception_handler([$handler, 'handle']);


$routes = require_once(__DIR__.'/Routes.php');

$router = new RouteFactory($routes, $request, \Skletter\View\ErrorPages::class);
$router->buildPaths('Skletter\Controller\\', 'Skletter\View\\');

$app = new Application($injector);
$app->run($request, $router);
Maksym Fedorov
  • 6,383
  • 2
  • 11
  • 31
  • >PHP has a native mechanism to configure a global exception handler with set_error_handler function. Did you mean `set_exception_handler`? In that case shouldn't that be used as a fallback for uncaught exceptions rather than handling globally for everything? PHP documentation states the intention as such: >Sets the default exception handler if an exception is not caught within a try/catch block. Execution will stop after the exception_handler is called. – twodee Jul 06 '19 at 07:07
  • @2dsharp I meant set_exception_handler. I fixed its mistake. Yes, an exception handler is used to handle an uncaught exception, but you shouldn't catch exceptions with help `try-catch` for the purpose of logging or notify about them. Try-catch should be used to try to resolve the concrete problem which made an exception (for example, modify data or try to repeat execution) or to do actions to restore an application state – Maksym Fedorov Jul 07 '19 at 08:50
0

I think in this situation you should write away to disk. Then you write a function that reads from that file and basically does what the logger did.

kPieczonka
  • 394
  • 1
  • 14
0

There are some solutions you can try.

  1. You can wrap the logger in another class of your own, handle all of the exceptions and possible errors there, and use your class for logging.

  2. Sometimes is it impossible to catch all of the errors, exceptions are left unhandled and rare cases happen (i.e IO error), You can decide to live with that. In that case, you can use the solution you raised yourself. Use the ELK package and you will be able to configure a watch monitor on the parameters you are interested in.

yariv_kohn
  • 94
  • 4
0

If I was you, I would catch that exception on "kernel.exception" event, for a simple reason: Your logger is E V E R Y W H E R E. Just write a listener, test something like

if ($e instanceof MONOLOG_SPECIFIC_EXCEPTION) { // handle exception here }

If you use commands too, do the same thing on "console.exception" event.

millenion
  • 1,218
  • 12
  • 16
0

There is no elegant solution to this problem. From your question, I understand that you are creating a library for exception handling that wraps the whole application and does some side effects (e.g. writes to a log file). The question is: how does this library handle the scenario where these side effects (parts of the library's core functionality) - fail?

The simplest and most intuitive answer, in my opinion, is: don't handle them. Act as if your library doesn't exist.

In other words, rethrow any exceptions that you caught but didn't manage to handle. Don't assume that your library is the only exception handler of the application. There may be another logging library/abstraction on top of yours that also capture exceptions and handle them differently than you (e.g. send them by email instead of writing to a file). Give those other players a chance to handle the exception.

Assuming the basic scenario where your library is the only error handling library that wraps the project: your library still failed to do its core function (capture the error and log it to a file) which is a fatal error, because the application can't function without its core function. Like many other fatal errors, it's best to "fail fast, fail early" and pass these errors to PHP to handle on its own, by writing them to its own error_log or displaying them to the end-user (depending on the error_reporting level).

Alternatively, you can always make the developer aware of this possibility of file or folder permission issues and give the developer the possibility to define business logic to be executed on such lower-level failures. The multiple catch blocks example you provided is one way to do this. Passing in an optional lambda function to be executed on low level failures is another. There are many options here.

Dzhuneyt
  • 8,437
  • 14
  • 64
  • 118