如何处理在处理另一个异常时记录器抛出的异常?

4
我正在开发一个 PHP 项目,其中我正在使用 Monolog 捕获异常并记录错误,并将友好的页面作为响应返回。
由于该项目目前处于早期阶段,因此我只是将错误记录到一个应用程序目录以外、公众无法访问的文件中,使用 Monolog 的 StreamHandler 类,随着项目的进展,我意识到如果出现某种IO错误,则可能会失败,因此我还将记录到数据库(可能是ElasticSearch)并发送关键错误消息给管理员。
由于我使用了StreamHandler,所以如果它无法打开文件,我可以看到它会引发异常。
现在,我应该如何处理这种情况下的异常,如果日志记录机制本身失败,我应该如何记录它?
我可以让另一个记录器处理异常,在这种关键情况下发送电子邮件,但问题又来了,如果邮件程序抛出异常,我该如何处理呢?
我认为页面将充满太多的try-catch块,而日志记录器散布在整个页面上看起来非常丑陋。
是否有一种优雅、简洁的解决方案,在大型项目中不涉及过多嵌套的try-catch块?(不流行的意见也欢迎)
以下是参考代码:
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();
    }
}

你使用的 PHP 版本是什么? - Maksym Fedorov
@maximfedorov PHP 7.3 - twodee
5个回答

4
记录异常和通知异常是整个项目必须全局解决的两个任务。它们不应该使用 try-catch 块来解决,因为通常情况下,try-catch 应该用于尝试解决导致异常的具体本地问题(例如修改数据或尝试重复执行),或者执行恢复应用程序状态的操作。记录异常和通知异常是应该通过全局异常处理器解决的任务。PHP 有一个本地机制可以使用 set_exception_handler 函数来配置异常处理器。例如:
function handle_exception(Exception $exception)
{
    //do something, for example, store an exception to log file
}

set_exception_handler('handle_exception');

配置处理程序后,所有抛出的异常将使用 handle_exception() 函数进行处理。例如:
function handle_exception(Exception $exception)
{
    echo $exception->getMessage();
}

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

同时,你可以随时使用restore_exception_handler函数来禁用当前的异常处理程序。

在你的情况下,你可以创建一个简单的异常处理程序类,其中包含记录方法和通知方法,并实现一个处理异常的机制,以选择必要的方法。例如:

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);
            }
        }

    }
}

在此之后,您可以注册此处理程序

$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);

PHP具有使用set_error_handler函数配置全局异常处理程序的本地机制。您是不是指set_exception_handler?在这种情况下,它不应该被用作未捕获异常的回退,而应该为所有内容全局处理吗? PHP文档说明了意图:
如果在try / catch块中未捕获异常,则设置默认异常处理程序。调用exception_handler后,执行将停止。
- twodee
@2dsharp 我的意思是 set_exception_handler。我已经修复了它的错误。是的,异常处理程序用于处理未捕获的异常,但您不应该使用 try-catch 来捕获异常以记录或通知它们。应该使用 try-catch 来尝试解决导致异常的具体问题(例如修改数据或尝试重复执行),或者执行操作以恢复应用程序状态。 - Maksym Fedorov

0

我认为在这种情况下,你应该将数据写入磁盘。然后编写一个从文件读取数据并执行日志记录操作的函数。


0

有一些解决方案可以尝试。

  1. 您可以将记录器包装在自己的另一个类中,在那里处理所有异常和可能的错误,并使用您的类进行日志记录。

  2. 有时候无法捕获所有错误,异常未被处理,会发生罕见情况(例如IO错误),您可以决定接受这种情况。在这种情况下,您可以使用您提出的解决方案。使用ELK软件包,您将能够在您感兴趣的参数上配置监视器。


0
如果我是你,我会在“kernel.exception”事件上捕获异常,原因很简单:你的日志记录器无处不在。只需编写一个监听器,测试类似于
if ($e instanceof MONOLOG_SPECIFIC_EXCEPTION) { // 在此处理异常 }
如果您也使用命令,请在“console.exception”事件上执行相同的操作。

0

这个问题没有优雅的解决方案。从你的问题中,我理解你正在创建一个异常处理库,它包装整个应用程序并执行一些副作用(例如写入日志文件)。问题是:当这些副作用(库的核心功能的一部分)失败时,该库如何处理?

在我看来,最简单和最直观的答案是:不要处理它们。就好像你的库不存在一样。

换句话说,重新抛出任何你捕获但未能处理的异常。不要假设你的库是应用程序唯一的异常处理程序。可能还有另一个日志记录库/抽象层在你的上面,也会捕获异常并以与你不同的方式处理它们(例如通过电子邮件发送而不是写入文件)。给那些其他参与者处理异常的机会。

假设基本情况是您的库是唯一包装项目的错误处理库:即使如此,您的库仍然未能完成其核心功能(捕获错误并将其记录到文件中),这是一个致命错误,因为应用程序在没有核心功能的情况下无法正常运行。与许多其他致命错误一样,最好是“快速失败,早期失败”,并将这些错误传递给PHP自己处理,通过将它们写入自己的error_log或将它们显示给最终用户(取决于error_reporting级别)。 另外,您始终可以让开发人员意识到文件或文件夹权限问题的可能性,并为开发人员提供定义在此类低级故障上执行的业务逻辑的可能性。您提供的多个catch块示例就是一种方法。传递一个可选的lambda函数以在低级故障上执行也是另一种方法。这里有很多选择。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接