189 lines
6.4 KiB
PHP
189 lines
6.4 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Enums\AnalysisMode;
|
|
use App\Services\LogAnalysisService;
|
|
use App\Services\SlsService;
|
|
use App\Services\AiService;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Console\Command;
|
|
|
|
class LogAnalysisCommand extends Command
|
|
{
|
|
protected $signature = 'log-analysis:run
|
|
{--from= : 开始时间 (Y-m-d H:i:s 或相对时间如 -1h, -30m, -1d)}
|
|
{--to= : 结束时间 (Y-m-d H:i:s 或相对时间,默认 now)}
|
|
{--query= : SLS 查询语句}
|
|
{--mode=logs : 分析模式 (logs|logs+code)}
|
|
{--output= : 输出 JSON 报告的文件路径}
|
|
{--push : 推送结果到钉钉}
|
|
{--no-save : 不保存报告到数据库}';
|
|
|
|
protected $description = '分析 SLS 日志,支持 AI 智能分析和钉钉推送';
|
|
|
|
public function handle(
|
|
LogAnalysisService $analysisService,
|
|
SlsService $slsService,
|
|
AiService $aiService
|
|
): int {
|
|
// 检查配置
|
|
if (!$slsService->isConfigured()) {
|
|
$this->error('SLS 服务未配置,请检查 .env 中的 SLS_* 配置项');
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
if (!$aiService->isConfigured()) {
|
|
$this->error('AI 服务未配置,请在页面上配置 AI 提供商或设置 .env 中的 AI_* 配置项');
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
// 解析时间参数
|
|
$from = $this->parseTime($this->option('from') ?? '-1h');
|
|
$to = $this->parseTime($this->option('to') ?? 'now');
|
|
|
|
if ($from >= $to) {
|
|
$this->error('开始时间必须早于结束时间');
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
// 解析分析模式
|
|
$modeOption = $this->option('mode');
|
|
$mode = $modeOption === 'logs+code'
|
|
? AnalysisMode::LogsWithCode
|
|
: AnalysisMode::Logs;
|
|
|
|
$query = $this->option('query');
|
|
|
|
$this->info("开始分析日志...");
|
|
$this->line(" 时间范围: {$from->format('Y-m-d H:i:s')} ~ {$to->format('Y-m-d H:i:s')}");
|
|
$this->line(" 查询语句: " . ($query ?: '*'));
|
|
$this->line(" 分析模式: {$mode->label()}");
|
|
$this->newLine();
|
|
|
|
try {
|
|
$result = $analysisService->analyze(
|
|
$from,
|
|
$to,
|
|
$query,
|
|
$mode,
|
|
!$this->option('no-save')
|
|
);
|
|
|
|
// 输出到文件
|
|
if ($outputPath = $this->option('output')) {
|
|
$json = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
file_put_contents($outputPath, $json);
|
|
$this->info("报告已保存到: {$outputPath}");
|
|
}
|
|
|
|
// 推送到钉钉
|
|
if ($this->option('push')) {
|
|
$this->line("正在推送到钉钉...");
|
|
$pushed = $analysisService->pushToNotification($result);
|
|
if ($pushed) {
|
|
$this->info("已推送到钉钉");
|
|
} else {
|
|
$this->warn("钉钉推送失败");
|
|
}
|
|
}
|
|
|
|
// 显示摘要
|
|
$this->displaySummary($result);
|
|
|
|
return Command::SUCCESS;
|
|
} catch (\Exception $e) {
|
|
$this->error("分析失败: {$e->getMessage()}");
|
|
return Command::FAILURE;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 解析时间参数
|
|
*/
|
|
private function parseTime(string $input): Carbon
|
|
{
|
|
// 相对时间格式: -1h, -30m, -1d, -2w
|
|
if (preg_match('/^-(\d+)([hmsdw])$/', $input, $matches)) {
|
|
$value = (int) $matches[1];
|
|
$unit = $matches[2];
|
|
|
|
return match ($unit) {
|
|
'h' => Carbon::now()->subHours($value),
|
|
'm' => Carbon::now()->subMinutes($value),
|
|
's' => Carbon::now()->subSeconds($value),
|
|
'd' => Carbon::now()->subDays($value),
|
|
'w' => Carbon::now()->subWeeks($value),
|
|
default => Carbon::now(),
|
|
};
|
|
}
|
|
|
|
// 特殊值
|
|
if ($input === 'now') {
|
|
return Carbon::now();
|
|
}
|
|
|
|
// 尝试解析为日期时间
|
|
return Carbon::parse($input);
|
|
}
|
|
|
|
/**
|
|
* 显示分析摘要
|
|
*/
|
|
private function displaySummary(array $result): void
|
|
{
|
|
$this->newLine();
|
|
$this->info('=== 分析摘要 ===');
|
|
$this->line("总日志数: {$result['metadata']['total_logs']}");
|
|
$this->line("分析应用数: {$result['metadata']['apps_analyzed']}");
|
|
$this->line("执行时间: {$result['metadata']['execution_time_ms']}ms");
|
|
$this->newLine();
|
|
|
|
if (empty($result['results'])) {
|
|
$this->warn('未找到匹配的日志');
|
|
return;
|
|
}
|
|
|
|
foreach ($result['results'] as $appName => $appResult) {
|
|
$this->line("【{$appName}】");
|
|
|
|
if (isset($appResult['error'])) {
|
|
$this->error(" 分析失败: {$appResult['error']}");
|
|
continue;
|
|
}
|
|
|
|
$impact = $appResult['impact'] ?? 'unknown';
|
|
$impactColor = match ($impact) {
|
|
'high' => 'red',
|
|
'medium' => 'yellow',
|
|
'low' => 'green',
|
|
default => 'white',
|
|
};
|
|
|
|
$this->line(" 日志数: {$appResult['log_count']}");
|
|
$this->line(" 代码上下文: " . ($appResult['has_code_context'] ? '是' : '否'));
|
|
$this->line(" 影响级别: <fg={$impactColor}>{$impact}</>");
|
|
$this->line(" 摘要: " . ($appResult['summary'] ?? 'N/A'));
|
|
|
|
$anomalies = $appResult['core_anomalies'] ?? [];
|
|
if (!empty($anomalies)) {
|
|
$this->line(" 异常数: " . count($anomalies));
|
|
|
|
$table = [];
|
|
foreach (array_slice($anomalies, 0, 5) as $anomaly) {
|
|
$table[] = [
|
|
$anomaly['type'] ?? 'N/A',
|
|
$anomaly['classification'] ?? 'N/A',
|
|
$anomaly['count'] ?? 1,
|
|
mb_substr($anomaly['possible_cause'] ?? 'N/A', 0, 40),
|
|
];
|
|
}
|
|
|
|
$this->table(['类型', '分类', '数量', '可能原因'], $table);
|
|
}
|
|
|
|
$this->newLine();
|
|
}
|
|
}
|
|
}
|