了解PHP错误机制
注册PHP的异常处理函数、错误处理函数、脚本退出函数
配置PHP预加载(重要)
搭建日志中心(ELK,ElasticSearch + Logstash + Kibana)
基于 ElasticSearch RESTful 实现告警
要达到的目的?
所有PHP程序里的错误或潜在错误(支持自定义)都全部写入日志并作警,包含错误的所在文件名、行号、函数(如果有)、错误信息等等
PHP程序不需要做任何修改或接入!
一、了解PHP错误机制
截止PHP7.1,PHP共有16个错误级别:
http://php.net/manual/zh/errorfunc.constants.php
如果你对PHP错误机制还不太了解,网上有很多关于PHP错误机制的总结,因为这个不是文章的重点,这里不再详细介绍!
二、注册PHP的异常处理函数、错误处理函数、脚本退出函数
PHP有三个很重要的注册回调函数的函数
register_shutdown_function 注册PHP退出时的回调函数
set_error_handler 注册错误处理函数
set_exception_handler 注册异常处理函数
我们先定义一个日志处理类,专门用于写日志(也可以用于用户日志哦)
<?php /** * 日志接口 * @filename Loger.php * @since 2016-12-08 12:13:50 * @author 979137.com * @version $Id$ */ class Loger { // 日志目录,建议独立于Web目录,这个目录将作用于 Logstash 日志收集 protected static $log_path = '/data/logs/'; // 日志类型 protected static $type = array('ERROR', 'INFO', 'WARN', 'CRITICAL'); /** * 写入日志信息 * @param mixed $_msg 调试信息 * @param string $_type 信息类型 * @param string $file_prefix 日志文件名默认取当前日期,可以通过文件名前缀区分不同的业务 * @param array $trace TRACE信息,如果为空,则会从debug_backtrace里获取 * @return bool */ public static function write($_msg, $_type = 'info', $file_prefix = '', $trace = array()) { $_type = strtoupper($_type); $_msg = is_string($_msg) ? $_msg : var_export($_msg, true); if (!in_array($_type, self::$type)) { return false; } $server = isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : '0.0.0.0'; $remote = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0'; $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'CLI'; if (!is_array($trace) || empty($trace)) { $dtrace = debug_backtrace(); $trace = $dtrace[0]; if (count($dtrace) == 1) { //不是在类或函数内调用 $trace['function'] = ''; } else { if ($dtrace[1]['function'] == '__callStatic') { $trace['file'] = $dtrace[2]['file']; $trace['line'] = $dtrace[2]['line']; $trace['function'] = empty($dtrace[3]['function']) ? '' : $dtrace[3]['function']; } else { $trace['function'] = $dtrace[1]['function']; } } } $ace = $trace; $now = date('Y-m-d H:i:s'); $pre = "[{$now}][%s][{$ace['file']}][{$ace['line']}][{$ace['function']}][{$remote}][{$method}][{$server}]%s"; $msg = sprintf($pre, $_type, $_msg); $filename = 'phplog_' . ($file_prefix ?: 'netbar') . '_' . date('Ymd') . '.log'; $destination = self::$log_path . $filename; is_dir(self::$log_path) || mkdir(self::$log_path, 0777, true); //文件不存在,则创建文件并加入可写权限 if (!file_exists($destination)) { touch($destination); chmod($destination, 0777); } return error_log($msg.PHP_EOL, 3, $destination) ?: false; } /** * 静态魔术调用 * @param $method * @param $args * @return mixed * * @method void error($msg) static * @method void info($msg) static * @method void warn($msg) static */ public static function __callStatic($method, $args) { $method = strtoupper($method); if (in_array($method, self::$type)) { $_msg = array_shift($args); return self::write($_msg, $method); } return false; } }
接下来,再写一个系统处理类,定义回调函数和注册回调函数
<?php /** * 注册系统处理函数 * @filename Handler.php * @since 2016-12-08 12:13:50 * @author 979137.com * @version $Id$ */ class Handler { const LOG_FILE_PREFIX = 'handler'; const LOG_TYPE = 'CRITICAL'; /** * 函数注册 * @return none */ static public function set() { //注册致命错误处理方法 register_shutdown_function(array(__CLASS__, 'fatalError')); //注册自定义错误处理方法 set_error_handler(array(__CLASS__, 'appError')); //注册异常处理方法 set_exception_handler(array(__CLASS__, 'appException')); } /** * 致命错误捕获,PHP错误级别预定义常量参考: * http://php.net/manual/zh/errorfunc.constants.php * @return none */ static public function fatalError() { $error = error_get_last() ?: null; if (!is_null($error) && in_array($error['type'], array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR))) { $error['class'] = $error['function'] = ''; Loger::write($error['message'], self::LOG_TYPE, self::LOG_FILE_PREFIX, $error); self::halt($error); } } /** * 自定义错误处理 * @param int $errno 错误类型 * @param string $errstr 错误信息 * @param string $errfile 错误文件 * @param int $errline 错误行数 * @return void */ static public function appError($errno, $errstr, $errfile, $errline) { $error['message'] = "[$errno] $errstr"; $error['file'] = $errfile; $error['line'] = $errline; $error['class'] = $error['function'] = ''; if (!in_array($errno, array(E_STRICT, E_DEPRECATED))) { Loger::write($error['message'], self::LOG_TYPE, self::LOG_FILE_PREFIX, $error); if (in_array($errno, array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR))) { self::halt($error); } } } /** * 自定义异常处理 * @param mixed $e 异常对象 * @return void */ static public function appException($e) { $error = array(); $error['message'] = $e->getMessage(); $error['file'] = $e->getFile(); $error['line'] = $e->getLine(); $trace = $e->getTrace(); if(empty($trace[0]['function']) && $trace[0]['function'] == 'exception') { $error['file'] = $trace[0]['file']; $error['line'] = $trace[0]['line']; } //$error['trace'] = $e->getTraceAsString(); $error['function'] = $error['class'] = ''; Loger::write($error['message'], self::LOG_TYPE, self::LOG_FILE_PREFIX, $error); self::halt($error); } /** * 错误输出 * @param mixed $error 错误 * @return void */ static public function halt($error) { ob_get_contents() && ob_end_clean(); $e = array(); if (IS_DEBUG || IS_CLI) { //调试模式下输出错误信息 $e = $error; if(IS_CLI){ $e_message = $e['message'].' in '.$e['file'].' on line '.$e['line'].PHP_EOL; if (isset($e['treace']) ) { $e_message .= $e['trace']; } exit($e_message); } } else { //线网不显示错误信息,显示固定字符串,保护系统安全 //TODO:比较友好的做法是重定向到一个漂亮的错误页面 exit('Sorry, the system error'); } // 包含异常页面模板 $exceptionFile = __DIR__ . '/exception.tpl'; include $exceptionFile; exit(0); } }
二、自动加载脚本(auto_append_file)
以上两个脚本准备就绪以后,我们可以把它们合并到一个文件,并增加两个重要常量:
//是否CLI模式 define('IS_CLI', PHP_SAPI == 'cli' ? true : false); //当前是否开发模式,用于区分线网和开发模式,默认false //开发模式下,所有错误会打印出来。非开发模式下,不会打印到页面,但会记录日志 define('IS_DEBUG', isset($_SERVER['DEV_ENV']) ? true : false); //注册系统处理函数 Handler::set();
假设合并后的文件名叫:auto_prepend_file.php
我们把这个文件进行预加载(即自动包含进所有PHP脚本)
这时候到了很重要的一步就是,就是配置 php.ini
auto_prepend_file = /your_path/auto_prepend_file.php
重启你的Web服务器,让配置生效!
写一个测试脚本 test.php
<?php var_dump($tencent);
因为 $tencent 未定义,所以这时候就会回调我们注册的函数,可以看到已经有错误日志了
[root@TENCENT64 /data/logs]# php -f test.php [root@TENCENT64 /data/logs]# tail -f phplog_handler_20161209.log [2016-12-09 16:01:05][CRITICAL][/data/logs/test.php][2][][0.0.0.0][CLI][0.0.0.0][8] Undefined variable: tencent