README.md000064400000001074144760133240006031 0ustar00 # webman-statistic 应用监控插件 ## 简介 > 用于监控上报 [webman](https://github.com/walkor/webman) 应用调用、请求量、调用耗时、状态码异常、sql日志、redis日志等。配合 [TransferStatistics](https://github.com/hsk99/transfer-statistics) v2+ 使用查看上报数据 ## 安装 ` composer require hsk99/webman-statistic ` ## 配置 修改 ` config/plugin/hsk99/statistic/app.php ` 文件,设置相关参数 ## 注意 ` illuminate/database ` 、` illuminate/redis ` 日志需要安装 ` composer require illuminate/events `composer.json000064400000000744144760133240007277 0ustar00{ "name": "hsk99/webman-statistic", "type": "library", "keywords": [ "webman" ], "homepage": "http://www.workerman.net/webman", "license": "MIT", "description": "webman-statistic 应用监控插件", "authors": [ { "name": "hsk99", "email": "hsk@hsk99.com.cn", "homepage": "http://hsk99.com.cn" } ], "require": { "workerman/http-client": "^1.0", "phpmyadmin/sql-parser": "^5.5" }, "autoload": { "psr-4": { "Hsk99\\WebmanStatistic\\": "src" } } }src/Bootstrap.php000064400000010541144760133240010026 0ustar00name; $options = [ 'max_conn_per_addr' => 128, // 每个地址最多维持多少并发连接 'keepalive_timeout' => 15, // 连接多长时间不通讯就关闭 'connect_timeout' => 30, // 连接超时时间 'timeout' => 30, // 等待响应的超时时间 ]; self::$_instance = new \Workerman\Http\Client($options); // 定时上报数据 \Workerman\Timer::add(config('plugin.hsk99.statistic.app.interval', 30), function () { Statistic::report(); }); // 执行监听所有进程 SQL、Redis if (config('plugin.hsk99.statistic.app.global_monitor', false)) { static::listen($worker); } } } /** * @author HSK * @date 2022-06-17 15:34:41 * * @return \Workerman\Http\Client */ 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) { } } } } } src/Install.php000064400000003017144760133240007457 0ustar00 'config/plugin/hsk99/statistic', ]; /** * Install * @return void */ public static function install() { static::installByRelation(); } /** * Uninstall * @return void */ public static function uninstall() { self::uninstallByRelation(); } /** * installByRelation * @return void */ public static function installByRelation() { foreach (static::$pathRelation as $source => $dest) { if ($pos = strrpos($dest, '/')) { $parent_dir = base_path() . '/' . substr($dest, 0, $pos); if (!is_dir($parent_dir)) { mkdir($parent_dir, 0777, true); } } //symlink(__DIR__ . "/$source", base_path()."/$dest"); copy_dir(__DIR__ . "/$source", base_path() . "/$dest"); } } /** * uninstallByRelation * @return void */ public static function uninstallByRelation() { foreach (static::$pathRelation as $source => $dest) { $path = base_path() . "/$dest"; if (!is_dir($path) && !is_file($path)) { continue; } /*if (is_link($path) { unlink($path); }*/ remove_dir($path); } } } src/Middleware.php000064400000015517144760133240010136 0ustar00getRealIp($safe_mode = true); $transfer = $request->controller . '::' . $request->action; if ('::' === $transfer) { $transfer = $request->path(); } /** * @var \Webman\Http\Response */ $response = $next($request); $finishTime = microtime(true); $costTime = $finishTime - $startTime; static $initialized; if (!$initialized) { if (class_exists(\think\facade\Db::class)) { \think\facade\Db::listen(function ($sql, $runtime, $master) { if ($sql === 'select 1' || !is_numeric($runtime)) { return; } $this->sqlLogs[] = trim($sql) . " [ RunTime: " . $runtime * 1000 . " ms ]"; }); } 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'"; } } } $log = vsprintf($sql, $query->bindings); $this->sqlLogs[] = "[connection:{$query->connectionName}] $log [ RunTime: {$query->time} ms ]"; }); } 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; } $this->redisLogs[] = "[connection:{$command->connectionName}] Redis::{$command->command}('" . $parameters . "') [ RunTime: {$command->time} ms ]"; }); } catch (\Throwable $e) { } } } $initialized = true; } 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')): $body = json_decode($response->rawBody(), true); break; default: $body = 'Non Json data'; break; } $code = $response->getStatusCode(); $success = $code < 400; $details = [ 'time' => date('Y-m-d H:i:s.', (int)$startTime) . substr($startTime, 11), // 请求时间(包含毫秒时间) 'run_time' => $costTime, // 运行时长 'ip' => $request->getRealIp($safe_mode = true) ?? '', // 请求客户端IP 'url' => $request->fullUrl() ?? '', // 请求URL 'method' => $request->method() ?? '', // 请求方法 'request_param' => $request->all() ?? [], // 请求参数 'request_header' => $request->header() ?? [], // 请求头 'cookie' => $request->cookie() ?? [], // 请求cookie 'session' => $request->session()->all() ?? [], // 请求session 'response_code' => $response->getStatusCode() ?? '', // 响应码 'response_header' => $response->getHeaders() ?? [], // 响应头 'response_body' => $body, // 响应数据 'sql' => $this->sqlLogs, // 运行SQL 'redis' => $this->redisLogs, // 运行Redis ]; $this->sqlLogs = []; $this->redisLogs = []; \Hsk99\WebmanStatistic\Statistic::$transfer .= json_encode([ 'time' => date('Y-m-d H:i:s.', (int)$startTime) . substr($startTime, 11), 'project' => $project, 'ip' => $ip, 'transfer' => $transfer, 'costTime' => $costTime, 'success' => $success ? 1 : 0, 'code' => $code, 'details' => json_encode($details, 320), ], 320) . "\n"; if (strlen(\Hsk99\WebmanStatistic\Statistic::$transfer) > 1024 * 1024) { \Hsk99\WebmanStatistic\Statistic::report(); } return $response; } } src/Statistic.php000064400000015203144760133240010020 0ustar00request( config('plugin.hsk99.statistic.app.address'), [ 'method' => 'POST', 'version' => '1.1', 'headers' => [ 'Connection' => 'keep-alive', 'authorization' => config('plugin.hsk99.statistic.app.authorization', md5(date('Y'))) ], 'data' => [ 'transfer' => static::$transfer ], 'success' => function ($response) { }, 'error' => function ($exception) { } ] ); static::$transfer = ''; } catch (\Throwable $th) { static::$transfer = ''; } } /** * 异常信息 * * @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 { if (!config('plugin.hsk99.statistic.app.enable')) { return; } $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 { if (!config('plugin.hsk99.statistic.app.enable')) { return; } $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 { if (!config('plugin.hsk99.statistic.app.enable')) { return; } $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) { } } } src/config/plugin/hsk99/statistic/app.php000064400000001063144760133240014351 0ustar00 true, 'project' => 'webman', // 应用名 'interval' => 30, // 上报间隔 'address' => 'http://127.0.0.1:8788/report/statistic/transfer', // 上报地址 'authorization' => null, // 上报认证key,默认 null 'global_monitor' => true, // 是否单独监听上报所有进程SQL、Redis ]; src/config/plugin/hsk99/statistic/bootstrap.php000064400000000077144760133240015612 0ustar00 [ Hsk99\WebmanStatistic\Middleware::class, ] ]; src/config/plugin/hsk99/statistic/route.php000064400000006104144760133240014730 0ustar00getRealIp($safe_mode = true); $transfer = $request->path(); if ($oldFallback instanceof \Closure) { $response = $oldFallback($request); } else { $response = new Response(404, [], \file_get_contents(public_path() . '/404.html')); } $finishTime = microtime(true); $costTime = $finishTime - $startTime; 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')): $body = json_decode($response->rawBody(), true); break; default: $body = 'Non Json data'; break; } $code = $response->getStatusCode(); $success = $code < 400; $details = [ 'time' => date('Y-m-d H:i:s.', (int)$startTime) . substr($startTime, 11), // 请求时间(包含毫秒时间) 'run_time' => $costTime, // 运行时长 'ip' => $request->getRealIp($safe_mode = true) ?? '', // 请求客户端IP 'url' => $request->fullUrl() ?? '', // 请求URL 'method' => $request->method() ?? '', // 请求方法 'request_param' => $request->all() ?? [], // 请求参数 'request_header' => $request->header() ?? [], // 请求头 'cookie' => $request->cookie() ?? [], // 请求cookie 'session' => $request->session()->all() ?? [], // 请求session 'response_code' => $response->getStatusCode() ?? '', // 响应码 'response_header' => $response->getHeaders() ?? [], // 响应头 'response_body' => $body, // 响应数据 ]; Statistic::$transfer .= json_encode([ 'time' => date('Y-m-d H:i:s.', (int)$startTime) . substr($startTime, 11), 'project' => $project, 'ip' => $ip, 'transfer' => $transfer, 'costTime' => $costTime, 'success' => $success ? 1 : 0, 'code' => $code, 'details' => json_encode($details, 320), ], 320) . "\n"; if (strlen(\Hsk99\WebmanStatistic\Statistic::$transfer) > 1024 * 1024) { \Hsk99\WebmanStatistic\Statistic::report(); } return $response; });