diff --git a/composer.json b/composer.json index d616ad6..4f9fbda 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,12 @@ { "name": "hsk99/webman-statistic", "type": "library", + "keywords": [ + "webman" + ], + "homepage": "http://www.workerman.net/webman", "license": "MIT", - "homepage": "http://hsk99.com.cn", + "description": "webman-statistic 应用监控插件", "authors": [ { "name": "hsk99", @@ -11,7 +15,8 @@ } ], "require": { - "workerman/http-client": "^1.0" + "workerman/http-client": "^1.0", + "phpmyadmin/sql-parser": "^5.5" }, "autoload": { "psr-4": { diff --git a/src/Bootstrap.php b/src/Bootstrap.php index 25ac1fe..8704e48 100644 --- a/src/Bootstrap.php +++ b/src/Bootstrap.php @@ -2,6 +2,8 @@ namespace Hsk99\WebmanStatistic; +use Hsk99\WebmanStatistic\Statistic; + class Bootstrap implements \Webman\Bootstrap { /** @@ -9,6 +11,11 @@ class Bootstrap implements \Webman\Bootstrap */ protected static $_instance = null; + /** + * @var string + */ + public static $process = null; + /** * @author HSK * @date 2022-06-17 15:33:53 @@ -20,6 +27,8 @@ class Bootstrap implements \Webman\Bootstrap public static function start($worker) { if ($worker) { + static::$process = $worker->name; + $options = [ 'max_conn_per_addr' => 128, // 每个地址最多维持多少并发连接 'keepalive_timeout' => 15, // 连接多长时间不通讯就关闭 @@ -28,13 +37,18 @@ public static function start($worker) ]; self::$_instance = new \Workerman\Http\Client($options); + // 定时上报数据 \Workerman\Timer::add(config('plugin.hsk99.statistic.app.interval', 30), function () { - \Hsk99\WebmanStatistic\Statistic::report(); + Statistic::report(); }); - $worker->onWorkerStop = function () { - \Hsk99\WebmanStatistic\Statistic::report(); - }; + // 执行监听所有进程 SQL、Redis + static::listen($worker); + + // 延迟接管进程业务回调,执行监控 + \Workerman\Timer::add(1, function () use (&$worker) { + static::monitor($worker); + }, '', false); } } @@ -48,4 +62,126 @@ public static function instance(): \Workerman\Http\Client { return self::$_instance; } + + /** + * SQL、Redis 监听 + * + * @author HSK + * @date 2022-07-20 17:22:06 + * + * @param \Workerman\Worker $worker + * + * @return void + */ + protected static function listen(\Workerman\Worker $worker) + { + if (class_exists(\think\facade\Db::class)) { + \think\facade\Db::listen(function ($sql, $runtime, $master) { + if ($sql === 'select 1' || !is_numeric($runtime)) { + return; + } + Statistic::sql(trim($sql), $runtime * 1000, ['master' => $master]); + }); + } + + if (class_exists(\Illuminate\Database\Events\QueryExecuted::class)) { + try { + \support\Db::listen(function (\Illuminate\Database\Events\QueryExecuted $query) { + $sql = trim($query->sql); + if (strtolower($sql) === 'select 1') { + return; + } + $sql = str_replace("?", "%s", $sql); + foreach ($query->bindings as $i => $binding) { + if ($binding instanceof \DateTime) { + $query->bindings[$i] = $binding->format("'Y-m-d H:i:s'"); + } else { + if (is_string($binding)) { + $query->bindings[$i] = "'$binding'"; + } + } + } + $sql = vsprintf($sql, $query->bindings); + Statistic::sql($sql, $query->time, ['connection' => $query->connectionName]); + }); + } catch (\Throwable $e) { + } + } + + if (class_exists(\Illuminate\Redis\Events\CommandExecuted::class)) { + foreach (config('redis', []) as $key => $config) { + if (strpos($key, 'redis-queue') !== false) { + continue; + } + try { + \support\Redis::connection($key)->listen(function (\Illuminate\Redis\Events\CommandExecuted $command) { + $parameters = array_map(function ($item) { + if (is_array($item)) { + return json_encode($item, 320); + } + return $item; + }, $command->parameters); + $parameters = implode('\', \'', $parameters); + if ('get' === $command->command && 'ping' === $parameters) { + return; + } + Statistic::redis($command->command, $parameters, $command->time, ['connection' => $command->connectionName]); + }); + } catch (\Throwable $e) { + } + } + } + } + + /** + * 监控进程 + * + * @author HSK + * @date 2022-07-21 13:17:14 + * + * @param \Workerman\Worker $worker + * + * @return void + */ + protected static function monitor(\Workerman\Worker $worker) + { + // 接管所有进程 onWorkerStop 回调,上报缓存数据 + $oldWorkerStop = $worker->onWorkerStop; + $worker->onWorkerStop = function ($worker) use (&$oldWorkerStop) { + Statistic::report(); + + try { + if (is_callable($oldWorkerStop)) { + call_user_func($oldWorkerStop, $worker); + } + } catch (\Throwable $exception) { + } catch (\Exception $exception) { + } catch (\Error $exception) { + } finally { + if (isset($exception)) { + echo $exception . PHP_EOL; + } + } + }; + + // 接管自定义进程 onMessage 回调,监控其异常抛出 + if (config('server.listen') !== $worker->getSocketName()) { + $oldMessage = $worker->onMessage; + $worker->onMessage = function ($connection, $message) use (&$oldMessage) { + try { + if (is_callable($oldMessage)) { + call_user_func($oldMessage, $connection, $message); + } + } catch (\Throwable $exception) { + } catch (\Exception $exception) { + } catch (\Error $exception) { + } finally { + if (isset($exception)) { + Statistic::exception($exception); + echo $exception . PHP_EOL; + } + } + }; + } + } } diff --git a/src/Middleware.php b/src/Middleware.php index f8d76e8..aece7ec 100644 --- a/src/Middleware.php +++ b/src/Middleware.php @@ -53,29 +53,26 @@ public function process(\Webman\Http\Request $request, callable $next): \Webman\ } if (class_exists(\Illuminate\Database\Events\QueryExecuted::class)) { - foreach (config('database.connections', []) as $key => $config) { - $driver = $config['driver'] ?? 'mysql'; - try { - \support\Db::connection($key)->listen(function (\Illuminate\Database\Events\QueryExecuted $query) use ($key, $driver) { - $sql = trim($query->sql); - if (strtolower($sql) === 'select 1') { - return; - } - $sql = str_replace("?", "%s", $sql); - foreach ($query->bindings as $i => $binding) { - if ($binding instanceof \DateTime) { - $query->bindings[$i] = $binding->format("'Y-m-d H:i:s'"); - } else { - if (is_string($binding)) { - $query->bindings[$i] = "'$binding'"; - } + try { + \support\Db::listen(function (\Illuminate\Database\Events\QueryExecuted $query) { + $sql = trim($query->sql); + if (strtolower($sql) === 'select 1') { + return; + } + $sql = str_replace("?", "%s", $sql); + foreach ($query->bindings as $i => $binding) { + if ($binding instanceof \DateTime) { + $query->bindings[$i] = $binding->format("'Y-m-d H:i:s'"); + } else { + if (is_string($binding)) { + $query->bindings[$i] = "'$binding'"; } } - $log = vsprintf($sql, $query->bindings); - $this->sqlLogs[] = "[driver:$driver] [connection:$key] $log [ RunTime: {$query->time} ms ]"; - }); - } catch (\Throwable $e) { - } + } + $log = vsprintf($sql, $query->bindings); + $this->sqlLogs[] = "[connection:{$query->connectionName}] $log [ RunTime: {$query->time} ms ]"; + }); + } catch (\Throwable $e) { } } @@ -85,7 +82,7 @@ public function process(\Webman\Http\Request $request, callable $next): \Webman\ continue; } try { - \support\Redis::connection($key)->listen(function (\Illuminate\Redis\Events\CommandExecuted $command) use ($key) { + \support\Redis::connection($key)->listen(function (\Illuminate\Redis\Events\CommandExecuted $command) { $parameters = array_map(function ($item) { if (is_array($item)) { return json_encode($item, 320); @@ -98,7 +95,7 @@ public function process(\Webman\Http\Request $request, callable $next): \Webman\ return; } - $this->redisLogs[] = "[connection:$key] Redis::{$command->command}('" . $parameters . "') [ RunTime: {$command->time} ms ]"; + $this->redisLogs[] = "[connection:{$command->connectionName}] Redis::{$command->command}('" . $parameters . "') [ RunTime: {$command->time} ms ]"; }); } catch (\Throwable $e) { } @@ -110,6 +107,7 @@ public function process(\Webman\Http\Request $request, callable $next): \Webman\ switch (true) { case method_exists($response, 'exception') && $exception = $response->exception(): + \Hsk99\WebmanStatistic\Statistic::exception($exception); $body = (string)$exception; break; case 'application/json' === strtolower($response->getHeader('Content-Type')): diff --git a/src/Statistic.php b/src/Statistic.php index aa815e9..168b0eb 100644 --- a/src/Statistic.php +++ b/src/Statistic.php @@ -2,6 +2,8 @@ namespace Hsk99\WebmanStatistic; +use Hsk99\WebmanStatistic\Bootstrap; + class Statistic { /** @@ -10,6 +12,8 @@ class Statistic public static $transfer = ''; /** + * 数据上报 + * * @author HSK * @date 2022-06-23 15:10:07 * @@ -22,7 +26,7 @@ public static function report() return; } - \Hsk99\WebmanStatistic\Bootstrap::instance()->request( + Bootstrap::instance()->request( config('plugin.hsk99.statistic.app.address'), [ 'method' => 'POST', @@ -45,4 +49,152 @@ public static function report() } catch (\Throwable $th) { } } + + /** + * 异常信息 + * + * @author HSK + * @date 2022-07-21 16:23:09 + * + * @param \Throwable|\Exception|\Error|string $exception + * @param array $extra + * + * @return void + */ + public static function exception($exception, $extra = []) + { + try { + $time = microtime(true); + $details = [ + 'time' => date('Y-m-d H:i:s.', (int)$time) . substr($time, 11), + 'process' => Bootstrap::$process, + 'exception' => (string)$exception + ] + $extra; + + if ( + $exception instanceof \Throwable || + $exception instanceof \Exception || + $exception instanceof \Error + ) { + $code = $exception->getCode(); + $transfer = Bootstrap::$process . ' ' . $exception->getMessage(); + } else { + $code = 500; + $transfer = Bootstrap::$process . ' ' . md5($exception); + } + + static::$transfer .= json_encode([ + 'time' => date('Y-m-d H:i:s.', (int)$time) . substr($time, 11), + 'project' => config('plugin.hsk99.statistic.app.project') . "-Exception", + 'ip' => '127.0.0.1', + 'transfer' => $transfer, + 'costTime' => 0, + 'success' => 0, + 'code' => $code, + 'details' => json_encode($details, 320), + ], 320) . "\n"; + + if (strlen(static::$transfer) > 1024 * 1024) { + static::report(); + } + } catch (\Throwable $th) { + } + } + + /** + * SQL信息 + * + * @author HSK + * @date 2022-07-22 15:43:51 + * + * @param string $sql + * @param float $runtime + * @param array $extra + * + * @return void + */ + public static function sql(string $sql, float $runtime, $extra = []) + { + try { + $time = microtime(true); + $details = [ + 'time' => date('Y-m-d H:i:s.', (int)$time) . substr($time, 11), + 'process' => Bootstrap::$process, + 'sql' => $sql, + 'run_time' => $runtime . " ms" + ] + $extra; + + try { + $parser = new \PhpMyAdmin\SqlParser\Parser($sql); + $flags = \PhpMyAdmin\SqlParser\Utils\Query::getFlags($parser->statements[0]); + $tables = \PhpMyAdmin\SqlParser\Utils\Query::getTables($parser->statements[0]); + + if ('SHOW' === $flags['querytype']) { + $transfer = Bootstrap::$process . ' ' . $sql; + } else { + $transfer = Bootstrap::$process . ' ' . $flags['querytype'] . " " . implode(",", $tables); + } + } catch (\Throwable $th) { + $transfer = Bootstrap::$process . ' ' . md5($sql); + } + + static::$transfer .= json_encode([ + 'time' => date('Y-m-d H:i:s.', (int)$time) . substr($time, 11), + 'project' => config('plugin.hsk99.statistic.app.project') . "-SQL", + 'ip' => '127.0.0.1', + 'transfer' => $transfer, + 'costTime' => $runtime / 1000, + 'success' => 1, + 'code' => 3306, + 'details' => json_encode($details, 320), + ], 320) . "\n"; + + if (strlen(static::$transfer) > 1024 * 1024) { + static::report(); + } + } catch (\Throwable $th) { + } + } + + /** + * Redis信息 + * + * @author HSK + * @date 2022-07-22 15:51:14 + * + * @param string $command + * @param string $parameters + * @param float $runtime + * @param array $extra + * + * @return void + */ + public static function redis(string $command, string $parameters, float $runtime, $extra = []) + { + try { + $time = microtime(true); + $details = [ + 'time' => date('Y-m-d H:i:s.', (int)$time) . substr($time, 11), + 'process' => Bootstrap::$process, + 'command' => "Redis::{$command}('" . $parameters . "')", + 'run_time' => $runtime . " ms" + ] + $extra; + + static::$transfer .= json_encode([ + 'time' => date('Y-m-d H:i:s.', (int)$time) . substr($time, 11), + 'project' => config('plugin.hsk99.statistic.app.project') . "-Redis", + 'ip' => '127.0.0.1', + 'transfer' => Bootstrap::$process . ' ' . "Redis::{$command}", + 'costTime' => $runtime / 1000, + 'success' => 1, + 'code' => 6379, + 'details' => json_encode($details, 320), + ], 320) . "\n"; + + if (strlen(static::$transfer) > 1024 * 1024) { + static::report(); + } + } catch (\Throwable $th) { + } + } } diff --git a/src/config/plugin/hsk99/statistic/route.php b/src/config/plugin/hsk99/statistic/route.php index 599e60c..8f5436b 100644 --- a/src/config/plugin/hsk99/statistic/route.php +++ b/src/config/plugin/hsk99/statistic/route.php @@ -22,6 +22,7 @@ switch (true) { case method_exists($response, 'exception') && $exception = $response->exception(): + \Hsk99\WebmanStatistic\Statistic::exception($exception); $body = (string)$exception; break; case 'application/json' === strtolower($response->getHeader('Content-Type')):