增加测试机制;完善DebugMysql日志逻辑;更新debug_log表字段格式;发布新版本

This commit is contained in:
2024-01-03 16:22:01 +08:00
parent 58bb8166c9
commit e6779f4921
14 changed files with 767 additions and 76 deletions

View File

@@ -0,0 +1,9 @@
<?php
namespace app\common\command;
use base\common\command\TestBase;
class Test extends TestBase
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace app\common\interface\test;
use base\common\interface\test\CommandTestInterfaceBase;
interface CommandTestInterface extends CommandTestInterfaceBase
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace app\common\service;
use base\common\service\TestServiceBase;
class TestService extends TestServiceBase
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace app\common\service\test;
use base\common\service\test\LogTesServicetBase;
class LogTestService extends LogTesServicetBase
{
}

View File

@@ -0,0 +1,38 @@
<?php
use Phinx\Db\Adapter\AdapterInterface;
use Phinx\Db\Adapter\MysqlAdapter;
use think\migration\Migrator;
class FixDebugLogContent extends Migrator
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* renameColumn
* addIndex
* addForeignKey
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$table = $this->table('debug_log');
$table->changeColumn('content', AdapterInterface::PHINX_TYPE_TEXT, ['length' => MysqlAdapter::TEXT_LONG]);
$table->update();
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace base\common\command;
use app\common\interface\test\CommandTestInterface;
use app\common\service\test\LogTestService;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
class TestBase extends Command
{
protected $program = [
LogTestService::NAME => 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();
}
}

View File

@@ -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表字段格式',
'发布新版本',
];

View File

@@ -0,0 +1,19 @@
<?php
namespace base\common\interface\test;
use think\console\Input;
use think\console\Output;
/**
* 测试类接口.
*
* 实现了该接口的类,会在执行命令时,自动执行该接口的方法,比如传入参数,输出结果等。
* 要注意的是并不代表该接口只能在命令行中使用也可以在其他地方使用在其他地方使用时传入不同“实现”的output和input。例如在控制器中使用传入的output会输出到Response。
*/
interface CommandTestInterfaceBase
{
public function setOutput(Output $output);
public function setInput(Input $input);
}

View File

@@ -0,0 +1,32 @@
<?php
namespace base\common\service;
use Exception;
class TestServiceBase
{
public const NAME = null;
public const DESC = null;
public const RUN = 'run';
public function getName()
{
if ($this::NAME === null) {
throw new Exception('name is not set');
}
return $this::NAME;
}
public function getDesc()
{
if ($this::DESC === null) {
throw new Exception('desc is not set');
}
return $this::DESC;
}
}

View File

@@ -0,0 +1,315 @@
<?php
namespace base\common\service\test;
use app\common\interface\test\CommandTestInterface;
use app\common\service\TestService;
use think\console\Input;
use think\console\Output;
use think\console\Table;
use think\facade\Log;
class LogTesServicetBase extends TestService implements CommandTestInterface
{
public const NAME = 'log';
public const DESC = '测试mysqllog驱动的兼容性、性能、边界情况';
public const RUN = 'run';
/**
* @var Output
*/
protected $output;
/**
* @var Input
*/
protected $input;
protected $summary = [];
protected $configContent = '';
public function run()
{
$this->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;
}
}

View File

@@ -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];
}

View File

@@ -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);

View File

@@ -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);
}
}
}

View File

@@ -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,
]);
}
}