// +---------------------------------------------------------------------- // $Id$ /** * 将Trace信息输出到chrome浏览器的控制器,从而不影响ajax效果和页面的布局。 * 使用前,你需要先安装 chrome log 这个插件: http://craig.is/writing/chrome-logger。 * 定义应用的tags.php文件 Application/Common/Conf/tags.php, * * array( * 'Behavior\ChromeShowPageTrace' * ) * ); * * 如果trace信息没有正常输出,请查看您的日志。 * 这是通过http headers和chrome通信,所以要保证在输出trace信息之前不能有 * headers输出,你可以在入口文件第一行加入代码 ob_start(); 或者配置output_buffering * */ namespace Behavior; use Think\Log; /** * 系统行为扩展 页面Trace显示输出 */ class ChromeShowPageTraceBehavior { protected $tracePageTabs = array('BASE'=>'基本','FILE'=>'文件','INFO'=>'流程','ERR|NOTIC'=>'错误','SQL'=>'SQL','DEBUG'=>'调试'); // 行为扩展的执行入口必须是run public function run(&$params){ if(C('SHOW_PAGE_TRACE')) $this->showTrace(); } /** * 显示页面Trace信息 * @access private */ private function showTrace() { // 系统默认显示信息 $files = get_included_files(); $info = array(); foreach ($files as $key=>$file){ $info[] = $file.' ( '.number_format(filesize($file)/1024,2).' KB )'; } $trace = array(); $base = array( '请求信息' => date('Y-m-d H:i:s',$_SERVER['REQUEST_TIME']).' '.$_SERVER['SERVER_PROTOCOL'].' '.$_SERVER['REQUEST_METHOD'].' : '.__SELF__, '运行时间' => $this->showTime(), '吞吐率' => number_format(1/G('beginTime','viewEndTime'),2).'req/s', '内存开销' => MEMORY_LIMIT_ON?number_format((memory_get_usage() - $GLOBALS['_startUseMems'])/1024,2).' kb':'不支持', '查询信息' => N('db_query').' queries '.N('db_write').' writes ', '文件加载' => count(get_included_files()), '缓存信息' => N('cache_read').' gets '.N('cache_write').' writes ', '配置加载' => count(c()), '会话信息' => 'SESSION_ID='.session_id(), ); // 读取应用定义的Trace文件 $traceFile = COMMON_PATH.'Conf/trace.php'; if(is_file($traceFile)) { $base = array_merge($base,include $traceFile); } $debug = trace(); $tabs = C('TRACE_PAGE_TABS',null,$this->tracePageTabs); foreach ($tabs as $name=>$title){ switch(strtoupper($name)) { case 'BASE':// 基本信息 $trace[$title] = $base; break; case 'FILE': // 文件信息 $trace[$title] = $info; break; default:// 调试信息 $name = strtoupper($name); if(strpos($name,'|')) {// 多组信息 $array = explode('|',$name); $result = array(); foreach($array as $name){ $result += isset($debug[$name])?$debug[$name]:array(); } $trace[$title] = $result; }else{ $trace[$title] = isset($debug[$name])?$debug[$name]:''; } } } chrome_debug('TRACE信息:'.__SELF__,'group'); //输出日志 foreach($trace as $title=>$log){ '错误'==$title?chrome_debug($title,'group'):chrome_debug($title,'groupCollapsed'); foreach($log as $i=>$logstr){ chrome_debug($i.'.'.$logstr,'log'); } chrome_debug('','groupEnd'); } chrome_debug('','groupEnd'); if($save = C('PAGE_TRACE_SAVE')) { // 保存页面Trace日志 if(is_array($save)) {// 选择选项卡保存 $tabs = C('TRACE_PAGE_TABS',null,$this->tracePageTabs); $array = array(); foreach ($save as $tab){ $array[] = $tabs[$tab]; } } $content = date('[ c ]').' '.get_client_ip().' '.$_SERVER['REQUEST_URI']."\r\n"; foreach ($trace as $key=>$val){ if(!isset($array) || in_array($key,$array)) { $content .= '[ '.$key." ]\r\n"; if(is_array($val)) { foreach ($val as $k=>$v){ $content .= (!is_numeric($k)?$k.':':'').print_r($v,true)."\r\n"; } }else{ $content .= print_r($val,true)."\r\n"; } $content .= "\r\n"; } } error_log(str_replace('
',"\r\n",$content), 3,LOG_PATH.date('y_m_d').'_trace.log'); } unset($files,$info,$base); } /** * 获取运行时间 */ private function showTime() { // 显示运行时间 G('beginTime',$GLOBALS['_beginTime']); G('viewEndTime'); // 显示详细运行时间 return G('beginTime','viewEndTime').'s ( Load:'.G('beginTime','loadTime').'s Init:'.G('loadTime','initTime').'s Exec:'.G('initTime','viewStartTime').'s Template:'.G('viewStartTime','viewEndTime').'s )'; } } if(!function_exists('chrome_debug')){ //ChromePhp 输出trace的函数 function chrome_debug($msg,$type='trace',$trace_level=1){ if('trace'==$type){ ChromePhp::groupCollapsed($msg); $traces=debug_backtrace(false); $traces=array_reverse($traces); $max=count($traces)-$trace_level; for($i=0;$i<$max;$i++){ $trace=$traces[$i]; $fun=isset($trace['class'])?$trace['class'].'::'.$trace['function']:$trace['function']; $file=isset($trace['file'])?$trace['file']:'unknown file'; $line=isset($trace['line'])?$trace['line']:'unknown line'; $trace_msg='#'.$i.' '.$fun.' called at ['.$file.':'.$line.']'; if(!empty($trace['args'])){ ChromePhp::groupCollapsed($trace_msg); ChromePhp::log($trace['args']); ChromePhp::groupEnd(); }else{ ChromePhp::log($trace_msg); } } ChromePhp::groupEnd(); }else{ if(method_exists('Behavior\ChromePhp',$type)){ //支持type trace,warn,log,error,group, groupCollapsed, groupEnd等 call_user_func(array('Behavior\ChromePhp',$type),$msg); }else{ //如果type不为trace,warn,log等,则为log的标签 call_user_func_array(array('Behavior\ChromePhp','log'),func_get_args()); } } } /** * Server Side Chrome PHP debugger class * * @package ChromePhp * @author Craig Campbell */ class ChromePhp{ /** * @var string */ const VERSION = '4.1.0'; /** * @var string */ const HEADER_NAME = 'X-ChromeLogger-Data'; /** * @var string */ const BACKTRACE_LEVEL = 'backtrace_level'; /** * @var string */ const LOG = 'log'; /** * @var string */ const WARN = 'warn'; /** * @var string */ const ERROR = 'error'; /** * @var string */ const GROUP = 'group'; /** * @var string */ const INFO = 'info'; /** * @var string */ const GROUP_END = 'groupEnd'; /** * @var string */ const GROUP_COLLAPSED = 'groupCollapsed'; /** * @var string */ const TABLE = 'table'; /** * @var string */ protected $_php_version; /** * @var int */ protected $_timestamp; /** * @var array */ protected $_json = array( 'version' => self::VERSION, 'columns' => array('log', 'backtrace', 'type'), 'rows' => array() ); /** * @var array */ protected $_backtraces = array(); /** * @var bool */ protected $_error_triggered = false; /** * @var array */ protected $_settings = array( self::BACKTRACE_LEVEL => 1 ); /** * @var ChromePhp */ protected static $_instance; /** * Prevent recursion when working with objects referring to each other * * @var array */ protected $_processed = array(); /** * constructor */ private function __construct() { $this->_php_version = phpversion(); $this->_timestamp = $this->_php_version >= 5.1 ? $_SERVER['REQUEST_TIME'] : time(); $this->_json['request_uri'] = $_SERVER['REQUEST_URI']; } /** * gets instance of this class * * @return ChromePhp */ public static function getInstance() { if (self::$_instance === null) { self::$_instance = new self(); } return self::$_instance; } /** * logs a variable to the console * * @param mixed $data,... unlimited OPTIONAL number of additional logs [...] * @return void */ public static function log() { $args = func_get_args(); return self::_log('', $args); } /** * logs a warning to the console * * @param mixed $data,... unlimited OPTIONAL number of additional logs [...] * @return void */ public static function warn() { $args = func_get_args(); return self::_log(self::WARN, $args); } /** * logs an error to the console * * @param mixed $data,... unlimited OPTIONAL number of additional logs [...] * @return void */ public static function error() { $args = func_get_args(); return self::_log(self::ERROR, $args); } /** * sends a group log * * @param string value */ public static function group() { $args = func_get_args(); return self::_log(self::GROUP, $args); } /** * sends an info log * * @param mixed $data,... unlimited OPTIONAL number of additional logs [...] * @return void */ public static function info() { $args = func_get_args(); return self::_log(self::INFO, $args); } /** * sends a collapsed group log * * @param string value */ public static function groupCollapsed() { $args = func_get_args(); return self::_log(self::GROUP_COLLAPSED, $args); } /** * ends a group log * * @param string value */ public static function groupEnd() { $args = func_get_args(); return self::_log(self::GROUP_END, $args); } /** * sends a table log * * @param string value */ public static function table() { $args = func_get_args(); return self::_log(self::TABLE, $args); } /** * internal logging call * * @param string $type * @return void */ protected static function _log($type, array $args) { // nothing passed in, don't do anything if (count($args) == 0 && $type != self::GROUP_END) { return; } $logger = self::getInstance(); $logger->_processed = array(); $logs = array(); foreach ($args as $arg) { $logs[] = $logger->_convert($arg); } $backtrace = debug_backtrace(false); $level = $logger->getSetting(self::BACKTRACE_LEVEL); $backtrace_message = 'unknown'; if (isset($backtrace[$level]['file']) && isset($backtrace[$level]['line'])) { $backtrace_message = $backtrace[$level]['file'] . ' : ' . $backtrace[$level]['line']; } $logger->_addRow($logs, $backtrace_message, $type); } /** * converts an object to a better format for logging * * @param Object * @return array */ protected function _convert($object) { // if this isn't an object then just return it if (!is_object($object)) { return $object; } //Mark this object as processed so we don't convert it twice and it //Also avoid recursion when objects refer to each other $this->_processed[] = $object; $object_as_array = array(); // first add the class name $object_as_array['___class_name'] = get_class($object); // loop through object vars $object_vars = get_object_vars($object); foreach ($object_vars as $key => $value) { // same instance as parent object if ($value === $object || in_array($value, $this->_processed, true)) { $value = 'recursion - parent object [' . get_class($value) . ']'; } $object_as_array[$key] = $this->_convert($value); } $reflection = new ReflectionClass($object); // loop through the properties and add those foreach ($reflection->getProperties() as $property) { // if one of these properties was already added above then ignore it if (array_key_exists($property->getName(), $object_vars)) { continue; } $type = $this->_getPropertyKey($property); if ($this->_php_version >= 5.3) { $property->setAccessible(true); } try { $value = $property->getValue($object); } catch (ReflectionException $e) { $value = 'only PHP 5.3 can access private/protected properties'; } // same instance as parent object if ($value === $object || in_array($value, $this->_processed, true)) { $value = 'recursion - parent object [' . get_class($value) . ']'; } $object_as_array[$type] = $this->_convert($value); } return $object_as_array; } /** * takes a reflection property and returns a nicely formatted key of the property name * * @param ReflectionProperty * @return string */ protected function _getPropertyKey(ReflectionProperty $property) { $static = $property->isStatic() ? ' static' : ''; if ($property->isPublic()) { return 'public' . $static . ' ' . $property->getName(); } if ($property->isProtected()) { return 'protected' . $static . ' ' . $property->getName(); } if ($property->isPrivate()) { return 'private' . $static . ' ' . $property->getName(); } } /** * adds a value to the data array * * @var mixed * @return void */ protected function _addRow(array $logs, $backtrace, $type) { // if this is logged on the same line for example in a loop, set it to null to save space if (in_array($backtrace, $this->_backtraces)) { $backtrace = null; } // for group, groupEnd, and groupCollapsed // take out the backtrace since it is not useful if ($type == self::GROUP || $type == self::GROUP_END || $type == self::GROUP_COLLAPSED) { $backtrace = null; } if ($backtrace !== null) { $this->_backtraces[] = $backtrace; } $row = array($logs, $backtrace, $type); $this->_json['rows'][] = $row; $this->_writeHeader($this->_json); } protected function _writeHeader($data) { header(self::HEADER_NAME . ': ' . $this->_encode($data)); } /** * encodes the data to be sent along with the request * * @param array $data * @return string */ protected function _encode($data) { return base64_encode(utf8_encode(json_encode($data))); } /** * adds a setting * * @param string key * @param mixed value * @return void */ public function addSetting($key, $value) { $this->_settings[$key] = $value; } /** * add ability to set multiple settings in one call * * @param array $settings * @return void */ public function addSettings(array $settings) { foreach ($settings as $key => $value) { $this->addSetting($key, $value); } } /** * gets a setting * * @param string key * @return mixed */ public function getSetting($key) { if (!isset($this->_settings[$key])) { return null; } return $this->_settings[$key]; } } }