README.md000064400000001074144760116150006032 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.json000064400000000744144760116150007300 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.php000064400000010736144760116150010035 0ustar00name; $options = [ 'max_conn_per_addr' => 128, // 每个地址最多维持多少并发连接 'keepalive_timeout' => 15, // 连接多长时间不通讯就关闭 'connect_timeout' => 30, // 连接超时时间 'timeout' => 30, // 等待响应的超时时间 ]; self::$_instance = new Client(config('plugin.hsk99.statistic.app.http_options', $options)); // 定时上报数据 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 Client */ public static function instance(): Client { return self::$_instance; } /** * SQL、Redis 监听 * * @author HSK * @date 2022-07-20 17:22:06 * * @param Worker $worker * * @return void */ protected static function listen(Worker $worker) { if (class_exists(ThinkDb::class)) { ThinkDb::listen(function ($sql, $runtime, $master) { if ($sql === 'select 1' || !is_numeric($runtime)) { return; } Statistic::sql(trim($sql), $runtime * 1000, ['master' => $master]); }); } if (class_exists(QueryExecuted::class)) { try { LaravelDb::listen(function (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 = $sql; try { $log = vsprintf($sql, $query->bindings); } catch (\Throwable $e) { } Statistic::sql($log, $query->time, ['connection' => $query->connectionName]); }); } catch (\Throwable $e) { } } if (class_exists(CommandExecuted::class)) { foreach (config('redis', []) as $key => $config) { if (strpos($key, 'redis-queue') !== false) { continue; } try { Redis::connection($key)->listen(function (CommandExecuted $command) { foreach ($command->parameters as &$item) { if (is_array($item)) { $item = implode('\', \'', $item); } } if ("Redis::get('ping')" === "Redis::{$command->command}('" . implode('\', \'', $command->parameters) . "')") { return; } Statistic::redis($command->command, implode('\', \'', $command->parameters), $command->time, ['connection' => $command->connectionName]); }); } catch (\Throwable $e) { } } } } } src/Install.php000064400000003017144760116150007460 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.php000064400000016257144760116150010141 0ustar00getRealIp($safe_mode = true); $transfer = $request->controller . '::' . $request->action; if ('::' === $transfer) { $transfer = $request->path(); } /** * @var Response */ $response = $next($request); $finishTime = microtime(true); $costTime = $finishTime - $startTime; static $initialized; if (!$initialized) { if (class_exists(ThinkDb::class)) { ThinkDb::listen(function ($sql, $runtime, $master) { if ($sql === 'select 1' || !is_numeric($runtime)) { return; } // 兼容 webman/log 插件记录Sql日志 if (class_exists(\Webman\Log\Middleware::class) && config('plugin.webman.log.app.enable', false)) { ThinkDb::log($sql . " [$master RunTime: " . $runtime * 1000 . " ms]"); } $this->sqlLogs[] = trim($sql) . " [ RunTime: " . $runtime * 1000 . " ms ]"; }); } if (class_exists(QueryExecuted::class)) { try { LaravelDb::listen(function (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 = $sql; try { $log = vsprintf($sql, $query->bindings); } catch (\Throwable $e) { } $this->sqlLogs[] = "[connection:{$query->connectionName}] $log [ RunTime: {$query->time} ms ]"; }); } catch (\Throwable $e) { } } if (class_exists(CommandExecuted::class)) { foreach (config('redis', []) as $key => $config) { if (strpos($key, 'redis-queue') !== false) { continue; } try { Redis::connection($key)->listen(function (CommandExecuted $command) { foreach ($command->parameters as &$item) { if (is_array($item)) { $item = implode('\', \'', $item); } } if ("Redis::get('ping')" === "Redis::{$command->command}('" . implode('\', \'', $command->parameters) . "')") { return; } $this->redisLogs[] = "[connection:{$command->connectionName}] Redis::{$command->command}('" . implode('\', \'', $command->parameters) . "') ({$command->time} ms)"; }); } catch (\Throwable $e) { } } } $initialized = true; } switch (true) { case method_exists($response, 'exception') && $exception = $response->exception(): Statistic::exception($exception); $body = (string)$exception; break; case !empty($response->getHeader('Content-Type')) && 'application/json' === strtolower($response->getHeader('Content-Type')): $body = json_decode($response->rawBody(), true); break; default: $body = 'Non Json data'; break; } $code = $response->getStatusCode(); $code = is_int($code) ? $code : 500; $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 = []; Statistic::setTransfer(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)); return $response; } } src/Statistic.php000064400000015464144760116150010032 0ustar00 1024 * 1024) { static::report(); } } /** * 数据上报 * * @author HSK * @date 2022-06-23 15:10:07 * * @return void */ public static function report() { try { if (!config('plugin.hsk99.statistic.app.enable')) { static::$_transferCache = null; return; } if (0 === strlen(static::$_transferCache)) { return; } Bootstrap::instance()->request( 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::$_transferCache ], 'success' => function ($response) { }, 'error' => function ($exception) { } ] ); static::$_transferCache = null; } catch (\Throwable $th) { static::$_transferCache = null; } } /** * 异常信息 * * @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::setTransfer(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)); } 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 Parser($sql); $flags = Query::getFlags($parser->statements[0]); $tables = 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::setTransfer(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)); } 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::setTransfer(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)); } catch (\Throwable $th) { } } } src/config/plugin/hsk99/statistic/app.php000064400000001564144760116150014360 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 'http_options' => [ 'max_conn_per_addr' => 128, // 每个地址最多维持多少并发连接 'keepalive_timeout' => 15, // 连接多长时间不通讯就关闭 'connect_timeout' => 1, // 连接超时时间 'timeout' => 1, // 等待响应的超时时间 ] ]; src/config/plugin/hsk99/statistic/bootstrap.php000064400000000077144760116150015613 0ustar00 [ Hsk99\WebmanStatistic\Middleware::class, ] ]; src/config/plugin/hsk99/statistic/route.php000064400000005745144760116150014743 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 !empty($response->getHeader('Content-Type')) && '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::setTransfer(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)); return $response; });