PHP API调试工具--SocketLog使用
简介
介绍
SocketLog 是一款高效的、探针式的服务器端程序调试和分析工具。
将SocketLog 引入到目标项目后,SocketLog 会通过在服务端启动的一个 WebSocket 服务,将程序执行过程中收集到的调试信息推送到客户端。 客户端会通过一个 Chrome 插件将调试信息打印到浏览器的 Console 中,这些信息包括程序的运行时间、吞吐率、内存消耗;PHP 的 Error、warning、notice 信息;程序执行的 SQL 语句以及对 SQL 语句的 explain 等等。
SocketLog 特别适合用于调试 Ajax 方式发起的请求和 API 项目。
用途
- API bug 调试
- 正在运行的 API 有 bug,不能使用 var_dump() 进行调试,因为会影响 client 的调用。 将日志写到文件,查看也不方便,特别是带调用栈或大数据结构的文件日志,查看起来十分困难。
- 是时候让 SocketLog 大显身手了,它通过 WebSocket 将调试日志打印到浏览器的 Console 中,查看起来非常方便。
- 其他用途
- 你还可以用它来分析开源程序、分析 SQL 性能、结合 PHP Taint 分析程序漏洞。
场景
一、用 SocketLog 来做微信开发调试。
举一个常见的场景:你在做微信 API 开发的时候,是否遇到了 API 有 bug,微信只提示“该公众账号暂时无法提供服务,请稍候再试” ?
我们根本不知道 API 出了什么问题,由于不能打印信息,只能通过日志来排查,这种方式的调试效率实在是太低了。
现在有了 SocketLog 就不一样了,我们可以知道微信给 API 传递了哪些参数,程序有错误我们也能看见错误信息。
二、开源项目二次开发
针对一个开源的 Web 项目,我们想基于它做二次开发。如果能在浏览网站的时候,可以从浏览器的 Console 中知道程序都做了些什么,这对二次开发将十分有帮助。
在浏览 discuz 程序时,Console 中打印出了:
- 程序的运行时间、吞吐率和内存消耗信息。
- 程序的 warning,notice 等错误信息。
- 当前页面执行了哪些 SQL 语句,以及执行 SQL 语句的调用栈信息。
安装与使用
github 上 socketlog
包地址:https://github.com/luofei614/SocketLog
-
1,客户端,安装 Chrome 插件。
-
打开谷歌浏览器,地址栏输入 chrome://extensions/ 进入扩展项设置
然后加载已解压的扩展程序,选中解压后的目录中的 chrom文件夹,确定即可。
-
-
2,服务端,安装 Socket 服务并启动。
(请确保你的环境已经安装了 NodeJs)
注意:如果你的docker环境或服务器有防火墙,请开启1229和1116两个端口,这两个端口是socketlog要使用的。
-
根据实际情况选定目录(例如,在目标项目所在目录下)
## 下载项目代码或者 clone $ git clone https://github.com/luofei614/SocketLog.git
-
启动服务
## 请注意实际项目的 index.js 文件的路径 $ node server/index.js ## 后台运行: nohup node server/index.js > /dev/null &
将会在本地起一个websocket服务 ,监听端口是1229
-
-
3,配置:
tp3项目中引入代码
1、
ThinkPHP\Library\Org\SocketLog\Slog.class.php
<?php namespace Org\SocketLog; class Slog { public static $start_time = 0; public static $start_memory = 0; public static $port = 1116; // SocketLog 服务的 http 的端口号 public static $log_types = ['log', 'info', 'error', 'warn', 'table', 'group', 'groupCollapsed', 'groupEnd', 'alert']; protected static $_allowForceClientIds = []; // 配置强制推送且被授权的client_id protected static $_instance; protected static $config = [ 'enable' => true, // 是否记录日志的开关 'host' => 'localhost', 'optimize' => false, // 是否显示利于优化的参数,如果允许时间,消耗内存等 'show_included_files' => false, 'error_handler' => false, 'force_client_ids' => [], // 日志强制记录到配置的 client_id 'allow_client_ids' => [] // 限制允许读取日志的client_id ]; protected static $logs = []; protected static $css = [ 'sql' => 'color:#009bb4;', 'sql_warn' => 'color:#009bb4;font-size:14px;', 'error_handler' => 'color:#f4006b;font-size:14px;', 'page' => 'color:#40e2ff;background:#171717;' ]; /** * [__callStatic description] * @param [type] $method [description] * @param [type] $args [description] * @return [type] [description] */ public static function __callStatic($method, $args) { if (in_array($method, self::$log_types)) { array_unshift($args, $method); $ret = call_user_func_array([self::getInstance(), 'record'], $args); // 立即发送日志 self::sendLog(); self::$logs = []; return $ret; } } /** * 显示 SQL 语句调试信息 * @param [type] $sql [description] * @param [type] $link [description] * @return [type] [description] */ public static function sql($sql, $link) { // 使用 mysqli 方式连接 DB if (is_object($link) && 'mysqli' == get_class($link)) { return self::mysqliLog($sql, $link); } // 使用 mysql 方式连接 DB if (is_resource($link) && ('mysql link' == get_resource_type($link) || 'mysql link persistent' == get_resource_type($link))) { return self::mysqlLog($sql, $link); } // 使用 PDO 方式连接 DB if (is_object($link) && 'PDO' == get_class($link)) { return self::pdoLog($sql, $link); } throw new Exception('SocketLog can not support this database link'); } /** * 增大 log 日志的字体,颜色设置为 red * @param [type] $log [description] * @return [type] [description] */ public static function big($log) { self::log($log, 'font-size:20px;color:red;'); } public static function trace($msg, $trace_level = 1, $css = '') { if (!self::check()) { return; } self::groupCollapsed($msg, $css); $traces = debug_backtrace(false); $traces = array_reverse($traces); $trace_level = ($trace_level == '') ? 0 : intval($trace_level); $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(); } /** * 使用 mysqli 对象的 Log * @param [type] $sql [description] * @param [type] $db [description] * @return [type] [description] */ 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); } /** * 使用 mysql 对象的 Log * @param [type] $sql [description] * @param [type] $db [description] * @return [type] [description] */ 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); } /** * 使用 PDO 对象的 Log * @param [type] $sql [description] * @param [type] $db [description] * @return [type] [description] */ 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); } /** * 对 SQL 语句执行 Explain 解析 * @param [type] $arr [description] * @param [type] &$sql [description] * @param [type] &$css [description] * @return [type] [description] */ 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']; } } /** * 检查 SQL 语句是否还有 Where 条件,如果不含有则显示 warn 信息 * @param [type] &$sql [description] * @param [type] &$css [description] * @return [type] [description] */ 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([__CLASS__, 'errorHandler']); register_shutdown_function([__CLASS__, 'fatalError']); } /** * 设置错误处理函数 * @param [type] $errno [description] * @param [type] $errstr [description] * @param [type] $errfile [description] * @param [type] $errline [description] * @return [type] [description] */ public static function errorHandler($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']); } /** * Fatal Error 函数 * @return [type] [description] */ public static function fatalError() { // 保存日志记录 if ($e = error_get_last()) { self::errorHandler($e['type'], $e['message'], $e['file'], $e['line']); self::sendLog(); // 此类终止不会调用类的 __destruct 方法,所以此处手动 sendLog } } /** * 获取实例 * @return [type] [description] */ public static function getInstance() { if (self::$_instance === null) { self::$_instance = new self(); } return self::$_instance; } /** * 执行检查 * @return [type] [description] */ protected static function check() { if (!self::getConfig('enable')) { return false; } $tabid = self::getClientArg('tabid'); // 是否记录日志的检查 if (!$tabid && !self::getConfig('force_client_ids')) { return false; } // 用户认证 $allow_client_ids = self::getConfig('allow_client_ids'); if (!empty($allow_client_ids)) { // 通过数组交集运算,得出授权强制推送的 client_id self::$_allowForceClientIds = array_intersect($allow_client_ids, self::getConfig('force_client_ids')); if (!$tabid && count(self::$_allowForceClientIds)) { return true; } $client_id = self::getClientArg('client_id'); if (!in_array($client_id, $allow_client_ids)) { return false; } } else { self::$_allowForceClientIds = self::getConfig('force_client_ids'); } return true; } /** * 获取客户端参数 * @param [type] $name [description] * @return [type] [description] */ protected static function getClientArg($name) { static $args = []; $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 = ['tabid' => null]; return null; } parse_str($match[1], $args); } if (isset($args[$name])) { return $args[$name]; } return null; } /** * 按需设置项目的调试配置项 * @param [type] $config [description] * @return [type] [description] */ 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(); } } } /** * 获取配置项 * @param [type] $name [description] * @return [type] [description] */ public static function getConfig($name) { if (isset(self::$config[$name])) { return self::$config[$name]; } return null; } /** * 记录日志 * @param [type] $type [description] * @param string $msg [description] * @param string $css [description] * @return [type] [description] */ public function record($type, $msg = '', $css = '') { if (!self::check()) { return; } self::$logs[] = [ 'type' => $type, 'msg' => $msg, 'css' => $css ]; } /** * 调试后台 API 时,发送信息 * @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 = ["Content-Type: application/json;charset=UTF-8"]; curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); // 设置 header $txt = curl_exec($ch); return true; } /** * 发送 Log * @return [type] [description] */ 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, [ 'type' => 'group', 'msg' => $current_uri . $time_str . $memory_str, 'css' => self::$css['page'] ]); if (self::getConfig('show_included_files')) { self::$logs[] = [ 'type' => 'groupCollapsed', 'msg' => 'included_files', 'css' => '' ]; self::$logs[] = [ 'type' => 'log', 'msg' => implode("\n", get_included_files()), 'css' => '' ]; self::$logs[] = [ 'type' => 'groupEnd', 'msg' => '', 'css' => '', ]; } self::$logs[] = [ 'type' => 'groupEnd', 'msg' => '', 'css' => '', ]; $tabid = self::getClientArg('tabid'); if (!$client_id = self::getClientArg('client_id')) { $client_id = ''; } if (!empty(self::$_allowForceClientIds)) { // 强制推送到多个 client_id foreach (self::$_allowForceClientIds as $force_client_id) { $client_id = $force_client_id; self::sendToClient($tabid, $client_id, self::$logs, $force_client_id); } } else { self::sendToClient($tabid, $client_id, self::$logs, ''); } } /** * 发送给指定客户端 * @author Zjmainstay * @param $tabid * @param $client_id * @param $logs * @param $force_client_id */ protected static function sendToClient($tabid, $client_id, $logs, $force_client_id) { $logs = [ 'tabid' => $tabid, 'client_id' => $client_id, 'logs' => $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(); } }
2、
common\function.php
<?php use Org\SocketLog\Slog; function slog($log, $type = 'log', $css = '') { if (is_string($type)) { $type = preg_replace_callback('/_([a-zA-Z])/', function ($matches) { return strtoupper($matches[1]); }, $type); if (method_exists('\Org\SocketLog\Slog', $type) || in_array($type, Slog::$log_types)) { return call_user_func(['\Org\SocketLog\Slog', $type], $log, $css); } } if (is_object($type) && 'mysqli' == get_class($type)) { return Slog::mysqliLog($log, $type); } if (is_resource($type) && ('mysql link' == get_resource_type($type) || 'mysql link persistent' == get_resource_type($type))) { return Slog::mysqlLog($log, $type); } if (is_object($type) && 'PDO' == get_class($type)) { return Slog::pdoLog($log, $type); } throw new Exception($type . ' is not SocketLog method'); }
3、入口文件
index.php
// 引入ThinkPHP入口文件 require './ThinkPHP/ThinkPHP.php'; // 配置 slog([ 'host' => 'localhost', // Websocket 服务器地址,默认 localhost 'optimize' => false, // 是否显示有利于程序优化的信息,如运行时间、吞吐率、消耗内存等,默认为 false 'show_included_files' => false, // 是否显示本次程序运行加载了哪些文件,默认为 false 'error_handler' => false, // 是否接管程序错误,将程序错误显示在 Console 中,默认为 false 'allow_client_ids' => [ // 限制允许读取日志的 client_id,默认为空,表示所有人都可以获得日志。 'client_01', //'client_02', //'client_03', ], 'force_client_ids' => [ // 日志强制记录到配置的 client_id,默认为空,client_id 必须在 allow_client_ids 中 'client_01', //'client_02', ] ], 'config');
设置
client_id
: 在chrome浏览器中,可以设置插件的Client_ID
,Client_ID
是你任意指定的字符串。保存后查看状态是否连接成功。4、数据库进行调试
ThinkPHP/Library/Think/Db/Driver.class.php
的query
和execute
方法中添加
slog($this->queryStr,$this->_linkID)
即可$this->bind = array(); 后添加: slog($this->queryStr,$this->_linkID)
Tp3每次执行完sql语句都会调用
$this->debug
, 所以我们可以把slog($this->queryStr,$this->_linkID);
直接写在debug
方法中。