Files
toolbox/app/Jobs/LogAnalysisJob.php

225 lines
7.6 KiB
PHP

<?php
namespace App\Jobs;
use App\Enums\AnalysisMode;
use App\Models\LogAnalysisReport;
use App\Services\AiService;
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,
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);
}
// 获取代码上下文(如果需要)
$codeContext = null;
if ($this->mode === AnalysisMode::LogsWithCode) {
$repoPath = $codeContextService->getRepoPath($appName);
if ($repoPath) {
$codeContext = $codeContextService->extractRelevantCode($repoPath, $appLogsCollection);
}
}
// 准备日志内容
$logsContent = $this->formatLogsForAnalysis($appLogsCollection);
// AI 分析
try {
$results[$appName] = $aiService->analyzeLogs($logsContent, $codeContext);
$results[$appName]['log_count'] = $appLogsCollection->count();
$results[$appName]['has_code_context'] = $codeContext !== null;
} catch (\Exception $e) {
$results[$appName] = [
'error' => $e->getMessage(),
'log_count' => $appLogsCollection->count(),
'has_code_context' => false,
];
}
}
$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(),
]);
}
}
}