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_IDClient_ID是你任意指定的字符串。保存后查看状态是否连接成功。

    4、数据库进行调试

    ThinkPHP/Library/Think/Db/Driver.class.phpqueryexecute 方法中

    添加slog($this->queryStr,$this->_linkID)即可

    $this->bind =   array();  
    后添加:
    slog($this->queryStr,$this->_linkID)
    

    Tp3每次执行完sql语句都会调用$this->debug, 所以我们可以把slog($this->queryStr,$this->_linkID); 直接写在 debug方法中。

posted @ 2022-12-26 10:18  caibaotimes  阅读(155)  评论(0编辑  收藏  举报