diff --git a/convention.php b/convention.php index 7ea84e95..6592c427 100644 --- a/convention.php +++ b/convention.php @@ -32,11 +32,11 @@ return [ // url变量绑定 'url_params_bind' => true, // 异常页面的模板文件 - 'exception_tmpl' => THINK_PATH . 'Tpl/think_exception.tpl', + 'exception_tmpl' => THINK_PATH . 'tpl/think_exception.tpl', // 默认错误跳转对应的模板文件 - 'error_tmpl' => THINK_PATH . 'Tpl/dispatch_jump.tpl', + 'error_tmpl' => THINK_PATH . 'tpl/dispatch_jump.tpl', // 默认成功跳转对应的模板文件 - 'success_tmpl' => THINK_PATH . 'Tpl/dispatch_jump.tpl', + 'success_tmpl' => THINK_PATH . 'tpl/dispatch_jump.tpl', // 默认AJAX 数据返回格式,可选JSON XML ... 'default_ajax_return' => 'JSON', // 默认JSONP格式返回的处理方法 @@ -54,6 +54,9 @@ return [ 'error_page' => '', // 显示错误信息 'show_error_msg' => false, + //默认输出类型 + 'default_return_type' => 'html', + 'common_module' => '', 'log' => [ 'type' => 'File', @@ -106,4 +109,17 @@ return [ // 指定从服务器序号 'slave_no' => '', ], + /* SocketLog 调试 */ + 'slog'=> [ + 'enable'=>false, //是否记录日志的开关 + 'host'=>'localhost', + //是否显示利于优化的参数,如果允许时间,消耗内存等 + 'optimize'=>true, + 'show_included_files'=>true, + 'error_handler'=>true, + //日志强制记录到配置的client_id + 'force_client_id'=>'', + //限制允许读取日志的client_id + 'allow_client_ids'=>array() + ] ]; diff --git a/library/think/app.php b/library/think/app.php index 543080d4..6ca07bcd 100644 --- a/library/think/app.php +++ b/library/think/app.php @@ -56,6 +56,10 @@ class App if (!IS_CLI && $config['use_session']) { Session::init($config['session']); } + //判断,如果启动SocketLog调试, 进行SocketLog配置 + if($config['slog']['enable']) { + slog::config($config['slog']); + } // 应用URL调度 self::dispatch($config); diff --git a/library/think/db/driver.php b/library/think/db/driver.php index 150e5c7a..8986e521 100644 --- a/library/think/db/driver.php +++ b/library/think/db/driver.php @@ -17,6 +17,7 @@ use think\Debug; use think\Exception; use think\Lang; use think\Log; +use think\slog; abstract class Driver { @@ -742,14 +743,16 @@ abstract class Driver protected function parseOrder($order) { $array = []; - foreach ($order as $key => $val) { - if (is_numeric($key)) { - if (false === strpos($val, '(')) { - $array[] = $this->parseKey($val); + if(is_array($order)) { + foreach ($order as $key => $val) { + if (is_numeric($key)) { + if (false === strpos($val, '(')) { + $array[] = $this->parseKey($val); + } + } else { + $sort = in_array(strtolower(trim($val)), ['asc', 'desc']) ? ' ' . $val : ''; + $array[] = $this->parseKey($key) . ' ' . $sort; } - } else { - $sort = in_array(strtolower(trim($val)), ['asc', 'desc']) ? ' ' . $val : ''; - $array[] = $this->parseKey($key) . ' ' . $sort; } } $order = implode(',', $array); @@ -1159,6 +1162,11 @@ abstract class Driver Log::record($this->queryStr . ' [ RunTime:' . Debug::getUseTime('queryStartTime', 'queryEndTime') . 's ]', 'SQL'); } } + + $slog_config=Config::get('slog'); + if($slog_config['enable'] && $start) { + slog::sql($this->queryStr,$this->_linkID); + } } /** diff --git a/library/think/slog.php b/library/think/slog.php new file mode 100644 index 00000000..84e2bbdb --- /dev/null +++ b/library/think/slog.php @@ -0,0 +1,472 @@ + + */ +namespace think; +class slog +{ + public static $start_time=0; + public static $start_memory=0; + public static $port=1116;//SocketLog 服务的http的端口号 + public static $log_types=array('log','info','error','warn','table','group','groupCollapsed','groupEnd','alert'); + + protected static $_instance; + + protected static $config=array( + 'enable'=>true, //是否记录日志的开关 + 'host'=>'localhost', + //是否显示利于优化的参数,如果允许时间,消耗内存等 + 'optimize'=>false, + 'show_included_files'=>false, + 'error_handler'=>false, + //日志强制记录到配置的client_id + 'force_client_id'=>'', + //限制允许读取日志的client_id + 'allow_client_ids'=>array() + ); + + protected static $logs=array(); + + protected static $css=array( + 'sql'=>'color:#009bb4;', + 'sql_warn'=>'color:#009bb4;font-size:14px;', + 'error_handler'=>'color:#f4006b;font-size:14px;', + 'page'=>'color:#40e2ff;background:#171717;' + ); + + public static function __callStatic($method,$args) + { + if(in_array($method,self::$log_types)) + { + array_unshift($args,$method); + return call_user_func_array(array(self::getInstance(),'record'),$args); + } + } + + public static function sql($sql,$link) + { + if(is_object($link) && 'mysqli'==get_class($link)) + { + return self::mysqlilog($sql,$link); + } + + if(is_resource($link) && ('mysql link'==get_resource_type($link) || 'mysql link persistent'==get_resource_type($link))) + { + return self::mysqllog($sql,$link); + } + + + if(is_object($link) && 'PDO'==get_class($link)) + { + return self::pdolog($sql,$link); + } + + throw new Exception('SocketLog can not support this database link'); + } + + + + public static function big($log) + { + self::log($log,'font-size:20px;color:red;'); + } + + public static function trace($msg,$trace_level=2,$css='') + { + if(!self::check()) + { + return ; + } + self::groupCollapsed($msg,$css); + $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'])){ + self::groupCollapsed($trace_msg); + self::log($trace['args']); + self::groupEnd(); + }else{ + self::log($trace_msg); + } + } + self::groupEnd(); + } + + + public static function mysqlilog($sql,$db) + { + if(!self::check()) + { + return ; + } + + $css=self::$css['sql']; + if(preg_match('/^SELECT /i', $sql)) + { + //explain + $query = @mysqli_query($db,"EXPLAIN ".$sql); + $arr=mysqli_fetch_array($query); + self::sqlexplain($arr,$sql,$css); + } + self::sqlwhere($sql,$css); + self::trace($sql,2,$css); + } + + + public static function mysqllog($sql,$db) + { + if(!self::check()) + { + return ; + } + $css=self::$css['sql']; + if(preg_match('/^SELECT /i', $sql)) + { + //explain + $query = @mysql_query("EXPLAIN ".$sql,$db); + $arr=mysql_fetch_array($query); + self::sqlexplain($arr,$sql,$css); + } + //判断sql语句是否有where + self::sqlwhere($sql,$css); + self::trace($sql,2,$css); + } + + + public static function pdolog($sql,$pdo) + { + if(!self::check()) + { + return ; + } + $css=self::$css['sql']; + if(preg_match('/^SELECT /i', $sql)) + { + //explain + try { + $obj=$pdo->query( "EXPLAIN ".$sql); + if(is_object($obj) && method_exists($obj,'fetch')) + { + $arr=$obj->fetch(\PDO::FETCH_ASSOC); + self::sqlexplain($arr,$sql,$css); + } + } catch (Exception $e) { + + } + } + self::sqlwhere($sql,$css); + self::trace($sql,2,$css); + } + + private static function sqlexplain($arr,&$sql,&$css) + { + $arr = array_change_key_case($arr, CASE_LOWER); + if(false!==strpos($arr['extra'],'Using filesort')) + { + $sql.=' <---################[Using filesort]'; + $css=self::$css['sql_warn']; + } + if(false!==strpos($arr['extra'],'Using temporary')) + { + $sql.=' <---################[Using temporary]'; + $css=self::$css['sql_warn']; + } + } + private static function sqlwhere(&$sql,&$css) + { + //判断sql语句是否有where + if(preg_match('/^UPDATE |DELETE /i',$sql) && !preg_match('/WHERE.*(=|>|<|LIKE|IN)/i',$sql)) + { + $sql.='<---###########[NO WHERE]'; + $css=self::$css['sql_warn']; + } + } + + + /** + * 接管报错 + */ + public static function registerErrorHandler() + { + if(!self::check()) + { + return ; + } + + set_error_handler(array(__CLASS__,'error_handler')); + register_shutdown_function(array(__CLASS__,'fatalError')); + } + + public static function error_handler($errno, $errstr, $errfile, $errline) + { + switch($errno){ + case E_WARNING: $severity = 'E_WARNING'; break; + case E_NOTICE: $severity = 'E_NOTICE'; break; + case E_USER_ERROR: $severity = 'E_USER_ERROR'; break; + case E_USER_WARNING: $severity = 'E_USER_WARNING'; break; + case E_USER_NOTICE: $severity = 'E_USER_NOTICE'; break; + case E_STRICT: $severity = 'E_STRICT'; break; + case E_RECOVERABLE_ERROR: $severity = 'E_RECOVERABLE_ERROR'; break; + case E_DEPRECATED: $severity = 'E_DEPRECATED'; break; + case E_USER_DEPRECATED: $severity = 'E_USER_DEPRECATED'; break; + case E_ERROR: $severity = 'E_ERR'; break; + case E_PARSE: $severity = 'E_PARSE'; break; + case E_CORE_ERROR: $severity = 'E_CORE_ERROR'; break; + case E_COMPILE_ERROR: $severity = 'E_COMPILE_ERROR'; break; + case E_USER_ERROR: $severity = 'E_USER_ERROR'; break; + default: $severity= 'E_UNKNOWN_ERROR_'.$errno; break; + } + $msg="{$severity}: {$errstr} in {$errfile} on line {$errline} -- SocketLog error handler"; + self::trace($msg,2,self::$css['error_handler']); + } + + public static function fatalError() + { + // 保存日志记录 + if ($e = error_get_last()) + { + self::error_handler($e['type'],$e['message'],$e['file'],$e['line']); + self::sendLog();//此类终止不会调用类的 __destruct 方法,所以此处手动sendLog + } + } + + + + public static function getInstance() + { + if (self::$_instance === null) { + self::$_instance = new self(); + } + return self::$_instance; + } + + protected static function _log($type,$logs,$css='') + { + self::getInstance()->record($type,$logs,$css); + } + + + protected static function check() + { + if(!self::getConfig('enable')) + { + return false; + } + $tabid=self::getClientArg('tabid'); + //是否记录日志的检查 + if(!$tabid && !self::getConfig('force_client_id')) + { + return false; + } + //用户认证 + $allow_client_ids=self::getConfig('allow_client_ids'); + if(!empty($allow_client_ids)) + { + if (!$tabid && in_array(self::getConfig('force_client_id'), $allow_client_ids)) { + return true; + } + + $client_id=self::getClientArg('client_id'); + if(!in_array($client_id,$allow_client_ids)) + { + return false; + } + } + return true; + } + + protected static function getClientArg($name) + { + static $args=array(); + + $key = 'HTTP_USER_AGENT'; + + if (isset($_SERVER['HTTP_SOCKETLOG'])) { + $key = 'HTTP_SOCKETLOG'; + } + + if(!isset($_SERVER[$key])) + { + return null; + } + if(empty($args)) + { + if(!preg_match('/SocketLog\((.*?)\)/',$_SERVER[$key],$match)) + { + $args=array('tabid'=>null); + return null; + } + parse_str($match[1],$args); + } + if(isset($args[$name])) + { + return $args[$name]; + } + return null; + } + + + //设置配置 + public static function config($config) + { + $config=array_merge(self::$config,$config); + self::$config=$config; + if(self::check()) + { + self::getInstance(); //强制初始化SocketLog实例 + if($config['optimize']) + { + self::$start_time=microtime(true); + self::$start_memory=memory_get_usage(); + } + + if($config['error_handler']) + { + self::registerErrorHandler(); + } + } + } + + + //获得配置 + public static function getConfig($name) + { + if(isset(self::$config[$name])) + return self::$config[$name]; + return null; + } + + //记录日志 + public function record($type,$msg='',$css='') + { + if(!self::check()) + { + return ; + } + + self::$logs[]=array( + 'type'=>$type, + 'msg'=>$msg, + 'css'=>$css + ); + } + + /** + * @param null $host - $host of socket server + * @param string $message - 发送的消息 + * @param string $address - 地址 + * @return bool + */ + public static function send($host,$message='',$address='/') + { + $url='http://'.$host.':'.self::$port.$address; + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $message); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + $headers=array( + "Content-Type: application/json;charset=UTF-8" + ); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);//设置header + $txt = curl_exec($ch); + return true; + } + + public static function sendLog() + { + if(!self::check()) + { + return ; + } + + $time_str=''; + $memory_str=''; + if(self::$start_time) + { + $runtime=microtime(true)-self::$start_time; + $reqs=number_format(1/$runtime,2); + $time_str="[运行时间:{$runtime}s][吞吐率:{$reqs}req/s]"; + } + if(self::$start_memory) + { + $memory_use=number_format(memory_get_usage()-self::$start_memory/1024,2); + $memory_str="[内存消耗:{$memory_use}kb]"; + } + + if(isset($_SERVER['HTTP_HOST'])) + { + $current_uri=$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']; + } + else + { + $current_uri="cmd:".implode(' ',$_SERVER['argv']); + } + array_unshift(self::$logs,array( + 'type'=>'group', + 'msg'=>$current_uri.$time_str.$memory_str, + 'css'=>self::$css['page'] + )); + + if(self::getConfig('show_included_files')) + { + self::$logs[]=array( + 'type'=>'groupCollapsed', + 'msg'=>'included_files', + 'css'=>'' + ); + self::$logs[]=array( + 'type'=>'log', + 'msg'=>implode("\n",get_included_files()), + 'css'=>'' + ); + self::$logs[]=array( + 'type'=>'groupEnd', + 'msg'=>'', + 'css'=>'', + ); + } + + self::$logs[]=array( + 'type'=>'groupEnd', + 'msg'=>'', + 'css'=>'', + ); + + $tabid=self::getClientArg('tabid'); + if(!$client_id=self::getClientArg('client_id')) + { + $client_id=''; + } + if($force_client_id=self::getConfig('force_client_id')) + { + $client_id=$force_client_id; + } + $logs=array( + 'tabid'=>$tabid, + 'client_id'=>$client_id, + 'logs'=>self::$logs, + 'force_client_id'=>$force_client_id, + ); + $msg=@json_encode($logs); + $address='/'.$client_id; //将client_id作为地址, server端通过地址判断将日志发布给谁 + self::send(self::getConfig('host'),$msg,$address); + + } + + public function __destruct() + { + self::sendLog(); + } + +} + diff --git a/mode/common.php b/mode/common.php index 3d34c52b..7d32bdbb 100644 --- a/mode/common.php +++ b/mode/common.php @@ -44,6 +44,7 @@ return [ 'think\Input' => CORE_PATH . 'input' . EXT, 'think\Parser' => CORE_PATH . 'parser' . EXT, 'think\Lang' => CORE_PATH . 'lang' . EXT, + 'think\slog' => CORE_PATH . 'slog' . EXT ], 'init' => [],