Files
toolbox/app/Jobs/LogAnalysisJob.php

229 lines
7.8 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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(),
]);
}
}
}