攔截PHP各種異常和錯誤,發生致命錯誤時進行報警,萬事防患於未然

傑克.陳發表於2016-01-19
原文:攔截PHP各種異常和錯誤,發生致命錯誤時進行報警,萬事防患於未然

在日常開發中,大多數人的做法是在開發環境時開啟除錯模式,在產品環境關閉除錯模式。在開發的時候可以檢視各種錯誤、異常,但是線上上就把錯誤顯示的關閉。

上面的情形看似很科學,有人解釋為這樣很安全,別人看不到錯誤,以免洩露重要資訊…

但是你有沒有遇到這種情況,線下好好的,一上線卻執行不起來也找不到原因…

一個指令碼,跑了好長一段時間,一直沒有問題,有一天突然中斷了,然後了也沒有任何記錄都不造啥原因…

線上一個付款,別人明明付了款,但是我們卻沒有記錄到,自己親自去實驗,卻是好的…

 

種種以上,都是因為大家關閉了錯誤資訊,並且未將錯誤、異常記錄到日誌,導致那些隨機發生的錯誤很難追蹤。這樣矛盾就來了,即不要顯示錯誤,又要追蹤錯誤,這如何實現了?

以上問題都可以通過PHP的錯誤、異常機制及其內建函式`set_exception_handler`,`set_error_handler`,`register_shutdown_function` 來實現

 

`set_exception_handler` 函式 用於攔截各種未捕獲的異常,然後將這些交給使用者自定義的方式進行處理

`set_error_handler` 函式可以攔截各種錯誤,然後交給使用者自定義的方式進行處理

`register_shutdown_function` 函式是在PHP指令碼結束時呼叫的函式,配合`error_get_last`可以獲取最後的致命性錯誤

 

這個思路大體就是把錯誤、異常、致命性錯誤攔截下來,交給我們自定義的方法進行處理,我們辨別這些錯誤、異常是否致命,如果是則記錄的資料庫或者檔案系統,然後使用指令碼不停的掃描這些日誌,發現嚴重錯誤立即傳送郵件或傳送簡訊進行報警

 

首先我們定義錯誤攔截類,該類用於將錯誤、異常攔截下來,用我們自己定義的處理方式進行處理,該類放在檔名為`errorHandler.class.php`中,程式碼如下

/**
 * 檔名稱:baseErrorHandler.class.php
 * 摘    要:錯誤攔截器父類
 */
require `errorHandlerException.class.php`;//異常類
class errorHandler
{
    public $argvs = array();

    public     $memoryReserveSize = 262144;//備用記憶體大小

    private $_memoryReserve;//備用記憶體

    /**
     * 方      法:註冊自定義錯誤、異常攔截器
     * 參      數:void
     * 返      回:void
     */
    public function register()
    {
        ini_set(`display_errors`, 0);

        set_exception_handler(array($this, `handleException`));//截獲未捕獲的異常

        set_error_handler(array($this, `handleError`));//截獲各種錯誤 此處切不可掉換位置

        //留下備用記憶體 供後面攔截致命錯誤使用
        $this->memoryReserveSize > 0 && $this->_memoryReserve = str_repeat(`x`, $this->memoryReserveSize);

        register_shutdown_function(array($this, `handleFatalError`));//截獲致命性錯誤
    }

    /**
     * 方      法:取消自定義錯誤、異常攔截器
     * 參      數:void
     * 返      回:void
     */
    public function unregister()
    {
        restore_error_handler();
        restore_exception_handler();
    }

    /**
     * 方      法:處理截獲的未捕獲的異常
     * 參      數:Exception $exception
     * 返      回:void
     */
    public function handleException($exception)
    {
        $this->unregister();
        try
        {
            $this->logException($exception);
            exit(1);
        }
        catch(Exception $e)
        {
            exit(1);
        }
    }

    /**
     * 方      法:處理截獲的錯誤
     * 參      數:int     $code 錯誤程式碼
     * 參      數:string $message 錯誤資訊
     * 參      數:string $file 錯誤檔案
     * 參      數:int     $line 錯誤的行數
     * 返      回:boolean
     */
    public function handleError($code, $message, $file, $line)
    {
        //該處思想是將錯誤變成異常丟擲 統一交給異常處理函式進行處理
        if((error_reporting() & $code) && !in_array($code, array(E_NOTICE, E_WARNING, E_USER_NOTICE, E_USER_WARNING, E_DEPRECATED)))
        {//此處只記錄嚴重的錯誤 對於各種WARNING NOTICE不作處理
            $exception = new errorHandlerException($message, $code, $code, $file, $line);
            $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
            array_shift($trace);//trace的第一個元素為當前物件 移除
            foreach($trace as $frame) 
            {
                if($frame[`function`] == `__toString`) 
                {//如果錯誤出現在 __toString 方法中 不丟擲任何異常
                    $this->handleException($exception);
                    exit(1);
                }
            }
            throw $exception;
        }
        return false;
    }

