diff --git a/app/common/command/Test.php b/app/common/command/Test.php new file mode 100644 index 0000000..48c7679 --- /dev/null +++ b/app/common/command/Test.php @@ -0,0 +1,9 @@ +table('debug_log'); + + $table->changeColumn('content', AdapterInterface::PHINX_TYPE_TEXT, ['length' => MysqlAdapter::TEXT_LONG]); + + $table->update(); + } +} diff --git a/extend/base/common/command/TestBase.php b/extend/base/common/command/TestBase.php new file mode 100644 index 0000000..28c4ff8 --- /dev/null +++ b/extend/base/common/command/TestBase.php @@ -0,0 +1,58 @@ + LogTestService::class, + ]; + + protected function configure() + { + // 指令配置 + $this->setName('test') + ->addArgument('program', Option::VALUE_REQUIRED, '测试项目') + ->setDescription('the admin:update command'); + } + + protected function execute(Input $input, Output $output) + { + $program = $input->getArgument('program'); + + if (empty($program)) { + $output->writeln('请输入测试项目'); + + return; + } + + if (!isset($this->program[$program])) { + $output->writeln('测试项目不存在'); + + return; + } + + $class = $this->program[$program]; + + $run = $class::RUN; + + $instance = new $class(); + + $output->writeln('测试项目名称:' . $instance->getName()); + $output->writeln('测试项目描述:' . $instance->getDesc()); + + if ($instance instanceof CommandTestInterface) { + $instance->setInput($input); + $instance->setOutput($output); + } + + $instance->$run(); + } +} diff --git a/extend/base/common/command/admin/VersionBase.php b/extend/base/common/command/admin/VersionBase.php index 18cd59f..5710906 100644 --- a/extend/base/common/command/admin/VersionBase.php +++ b/extend/base/common/command/admin/VersionBase.php @@ -12,12 +12,14 @@ use think\console\Output; class VersionBase extends Command { - public const VERSION = 'v2.0.85'; + public const VERSION = 'v2.0.86'; public const LAYUI_VERSION = '2.8.17'; public const COMMENT = [ - '优化搜索查询构造逻辑', + '增加测试机制', + '完善DebugMysql日志逻辑', + '更新debug_log表字段格式', '发布新版本', ]; diff --git a/extend/base/common/interface/test/CommandTestInterfaceBase.php b/extend/base/common/interface/test/CommandTestInterfaceBase.php new file mode 100644 index 0000000..c380496 --- /dev/null +++ b/extend/base/common/interface/test/CommandTestInterfaceBase.php @@ -0,0 +1,19 @@ +getLogInfo(); + $this->output->writeln(str_repeat('=', 50)); + $this->testLogContent(); + $this->output->writeln(str_repeat('=', 50)); + $this->testIoTimes(); + $this->output->writeln(str_repeat('=', 50)); + $this->testIoSize(); + $this->output->writeln(str_repeat('=', 50)); + $this->testNetworkChange(); + $this->output->writeln(str_repeat('=', 50)); + + $this->output->writeln($this->configContent); + + $this->output->writeln('测试结果汇总'); + + + $output_table = new Table(); + + $output_table->setHeader(['测试项', '测试描述', '测试结果']); + + $output_table->setRows($this->summary); + + $table_content = $output_table->render(); + + $this->output->writeln($table_content); + } + + protected function getLogInfo() + { + $this->output->writeln('当前日志配置项'); + + $config = Log::getConfig(); + + $table = array_to_table($config); + + $output_table = new Table(); + + $output_table->setHeader(['配置项', '配置值']); + + $output_table->setRows($table); + + $table_content = $output_table->render(); + + $this->configContent = $table_content; + + $this->output->writeln($table_content); + + $this->output->writeln('当前日志驱动:' . $config['default']); + } + + public function testIoTimes() + { + $this->output->writeln('测试写入100条记录,统计写入时间'); + + $start_time = microtime(true); + + $total_times = 100; + for ($i = 0; $i < $total_times; $i++) { + $log_content = date('Y-m-d H:i:s'); + + Log::record($log_content, 'info'); + + $this->output->writeln("({$i}/{$total_times})写入日志:{$log_content}"); + } + + $end_time = microtime(true); + + $time = $end_time - $start_time; + + $this->output->writeln('测试完成'); + + $this->output->writeln("总写入时间:{$time}秒"); + + $this->summary[] = [ + 'title' => '写入次数测试', + 'desc' => '测试写入100条记录,统计写入时间', + 'result' => "总写入时间:{$time}秒", + ]; + } + + public function testIoSize() + { + $this->output->writeln('测试大内容写入,统计写入时间和写入大小'); + + $this->output->writeln('程序会写入100条记录,间隔1秒,日志内容为100KB的字符串'); + + $num = 0; + $total = 100; + + $log_content = str_repeat('test', 100 * 1000); + + $total_size = 0; + $total_time = 0; + + while ($num < $total) { + $num++; + + $start_time = microtime(true); + + Log::record($log_content, 'info'); + + $end_time = microtime(true); + + $time = $end_time - $start_time; + + $total_time += $time; + + $size = strlen($log_content); + + $total_size += $size; + + $size = format_bytes($size); + $this->output->writeln("({$num}/{$total})写入大小:{$size},写入时间:{$time}秒"); + } + + $this->output->writeln('测试完成'); + + $total_size = format_bytes($total_size); + $this->output->writeln("总写入大小:{$total_size},总写入时间:{$total_time}秒"); + + $this->summary[] = [ + 'title' => '写入大小测试', + 'desc' => '测试大内容写入,统计写入时间和写入大小', + 'result' => "总写入大小:{$total_size},总写入时间:{$total_time}秒", + ]; + } + + public function testNetworkChange() + { + $this->output->writeln('测试网络切换'); + + $this->output->writeln('程序会写入30条记录,间隔1秒,日志内容为当前时间,测试期间可以反复断开网络连接,查看是否丢失或报错'); + + $num = 0; + $total = 30; + + while ($num < $total) { + $num++; + + $log_content = date('Y-m-d H:i:s'); + + Log::record($log_content, 'info'); + + $this->output->writeln("({$num}/{$total})写入日志:{$log_content}"); + + sleep(1); + } + + $this->output->writeln('测试完成'); + + $this->summary[] = [ + 'title' => '网络切换测试', + 'desc' => '测试网络切换', + 'result' => '请查看是否有丢失', + ]; + } + + public function testLogContent() + { + $this->output->writeln('测试日志内容兼容性'); + + $test_content_item = []; + + // 生成测试内容,包括简单英文字符串、中文字符串、大文本、数字、大数字、负数、负数大数字、数组、对象、资源、布尔值、null、空字符串、空数组、空对象、空资源、空布尔值 + $test_content_item[] = [ + 'name' => '简单字符串', + 'content' => 'test', + ]; + $test_content_item[] = [ + 'name' => '中文字符串', + 'content' => '测试', + ]; + $test_content_item[] = [ + 'name' => '大文本', + 'content' => str_repeat('test测试', 1000), + ]; + $test_content_item[] = [ + 'name' => '数字', + 'content' => 1, + ]; + $test_content_item[] = [ + 'name' => '大数字', + 'content' => 100000 * 100000, + ]; + $test_content_item[] = [ + 'name' => '负数', + 'content' => -1, + ]; + $test_content_item[] = [ + 'name' => '负数大数字', + 'content' => -100000 * 100000, + ]; + $test_content_item[] = [ + 'name' => '数组', + 'content' => [ + 'test' => 'test', + '测试' => '测试', + ], + ]; + $test_content_item[] = [ + 'name' => '对象', + 'content' => new \stdClass(), + ]; + $test_content_item[] = [ + 'name' => '资源', + 'content' => fopen(__FILE__, 'r'), + ]; + $test_content_item[] = [ + 'name' => '布尔值', + 'content' => true, + ]; + $test_content_item[] = [ + 'name' => 'null', + 'content' => null, + ]; + $test_content_item[] = [ + 'name' => '空字符串', + 'content' => '', + ]; + $test_content_item[] = [ + 'name' => '空数组', + 'content' => [], + ]; + $test_content_item[] = [ + 'name' => '空对象', + 'content' => new \stdClass(), + ]; + $test_content_item[] = [ + 'name' => '空资源', + 'content' => fopen('php://memory', 'r'), + ]; + $test_content_item[] = [ + 'name' => '空布尔值', + 'content' => false, + ]; + $test_content_item[] = [ + 'name' => '异常', + 'content' => new \Exception('test'), + ]; + + $fail_count = 0; + + foreach ($test_content_item as $key => $value) { + try { + $total_count = count($test_content_item); + $num = $key + 1; + + Log::record($value['content'], 'info'); + $this->output->writeln("({$num}/{$total_count})" . '测试内容:' . $value['name'] . ',测试结果:成功'); + } catch (\Throwable $th) { + $this->output->writeln('测试内容失败:' . $value['name'] . ',测试结果:' . $th->getMessage()); + + if ($this->output->isDebug()) { + $this->output->error($th); + break; + } + + $fail_count++; + } + } + + $this->output->writeln('测试完成'); + + $this->summary[] = [ + 'title' => '日志内容兼容性测试', + 'desc' => '测试日志内容兼容性', + 'result' => "测试完成,失败{$fail_count}个", + ]; + } + + public function setOutput(Output $output) + { + $this->output = $output; + } + + public function setInput(Input $input) + { + $this->input = $input; + } +} diff --git a/extend/base/helper.php b/extend/base/helper.php index 0af9d0c..e8b3f50 100644 --- a/extend/base/helper.php +++ b/extend/base/helper.php @@ -167,9 +167,9 @@ if (!function_exists('auth')) { * auth权限验证 * @param $node * @return bool - * @throws \think\db\exception\DataNotFoundException - * @throws \think\db\exception\DbException - * @throws \think\db\exception\ModelNotFoundException + * @throws think\db\exception\DataNotFoundException + * @throws think\db\exception\DbException + * @throws think\db\exception\ModelNotFoundException */ function auth($node = null) { @@ -321,3 +321,31 @@ function app_file_path($file_path) return $app_file_path; } + +function array_to_table($array_tree, $prefix_key = '') +{ + $table = []; + + foreach ($array_tree as $key => $value) { + if (is_array($value)) { + $table = array_merge($table, array_to_table($value, $key . '.')); + } else { + $table[] = [ + 'key' => $prefix_key . $key, + 'value' => $value, + ]; + } + } + + return $table; +} + +function format_bytes($size, $delimiter = '') +{ + $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + for ($i = 0; $size >= 1024 && $i < 5; $i++) { + $size /= 1024; + } + + return round($size, 2) . $delimiter . $units[$i]; +} diff --git a/extend/think/UlthonAdminService.php b/extend/think/UlthonAdminService.php index d7644c1..a5f169f 100644 --- a/extend/think/UlthonAdminService.php +++ b/extend/think/UlthonAdminService.php @@ -2,6 +2,7 @@ namespace think; +use app\common\command\Test; use app\common\event\AdminLoginSuccess\LogEvent; use app\common\event\AdminLoginType\DemoEvent; use app\common\provider\ExceptionHandle; @@ -10,9 +11,9 @@ use app\common\provider\View; use think\app\Service as AppService; use think\captcha\CaptchaService; use think\facade\App; -use think\migration\Service; +use think\migration\Service as MigrateService; -class UlthonAdminService extends \think\Service +class UlthonAdminService extends Service { public function boot() { @@ -40,10 +41,14 @@ class UlthonAdminService extends \think\Service $this->app->register(AppService::class); // 注册数据库迁移服务 - $this->app->register(Service::class); + $this->app->register(MigrateService::class); + + // 绑定命令行 + $this->commands([ + Test::class, + ]); // 绑定标识容器 - $provider_default = [ 'think\Request' => Request::class, 'think\exception\Handle' => ExceptionHandle::class, @@ -68,7 +73,7 @@ class UlthonAdminService extends \think\Service // 多语言加载 // \think\middleware\LoadLangPack::class, // Session初始化 - 100 => \think\middleware\SessionInit::class, + 100 => middleware\SessionInit::class, ]; $this->app->middleware->import($middleware); diff --git a/extend/think/log/driver/DebugMysql.php b/extend/think/log/driver/DebugMysql.php index f5687f9..524ae5c 100644 --- a/extend/think/log/driver/DebugMysql.php +++ b/extend/think/log/driver/DebugMysql.php @@ -2,77 +2,90 @@ namespace think\log\driver; +use PDO; use think\contract\LogHandlerInterface; use think\facade\App; -use PDO; -class DebugMysql implements LogHandlerInterface +class DebugMysql implements LogHandlerInterface { - protected $enableLog = true; protected $config = []; + /** + * @var PDO + */ protected $pdo = null; + protected $file = null; + protected $fileRescource = null; protected $tableName = ''; + protected $reConnectTimes = 0; + + protected $fileLogTimes = 0; + + public $devMode = true; + + /** + * 服务器断线标识字符. + * + * @var array + */ + protected $breakMatchStr = [ + 'server has gone away', + 'no connection to the server', + 'Lost connection', + 'is dead or not enabled', + 'Error while sending', + 'decryption failed or bad record mac', + 'server closed the connection unexpectedly', + 'SSL connection has been closed unexpectedly', + 'Error writing data to the connection', + 'Resource deadlock avoided', + 'failed with errno', + 'child connection forced to terminate due to client_idle_limit', + 'query_wait_timeout', + 'reset by peer', + 'Physical connection is not usable', + 'TCP Provider: Error code 0x68', + 'ORA-03114', + 'Packets out of order. Expected', + 'Adaptive Server connection failed', + 'Communication link failure', + 'connection is no longer usable', + 'Login timeout expired', + 'SQLSTATE[HY000] [2002] Connection refused', + 'running with the --read-only option so it cannot execute this statement', + 'The connection is broken and recovery is not possible. The connection is marked by the client driver as unrecoverable. No attempt was made to restore the connection.', + 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Try again', + 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Name or service not known', + 'SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: EOF detected', + 'SQLSTATE[HY000] [2002] Connection timed out', + 'SSL: Connection timed out', + 'SQLSTATE[HY000]: General error: 1105 The last transaction was aborted due to Seamless Scaling. Please retry.', + ]; + public function __construct(App $app, $config = []) { if (is_array($config)) { $this->config = array_merge($this->config, $config); } - $dsn = $this->parseDsn($config); - try { - - $pdo = $this->createPdo($dsn, $config['username'], $config['password'], $config['params']); - - $this->pdo = $pdo; + $this->initConnect(); } catch (\Throwable $th) { $this->pdo = null; - - $log_path = App::getRuntimePath() . 'log/' . date('ymd') . '.csv'; - - $dirname = dirname($log_path); - - if (!is_dir($dirname)) { - mkdir($log_path, 0777, true); - } - - $first_line = false; - if (!file_exists($log_path)) { - $first_line = true; - } - - $this->fileRescource = fopen($log_path, 'a'); - - if ($first_line) { - $fields = [ - 'level', - 'content', - 'create_time', - 'create_time_title', - 'uid', - 'app_name', - 'controller_name', - 'action_name', - ]; - fputcsv($this->fileRescource, $fields); - } + $this->initFile(); } - $this->tableName = $config['prefix'] . 'debug_log'; } public function save(array $log): bool { - - $app_name = app('http')->getName() ?: ''; $controller_name = ''; @@ -81,7 +94,6 @@ class DebugMysql implements LogHandlerInterface if (App::runningInConsole()) { $app_name = 'cli'; } else { - $controller_name = request()->controller(); $action_name = request()->action(); } @@ -100,7 +112,10 @@ class DebugMysql implements LogHandlerInterface foreach ($log as $log_level => $log_list) { foreach ($log_list as $key => $log_item) { - + if (!is_string($log_item)) { + $log_item = print_r($log_item, true); + } + $log_data = [ 'level' => $log_level, 'content' => $log_item, @@ -109,30 +124,17 @@ class DebugMysql implements LogHandlerInterface 'uid' => $log_key, 'app_name' => $app_name, 'controller_name' => $controller_name, - 'action_name' => $action_name + 'action_name' => $action_name, ]; - if (!is_null($this->pdo)) { - - $prepare_name = []; - foreach ($log_data as $key => $value) { - $prepare_name[] = ':' . $key; + try { + if (!is_null($this->pdo)) { + $this->saveByConnect($log_data); + } else { + $this->saveByFile($log_data); } - - $data_keys = array_keys($log_data); - - $data_keys_in_sql = join(',', $data_keys); - - $prepare_name_in_sql = join(',', $prepare_name); - - $sql = "INSERT INTO {$this->tableName} ($data_keys_in_sql) VALUES ($prepare_name_in_sql);"; - - $stmt = $this->pdo->prepare($sql); - - $stmt->execute($log_data); - } else { - - fputcsv($this->fileRescource, $log_data); + } catch (\Throwable $th) { + $this->saveByFile($log_data); } } } @@ -140,9 +142,156 @@ class DebugMysql implements LogHandlerInterface return true; } + protected function saveByConnect($log_data) + { + if (is_null($this->pdo)) { + $this->saveByFile($log_data); + + return; + } + + $this->devLog('save by connect'); + $prepare_name = []; + foreach ($log_data as $key => $value) { + $prepare_name[] = ':' . $key; + } + + $data_keys = array_keys($log_data); + + $data_keys_in_sql = implode(',', $data_keys); + + $prepare_name_in_sql = implode(',', $prepare_name); + + $sql = "INSERT INTO {$this->tableName} ($data_keys_in_sql) VALUES ($prepare_name_in_sql);"; + + try { + $stmt = $this->pdo->prepare($sql); + $stmt->execute($log_data); + } catch (\Exception $th) { + if ($this->isBreak($th)) { + if ($this->reConnectTimes > 3) { + $this->initFile(); + throw $th; + } + $this->initConnect(); + $this->reConnectTimes++; + $this->devLog('reconnect ' . $this->reConnectTimes); + $this->saveByConnect($log_data); + } else { + dump($th); + $this->saveByFile($log_data); + } + } + } + + protected function saveByFile($log_data) + { + $this->devLog('save by file'); + + // 如果文件日志超过100条,尝试重新通过数据库连接 + if ($this->fileLogTimes > 10) { + $this->fileLogTimes = 0; + $this->initConnect(); + $this->saveByConnect($log_data); + + return; + } + + try { + fputcsv($this->fileRescource, $log_data); + $this->fileLogTimes++; + } catch (\Throwable $th) { + $this->initFile(); + $this->fileLogTimes++; + $this->saveByFile($log_data); + } + } + + protected function initConnect() + { + $this->devLog('init connect'); + + if (!is_null($this->pdo)) { + $this->pdo = null; + } + + $this->reConnectTimes = 0; + + $config = $this->config; + + $dsn = $this->parseDsn($config); + try { + $pdo = $this->createPdo($dsn, $config['username'], $config['password'], $config['params']); + $this->pdo = $pdo; + } catch (\Throwable $th) { + $this->pdo = null; + } + + return $this; + } + + protected function initFile() + { + $this->devLog('init file'); + + if (!is_null($this->fileRescource)) { + return $this; + } + + $log_path = App::getRuntimePath() . 'log/' . date('ymd') . '.csv'; + + $dirname = dirname($log_path); + + if (!is_dir($dirname)) { + mkdir($log_path, 0777, true); + } + + $first_line = false; + if (!file_exists($log_path)) { + $first_line = true; + } + + $this->fileRescource = fopen($log_path, 'a'); + + if ($first_line) { + $fields = [ + 'level', + 'content', + 'create_time', + 'create_time_title', + 'uid', + 'app_name', + 'controller_name', + 'action_name', + ]; + fputcsv($this->fileRescource, $fields); + } + + return $this; + } + /** - * 解析pdo连接的dsn信息 - * @access protected + * 是否断线 + * + * @param \PDOException|\Exception $e 异常对象 + * + * @return bool + */ + protected function isBreak($e): bool + { + $error = $e->getMessage(); + + foreach ($this->breakMatchStr as $msg) { + if (false !== stripos($error, $msg)) { + return true; + } + } + + return false; + } + + /** + * 解析pdo连接的dsn信息. * @param array $config 连接信息 * @return string */ @@ -177,4 +326,11 @@ class DebugMysql implements LogHandlerInterface fclose($this->fileRescource); } } + + protected function devLog($content) + { + if ($this->devMode) { + dump($content); + } + } } diff --git a/extend/think/migration/Service.php b/extend/think/migration/Service.php index 614e9e0..0968279 100644 --- a/extend/think/migration/Service.php +++ b/extend/think/migration/Service.php @@ -27,6 +27,7 @@ class Service extends \think\Service public function boot() { + $this->app->bind(FakerGenerator::class, function () { return FakerFactory::create($this->app->config->get('app.faker_locale', 'zh_CN')); }); @@ -47,5 +48,6 @@ class Service extends \think\Service SeedRun::class, FactoryCreate::class, ]); + } }