229 lines
7.8 KiB
PHP
229 lines
7.8 KiB
PHP
<?php
|
||
|
||
namespace App\Jobs;
|
||
|
||
use App\Enums\AnalysisMode;
|
||
use App\Models\LogAnalysisReport;
|
||
use App\Services\AiService;
|
||
use App\Services\CodeAnalysisService;
|
||
use App\Services\CodeContextService;
|
||
use App\Services\ConfigService;
|
||
use App\Services\DingTalkService;
|
||
use App\Services\SlsService;
|
||
use Carbon\Carbon;
|
||
use Illuminate\Bus\Queueable;
|
||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||
use Illuminate\Foundation\Bus\Dispatchable;
|
||
use Illuminate\Queue\InteractsWithQueue;
|
||
use Illuminate\Queue\SerializesModels;
|
||
use Illuminate\Support\Collection;
|
||
use Illuminate\Support\Facades\Log;
|
||
|
||
class LogAnalysisJob implements ShouldQueue
|
||
{
|
||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||
|
||
public int $timeout = 600; // 10 分钟超时
|
||
public int $tries = 1; // 只尝试一次
|
||
|
||
public function __construct(
|
||
private readonly int $reportId,
|
||
private readonly Carbon $from,
|
||
private readonly Carbon $to,
|
||
private readonly ?string $query,
|
||
private readonly AnalysisMode $mode,
|
||
private readonly bool $pushNotification = false
|
||
) {}
|
||
|
||
public function handle(
|
||
SlsService $slsService,
|
||
AiService $aiService,
|
||
CodeContextService $codeContextService,
|
||
CodeAnalysisService $codeAnalysisService,
|
||
ConfigService $configService,
|
||
DingTalkService $dingTalkService
|
||
): void {
|
||
$report = LogAnalysisReport::find($this->reportId);
|
||
if (!$report) {
|
||
Log::error("LogAnalysisJob: Report not found", ['report_id' => $this->reportId]);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
$startTime = microtime(true);
|
||
|
||
// 1. 获取日志
|
||
$logs = $slsService->fetchLogs($this->from, $this->to, $this->query);
|
||
|
||
if ($logs->isEmpty()) {
|
||
$report->update([
|
||
'status' => 'completed',
|
||
'total_logs' => 0,
|
||
'results' => [],
|
||
'metadata' => [
|
||
'total_logs' => 0,
|
||
'apps_analyzed' => 0,
|
||
'execution_time_ms' => 0,
|
||
'analyzed_at' => Carbon::now()->format('Y-m-d H:i:s'),
|
||
'message' => '未找到匹配的日志',
|
||
],
|
||
]);
|
||
return;
|
||
}
|
||
|
||
// 2. 按 app_name 分组
|
||
$grouped = $slsService->groupByAppName($logs);
|
||
|
||
// 3. 分析每个分组
|
||
$results = [];
|
||
$settings = $configService->get('log_analysis.settings', []);
|
||
$maxLogsPerApp = $settings['max_logs_per_app'] ?? 500;
|
||
|
||
foreach ($grouped as $appName => $appLogs) {
|
||
$appLogsCollection = collect($appLogs);
|
||
|
||
// 限制每个 app 的日志数量
|
||
if ($appLogsCollection->count() > $maxLogsPerApp) {
|
||
$appLogsCollection = $appLogsCollection->take($maxLogsPerApp);
|
||
}
|
||
|
||
// 准备日志内容
|
||
$logsContent = $this->formatLogsForAnalysis($appLogsCollection);
|
||
|
||
// AI 分析(不再携带代码上下文)
|
||
try {
|
||
$results[$appName] = $aiService->analyzeLogs($logsContent, null);
|
||
$results[$appName]['log_count'] = $appLogsCollection->count();
|
||
|
||
// 如果是 logs+code 模式,且 impact 为 high/medium,触发代码分析
|
||
if ($this->mode === AnalysisMode::LogsWithCode) {
|
||
$impact = $results[$appName]['impact'] ?? 'unknown';
|
||
if (in_array($impact, ['high', 'medium'])) {
|
||
$codeAnalysisResult = $codeAnalysisService->analyze(
|
||
$appName,
|
||
$logsContent,
|
||
$results[$appName]['summary'] ?? null
|
||
);
|
||
$results[$appName]['code_analysis'] = $codeAnalysisResult;
|
||
}
|
||
}
|
||
} catch (\Exception $e) {
|
||
$results[$appName] = [
|
||
'error' => $e->getMessage(),
|
||
'log_count' => $appLogsCollection->count(),
|
||
];
|
||
}
|
||
}
|
||
|
||
$executionTime = (microtime(true) - $startTime) * 1000;
|
||
|
||
// 4. 更新报告
|
||
$report->update([
|
||
'status' => 'completed',
|
||
'total_logs' => $logs->count(),
|
||
'results' => $results,
|
||
'metadata' => [
|
||
'total_logs' => $logs->count(),
|
||
'apps_analyzed' => count($results),
|
||
'execution_time_ms' => round($executionTime),
|
||
'analyzed_at' => Carbon::now()->format('Y-m-d H:i:s'),
|
||
],
|
||
]);
|
||
|
||
// 5. 推送通知(如果需要)
|
||
if ($this->pushNotification) {
|
||
$this->pushToNotification($report, $dingTalkService);
|
||
}
|
||
|
||
Log::info("LogAnalysisJob: Completed", [
|
||
'report_id' => $this->reportId,
|
||
'total_logs' => $logs->count(),
|
||
'execution_time_ms' => round($executionTime),
|
||
]);
|
||
|
||
} catch (\Exception $e) {
|
||
Log::error("LogAnalysisJob: Failed", [
|
||
'report_id' => $this->reportId,
|
||
'error' => $e->getMessage(),
|
||
]);
|
||
|
||
$report->update([
|
||
'status' => 'failed',
|
||
'error_message' => $e->getMessage(),
|
||
]);
|
||
}
|
||
}
|
||
|
||
private function formatLogsForAnalysis(Collection $logs): string
|
||
{
|
||
$formatted = [];
|
||
|
||
foreach ($logs as $log) {
|
||
$line = sprintf(
|
||
"[%s] [%s] %s",
|
||
$log['time'] ?? 'N/A',
|
||
$log['level'] ?? 'N/A',
|
||
$log['message'] ?? json_encode($log['_raw'] ?? $log)
|
||
);
|
||
|
||
if (!empty($log['trace'])) {
|
||
$line .= "\n" . $log['trace'];
|
||
}
|
||
|
||
$formatted[] = $line;
|
||
}
|
||
|
||
return implode("\n\n", $formatted);
|
||
}
|
||
|
||
private function pushToNotification(LogAnalysisReport $report, DingTalkService $dingTalkService): void
|
||
{
|
||
$lines = [];
|
||
$lines[] = "📊 SLS 日志分析报告";
|
||
$lines[] = "时间范围: {$report->from_time->format('Y-m-d H:i:s')} ~ {$report->to_time->format('Y-m-d H:i:s')}";
|
||
$lines[] = "总日志数: {$report->total_logs}";
|
||
$lines[] = "";
|
||
|
||
foreach ($report->results as $appName => $appResult) {
|
||
$lines[] = "【{$appName}】";
|
||
|
||
if (isset($appResult['error'])) {
|
||
$lines[] = " 分析失败: {$appResult['error']}";
|
||
continue;
|
||
}
|
||
|
||
$impact = $appResult['impact'] ?? 'unknown';
|
||
$impactEmoji = match ($impact) {
|
||
'high' => '🔴',
|
||
'medium' => '🟡',
|
||
'low' => '🟢',
|
||
default => '⚪',
|
||
};
|
||
|
||
$lines[] = " 影响级别: {$impactEmoji} {$impact}";
|
||
$lines[] = " 摘要: " . ($appResult['summary'] ?? 'N/A');
|
||
|
||
$anomalies = $appResult['core_anomalies'] ?? [];
|
||
if (!empty($anomalies)) {
|
||
$lines[] = " 异常数: " . count($anomalies);
|
||
foreach (array_slice($anomalies, 0, 3) as $anomaly) {
|
||
$lines[] = " - [{$anomaly['classification']}] {$anomaly['possible_cause']}";
|
||
}
|
||
}
|
||
|
||
$lines[] = "";
|
||
}
|
||
|
||
$message = implode("\n", $lines);
|
||
|
||
try {
|
||
$dingTalkService->sendText($message);
|
||
} catch (\Exception $e) {
|
||
Log::warning("LogAnalysisJob: Failed to push notification", [
|
||
'report_id' => $this->reportId,
|
||
'error' => $e->getMessage(),
|
||
]);
|
||
}
|
||
}
|
||
}
|