    /**
     * 方      法:截獲致命性錯誤
     * 參      數:void
     * 返      回:void
     */
    public function handleFatalError()
    {
        unset($this->_memoryReserve);//釋放記憶體供下面處理程式使用

        $error = error_get_last();//最後一條錯誤資訊
        if(errorHandlerException::isFatalError($error))
        {//如果是致命錯誤進行處理
            $exception = new errorHandlerException($error[`message`], $error[`type`], $error[`type`], $error[`file`], $error[`line`]);
            $this->logException($exception);
            exit(1);
        }
    }

    /**
     * 方      法:獲取伺服器IP
     * 參      數:void
     * 返      回:string
     */
    final public function getServerIp()
    {
        $serverIp = ``;
        if(isset($_SERVER[`SERVER_ADDR`]))
        {
            $serverIp = $_SERVER[`SERVER_ADDR`];
        }
        elseif(isset($_SERVER[`LOCAL_ADDR`]))
        {
            $serverIp = $_SERVER[`LOCAL_ADDR`];
        }
        elseif(isset($_SERVER[`HOSTNAME`]))
        {
            $serverIp = gethostbyname($_SERVER[`HOSTNAME`]);
        }
        else
        {
            $serverIp = getenv(`SERVER_ADDR`);
        }        
        
        return $serverIp; 
    }

    /**
     * 方      法:獲取當前URI資訊
     * 參      數:void
     * 返      回:string $url
     */
    public function getCurrentUri()
    {
        $uri = ``;
        if($_SERVER ["REMOTE_ADDR"])
        {//瀏覽器瀏覽模式
            $uri = `http://` . $_SERVER[`SERVER_NAME`] . $_SERVER[`REQUEST_URI`];
        }
        else
        {//命令列模式
            $params = $this->argvs;
            $uri = $params[0];
            array_shift($params);
            for($i = 0, $len = count($params); $i < $len; $i++)
            {
                $uri .= ` ` . $params[$i];
            }
        }
        return $uri;
    }

    /**
     * 方      法:記錄異常資訊
     * 參      數:errorHandlerException $e 錯誤異常
     * 返      回:boolean 是否儲存成功
     */
    final public function logException($e)
    {
        $error = array(
                        `add_time`     =>     time(),
                        `title`     =>     errorHandlerException::getName($e->getCode()),//這裡獲取使用者友好型名稱
                        `message`     =>     array(),
                        `server_ip` =>     $this->getServerIp(),
                        `code`         =>     errorHandlerException::getLocalCode($e->getCode()),//這裡為各種錯誤定義一個編號以便查詢
                        `file`         =>  $e->getFile(),
                        `line`         =>     $e->getLine(),
                        `url`        =>  $this->getCurrentUri(),
                    );
        do
        {
            //$e->getFile() . `:` . $e->getLine() . ` ` . $e->getMessage() . `(` . $e->getCode() . `)`
            $message = (string)$e;
            $error[`message`][] = $message;
        } while($e = $e->getPrevious());
        $error[`message`] = implode("
", $error[`message`]);
        $this->logError($error);
    }

    /**
     * 方      法:記錄異常資訊
     * 參      數:array $error = array(
     *                                    `time` => int, 
     *                                    `title` => `string`, 
     *                                    `message` => `string`, 
     *                                    `code` => int,
     *                                    `server_ip` => `string`
     *                                     `file`     =>  `string`,
     *                                    `line` => int,
     *                                    `url` => `string`,
     *                                );
     * 返      回:boolean 是否儲存成功
     */
    public function logError($error)
    {
        /*這裡去實現如何將錯誤資訊記錄到日誌*/
    }
}

上述程式碼中,有個`errorHandlerException`類,該類放在檔案`errorHandlerException.class.php`中,該類用於將錯誤轉換為異常,以便記錄錯誤發生的檔案、行號、錯誤程式碼、錯誤資訊等資訊,同時其方法`isFatalError`用於辨別該錯誤是否是致命性錯誤。這裡我們為了方便管理,將錯誤進行編號並命名。該類的程式碼如下

/**
 * 檔名稱:errorHandlerException.class.php
 * 摘    要:自定義錯誤異常類 該類繼承至PHP內建的錯誤異常類
 */
class errorHandlerException extends ErrorException
{
    public static $localCode = array(
                                        E_COMPILE_ERROR => 4001,
                                        E_COMPILE_WARNING => 4002,
                                        E_CORE_ERROR => 4003,
                                        E_CORE_WARNING => 4004,
                                        E_DEPRECATED => 4005,
                                        E_ERROR => 4006,
                                        E_NOTICE => 4007,
                                        E_PARSE => 4008,
                                        E_RECOVERABLE_ERROR => 4009,
                                        E_STRICT => 4010,
                                        E_USER_DEPRECATED => 4011,
                                        E_USER_ERROR => 4012,
                                        E_USER_NOTICE => 4013,
                                        E_USER_WARNING => 4014,
                                        E_WARNING => 4015,
                                        4016 => 4016,
                                    );

