#feature: add AI log analysis & some bugfix
This commit is contained in:
437
app/Services/LogAnalysisService.php
Normal file
437
app/Services/LogAnalysisService.php
Normal file
@@ -0,0 +1,437 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\AnalysisMode;
|
||||
use App\Jobs\LogAnalysisJob;
|
||||
use App\Models\LogAnalysisReport;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class LogAnalysisService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SlsService $slsService,
|
||||
private readonly AiService $aiService,
|
||||
private readonly CodeContextService $codeContextService,
|
||||
private readonly ConfigService $configService,
|
||||
private readonly DingTalkService $dingTalkService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 异步执行日志分析(创建后台任务)
|
||||
*
|
||||
* @param Carbon $from 开始时间
|
||||
* @param Carbon $to 结束时间
|
||||
* @param string|null $query SLS 查询语句
|
||||
* @param AnalysisMode $mode 分析模式
|
||||
* @param bool $pushNotification 是否推送通知
|
||||
* @return LogAnalysisReport 创建的报告(状态为 pending)
|
||||
*/
|
||||
public function analyzeAsync(
|
||||
Carbon $from,
|
||||
Carbon $to,
|
||||
?string $query = null,
|
||||
AnalysisMode $mode = AnalysisMode::Logs,
|
||||
bool $pushNotification = false
|
||||
): LogAnalysisReport {
|
||||
// 如果没有指定查询条件,默认只获取 ERROR 和 WARNING 级别的日志
|
||||
$effectiveQuery = $query;
|
||||
if (empty($query)) {
|
||||
$effectiveQuery = 'ERROR or WARNING';
|
||||
}
|
||||
|
||||
// 创建 pending 状态的报告
|
||||
$report = LogAnalysisReport::create([
|
||||
'from_time' => $from,
|
||||
'to_time' => $to,
|
||||
'query' => $effectiveQuery,
|
||||
'mode' => $mode->value,
|
||||
'total_logs' => 0,
|
||||
'results' => [],
|
||||
'metadata' => [],
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
// 分发后台任务
|
||||
LogAnalysisJob::dispatch(
|
||||
$report->id,
|
||||
$from,
|
||||
$to,
|
||||
$effectiveQuery,
|
||||
$mode,
|
||||
$pushNotification
|
||||
);
|
||||
|
||||
return $report;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行日志分析
|
||||
*
|
||||
* @param Carbon $from 开始时间
|
||||
* @param Carbon $to 结束时间
|
||||
* @param string|null $query SLS 查询语句
|
||||
* @param AnalysisMode $mode 分析模式
|
||||
* @param bool $saveReport 是否保存报告
|
||||
* @return array 分析结果
|
||||
*/
|
||||
public function analyze(
|
||||
Carbon $from,
|
||||
Carbon $to,
|
||||
?string $query = null,
|
||||
AnalysisMode $mode = AnalysisMode::Logs,
|
||||
bool $saveReport = true
|
||||
): array {
|
||||
$startTime = microtime(true);
|
||||
|
||||
// 1. 获取日志
|
||||
$logs = $this->slsService->fetchLogs($from, $to, $query);
|
||||
|
||||
if ($logs->isEmpty()) {
|
||||
return $this->buildEmptyResult($from, $to, $query, $mode);
|
||||
}
|
||||
|
||||
// 2. 按 app_name 分组
|
||||
$grouped = $this->slsService->groupByAppName($logs);
|
||||
|
||||
// 3. 分析每个分组
|
||||
$results = [];
|
||||
$settings = $this->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 ($mode === AnalysisMode::LogsWithCode) {
|
||||
$repoPath = $this->codeContextService->getRepoPath($appName);
|
||||
if ($repoPath) {
|
||||
$codeContext = $this->codeContextService->extractRelevantCode($repoPath, $appLogsCollection);
|
||||
}
|
||||
}
|
||||
|
||||
// 准备日志内容
|
||||
$logsContent = $this->formatLogsForAnalysis($appLogsCollection);
|
||||
|
||||
// AI 分析
|
||||
try {
|
||||
$results[$appName] = $this->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. 构建结果
|
||||
$result = [
|
||||
'request' => [
|
||||
'from' => $from->format('Y-m-d H:i:s'),
|
||||
'to' => $to->format('Y-m-d H:i:s'),
|
||||
'query' => $query,
|
||||
'mode' => $mode->value,
|
||||
],
|
||||
'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 ($saveReport) {
|
||||
$this->saveReport($result, $from, $to, $query, $mode, $logs->count());
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅查询日志(不进行 AI 分析)
|
||||
*
|
||||
* @param Carbon $from
|
||||
* @param Carbon $to
|
||||
* @param string|null $query
|
||||
* @param int|null $limit
|
||||
* @return array
|
||||
*/
|
||||
public function queryLogs(
|
||||
Carbon $from,
|
||||
Carbon $to,
|
||||
?string $query = null,
|
||||
?int $limit = null
|
||||
): array {
|
||||
$logs = $this->slsService->fetchLogs($from, $to, $query, $limit);
|
||||
$grouped = $this->slsService->groupByAppName($logs);
|
||||
$statistics = $this->slsService->getStatistics($logs);
|
||||
|
||||
return [
|
||||
'total' => $logs->count(),
|
||||
'statistics' => $statistics,
|
||||
'grouped' => array_map(fn($g) => count($g), $grouped),
|
||||
'logs' => $logs->take(100)->values()->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 从多个 logstore 查询日志(不进行 AI 分析)
|
||||
*
|
||||
* @param Carbon $from
|
||||
* @param Carbon $to
|
||||
* @param string|null $query
|
||||
* @param int|null $limit
|
||||
* @param array|null $logstores
|
||||
* @return array
|
||||
*/
|
||||
public function queryLogsFromMultipleStores(
|
||||
Carbon $from,
|
||||
Carbon $to,
|
||||
?string $query = null,
|
||||
?int $limit = null,
|
||||
?array $logstores = null
|
||||
): array {
|
||||
$logstoreData = $this->slsService->fetchLogsFromMultipleStores($from, $to, $query, $limit, $logstores);
|
||||
$statistics = $this->slsService->getMultiStoreStatistics($logstoreData);
|
||||
|
||||
// 合并所有 logstore 的日志用于预览
|
||||
$allLogs = collect();
|
||||
$groupedByLogstore = [];
|
||||
|
||||
foreach ($logstoreData as $logstore => $data) {
|
||||
if ($data['success']) {
|
||||
$allLogs = $allLogs->merge($data['logs']);
|
||||
$groupedByLogstore[$logstore] = [
|
||||
'count' => $data['count'],
|
||||
'success' => true,
|
||||
];
|
||||
} else {
|
||||
$groupedByLogstore[$logstore] = [
|
||||
'count' => 0,
|
||||
'success' => false,
|
||||
'error' => $data['error'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'total' => $statistics['total'],
|
||||
'statistics' => $statistics,
|
||||
'grouped_by_logstore' => $groupedByLogstore,
|
||||
'logs' => $allLogs->take(100)->values()->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取历史报告列表
|
||||
*
|
||||
* @param int $limit
|
||||
* @param int $offset
|
||||
* @return array
|
||||
*/
|
||||
public function getReports(int $limit = 20, int $offset = 0): array
|
||||
{
|
||||
$query = LogAnalysisReport::query()
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
$total = $query->count();
|
||||
$reports = $query->skip($offset)->take($limit)->get();
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'reports' => $reports->map(function ($report) {
|
||||
return [
|
||||
'id' => $report->id,
|
||||
'from_time' => $report->from_time->format('Y-m-d H:i:s'),
|
||||
'to_time' => $report->to_time->format('Y-m-d H:i:s'),
|
||||
'query' => $report->query,
|
||||
'mode' => $report->mode,
|
||||
'total_logs' => $report->total_logs,
|
||||
'status' => $report->status,
|
||||
'created_at' => $report->created_at->format('Y-m-d H:i:s'),
|
||||
];
|
||||
})->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个报告详情
|
||||
*
|
||||
* @param int $id
|
||||
* @return array|null
|
||||
*/
|
||||
public function getReport(int $id): ?array
|
||||
{
|
||||
$report = LogAnalysisReport::find($id);
|
||||
|
||||
if (!$report) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $report->id,
|
||||
'request' => [
|
||||
'from' => $report->from_time->format('Y-m-d H:i:s'),
|
||||
'to' => $report->to_time->format('Y-m-d H:i:s'),
|
||||
'query' => $report->query,
|
||||
'mode' => $report->mode,
|
||||
],
|
||||
'results' => $report->results,
|
||||
'metadata' => $report->metadata,
|
||||
'status' => $report->status,
|
||||
'error_message' => $report->error_message,
|
||||
'created_at' => $report->created_at->format('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送分析结果到钉钉
|
||||
*
|
||||
* @param array $result 分析结果
|
||||
* @return bool
|
||||
*/
|
||||
public function pushToNotification(array $result): bool
|
||||
{
|
||||
$message = $this->formatNotificationMessage($result);
|
||||
|
||||
try {
|
||||
$this->dingTalkService->sendText($message);
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日志用于 AI 分析
|
||||
*/
|
||||
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 formatNotificationMessage(array $result): string
|
||||
{
|
||||
$lines = [];
|
||||
$lines[] = "📊 SLS 日志分析报告";
|
||||
$lines[] = "时间范围: {$result['request']['from']} ~ {$result['request']['to']}";
|
||||
$lines[] = "总日志数: {$result['metadata']['total_logs']}";
|
||||
$lines[] = "";
|
||||
|
||||
foreach ($result['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[] = "";
|
||||
}
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存分析报告
|
||||
*/
|
||||
private function saveReport(
|
||||
array $result,
|
||||
Carbon $from,
|
||||
Carbon $to,
|
||||
?string $query,
|
||||
AnalysisMode $mode,
|
||||
int $totalLogs
|
||||
): LogAnalysisReport {
|
||||
return LogAnalysisReport::create([
|
||||
'from_time' => $from,
|
||||
'to_time' => $to,
|
||||
'query' => $query,
|
||||
'mode' => $mode->value,
|
||||
'total_logs' => $totalLogs,
|
||||
'results' => $result['results'],
|
||||
'metadata' => $result['metadata'],
|
||||
'status' => 'completed',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建空结果
|
||||
*/
|
||||
private function buildEmptyResult(
|
||||
Carbon $from,
|
||||
Carbon $to,
|
||||
?string $query,
|
||||
AnalysisMode $mode
|
||||
): array {
|
||||
return [
|
||||
'request' => [
|
||||
'from' => $from->format('Y-m-d H:i:s'),
|
||||
'to' => $to->format('Y-m-d H:i:s'),
|
||||
'query' => $query,
|
||||
'mode' => $mode->value,
|
||||
],
|
||||
'results' => [],
|
||||
'metadata' => [
|
||||
'total_logs' => 0,
|
||||
'apps_analyzed' => 0,
|
||||
'execution_time_ms' => 0,
|
||||
'analyzed_at' => Carbon::now()->format('Y-m-d H:i:s'),
|
||||
'message' => '未找到匹配的日志',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user