<?php // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK IT ] // +---------------------------------------------------------------------- // | Copyright (c) 2006-2014 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- // | Author: luofei614 <weibo.com/luofei614> // +---------------------------------------------------------------------- // $Id$ /** * 将Trace信息输出到chrome浏览器的控制器,从而不影响ajax效果和页面的布局。 * 使用前,你需要先安装 chrome log 这个插件: http://craig.is/writing/chrome-logger。 * 定义应用的tags.php文件 Application/Common/Conf/tags.php, * <code> * <?php return array( * 'app_end'=>array( * 'Behavior\ChromeShowPageTrace' * ) * ); * </code> * 如果trace信息没有正常输出,请查看您的日志。 * 这是通过http headers和chrome通信,所以要保证在输出trace信息之前不能有 * headers输出,你可以在入口文件第一行加入代码 ob_start(); 或者配置output_buffering * */ namespace Behavior; use Behavior\ChromePhp as ChromePhp; 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] : ''; } } } chromeDebug('TRACE信息:' . __SELF__, 'group'); //输出日志 foreach ($trace as $title => $log) { '错误' == $title ? chromeDebug($title, 'group') : chromeDebug($title, 'groupCollapsed'); foreach ($log as $i => $logstr) { chromeDebug($i . '.' . $logstr, 'log'); } chromeDebug('', 'groupEnd'); } chromeDebug('', '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('<br/>', "\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 chromeDebug($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 <iamcraigcampbell@gmail.com> */ 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 (null === self::$_instance) { 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 && self::GROUP_END != $type) { 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 (self::GROUP == $type || self::GROUP_END == $type || self::GROUP_COLLAPSED == $type) { $backtrace = null; } if (null !== $backtrace) { $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]; } } }