ReproduceErrorCommand.php 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. <?php
  2. namespace App\Console\Commands;
  3. use GuzzleHttp\Client;
  4. use Illuminate\Console\Command;
  5. use UCore\Model\RequestLog;
  6. use Illuminate\Support\Facades\Log;
  7. use UCore\Helper\Logger;
  8. /**
  9. * 错误复现命令
  10. *
  11. * 通过 sys_request_logs 表的记录来复现请求,用于调试和错误排查
  12. * php artisan debug:reproduce-error 68973982
  13. * php artisan debug:reproduce-error 68973982 --type=request_unid
  14. * php artisan debug:reproduce-error 68973982 --clear-logs
  15. *
  16. *
  17. */
  18. class ReproduceErrorCommand extends Command
  19. {
  20. /**
  21. * 命令签名
  22. *
  23. * @var string
  24. */
  25. protected $signature = 'debug:reproduce-error {identifier : 请求标识符(可以是id、request_unid或run_unid)} {--type=auto : 标识符类型(id|request_unid|run_unid|auto),auto为自动检测} {--timeout=30 : 请求超时时间(秒)} {--clear-logs : 运行前清空当前日志文件}';
  26. /**
  27. * 命令描述
  28. *
  29. * @var string
  30. */
  31. protected $description = '通过请求日志记录复现错误请求,用于调试和问题排查。支持运行前清空日志文件';
  32. /**
  33. * HTTP 客户端
  34. *
  35. * @var Client
  36. */
  37. protected Client $client;
  38. /**
  39. * 基础URL
  40. *
  41. * @var string
  42. */
  43. protected string $baseUrl;
  44. /**
  45. * 执行命令
  46. */
  47. public function handle()
  48. {
  49. $identifier = $this->argument('identifier');
  50. $type = $this->option('type');
  51. $timeout = (int) $this->option('timeout');
  52. $clearLogs = $this->option('clear-logs');
  53. // 如果开启了清空日志选项,则清空日志文件
  54. if ($clearLogs) {
  55. $this->clearLogFiles();
  56. }
  57. $this->info("开始查找请求记录...");
  58. $this->info("标识符: {$identifier}");
  59. $this->info("类型: {$type}");
  60. // 查找请求记录
  61. $requestLog = $this->findRequestLog($identifier, $type);
  62. if (!$requestLog) {
  63. $this->error("未找到匹配的请求记录");
  64. return 1;
  65. }
  66. $this->info("找到请求记录:");
  67. $this->line(" ID: {$requestLog->id}");
  68. $this->line(" Request UNID: {$requestLog->request_unid}");
  69. $this->line(" Run UNID: {$requestLog->run_unid}");
  70. $this->line(" 路径: {$requestLog->path}");
  71. $this->line(" 方法: {$requestLog->method}");
  72. $this->line(" 创建时间: {$requestLog->created_at}");
  73. // 检查必要的数据
  74. if (empty($requestLog->protobuf_json)) {
  75. $this->error("请求记录中缺少 protobuf_json 数据");
  76. return 1;
  77. }
  78. // 解析 headers 获取 token
  79. $token = $this->extractToken($requestLog->headers);
  80. if (!$token) {
  81. $this->warn("未找到 token,将不携带 token 发起请求");
  82. } else {
  83. $this->info("提取到 token: " . substr($token, 0, 10) . "...");
  84. }
  85. // 初始化 HTTP 客户端
  86. $this->initializeHttpClient($timeout);
  87. // 发起请求
  88. $this->info("开始发起请求...");
  89. $response = $this->makeRequest($requestLog->protobuf_json, $token);
  90. if ($response === null) {
  91. return 1;
  92. }
  93. // 输出结果
  94. $this->info("请求完成,响应结果:");
  95. $this->line("状态码: " . $response['status_code']);
  96. $this->line("响应头:");
  97. foreach ($response['headers'] as $name => $values) {
  98. $this->line(" {$name}: " . implode(', ', $values));
  99. }
  100. $this->line("响应内容:");
  101. $this->line($response['body']);
  102. return 0;
  103. }
  104. /**
  105. * 查找请求记录
  106. *
  107. * @param string $identifier 标识符
  108. * @param string $type 类型
  109. * @return RequestLog|null
  110. */
  111. protected function findRequestLog(string $identifier, string $type): ?RequestLog
  112. {
  113. $query = RequestLog::query();
  114. if ($type === 'auto') {
  115. // 自动检测类型
  116. if (is_numeric($identifier)) {
  117. // 纯数字,优先按 ID 查找
  118. $requestLog = $query->where('id', $identifier)->first();
  119. if ($requestLog) {
  120. return $requestLog;
  121. }
  122. }
  123. // 按 request_unid 查找
  124. $requestLog = RequestLog::query()->where('request_unid', $identifier)->first();
  125. if ($requestLog) {
  126. return $requestLog;
  127. }
  128. // 按 run_unid 查找
  129. $requestLog = RequestLog::query()->where('run_unid', $identifier)->first();
  130. if ($requestLog) {
  131. return $requestLog;
  132. }
  133. return null;
  134. }
  135. // 指定类型查找
  136. switch ($type) {
  137. case 'id':
  138. return $query->where('id', $identifier)->first();
  139. case 'request_unid':
  140. return $query->where('request_unid', $identifier)->first();
  141. case 'run_unid':
  142. return $query->where('run_unid', $identifier)->first();
  143. default:
  144. $this->error("不支持的类型: {$type}");
  145. return null;
  146. }
  147. }
  148. /**
  149. * 从 headers JSON 中提取 token
  150. *
  151. * @param string|null $headersJson
  152. * @return string|null
  153. */
  154. protected function extractToken(?string $headersJson): ?string
  155. {
  156. if (empty($headersJson)) {
  157. return null;
  158. }
  159. try {
  160. $headers = json_decode($headersJson, true);
  161. if (!is_array($headers)) {
  162. return null;
  163. }
  164. // 查找 token 字段(可能在不同的键名下)
  165. $tokenKeys = ['token', 'Token', 'authorization', 'Authorization'];
  166. foreach ($tokenKeys as $key) {
  167. if (isset($headers[$key])) {
  168. $tokenValue = $headers[$key];
  169. // headers 中的值可能是数组
  170. if (is_array($tokenValue)) {
  171. return $tokenValue[0] ?? null;
  172. }
  173. return $tokenValue;
  174. }
  175. }
  176. return null;
  177. } catch (\Exception $e) {
  178. $this->warn("解析 headers 失败: " . $e->getMessage());
  179. return null;
  180. }
  181. }
  182. /**
  183. * 初始化 HTTP 客户端
  184. *
  185. * @param int $timeout
  186. */
  187. protected function initializeHttpClient(int $timeout): void
  188. {
  189. $this->baseUrl = env('UNITTEST_URL', 'http://localhost:8000');
  190. $this->info("目标地址: {$this->baseUrl}");
  191. $this->client = new Client([
  192. 'base_uri' => $this->baseUrl,
  193. 'timeout' => $timeout,
  194. 'http_errors' => false,
  195. 'verify' => false, // 禁用 SSL 验证
  196. ]);
  197. }
  198. /**
  199. * 发起请求
  200. *
  201. * @param string $protobufJson
  202. * @param string|null $token
  203. * @return array|null
  204. */
  205. protected function makeRequest(string $protobufJson, ?string $token): ?array
  206. {
  207. try {
  208. $headers = [
  209. 'Content-Type' => 'application/json',
  210. 'Accept' => 'application/json'
  211. ];
  212. if ($token) {
  213. $headers['token'] = $token;
  214. }
  215. Log::info('复现请求开始', [
  216. 'url' => $this->baseUrl . '/gameapi',
  217. 'headers' => $headers,
  218. 'body_length' => strlen($protobufJson)
  219. ]);
  220. $response = $this->client->post('/gameapi', [
  221. 'body' => $protobufJson,
  222. 'headers' => $headers
  223. ]);
  224. $statusCode = $response->getStatusCode();
  225. $responseHeaders = $response->getHeaders();
  226. $responseBody = $response->getBody()->getContents();
  227. Log::info('复现请求完成', [
  228. 'status_code' => $statusCode,
  229. 'response_length' => strlen($responseBody)
  230. ]);
  231. return [
  232. 'status_code' => $statusCode,
  233. 'headers' => $responseHeaders,
  234. 'body' => $responseBody
  235. ];
  236. } catch (\Exception $e) {
  237. $this->error("请求失败: " . $e->getMessage());
  238. Log::error('复现请求失败', [
  239. 'error' => $e->getMessage(),
  240. 'trace' => $e->getTraceAsString()
  241. ]);
  242. return null;
  243. }
  244. }
  245. /**
  246. * 清空当前日志文件
  247. */
  248. protected function clearLogFiles(): void
  249. {
  250. $this->info("正在清空日志文件...");
  251. try {
  252. // 获取当前日志文件路径
  253. $logPath = storage_path('logs');
  254. $currentDate = date('Y-m-d');
  255. // Laravel 默认日志文件名格式
  256. $logFiles = [
  257. $logPath . '/laravel.log',
  258. $logPath . "/laravel-{$currentDate}.log",
  259. ];
  260. $clearedCount = 0;
  261. foreach ($logFiles as $logFile) {
  262. if (file_exists($logFile)) {
  263. // 清空文件内容但保留文件
  264. file_put_contents($logFile, '');
  265. $clearedCount++;
  266. $this->line("已清空: " . basename($logFile));
  267. }
  268. }
  269. if ($clearedCount > 0) {
  270. $this->info("成功清空 {$clearedCount} 个日志文件");
  271. } else {
  272. $this->warn("未找到需要清空的日志文件");
  273. }
  274. Logger::debug('旧的日志已经清理');
  275. } catch (\Exception $e) {
  276. $this->error("清空日志文件失败: " . $e->getMessage());
  277. }
  278. }
  279. }