#feature: AI analysis update
This commit is contained in:
@@ -104,9 +104,10 @@ class AiClient
|
||||
*
|
||||
* @param array $messages 消息数组
|
||||
* @param string|null $systemPrompt 系统提示词
|
||||
* @param int $maxRetries 最大重试次数
|
||||
* @return string AI 响应内容
|
||||
*/
|
||||
public function chat(array $messages, ?string $systemPrompt = null): string
|
||||
public function chat(array $messages, ?string $systemPrompt = null, int $maxRetries = 3): string
|
||||
{
|
||||
$provider = $this->getActiveProvider();
|
||||
if (!$provider) {
|
||||
@@ -124,24 +125,45 @@ class AiClient
|
||||
|
||||
$endpoint = rtrim($provider['endpoint'], '/') . '/chat/completions';
|
||||
|
||||
$response = Http::timeout($provider['timeout'] ?? 120)
|
||||
->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $provider['api_key'],
|
||||
'Content-Type' => 'application/json',
|
||||
])
|
||||
->post($endpoint, [
|
||||
'model' => $provider['model'],
|
||||
'messages' => $allMessages,
|
||||
'temperature' => $provider['temperature'] ?? 0.3,
|
||||
'max_tokens' => $provider['max_tokens'] ?? 4096,
|
||||
]);
|
||||
$lastException = null;
|
||||
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
|
||||
$response = Http::timeout($provider['timeout'] ?? 120)
|
||||
->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $provider['api_key'],
|
||||
'Content-Type' => 'application/json',
|
||||
])
|
||||
->post($endpoint, [
|
||||
'model' => $provider['model'],
|
||||
'messages' => $allMessages,
|
||||
'temperature' => $provider['temperature'] ?? 0.3,
|
||||
'max_tokens' => $provider['max_tokens'] ?? 4096,
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
return $response->json('choices.0.message.content', '');
|
||||
}
|
||||
|
||||
// 处理 429 Too Many Requests 错误
|
||||
if ($response->status() === 429) {
|
||||
$retryAfter = $response->header('Retry-After');
|
||||
$waitSeconds = $retryAfter ? (int) $retryAfter : min(pow(2, $attempt) * 5, 60);
|
||||
|
||||
if ($attempt < $maxRetries) {
|
||||
sleep($waitSeconds);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$response->successful()) {
|
||||
$error = $response->json('error.message') ?? $response->body();
|
||||
throw new RuntimeException("AI 请求失败: {$error}");
|
||||
$lastException = new RuntimeException("AI 请求失败: {$error}");
|
||||
|
||||
// 对于非 429 错误,不重试
|
||||
if ($response->status() !== 429) {
|
||||
throw $lastException;
|
||||
}
|
||||
}
|
||||
|
||||
return $response->json('choices.0.message.content', '');
|
||||
throw $lastException ?? new RuntimeException('AI 请求失败: 未知错误');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,7 +222,7 @@ class AiClient
|
||||
return <<<'PROMPT'
|
||||
你是一个专业的日志分析专家。你的任务是分析提供的日志内容,识别异常和问题,并给出分类和建议。
|
||||
|
||||
请按照以下 JSON 格式返回分析结果:
|
||||
请严格按照以下 JSON 格式返回分析结果,不要添加任何额外的文字说明或代码块标记:
|
||||
{
|
||||
"core_anomalies": [
|
||||
{
|
||||
@@ -222,7 +244,7 @@ class AiClient
|
||||
- classification: database(数据库), network(网络), application(应用逻辑), configuration(配置), resource(资源), other(其他)
|
||||
- impact: high(高影响), medium(中等影响), low(低影响)
|
||||
|
||||
请确保返回有效的 JSON 格式。
|
||||
重要:只返回纯 JSON,不要使用 markdown 代码块(```json)包裹。
|
||||
PROMPT;
|
||||
}
|
||||
|
||||
@@ -239,16 +261,47 @@ PROMPT;
|
||||
|
||||
private function parseAnalysisResponse(string $response): array
|
||||
{
|
||||
// 尝试提取 JSON
|
||||
if (preg_match('/\{[\s\S]*}/', $response, $matches)) {
|
||||
$json = $matches[0];
|
||||
$decoded = json_decode($json, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$jsonContent = $response;
|
||||
|
||||
// 1. 首先尝试从 markdown 代码块中提取 JSON
|
||||
// 匹配 ```json ... ``` 或 ``` ... ``` 格式(使用贪婪匹配获取最后一个代码块)
|
||||
if (preg_match_all('/```(?:json)?\s*([\s\S]*?)```/', $response, $matches)) {
|
||||
// 尝试每个匹配的代码块,找到有效的 JSON
|
||||
foreach ($matches[1] as $match) {
|
||||
$trimmed = trim($match);
|
||||
$decoded = json_decode($trimmed, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded) && isset($decoded['core_anomalies'])) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
// 如果没有找到有效的 JSON,使用第一个匹配
|
||||
$jsonContent = trim($matches[1][0]);
|
||||
}
|
||||
|
||||
// 2. 尝试直接解析(可能已经是纯 JSON)
|
||||
$decoded = json_decode($jsonContent, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
// 3. 尝试从内容中提取 JSON 对象(处理前后有其他文本的情况)
|
||||
// 使用更精确的匹配:找到包含 core_anomalies 的 JSON 对象
|
||||
if (preg_match('/\{[^{}]*"core_anomalies"[\s\S]*}/', $response, $matches)) {
|
||||
$decoded = json_decode($matches[0], true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果无法解析 JSON,返回原始响应
|
||||
// 4. 最后尝试匹配任意 JSON 对象
|
||||
if (preg_match('/\{[\s\S]*}/', $jsonContent, $matches)) {
|
||||
$decoded = json_decode($matches[0], true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 如果无法解析 JSON,返回原始响应
|
||||
return [
|
||||
'core_anomalies' => [],
|
||||
'summary' => $response,
|
||||
|
||||
@@ -207,7 +207,8 @@ class SlsClient
|
||||
|
||||
$allLogs = array_merge($allLogs, $result['logs']);
|
||||
|
||||
if ($result['complete'] || count($result['logs']) < $batchSize) {
|
||||
// 如果返回的日志数量小于批次大小,说明没有更多数据了
|
||||
if (count($result['logs']) < $batchSize) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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;
|
||||
@@ -38,6 +39,7 @@ class LogAnalysisJob implements ShouldQueue
|
||||
SlsService $slsService,
|
||||
AiService $aiService,
|
||||
CodeContextService $codeContextService,
|
||||
CodeAnalysisService $codeAnalysisService,
|
||||
ConfigService $configService,
|
||||
DingTalkService $dingTalkService
|
||||
): void {
|
||||
@@ -85,28 +87,30 @@ class LogAnalysisJob implements ShouldQueue
|
||||
$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 分析
|
||||
// AI 分析(不再携带代码上下文)
|
||||
try {
|
||||
$results[$appName] = $aiService->analyzeLogs($logsContent, $codeContext);
|
||||
$results[$appName] = $aiService->analyzeLogs($logsContent, null);
|
||||
$results[$appName]['log_count'] = $appLogsCollection->count();
|
||||
$results[$appName]['has_code_context'] = $codeContext !== null;
|
||||
|
||||
// 如果是 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(),
|
||||
'has_code_context' => false,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
196
app/Services/CodeAnalysisService.php
Normal file
196
app/Services/CodeAnalysisService.php
Normal file
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\Process\Exception\ProcessFailedException;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
class CodeAnalysisService
|
||||
{
|
||||
public const TOOL_CLAUDE = 'claude';
|
||||
public const TOOL_CODEX = 'codex';
|
||||
|
||||
private int $timeout = 300;
|
||||
|
||||
public function __construct(
|
||||
private readonly CodeContextService $codeContextService,
|
||||
private readonly ConfigService $configService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 使用配置的工具在项目中分析日志问题
|
||||
*
|
||||
* @param string $appName 应用名称
|
||||
* @param string $logsContent 日志内容
|
||||
* @param string|null $aiSummary AI 初步分析摘要
|
||||
* @return array 分析结果
|
||||
*/
|
||||
public function analyze(string $appName, string $logsContent, ?string $aiSummary): array
|
||||
{
|
||||
$repoPath = $this->codeContextService->getRepoPath($appName);
|
||||
|
||||
if (!$repoPath) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => "未配置项目路径: {$appName}",
|
||||
];
|
||||
}
|
||||
|
||||
$tool = $this->getConfiguredTool();
|
||||
|
||||
try {
|
||||
$prompt = $this->buildPrompt($logsContent, $aiSummary);
|
||||
$output = $this->runTool($tool, $repoPath, $prompt);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'output' => $output,
|
||||
'repo_path' => $repoPath,
|
||||
'tool' => $tool,
|
||||
];
|
||||
} catch (ProcessFailedException $e) {
|
||||
Log::error("{$tool} execution failed", [
|
||||
'app_name' => $appName,
|
||||
'repo_path' => $repoPath,
|
||||
'tool' => $tool,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => "{$tool} 执行失败: " . $e->getProcess()->getErrorOutput(),
|
||||
'tool' => $tool,
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Code analysis failed", [
|
||||
'app_name' => $appName,
|
||||
'repo_path' => $repoPath,
|
||||
'tool' => $tool,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
'tool' => $tool,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置的分析工具
|
||||
*/
|
||||
public function getConfiguredTool(): string
|
||||
{
|
||||
$tool = $this->configService->get('log_analysis.code_analysis_tool', self::TOOL_CLAUDE);
|
||||
|
||||
if (!in_array($tool, [self::TOOL_CLAUDE, self::TOOL_CODEX])) {
|
||||
return self::TOOL_CLAUDE;
|
||||
}
|
||||
|
||||
return $tool;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置分析工具
|
||||
*/
|
||||
public function setTool(string $tool): void
|
||||
{
|
||||
if (!in_array($tool, [self::TOOL_CLAUDE, self::TOOL_CODEX])) {
|
||||
throw new \InvalidArgumentException("不支持的工具: {$tool}");
|
||||
}
|
||||
|
||||
$this->configService->set(
|
||||
'log_analysis.code_analysis_tool',
|
||||
$tool,
|
||||
'代码分析工具 (claude/codex)'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的工具列表
|
||||
*/
|
||||
public function getAvailableTools(): array
|
||||
{
|
||||
return [
|
||||
self::TOOL_CLAUDE => [
|
||||
'name' => 'Claude CLI',
|
||||
'description' => 'Anthropic Claude 命令行工具',
|
||||
],
|
||||
self::TOOL_CODEX => [
|
||||
'name' => 'Codex CLI',
|
||||
'description' => 'OpenAI Codex 命令行工具',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建提示词
|
||||
*/
|
||||
private function buildPrompt(string $logsContent, ?string $aiSummary): string
|
||||
{
|
||||
$prompt = "分析以下错误日志,在代码库中排查根本原因并给出具体优化方案:\n\n";
|
||||
$prompt .= "=== 日志内容 ===\n{$logsContent}\n\n";
|
||||
|
||||
if ($aiSummary) {
|
||||
$prompt .= "=== AI 初步分析 ===\n{$aiSummary}\n\n";
|
||||
}
|
||||
|
||||
$prompt .= "请:\n";
|
||||
$prompt .= "1. 定位相关代码文件\n";
|
||||
$prompt .= "2. 分析根本原因\n";
|
||||
$prompt .= "3. 给出具体修复方案\n";
|
||||
|
||||
return $prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行分析工具
|
||||
*/
|
||||
private function runTool(string $tool, string $workingDirectory, string $prompt): string
|
||||
{
|
||||
return match ($tool) {
|
||||
self::TOOL_CODEX => $this->runCodex($workingDirectory, $prompt),
|
||||
default => $this->runClaude($workingDirectory, $prompt),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 Claude CLI 命令
|
||||
*/
|
||||
private function runClaude(string $workingDirectory, string $prompt): string
|
||||
{
|
||||
$process = new Process(
|
||||
['claude', '--print', $prompt],
|
||||
$workingDirectory
|
||||
);
|
||||
$process->setTimeout($this->timeout);
|
||||
$process->mustRun();
|
||||
|
||||
return trim($process->getOutput());
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 Codex CLI 命令
|
||||
*/
|
||||
private function runCodex(string $workingDirectory, string $prompt): string
|
||||
{
|
||||
$process = new Process(
|
||||
['codex', '--quiet', '--full-auto', $prompt],
|
||||
$workingDirectory
|
||||
);
|
||||
$process->setTimeout($this->timeout);
|
||||
$process->mustRun();
|
||||
|
||||
return trim($process->getOutput());
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置超时时间
|
||||
*/
|
||||
public function setTimeout(int $timeout): void
|
||||
{
|
||||
$this->timeout = $timeout;
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,7 @@ class LogAnalysisService
|
||||
public function __construct(
|
||||
private readonly SlsService $slsService,
|
||||
private readonly AiService $aiService,
|
||||
private readonly CodeContextService $codeContextService,
|
||||
private readonly ConfigService $configService,
|
||||
private readonly CodeAnalysisService $codeAnalysisService,
|
||||
private readonly DingTalkService $dingTalkService
|
||||
) {}
|
||||
|
||||
@@ -97,39 +96,34 @@ class LogAnalysisService
|
||||
|
||||
// 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] = $this->aiService->analyzeLogs($logsContent, null);
|
||||
$results[$appName]['log_count'] = $appLogsCollection->count();
|
||||
$results[$appName]['has_code_context'] = $codeContext !== null;
|
||||
|
||||
// 如果是 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(),
|
||||
'has_code_context' => false,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -180,11 +174,14 @@ class LogAnalysisService
|
||||
$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(100)->values()->toArray(),
|
||||
'logs' => $logs->take($displayLimit)->values()->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -228,11 +225,14 @@ class LogAnalysisService
|
||||
}
|
||||
}
|
||||
|
||||
// 使用传入的 limit 参数,如果未指定则默认显示全部
|
||||
$displayLimit = $limit ?? $allLogs->count();
|
||||
|
||||
return [
|
||||
'total' => $statistics['total'],
|
||||
'statistics' => $statistics,
|
||||
'grouped_by_logstore' => $groupedByLogstore,
|
||||
'logs' => $allLogs->take(100)->values()->toArray(),
|
||||
'logs' => $allLogs->take($displayLimit)->values()->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -356,10 +356,39 @@
|
||||
<span class="px-2 py-0.5 bg-blue-100 text-blue-700 rounded text-xs">{{ anomaly.classification }}</span>
|
||||
<span class="text-sm text-gray-500">x{{ anomaly.count }}</span>
|
||||
</div>
|
||||
<!-- 原始日志示例 -->
|
||||
<div v-if="anomaly.sample" class="mb-2 p-2 bg-gray-100 rounded border border-gray-200 overflow-x-auto">
|
||||
<p class="text-xs text-gray-500 mb-1"><strong>原始日志:</strong></p>
|
||||
<pre class="text-xs text-gray-700 whitespace-pre-wrap break-all font-mono">{{ anomaly.sample }}</pre>
|
||||
</div>
|
||||
<p class="text-sm text-gray-700 mb-1"><strong>可能原因:</strong> {{ anomaly.possible_cause }}</p>
|
||||
<p class="text-sm text-gray-600"><strong>建议:</strong> {{ anomaly.suggestion }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 代码深度排查结果 -->
|
||||
<div v-if="result.code_analysis" class="mt-6 border-t border-gray-200 pt-4">
|
||||
<h4 class="font-medium text-purple-700 mb-3 flex items-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
代码深度排查
|
||||
<span class="ml-2 px-2 py-0.5 bg-purple-100 text-purple-700 rounded text-xs">
|
||||
{{ result.code_analysis.tool === 'codex' ? 'Codex' : 'Claude CLI' }}
|
||||
</span>
|
||||
</h4>
|
||||
<div v-if="result.code_analysis.success" class="bg-purple-50 p-4 rounded-lg border border-purple-200">
|
||||
<div class="text-xs text-gray-500 mb-3">
|
||||
<span class="font-medium">项目路径:</span> {{ result.code_analysis.repo_path }}
|
||||
</div>
|
||||
<pre class="whitespace-pre-wrap text-sm text-gray-800 font-mono leading-relaxed">{{ result.code_analysis.output }}</pre>
|
||||
</div>
|
||||
<div v-else class="bg-red-50 p-4 rounded-lg border border-red-200">
|
||||
<p class="text-red-600 text-sm">
|
||||
<strong>排查失败:</strong> {{ result.code_analysis.error }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,6 @@ Schedule::command('git-monitor:cache')
|
||||
// SLS 日志分析定时任务 - 每天凌晨 2 点执行
|
||||
// 分析过去 24 小时的 ERROR 和 WARNING 日志并推送到钉钉
|
||||
// 可通过数据库配置 log_analysis.settings.daily_schedule_enabled 控制是否启用
|
||||
/*
|
||||
Schedule::command('log-analysis:run --from="-24h" --to="now" --query="ERROR or WARNING" --push')
|
||||
->dailyAt('02:00')
|
||||
->withoutOverlapping()
|
||||
@@ -53,7 +52,6 @@ Schedule::command('log-analysis:run --from="-24h" --to="now" --query="ERROR or W
|
||||
->onFailure(function () {
|
||||
Log::error('每日日志分析定时任务执行失败');
|
||||
});
|
||||
*/
|
||||
|
||||
// SLS 日志分析定时任务 - 每 4 小时执行一次
|
||||
// 分析过去 6 小时的 ERROR 和 WARNING 日志并推送到钉钉
|
||||
|
||||
Reference in New Issue
Block a user