Files
toolbox/app/Services/LogAnalysisService.php

438 lines
13 KiB
PHP
Raw 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\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 CodeAnalysisService $codeAnalysisService,
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 = [];
foreach ($grouped as $appName => $appLogs) {
$appLogsCollection = collect($appLogs);
// 准备日志内容
$logsContent = $this->formatLogsForAnalysis($appLogsCollection);
// AI 分析
try {
$results[$appName] = $this->aiService->analyzeLogs($logsContent, null);
$results[$appName]['log_count'] = $appLogsCollection->count();
// 如果是 logs+code 模式,且 impact 为 high/medium触发代码分析
if ($mode === AnalysisMode::LogsWithCode) {
$impact = $results[$appName]['impact'] ?? 'unknown';
if (in_array($impact, ['high', 'medium'])) {
$codeAnalysisResult = $this->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. 构建结果
$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);
// 使用传入的 limit 参数,如果未指定则默认显示全部
$displayLimit = $limit ?? $logs->count();
return [
'total' => $logs->count(),
'statistics' => $statistics,
'grouped' => array_map(fn($g) => count($g), $grouped),
'logs' => $logs->take($displayLimit)->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'],
];
}
}
// 使用传入的 limit 参数,如果未指定则默认显示全部
$displayLimit = $limit ?? $allLogs->count();
return [
'total' => $statistics['total'],
'statistics' => $statistics,
'grouped_by_logstore' => $groupedByLogstore,
'logs' => $allLogs->take($displayLimit)->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' => '未找到匹配的日志',
],
];
}
}