    public static $localName = array(
                                        E_COMPILE_ERROR => `PHP Compile Error`,
                                        E_COMPILE_WARNING => `PHP Compile Warning`,
                                        E_CORE_ERROR => `PHP Core Error`,
                                        E_CORE_WARNING => `PHP Core Warning`,
                                        E_DEPRECATED => `PHP Deprecated Warning`,
                                        E_ERROR => `PHP Fatal Error`,
                                        E_NOTICE => `PHP Notice`,
                                        E_PARSE => `PHP Parse Error`,
                                        E_RECOVERABLE_ERROR => `PHP Recoverable Error`,
                                        E_STRICT => `PHP Strict Warning`,
                                        E_USER_DEPRECATED => `PHP User Deprecated Warning`,
                                        E_USER_ERROR => `PHP User Error`,
                                        E_USER_NOTICE => `PHP User Notice`,
                                        E_USER_WARNING => `PHP User Warning`,
                                        E_WARNING => `PHP Warning`,
                                        4016 => `Customer`s Error`,
                                    );

    /**
     * 方      法:建構函式
     * 摘      要:相關知識請檢視 http://php.net/manual/en/errorexception.construct.php
     *   
     * 參      數:string        $message     異常資訊(可選)
     *              int         $code         異常程式碼(可選)
     *              int         $severity
     *              string     $filename     異常檔案(可選)
     *              int         $line         異常的行數(可選)
     *           Exception  $previous   上一個異常(可選)
     *
     * 返      回:void
     */
    public function __construct($message = ``, $code = 0, $severity = 1, $filename = __FILE__, $line = __LINE__, Exception $previous = null)
    {
        parent::__construct($message, $code, $severity, $filename, $line, $previous);
    }

    /**
     * 方      法:是否是致命性錯誤
     * 參      數:array $error
     * 返      回:boolean
     */
    public static function isFatalError($error)
    {
        $fatalErrors = array(
                                E_ERROR, 
                                E_PARSE, 
                                E_CORE_ERROR,
                                E_CORE_WARNING, 
                                E_COMPILE_ERROR, 
                                E_COMPILE_WARNING
                            );
        return isset($error[`type`]) && in_array($error[`type`], $fatalErrors);
    }

    /**
     * 方      法:根據原始的錯誤程式碼得到本地的錯誤程式碼
     * 參      數:int $code
     * 返      回:int $localCode
     */
    public static function getLocalCode($code)
    {
        return isset(self::$localCode[$code]) ? self::$localCode[$code] : self::$localCode[4016];
    }

    /**
     * 方      法:根據原始的錯誤程式碼獲取使用者友好型名稱
     * 參      數:int 
     * 返      回:string $name
     */
    public static function getName($code)
    {
        return isset(self::$localName[$code]) ? self::$localName[$code] : self::$localName[4016];
    }

在錯誤攔截類中,需要使用者自己定義實現錯誤記錄的方法(`logException`),這個地方需要注意,有些錯誤可能在一段時間內不斷髮生,因此只需記錄一次即可,你可以使用錯誤程式碼、檔案、行號、錯誤詳情 生成一個MD5值用於記錄該錯誤是否已經被記錄,如果在規定時間內(一個小時)已經被記錄過則不需要再進行記錄

 

然後我們定義一個檔案,用於例項化以上類,捕獲各種錯誤、異常,該檔案命名為`registerErrorHandler.php`, 內如如下

/*
* 使用方法介紹:
* 在入口處引入該檔案即可,然後可以在該檔案中定義除錯模式常量`DEBUG_ERROR`
*
* <?php
*   
*    require `registerErrorHandler.php`;
*   
* ?>
*/

/**
* 除錯錯誤模式:
* 0                =>            非除錯模式,不顯示異常、錯誤資訊但記錄異常、錯誤資訊
* 1                =>            除錯模式,顯示異常、錯誤資訊但不記錄異常、錯誤資訊
*/
define(`DEBUG_ERROR`, 0);
require `errorHandler.class.php`;

class registerErrorHandler
{
    /**
     * 方      法:註冊異常、錯誤攔截
     * 參      數:void
     * 返      回:void
     */
    public static function register()
    {
        global $argv;
        if(DEBUG_ERROR)
        {//如果開啟除錯模式
            ini_set(`display_errors`, 1);
            return;
        }

        //如果不開啟除錯模式
        ini_set(`error_reporting`, -1);
        ini_set(`display_errors`, 0);
        $handler = new errorHandler();
        $handler->argvs = $argv;//此處主要相容命令列模式下獲取引數
        $handler->register();
    }    
}
registerErrorHandler::register();

剩下的就是需要你在你的入口檔案引入該檔案,定義除錯模式,然後實現你自己記錄錯誤的方法即可

需要注意的是,有些錯誤在你進行註冊之前已經發生並且導致指令碼中斷是無法記錄下來的,因為此時`registerErrorHandler::register()` 尚未執行已經中斷了

還有就是`set_error_handler`這個函式不能捕獲下面型別的錯誤 E_ERROR、 E_PARSE、 E_CORE_ERROR、 E_CORE_WARNINGE_COMPILE_ERROR、 E_COMPILE_WARNING, 這個可以在官方文件中看到,但是本處無妨,因為以上錯誤是解析、編譯錯誤,這些都沒有通過,你是不可能釋出上線的

 

以上程式碼經過嚴格測試,並且已經應用線上上環境,大家可以根據自己需要進行更改使用

 


相關文章