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

时间:2019-07-02 09:29:48

标签: php exception logging try-catch

我正在一个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();
    }
}

5 个答案:

答案 0 :(得分: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);

答案 1 :(得分:0)

我认为在这种情况下,您应该写到磁盘上。然后,您编写一个从该文件读取的函数,并且基本上完成记录器的工作。

答案 2 :(得分:0)

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

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

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

答案 3 :(得分:0)

如果我是你,我会在“ kernel.exception”事件中捕获该异常,原因很简单:您的记录器为E V E R Y W H E RE。 只需编写一个侦听器,测试

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

如果您也使用命令,请对“ console.exception”事件执行相同的操作。

答案 4 :(得分:0)

没有完美的解决方案。根据您的问题,我知道您正在创建一个用于异常处理的库,该库包装了整个应用程序并产生了一些副作用(例如,写入日志文件)。问题是:该库如何处理这些副作用(库的部分核心功能)失败的情况?

我认为,最简单,最直观的答案是:不要处理。就像您的图书馆不存在一样。

换句话说,重新抛出您捕获但无法处理的所有异常。不要以为您的库是应用程序的唯一异常处理程序。您可能还有另一个日志记录库/摘要,它可以捕获异常并以与您不同的方式处理异常(例如,通过电子邮件发送异常,而不是写入文件)。给其他玩家一个处理异常的机会。

假设基本情况是您的库是包装项目的唯一错误处理库:您的库仍然无法执行其核心功能(捕获错误并将其记录到文件中),这是致命错误,因为应用程序没有核心功能就无法运作。像许多其他致命错误一样,最好“快速失败,尽早失败”,并将这些错误传递给PHP自行处理,方法是将它们写入自己的error_log或将其显示给最终用户(具体取决于错误报告级别)。

或者,您始终可以使开发人员意识到文件或文件夹权限问题的可能性,并使开发人员可以定义在此类较低级别的故障上执行的业务逻辑。您提供的多个catch块示例是执行此操作的一种方法。传递可选的lambda函数以在低级别故障时执行。这里有很多选择。