以平稳的方式logging错误
我一直在阅读特别是“错误logging”我已经拿出了函数“error_log”这似乎是一个很好的工具来处理错误logging。 但如何使用它最顺利和最好的方式?
如果我有一个
try { //try a database connection... } catch (PDOException $e) { error_log($e->getMessage(), 3, "/var/tmp/my-errors.log"); }
这会在my-errors.log文件中logging错误。 但是,如果我有时需要改变文件的位置,一个新的文件夹,或其他东西。 如果我有大量的文件,我需要将它们全部更改。
现在我开始考虑使用一个variables来设置错误日志的path。 当然可以工作,但是如果我想在函数或类方法中使用error_log呢? 那么我需要将variables设置为全局variables,但这被认为是不好的做法! 但是如果我不能在课堂上深入使用这个function,那么这也不算是不好的做法吗? 这里有什么好的解决scheme?
<?php function legit() { try { if (1 == 1) { throw new Exception('There was an error here'); } } catch (Exception $e) { throw new Exception('throw the error to the try-catch outside the function...'); } } try { legit(); } catch (Exception $e) { echo 'error here' . $e->getMessage(); //log it }
这是我刚才在谈论的一个例子(没有在类/函数中深入logging…这是一个好方法吗?)
进一步:
我不太清楚我应该如何使用一般的例外。 比方说,我想在一个方法内使用SQL执行一个INSERT数据库,我会使用一个try / catch,然后重新抛出exception,如果它失败? 这被认为是良好的做法? 请举例。
首先,我想推荐您查看PHP中的标准错误方法。 不幸的是, error_log
有一些限制,因为你发现了。
这是一个很长的答案,阅读了解有关:
- 错误
- 直接logging错误与
trigger_error
和set_error_handler
- 好错误在哪里坏 – 致命的错误。
- 直接logging错误与
- 例外
- SPL
- 该怎么办?
- 码
- build立
- 用法
TL; DR使用trigger_error
来引发错误,使用set_error_handler
来logging它们。
1.错误
如果程序中的事情不像预期的那样发生,您通常会想要提出错误,以便通知某人或某事。 错误是程序可能继续的情况,但发生了一些值得注意的,可能有害的或错误的事情。 在这一点上,许多人希望立即用他们的日志包selectlogging错误。 我相信这是完全错误的事情。 我build议使用trigger_error
来引发错误,以便使用set_error_handler
设置的callback进行处理。 让我们比较这些选项:
直接logging错误
所以,你已经select了你的日志包。 现在,您已准备好将代码中发生错误的地方传送到logging器。 让我们看看你可能做的一个单独的调用(我将使用一个类似的logging器,以杰克的答案):
Logger::getLogger('standard')->error('Ouch, this hurts');
你需要什么来运行这个代码?
类 :logging器 方法 :getLogger 返回 :方法“错误”的对象
这些是使用此代码所需的依赖关系。 每个想要重新使用这个代码的人都必须提供这些依赖关系。 这意味着标准的PHPconfiguration将不再足以重用您的代码。 最好的情况下,使用dependency injection,你仍然需要一个logging器对象被传递到你的所有代码,可以发出错误。
另外,除了代码负责之外,还有logging错误的责任。 这违背了单一责任原则 。
我们可以看到, 直接logging错误是不好的 。
trigger_error来救援
PHP有一个名为trigger_error
的函数,它可以像标准函数一样引发一个错误。 您使用的错误级别是在错误级别常量中定义的。 作为用户,您必须使用其中一个用户错误: E_USER_ERROR
, E_USER_WARNING
或默认值E_USER_NOTICE
(其他错误级别保留用于标准function等)。 使用标准的PHP函数来提高错误,可以使代码重新用于任何标准的PHP安装。 我们的代码不再负责logging错误(只有确保它被提出)。
使用trigger_error
我们只执行错误logging过程的一半(引发错误),并保存error handling程序错误的责任,这将在下面介绍。
error handling程序
我们用set_error_handler
函数设置一个自定义的error handling程序(参见代码设置)。 此自定义error handling程序取代了通常根据PHPconfiguration设置在Web服务器错误日志中logging消息的标准PHPerror handling程序。 我们仍然可以在我们的自定义error handling程序中使用这个标准的error handling程
自定义error handling程序有一个单独的责任:对错误作出响应(包括您想要执行的任何日志logging)。 在自定义error handling程序中,您可以完全访问系统,并可以运行任何您想要的日志logging。 实际上,使用Observerdevise模式的任何logging器都可以(我不打算进去,因为我认为它是次要的)。 这应该允许你挂钩新的日志观察者发送输出到你需要的地方。
您可以完全控制自己喜欢的代码中单个可维护部分的错误。 现在,错误日志可以快速轻松地从项目更改为项目或单个项目内的页面之间更改。 有趣的是,即使@
被压制的错误也使得error handling器的errno
为0,如果错误报告掩码被尊重的话不应该被报告。
好的错误不好 – 致命的错误
从某些错误继续是不可能的。 以下错误级别不能由自定义error handling程序处理: E_ERROR
, E_PARSE
, E_CORE_ERROR
, E_CORE_WARNING
, E_COMPILE_ERROR
, E_COMPILE_WARNING
。 当标准函数调用触发这些types的错误时,跳过自定义error handling程序并closures系统。 这可以通过以下方式产生:
call_this_function_that_obviously_does_not_exist_or_was_misspelt();
这是一个严重的错误! 这是不可能恢复的,系统即将closures。 我们唯一的select是有一个register_shutdown_function
处理关机。 然而,只要脚本完成(成功,也是不成功),就会执行这个函数。 使用这个和error_get_last
当最后一个错误是一个致命的错误时,可以logging一些基本的信息(系统几乎closures)。 发送正确的状态码并显示您select的内部服务器错误types页面也很有用。
2.例外
例外可以用与基本错误非常类似的方式来处理。 代替trigger_error
,代码将抛出exception(手动throw new Exception
或从标准函数调用中)。 使用set_exception_handler
来定义要用来处理exception的callbackset_exception_handler
。
SPL
标准PHP库(SPL)提供了例外 。 他们是我提出的exception的首选方式,因为像trigger_error
他们是PHP的标准的一部分,不会引入额外的依赖到您的代码。
该怎么办?
当抛出exception时,可以做三个select:
- 赶上并修复它(代码然后继续,如果没有什么不幸发生)。
- 抓住它,追加有用的信息,并重新扔它。
- 让它起泡到更高的水平。
在堆栈的每一层都做出这些select。 最终,一旦它达到最高级别, set_exception_handler
设置的callback将被执行。 这是您的日志代码所属的地方(出于与error handling相同的原因),而不是遍及代码中的catch
语句。
3.代码
build立
error handling程序
function errorHandler($errno , $errstr, $errfile, $errline, $errcontext) { // Perform your error handling here, respecting error_reporting() and // $errno. This is where you can log the errors. The choice of logger // that you use is based on your preference. So long as it implements // the observer pattern you will be able to easily add logging for any // type of output you desire. } $previousErrorHandler = set_error_handler('errorHandler');
exception处理程序
function exceptionHandler($e) { // Perform your exception handling here. } $previousExceptionHandler = set_exception_handler('exceptionHandler');
关机function
function shutdownFunction() { $err = error_get_last(); if (!isset($err)) { return; } $handledErrorTypes = array( E_USER_ERROR => 'USER ERROR', E_ERROR => 'ERROR', E_PARSE => 'PARSE', E_CORE_ERROR => 'CORE_ERROR', E_CORE_WARNING => 'CORE_WARNING', E_COMPILE_ERROR => 'COMPILE_ERROR', E_COMPILE_WARNING => 'COMPILE_WARNING'); // If our last error wasn't fatal then this must be a normal shutdown. if (!isset($handledErrorTypes[$err['type']])) { return; } if (!headers_sent()) { header('HTTP/1.1 500 Internal Server Error'); } // Perform simple logging here. } register_shutdown_function('shutdownFunction');
用法
错误
// Notices. trigger_error('Disk space is below 20%.', E_USER_NOTICE); trigger_error('Disk space is below 20%.'); // Defaults to E_USER_NOTICE // Warnings. fopen('BAD_ARGS'); // E_WARNING fopen() expects at least 2 parameters, 1 given trigger_error('Warning, this mode could be dangerous', E_USER_WARNING); // Fatal Errors. // This function has not been defined and so a fatal error is generated that // does not reach the custom error handler. this_function_has_not_been_defined(); // Execution does not reach this point. // The following will be received by the custom error handler but is fatal. trigger_error('Error in the code, cannot continue.', E_USER_ERROR); // Execution does not reach this point.
例外
以前的三个select中的每一个都以一般方式列在这里,修正它,附加到它并让它冒泡。
1可修复:
try { $value = code_that_can_generate_exception(); } catch (Exception $e) { // We decide to emit a notice here (a warning could also be used). trigger_error('We had to use the default value instead of ' . 'code_that_can_generate_exception\'s', E_USER_NOTICE); // Fix the exception. $value = DEFAULT_VALUE; } // Code continues executing happily here.
2追加:
请注意下面code_that_can_generate_exception()
不知道$context
。 在这个级别的catch块有更多的信息,它可以附加到例外,如果它通过重新抛出它是有用的。
try { $context = 'foo'; $value = code_that_can_generate_exception(); } catch (Exception $e) { // Raise another exception, with extra information and the existing // exception set as the previous exception. throw new Exception('Context: ' . $context, 0, $e); }
3让它起泡:
// Don't catch it.
它被要求使这个答案更适用于更多的观众,所以在这里。
前言
在编写应用程序时,error handling通常不是您想要考虑的第一件事情; 作为一个间接的结果,随着需求的增加, 但是,在PHP中利用现有机制也不必花费太多。
这是一篇相当长的文章,所以我把它分解成了逻辑上的文本集。
触发错误
在PHP中,有两种不同的错误触发方式:
- PHP本身的错误(例如使用未定义的variables)或内部函数(例如
imagecreatefromjpeg
无法打开文件), - 用户代码触发的错误使用
trigger_error
,
这些通常是打印在你的页面上(除非display_errors
被closures,或者error_reporting
为零),除非你像我这样编写完美的代码,否则这应该是生产机器的标准。 这些错误也可以被捕获,通过使用稍后解释的set_error_handler
,让您了解代码中的任何问题。
抛出exception
例外情况与三种主要方式中的错误不同:
- 处理它们的代码可能远离它们被抛出的地方。 原点处的variables状态必须显式传递给Exception构造函数,否则只有堆栈跟踪。
- exception和catch之间的代码完全被跳过,而在发生错误(并且不是致命的)之后,代码仍然继续。
- 它们可以从主
Exception
类扩展; 这可以让你捕捉和处理特定的exception,但是让其他人在堆栈中冒泡,直到被其他代码抓住。 另见: http : //www.php.net/manual/en/language.exceptions.php
抛出exception的例子在稍后给出。
处理错误
通过注册error handling程序来捕获和处理错误非常简单,例如:
function my_error_handler($errno, $errstr, $errfile = 'unknown', $errline = 0, array $errcontext = array()) { // $errcontext is very powerful, it gives you the variable state at the point of error; this can be a pretty big variable in certain cases, but it may be extremely valuable for debugging // if error_reporting() returns 0, it means the error control operator was used (@) printf("%s [%d] occurred in %s:%d\n%s\n", $errstr, $errno, $errfile, $errline, print_r($errcontext, true)); // if necessary, you can retrieve the stack trace that led up to the error by calling debug_backtrace() // if you return false here, the standard PHP error reporting is performed } set_error_handler('my_error_handler');
对于踢腿,您可以通过注册以下error handling程序(PHP> = 5.1)将所有错误转换为ErrorException
:
function exception_error_handler($errno, $errstr, $errfile, $errline) { throw new ErrorException($errstr, $errno, 0, $errfile, $errline); } set_error_handler("exception_error_handler");
处理exception
在大多数情况下,您尽可能接近导致允许备份计划的代码处理exception。 例如,您尝试插入数据库logging并引发主键约束exception; 你可以通过更新logging来恢复(由于大多数数据库可以自行处理)。 有些例外情况不能在本地处理,所以您希望这些例外级联。 例:
function insertRecord($user, $name) { try { if (true) { throw new Exception('This exception should not be handled here'); } // this code is not executed $this->db->insert('users', array('uid' => $user, 'name' => $name)); } catch (PDOException $e) { // attempt to fix; an exception thrown here will cascade down throw $e; // rethrow exception // since PHP 5.3.0 you can also nest exceptions throw new Exception("Could not insert '$name'", -1, $e); } catch (WhatEverException $e) { // guess what, we can handle whatever too } }
滑的例外
那么当你不在任何地方发现exception时会发生什么呢? 你也可以使用set_exception_handler
来捕获它。
function my_exception_handler(Exception $exception) { // do your stuff here, just don't throw another exception here } set_exception_handler('my_exception_handler');
这是不鼓励,除非你没有有意义的方式来处理代码中的任何地方的exception。
logging错误/exception
现在你正在处理错误,你必须logging在某个地方。 对于我的例子,我使用Apache从PHP移植到PHP的项目,称为LOG4PHP 。 还有其他一些,但它说明了一个灵活的日志logging工具的重要性。
它使用以下概念:
- logging器 – 以您的名义执行logging的命名实体; 他们可以是特定于您的项目中的一个类或共享作为一个普通的logging器,
- 附加程序 – 每个日志请求可以基于预定义的条件(如日志级别)发送到一个或多个目标(电子邮件,数据库,文本文件),
- 级别 – 日志从debugging消息分类为致命错误。
说明不同消息级别的基本用法:
Logger::getLogger('main')->info('We have lift off'); Logger::getLogger('main')->warn('Rocket is a bit hot'); Logger::getLogger('main')->error('Houston, we have a problem');
使用这些概念,您可以build立一个非常强大的日志logging功 例如,在不更改上面的代码的情况下,可以执行以下设置:
- 将所有debugging消息收集到数据库中以供开发人员查看; 您可能会在生产服务器上禁用此function,
- 将警告收集到日常文件中,您可以在一天结束时发送电子邮件,
- 立即发送致命错误的电子邮件。
定义它,然后使用它:)
define('ERRORLOG_PATH', '/var/tmp/my-errors.log'); error_log($e->getMessage(), 3, ERRORLOG_PATH);
或者,只需将error_log
的第三个参数设置为可选,将其默认为所需的path。
如果你仍然需要处理日志的自定义方式(即你不想使用标准的trigger_error()),我build议看看Zend_Log(http://framework.zend.com/manual/en/zend.log .overview.html)由于这些原因:
-
这可以作为一个独立的组件,ZF不是一个完整的框架。 您只能复制Zend_Loader和Zend_Log命名空间,实例化Zend_Loader并使用它。 见下文:
require_once('Zend/Loader/Autoloader.php'); $loader = Zend_Loader_Autoloader::getInstance(); $logger = new Zend_Log(); $writer = new Zend_Log_Writer_Stream('php://output'); $logger->addWriter($writer); $logger->log('Informational message', Zend_Log::INFO);
-
您提供了许多日志logging库,但是我相信Zend团队(PHP lang的创始人)知道他们在做什么
- 您可以使用任何编写器(数据库,STDOUT – 见上文,文件,无论如何,您可以自定义编写自己的日志消息甚至发布到Web服务)
- 日志级别
-
可能会改变日志格式(但是对我来说是非常好的)。 上面的标准格式化的例子会产生这样的东西:
2012-05-07T23:57:23 + 03:00信息(6):信息性讯息
- 只读参考,它可能被configuration为捕获PHP错误
另外,对于错误日志logging(事实上所有的日志logging),我会使用事件调度器,symfony框架的方式。
看看这个SF组件(它非常轻量级的依赖,整个框架是不需要的,也许有3个相关的PHP类和2个接口)
https://github.com/symfony/EventDispatcher
这样你可以在你的应用程序引导程序的某个地方创build调度程序:
use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\Event; $dispatcher = new EventDispatcher(); //register listeners $dispatcher->addListener('application.log', function (Event $event) { //do anything you want });
然后,您可以通过类似的方式在代码的任何地方举办活动
$dispatcher->dispatch(new GenericEvent('application.log', array('message' => 'some log', 'priority' => 'high'));
当然你可以用自己的事件inheritance事件类:
class LogEvent extends GenericEvent { public function __construct($message, $priority = 'INFO') { parent::__construct('application.log', array('message'=>$message,'priority'=>$priority)); } public function getMessage() { return $this->getArgument('message'); } public function getPriority() { return $this->getArgument('priority'); } } // now raising LogEvent is much cleaner: $dispatcher->dispatch(new LogEvent('some log'));
这也将允许您创build更多自定义事件,如ExceptionEvent
class ExceptionEvent extends GenericEvent { public function __construct(Exception $cause) { parent::__construct('exception.event', array('cause' => $cause)); } }
并相应地处理它们。
优点
- 您将日志逻辑与应用程序分开
- 您可以在运行时轻松添加和删除logging器
- 您可以轻松注册所需的多个logging器(例如,将所有内容logging到文本文件中的DebugLogger,仅loggingerror_log错误的ErrorLogger,仅在生产环境中logging严重错误的CriticalLogger,并通过电子邮件发送给pipe理员等)
- 你可以使用事件调度器来进行更多的事情,而不仅仅是日志logging(事实上,对于每个适合观察者模式的工作)
- 实际的logging器只不过是“实现细节”而已 – 它很容易replace,不需要重新logging日志的位置 – 您将能够随时replace日志目标,而不必重构方法的名称或更改任何内容在代码中。
- 可以很容易地实现复杂的日志路由逻辑或全局更改日志格式(通过configurationlogging器)
- 如果您对侦听器(logging器)和调度器(通知日志事件的类)使用dependency injection,则一切变得更加灵活,
实际logging
正如有人已经说过,我会build议去开箱即用的库,如提到的Monolog,Zend_Log或log4php,可能没有理由手动编写这些东西(你最后要做的就是打破错误logging器!)
PS:将代码段视为伪代码,我没有testing它们。 详细信息可以在上述库的文档中find。
如果PHP处理错误的方式对于你来说不够灵活(例如,有时你想login到数据库,有时候是文件,有时候是其他的),你需要使用/创build一个自定义的PHP日志框架。
您可以浏览https://stackoverflow.com/questions/341154/php-logging-framework中的讨论,或者只是去顶级select,; KLogger ,一试。 我不确定,但是,如果它支持日志logging的自定义目标。 但至less,这是一个小而易读的课程,您应该可以根据自己的需要进一步扩展。
我会和Tom vand der Woerdt的日志logging解决scheme一起,最简单,最有效地满足您的需求。
至于另一个问题:
你不需要在函数内部捕获/重新抛出exception,除非你有一个特定的例外,你有一个解决scheme。
有点简单的例子:
define('ERRORLOG_PATH', '/var/tmp/my-errors.log'); function do_something($in) { if (is_good($in)) { try { return get_data($in); } catch (NoDataException $e) { // Since it's not too big a deal that nothing // was found, we just return false. return false; } } else { throw new InvalidArguementException('$in is not good'); } } function get_data($data) { if (!is_int($data)) { InvalidArguementException('No'); } $get = //do some getting. if (!$get) { throw new NoDataException('No data was found.'); } else { return $get; } } try { do_something('value'); } catch (Exception $e) { error_log($e->getMessage(), 3, ERRORLOG_PATH); die ('Something went wrong :('); }
在这里,你只能捕获NoDataException
因为你有其他的逻辑来sorting,所有其他的错误都落在了第一个catch上,并且被top catch处理,因为所有抛出的exception都必须在它们的层次结构的某个点inheritanceException
。
显然,如果你再次抛出一个Exception
(在初始try {}
或在顶部catch {}
),你的脚本将会退出一个Uncaught Exception错误,并且错误日志logging将会丢失。
如果你想要一路走下去,你也可以使用set_error_handler()
来实现一个自定义的error handling函数,并把你的日志logging放在那里。
遇到两个挑战。 首先是灵活的login不同的渠道。 在这种情况下,你应该看看例如Monolog 。
第二个挑战是把这个日志编入你的应用程序中。 Imho最好的情况是不使用明确的日志logging。 这里例如方面的方向派上用场。 一个好的样本是flow3 。
但是这个问题更多的是鸟瞰问题
我使用自己的function,允许我通过设置或更改第二个参数来写入多种types的日志文件。
通过将日志function包含在我认为对本人开发项目来说是“本地”的function库中,我可以清楚地理解您所提出的概念性问题,“正确的方法是什么”。 这样我可以考虑这些函数只是“我的”PHP核心的一部分,如date()
或time()
在这个dlog的基本版本中,我也处理数组。 当我最初使用这个来logging错误时,我最终使用它来进行其他“快速和肮脏的”短期跟踪,例如logging代码进入特定部分的时间以及用户login等。
function dlog($message,$type="php-dlog") { if(!is_array($message) ) $message=trim($message); error_log(date("m/d/Y h:i:s").":".print_r($message,true)."\n",3, "/data/web/logs/$_SERVER[HTTP_HOST]-$type.log"); }
大多数错误logging器和exceptionlogging器对于大多数人来说都是无用的,因为他们没有访问日志文件。
我更喜欢使用自定义error handling程序和自定义exception处理程序,并且在生产过程中,如果系统正在数据库上运行,则直接将错误logging到数据库。
在开发过程中,当display_errors被设置时,它们不会logging所有的错误。
作为一个方面的说明:不要让你的自定义error handling程序抛出exception! 这是一个非常糟糕的主意。 它可能会导致缓冲区处理程序和一些扩展中的错误。 另外一些像fopen()这样的核心PHP函数会导致一个警告或通知失败,应该相应地处理这些函数,并且不应该暂停应用程序。
提到error handling程序在PHP文档中抛出exception是一个音符错误。
正如KNL所说的那样,这是非常正确的,但不幸的是至今还没有文档logging,抛出exception的错误并不是PHP开发人员推荐的,有人在文档中犯了一个错误。 它确实可以导致许多扩展的错误,所以不要这样做。
这已经在irc上的#PHP上讨论过了。
“但是,错误可以通过ErrorException简单地转换为exception。 在http://php.net/manual/en/language.exceptions.php将被删除。;