#feature: add Jenkins deploy monitor & log clean task
This commit is contained in:
@@ -126,3 +126,12 @@ AI_TEMPERATURE=0.3
|
||||
AI_TIMEOUT=120
|
||||
AI_MAX_TOKENS=4096
|
||||
|
||||
DINGTALK_WEBHOOK=
|
||||
DINGTALK_SECRET=
|
||||
|
||||
# Jenkins Configuration
|
||||
JENKINS_HOST=http://jenkins.example.com
|
||||
JENKINS_USERNAME=
|
||||
JENKINS_API_TOKEN=
|
||||
JENKINS_TIMEOUT=30
|
||||
|
||||
|
||||
97
app/Clients/JenkinsClient.php
Normal file
97
app/Clients/JenkinsClient.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Clients;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class JenkinsClient
|
||||
{
|
||||
private ?string $host;
|
||||
private ?string $username;
|
||||
private ?string $apiToken;
|
||||
private int $timeout;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$config = config('jenkins', []);
|
||||
$this->host = rtrim($config['host'] ?? '', '/');
|
||||
$this->username = $config['username'] ?? null;
|
||||
$this->apiToken = $config['api_token'] ?? null;
|
||||
$this->timeout = $config['timeout'] ?? 30;
|
||||
}
|
||||
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return !empty($this->host) && !empty($this->username) && !empty($this->apiToken);
|
||||
}
|
||||
|
||||
public function getJobInfo(string $jobName): ?array
|
||||
{
|
||||
return $this->request("/job/{$jobName}/api/json");
|
||||
}
|
||||
|
||||
public function getBuildInfo(string $jobName, int $buildNumber): ?array
|
||||
{
|
||||
return $this->request("/job/{$jobName}/{$buildNumber}/api/json");
|
||||
}
|
||||
|
||||
public function getLastBuild(string $jobName): ?array
|
||||
{
|
||||
return $this->request("/job/{$jobName}/lastBuild/api/json");
|
||||
}
|
||||
|
||||
public function getBuilds(string $jobName, int $limit = 10): array
|
||||
{
|
||||
$jobInfo = $this->getJobInfo($jobName);
|
||||
if (!$jobInfo || empty($jobInfo['builds'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$builds = array_slice($jobInfo['builds'], 0, $limit);
|
||||
$result = [];
|
||||
|
||||
foreach ($builds as $build) {
|
||||
$buildInfo = $this->getBuildInfo($jobName, $build['number']);
|
||||
if ($buildInfo) {
|
||||
$result[] = $buildInfo;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function request(string $path): ?array
|
||||
{
|
||||
if (!$this->isConfigured()) {
|
||||
Log::warning('Jenkins client is not configured');
|
||||
return null;
|
||||
}
|
||||
|
||||
$url = $this->host . $path;
|
||||
|
||||
try {
|
||||
$response = Http::timeout($this->timeout)
|
||||
->withBasicAuth($this->username, $this->apiToken)
|
||||
->get($url);
|
||||
|
||||
if ($response->successful()) {
|
||||
return $response->json();
|
||||
}
|
||||
|
||||
Log::warning('Jenkins API request failed', [
|
||||
'url' => $url,
|
||||
'status' => $response->status(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Jenkins API request error', [
|
||||
'url' => $url,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
72
app/Console/Commands/CleanScheduledTaskLogsCommand.php
Normal file
72
app/Console/Commands/CleanScheduledTaskLogsCommand.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class CleanScheduledTaskLogsCommand extends Command
|
||||
{
|
||||
protected $signature = 'logs:clean-scheduled-tasks {--days=7 : 保留最近几天的日志}';
|
||||
|
||||
protected $description = '清理定时任务日志文件,删除指定天数之前的日志';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$days = (int) $this->option('days');
|
||||
$logPath = storage_path('logs/scheduled-tasks');
|
||||
|
||||
if (!File::exists($logPath)) {
|
||||
$this->info('日志目录不存在,无需清理');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$cutoffDate = Carbon::now()->subDays($days);
|
||||
$this->info("开始清理 {$days} 天前的定时任务日志...");
|
||||
$this->info("截止日期: {$cutoffDate->format('Y-m-d')}");
|
||||
|
||||
$files = File::files($logPath);
|
||||
$deletedCount = 0;
|
||||
$totalSize = 0;
|
||||
|
||||
foreach ($files as $file) {
|
||||
$filename = $file->getFilename();
|
||||
|
||||
// 匹配日志文件名格式: task-name-YYYY-MM-DD.log
|
||||
if (preg_match('/-(\d{4}-\d{2}-\d{2})\.log$/', $filename, $matches)) {
|
||||
$fileDate = Carbon::parse($matches[1]);
|
||||
|
||||
if ($fileDate->lt($cutoffDate)) {
|
||||
$fileSize = $file->getSize();
|
||||
$totalSize += $fileSize;
|
||||
|
||||
File::delete($file->getPathname());
|
||||
$deletedCount++;
|
||||
|
||||
$this->line("已删除: {$filename} (" . $this->formatBytes($fileSize) . ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($deletedCount > 0) {
|
||||
$this->info("清理完成!共删除 {$deletedCount} 个日志文件,释放空间: " . $this->formatBytes($totalSize));
|
||||
} else {
|
||||
$this->info('没有需要清理的日志文件');
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function formatBytes(int $bytes): string
|
||||
{
|
||||
if ($bytes >= 1073741824) {
|
||||
return number_format($bytes / 1073741824, 2) . ' GB';
|
||||
} elseif ($bytes >= 1048576) {
|
||||
return number_format($bytes / 1048576, 2) . ' MB';
|
||||
} elseif ($bytes >= 1024) {
|
||||
return number_format($bytes / 1024, 2) . ' KB';
|
||||
}
|
||||
return $bytes . ' B';
|
||||
}
|
||||
}
|
||||
41
app/Console/Commands/JenkinsMonitorCommand.php
Normal file
41
app/Console/Commands/JenkinsMonitorCommand.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\JenkinsMonitorService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class JenkinsMonitorCommand extends Command
|
||||
{
|
||||
protected $signature = 'jenkins:monitor';
|
||||
|
||||
protected $description = '轮询 Jenkins 检查新构建并发送钉钉通知';
|
||||
|
||||
public function handle(JenkinsMonitorService $service): void
|
||||
{
|
||||
$this->info('开始检查 Jenkins 构建...');
|
||||
|
||||
$results = $service->checkAllProjects();
|
||||
|
||||
if (isset($results['skipped'])) {
|
||||
$this->warn('跳过检查: ' . ($results['reason'] ?? 'unknown'));
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($results as $slug => $result) {
|
||||
if (isset($result['skipped'])) {
|
||||
$this->line(sprintf('[%s] 跳过: %s', $slug, $result['reason'] ?? 'unknown'));
|
||||
continue;
|
||||
}
|
||||
|
||||
$newBuilds = $result['new_builds'] ?? [];
|
||||
if (empty($newBuilds)) {
|
||||
$this->line(sprintf('[%s] 无新构建', $slug));
|
||||
} else {
|
||||
$this->info(sprintf('[%s] 发现 %d 个新构建: #%s', $slug, count($newBuilds), implode(', #', $newBuilds)));
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('检查完成');
|
||||
}
|
||||
}
|
||||
40
app/Console/Commands/ScheduledTaskRefreshCommand.php
Normal file
40
app/Console/Commands/ScheduledTaskRefreshCommand.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\ScheduledTaskService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ScheduledTaskRefreshCommand extends Command
|
||||
{
|
||||
protected $signature = 'scheduled-task:refresh';
|
||||
|
||||
protected $description = '刷新定时任务列表,同步 console.php 中的任务配置到数据库';
|
||||
|
||||
public function handle(ScheduledTaskService $taskService): int
|
||||
{
|
||||
try {
|
||||
$this->info('开始刷新定时任务列表...');
|
||||
|
||||
$tasks = $taskService->getAllTasks();
|
||||
|
||||
$this->info(sprintf('成功刷新 %d 个定时任务', count($tasks)));
|
||||
|
||||
// 显示任务列表
|
||||
$this->table(
|
||||
['任务名称', '描述', '执行频率', '状态'],
|
||||
array_map(fn($task) => [
|
||||
$task['name'],
|
||||
$task['description'],
|
||||
$task['frequency'],
|
||||
$task['enabled'] ? '已启用' : '已禁用',
|
||||
], $tasks)
|
||||
);
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (\Exception $e) {
|
||||
$this->error("刷新失败: {$e->getMessage()}");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
app/Http/Controllers/Admin/JenkinsDeploymentController.php
Normal file
41
app/Http/Controllers/Admin/JenkinsDeploymentController.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\JenkinsDeployment;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class JenkinsDeploymentController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = JenkinsDeployment::with('project:id,slug,name')
|
||||
->orderByDesc('created_at');
|
||||
|
||||
if ($request->filled('project_id')) {
|
||||
$query->where('project_id', $request->input('project_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('job_name')) {
|
||||
$query->where('job_name', $request->input('job_name'));
|
||||
}
|
||||
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->input('status'));
|
||||
}
|
||||
|
||||
$perPage = min((int) $request->input('per_page', 20), 100);
|
||||
$deployments = $query->paginate($perPage);
|
||||
|
||||
return response()->json($deployments);
|
||||
}
|
||||
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$deployment = JenkinsDeployment::with('project:id,slug,name')->findOrFail($id);
|
||||
|
||||
return response()->json($deployment);
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,8 @@ class ProjectController extends Controller
|
||||
'log_app_names' => ['nullable', 'array'],
|
||||
'log_app_names.*' => ['string', 'max:100'],
|
||||
'log_env' => ['nullable', 'string', 'max:50'],
|
||||
'jenkins_job_name' => ['nullable', 'string', 'max:255'],
|
||||
'jenkins_notify_enabled' => ['nullable', 'boolean'],
|
||||
]);
|
||||
|
||||
$project = $this->projectService->create($data);
|
||||
@@ -126,6 +128,8 @@ class ProjectController extends Controller
|
||||
'log_app_names' => ['nullable', 'array'],
|
||||
'log_app_names.*' => ['string', 'max:100'],
|
||||
'log_env' => ['nullable', 'string', 'max:50'],
|
||||
'jenkins_job_name' => ['nullable', 'string', 'max:255'],
|
||||
'jenkins_notify_enabled' => ['nullable', 'boolean'],
|
||||
]);
|
||||
|
||||
$project = $this->projectService->update($project, $data);
|
||||
|
||||
69
app/Models/JenkinsDeployment.php
Normal file
69
app/Models/JenkinsDeployment.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class JenkinsDeployment extends BaseModel
|
||||
{
|
||||
protected $fillable = [
|
||||
'project_id',
|
||||
'build_number',
|
||||
'job_name',
|
||||
'status',
|
||||
'branch',
|
||||
'commit_sha',
|
||||
'triggered_by',
|
||||
'duration',
|
||||
'build_url',
|
||||
'raw_data',
|
||||
'build_params',
|
||||
'notified',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'raw_data' => 'array',
|
||||
'build_params' => 'array',
|
||||
'notified' => 'boolean',
|
||||
];
|
||||
|
||||
public function project(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Project::class);
|
||||
}
|
||||
|
||||
public function getFormattedDuration(): string
|
||||
{
|
||||
if (!$this->duration) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
$seconds = (int) ($this->duration / 1000);
|
||||
$minutes = (int) ($seconds / 60);
|
||||
$seconds = $seconds % 60;
|
||||
|
||||
return sprintf('%02d:%02d', $minutes, $seconds);
|
||||
}
|
||||
|
||||
public function getStatusEmoji(): string
|
||||
{
|
||||
return match ($this->status) {
|
||||
'SUCCESS' => '✅',
|
||||
'FAILURE' => '❌',
|
||||
'ABORTED' => '⏹️',
|
||||
'UNSTABLE' => '⚠️',
|
||||
default => '❓',
|
||||
};
|
||||
}
|
||||
|
||||
public function getStatusLabel(): string
|
||||
{
|
||||
return match ($this->status) {
|
||||
'SUCCESS' => '成功',
|
||||
'FAILURE' => '失败',
|
||||
'ABORTED' => '已中止',
|
||||
'UNSTABLE' => '不稳定',
|
||||
default => '未知',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -20,12 +20,16 @@ class Project extends BaseModel
|
||||
'git_version_cached_at',
|
||||
'log_app_names',
|
||||
'log_env',
|
||||
'jenkins_job_name',
|
||||
'jenkins_notify_enabled',
|
||||
'jenkins_last_notified_build',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'git_monitor_enabled' => 'boolean',
|
||||
'auto_create_release_branch' => 'boolean',
|
||||
'is_important' => 'boolean',
|
||||
'jenkins_notify_enabled' => 'boolean',
|
||||
'log_app_names' => 'array',
|
||||
'git_version_cached_at' => 'datetime',
|
||||
];
|
||||
@@ -86,4 +90,15 @@ class Project extends BaseModel
|
||||
->whereJsonContains('log_app_names', $appName)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有启用 Jenkins 通知的项目
|
||||
*/
|
||||
public static function getJenkinsNotifyEnabled(): \Illuminate\Database\Eloquent\Collection
|
||||
{
|
||||
return static::query()
|
||||
->where('jenkins_notify_enabled', true)
|
||||
->whereNotNull('jenkins_job_name')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
||||
21
app/Models/ScheduledTask.php
Normal file
21
app/Models/ScheduledTask.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ScheduledTask extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'command',
|
||||
'description',
|
||||
'frequency',
|
||||
'cron',
|
||||
'enabled',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'enabled' => 'boolean',
|
||||
];
|
||||
}
|
||||
@@ -11,8 +11,7 @@ class CodeContextService
|
||||
private int $contextLines = 10;
|
||||
|
||||
public function __construct(
|
||||
private readonly ConfigService $configService,
|
||||
private readonly EnvService $envService
|
||||
private readonly ConfigService $configService
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -23,49 +22,16 @@ class CodeContextService
|
||||
*/
|
||||
public function getRepoPath(string $appName): ?string
|
||||
{
|
||||
// 优先从 Project 模型查找
|
||||
// 从 Project 模型查找,直接使用项目路径
|
||||
$project = Project::findByAppName($appName);
|
||||
|
||||
if ($project) {
|
||||
$env = $project->log_env ?? 'production';
|
||||
try {
|
||||
$envContent = $this->envService->getEnvContent($project->slug, $env);
|
||||
$repoPath = $this->parseEnvValue($envContent, 'LOG_ANALYSIS_CODE_REPO_PATH');
|
||||
|
||||
if ($repoPath && is_dir($repoPath)) {
|
||||
return $repoPath;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// 忽略错误,继续尝试旧配置
|
||||
$projectsPath = $this->configService->get('workspace.projects_path', '');
|
||||
if ($projectsPath && $project->isPathValid($projectsPath)) {
|
||||
return $project->getFullPath($projectsPath);
|
||||
}
|
||||
}
|
||||
|
||||
// 回退到旧的配置方式(兼容迁移前的情况)
|
||||
$appEnvMap = $this->configService->get('log_analysis.app_env_map', []);
|
||||
|
||||
if (!isset($appEnvMap[$appName])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$mapping = $appEnvMap[$appName];
|
||||
$projectSlug = $mapping['project'] ?? null;
|
||||
$env = $mapping['env'] ?? null;
|
||||
|
||||
if (!$projectSlug || !$env) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$envContent = $this->envService->getEnvContent($projectSlug, $env);
|
||||
$repoPath = $this->parseEnvValue($envContent, 'LOG_ANALYSIS_CODE_REPO_PATH');
|
||||
|
||||
if ($repoPath && is_dir($repoPath)) {
|
||||
return $repoPath;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// 忽略错误,返回 null
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -246,36 +212,6 @@ class CodeContextService
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 .env 内容中解析指定键的值
|
||||
*
|
||||
* @param string $envContent
|
||||
* @param string $key
|
||||
* @return string|null
|
||||
*/
|
||||
private function parseEnvValue(string $envContent, string $key): ?string
|
||||
{
|
||||
$lines = explode("\n", $envContent);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
|
||||
// 跳过注释和空行
|
||||
if (empty($line) || str_starts_with($line, '#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_starts_with($line, "{$key}=")) {
|
||||
$value = substr($line, strlen($key) + 1);
|
||||
// 移除引号
|
||||
$value = trim($value, '"\'');
|
||||
return $value ?: null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置上下文行数
|
||||
*/
|
||||
|
||||
294
app/Services/JenkinsMonitorService.php
Normal file
294
app/Services/JenkinsMonitorService.php
Normal file
@@ -0,0 +1,294 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Clients\JenkinsClient;
|
||||
use App\Models\JenkinsDeployment;
|
||||
use App\Models\Project;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class JenkinsMonitorService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly JenkinsClient $jenkinsClient,
|
||||
private readonly DingTalkService $dingTalkService,
|
||||
private readonly ConfigService $configService
|
||||
) {}
|
||||
|
||||
public function checkAllProjects(): array
|
||||
{
|
||||
if (!$this->jenkinsClient->isConfigured()) {
|
||||
Log::warning('Jenkins client is not configured, skipping monitor');
|
||||
return ['skipped' => true, 'reason' => 'Jenkins not configured'];
|
||||
}
|
||||
|
||||
$projects = Project::getJenkinsNotifyEnabled();
|
||||
$results = [];
|
||||
|
||||
foreach ($projects as $project) {
|
||||
$results[$project->slug] = $this->checkProject($project);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function checkProject(Project $project): array
|
||||
{
|
||||
if (empty($project->jenkins_job_name)) {
|
||||
return ['skipped' => true, 'reason' => 'No Jenkins job configured'];
|
||||
}
|
||||
|
||||
$jobName = $project->jenkins_job_name;
|
||||
$lastNotifiedBuild = $project->jenkins_last_notified_build ?? 0;
|
||||
$allowedTriggers = $this->getAllowedTriggers();
|
||||
|
||||
$builds = $this->jenkinsClient->getBuilds($jobName, 5);
|
||||
$newBuilds = [];
|
||||
|
||||
foreach ($builds as $build) {
|
||||
// 只处理已完成的构建
|
||||
if ($build['building'] ?? true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$buildNumber = $build['number'];
|
||||
|
||||
// 跳过已通知的构建
|
||||
if ($buildNumber <= $lastNotifiedBuild) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否已存在记录
|
||||
$exists = JenkinsDeployment::where('job_name', $jobName)
|
||||
->where('build_number', $buildNumber)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 解析构建信息
|
||||
$triggeredBy = $this->extractTriggeredBy($build);
|
||||
$branch = $this->extractBranch($build);
|
||||
$commitSha = $this->extractCommitSha($build);
|
||||
$buildParams = $this->extractBuildParams($build);
|
||||
|
||||
// 过滤触发者(只通知指定用户触发的构建)
|
||||
// 如果没有配置允许的触发者列表,则跳过所有通知
|
||||
if (empty($allowedTriggers)) {
|
||||
Log::info('Skipping build - no allowed triggers configured', [
|
||||
'job' => $jobName,
|
||||
'build' => $buildNumber,
|
||||
'triggered_by' => $triggeredBy,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查触发者是否在允许列表中
|
||||
if (!$this->isAllowedTrigger($triggeredBy, $allowedTriggers)) {
|
||||
Log::info('Skipping build due to trigger filter', [
|
||||
'job' => $jobName,
|
||||
'build' => $buildNumber,
|
||||
'triggered_by' => $triggeredBy,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 保存发布记录
|
||||
$deployment = JenkinsDeployment::create([
|
||||
'project_id' => $project->id,
|
||||
'build_number' => $buildNumber,
|
||||
'job_name' => $jobName,
|
||||
'status' => $build['result'] ?? 'UNKNOWN',
|
||||
'branch' => $branch,
|
||||
'commit_sha' => $commitSha,
|
||||
'triggered_by' => $triggeredBy,
|
||||
'duration' => $build['duration'] ?? null,
|
||||
'build_url' => $build['url'] ?? null,
|
||||
'raw_data' => $build,
|
||||
'build_params' => $buildParams,
|
||||
'notified' => false,
|
||||
]);
|
||||
|
||||
// 发送通知
|
||||
$this->sendNotification($project, $deployment);
|
||||
|
||||
$deployment->update(['notified' => true]);
|
||||
$newBuilds[] = $buildNumber;
|
||||
}
|
||||
|
||||
// 更新最后通知的构建号
|
||||
if (!empty($newBuilds)) {
|
||||
$project->update([
|
||||
'jenkins_last_notified_build' => max($newBuilds),
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
'job' => $jobName,
|
||||
'new_builds' => $newBuilds,
|
||||
];
|
||||
}
|
||||
|
||||
private function sendNotification(Project $project, JenkinsDeployment $deployment): void
|
||||
{
|
||||
$lines = [];
|
||||
$lines[] = sprintf(
|
||||
"%s 【Jenkins 发布通知】",
|
||||
$deployment->getStatusEmoji()
|
||||
);
|
||||
$lines[] = sprintf("项目: %s", $project->name);
|
||||
$lines[] = sprintf("状态: %s", $deployment->getStatusLabel());
|
||||
$lines[] = sprintf("构建号: #%d", $deployment->build_number);
|
||||
$lines[] = sprintf("触发者: %s", $deployment->triggered_by ?? '-');
|
||||
$lines[] = sprintf("耗时: %s", $deployment->getFormattedDuration());
|
||||
|
||||
// 添加构建参数
|
||||
if (!empty($deployment->build_params)) {
|
||||
$lines[] = "\n构建参数:";
|
||||
foreach ($deployment->build_params as $key => $value) {
|
||||
// 格式化参数值
|
||||
if (is_bool($value)) {
|
||||
$value = $value ? 'true' : 'false';
|
||||
} elseif (is_array($value)) {
|
||||
$value = json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
} elseif ($value === null || $value === '') {
|
||||
continue; // 跳过空值
|
||||
}
|
||||
$lines[] = sprintf(" %s: %s", $key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
$lines[] = sprintf("\n详情: %s", $deployment->build_url ?? '-');
|
||||
|
||||
$message = implode("\n", $lines);
|
||||
$this->dingTalkService->sendText($message);
|
||||
}
|
||||
|
||||
private function extractTriggeredBy(array $build): ?string
|
||||
{
|
||||
$actions = $build['actions'] ?? [];
|
||||
|
||||
foreach ($actions as $action) {
|
||||
// UserIdCause - 用户手动触发
|
||||
if (isset($action['causes'])) {
|
||||
foreach ($action['causes'] as $cause) {
|
||||
if (isset($cause['userId'])) {
|
||||
return $cause['userId'];
|
||||
}
|
||||
if (isset($cause['userName'])) {
|
||||
return $cause['userName'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function extractBranch(array $build): ?string
|
||||
{
|
||||
$actions = $build['actions'] ?? [];
|
||||
|
||||
// 优先从参数中获取 branchName
|
||||
foreach ($actions as $action) {
|
||||
if (isset($action['parameters'])) {
|
||||
foreach ($action['parameters'] as $param) {
|
||||
if (in_array($param['name'] ?? '', ['branchName', 'BRANCH', 'branch', 'GIT_BRANCH', 'BRANCH_NAME'])) {
|
||||
$value = $param['value'] ?? null;
|
||||
if (!empty($value)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果参数中没有,再从 Git 分支信息中获取
|
||||
foreach ($actions as $action) {
|
||||
if (isset($action['lastBuiltRevision']['branch'])) {
|
||||
foreach ($action['lastBuiltRevision']['branch'] as $branch) {
|
||||
$name = $branch['name'] ?? '';
|
||||
// 移除 origin/ 和 refs/remotes/origin/ 前缀
|
||||
return preg_replace('/^(refs\/remotes\/origin\/|origin\/)/', '', $name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function extractCommitSha(array $build): ?string
|
||||
{
|
||||
$actions = $build['actions'] ?? [];
|
||||
|
||||
foreach ($actions as $action) {
|
||||
if (isset($action['lastBuiltRevision']['SHA1'])) {
|
||||
return substr($action['lastBuiltRevision']['SHA1'], 0, 8);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getAllowedTriggers(): array
|
||||
{
|
||||
$config = $this->configService->get('jenkins_allowed_triggers', []);
|
||||
|
||||
// 如果配置为空,返回空数组
|
||||
if (empty($config)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 如果配置是数组,直接返回(过滤空值)
|
||||
if (is_array($config)) {
|
||||
return array_filter(array_map('trim', $config));
|
||||
}
|
||||
|
||||
// 如果配置是字符串(兼容旧格式),按逗号分隔
|
||||
if (is_string($config)) {
|
||||
return array_filter(array_map('trim', explode(',', $config)));
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function isAllowedTrigger(?string $triggeredBy, array $allowedTriggers): bool
|
||||
{
|
||||
if (empty($triggeredBy)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($allowedTriggers as $allowed) {
|
||||
if (strcasecmp($triggeredBy, $allowed) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function extractBuildParams(array $build): array
|
||||
{
|
||||
$actions = $build['actions'] ?? [];
|
||||
|
||||
foreach ($actions as $action) {
|
||||
// 查找 ParametersAction
|
||||
if (isset($action['_class']) && $action['_class'] === 'hudson.model.ParametersAction') {
|
||||
if (isset($action['parameters']) && is_array($action['parameters'])) {
|
||||
$params = [];
|
||||
foreach ($action['parameters'] as $param) {
|
||||
$name = $param['name'] ?? null;
|
||||
$value = $param['value'] ?? null;
|
||||
if ($name !== null) {
|
||||
$params[$name] = $value;
|
||||
}
|
||||
}
|
||||
return $params;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ class ScheduledTaskService
|
||||
$tasks[] = [
|
||||
'name' => $name,
|
||||
'command' => $this->getEventCommand($event),
|
||||
'description' => $event->description ?: $name,
|
||||
'description' => $this->getTaskDescription($name),
|
||||
'frequency' => $this->getFrequencyLabel($event->expression),
|
||||
'cron' => $event->expression,
|
||||
'enabled' => $enabledTasks[$name] ?? false,
|
||||
@@ -92,9 +92,15 @@ class ScheduledTaskService
|
||||
|
||||
private function getEventName($event): string
|
||||
{
|
||||
if (property_exists($event, 'mutexName') && $event->mutexName) {
|
||||
return $event->mutexName;
|
||||
// Laravel Schedule 事件的 description 属性存储任务名称
|
||||
// 我们在 routes/console.php 中通过 ->description() 设置
|
||||
|
||||
// 1. 优先使用 description (我们设置的任务标识符)
|
||||
if (property_exists($event, 'description') && $event->description) {
|
||||
return $event->description;
|
||||
}
|
||||
|
||||
// 2. 最后使用命令作为名称
|
||||
return $this->getEventCommand($event);
|
||||
}
|
||||
|
||||
@@ -125,9 +131,27 @@ class ScheduledTaskService
|
||||
'0 */12 * * *' => '每 12 小时',
|
||||
'0 0 * * *' => '每天凌晨 0:00',
|
||||
'0 2 * * *' => '每天凌晨 2:00',
|
||||
'0 3 * * *' => '每天凌晨 3:00',
|
||||
'0 0 * * 0' => '每周日凌晨',
|
||||
'0 0 1 * *' => '每月 1 日凌晨',
|
||||
];
|
||||
return $map[$expression] ?? $expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务的友好描述文本
|
||||
*/
|
||||
private function getTaskDescription(string $name): string
|
||||
{
|
||||
$descriptions = [
|
||||
'git-monitor-check' => 'Git 监控 - 检查 release 分支变化',
|
||||
'git-monitor-cache' => 'Git 监控 - 刷新 release 缓存',
|
||||
'daily-log-analysis' => 'SLS 日志分析 - 每日分析过去 24 小时日志',
|
||||
'frequent-log-analysis' => 'SLS 日志分析 - 定期分析过去 6 小时日志',
|
||||
'jenkins-monitor' => 'Jenkins 发布监控 - 检查新构建并发送通知',
|
||||
'scheduled-task-refresh' => '定时任务管理 - 刷新定时任务列表',
|
||||
'logs-cleanup' => '日志清理 - 自动删除 7 天前的定时任务日志',
|
||||
];
|
||||
return $descriptions[$name] ?? $name;
|
||||
}
|
||||
}
|
||||
|
||||
8
config/jenkins.php
Normal file
8
config/jenkins.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'host' => env('JENKINS_HOST'),
|
||||
'username' => env('JENKINS_USERNAME'),
|
||||
'api_token' => env('JENKINS_API_TOKEN'),
|
||||
'timeout' => (int) env('JENKINS_TIMEOUT', 30),
|
||||
];
|
||||
@@ -127,6 +127,39 @@ return [
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
],
|
||||
|
||||
// 定时任务日志通道
|
||||
'scheduled-tasks' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/scheduled-tasks/scheduled-tasks.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'days' => 7,
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'git-monitor' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/scheduled-tasks/git-monitor.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'days' => 7,
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'log-analysis' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/scheduled-tasks/log-analysis.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'days' => 7,
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'jenkins-monitor' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/scheduled-tasks/jenkins-monitor.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'days' => 7,
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('projects', function (Blueprint $table) {
|
||||
$table->string('jenkins_job_name', 255)->nullable()->comment('Jenkins Job 名称');
|
||||
$table->boolean('jenkins_notify_enabled')->default(false)->comment('是否启用 Jenkins 通知');
|
||||
$table->integer('jenkins_last_notified_build')->nullable()->comment('最后通知的构建号');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('projects', function (Blueprint $table) {
|
||||
$table->dropColumn(['jenkins_job_name', 'jenkins_notify_enabled', 'jenkins_last_notified_build']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('jenkins_deployments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('project_id')->nullable()->constrained()->onDelete('cascade');
|
||||
$table->integer('build_number');
|
||||
$table->string('job_name', 255);
|
||||
$table->string('status', 20)->comment('SUCCESS, FAILURE, ABORTED, UNSTABLE');
|
||||
$table->string('branch', 255)->nullable();
|
||||
$table->string('commit_sha', 64)->nullable();
|
||||
$table->string('triggered_by', 100)->nullable();
|
||||
$table->integer('duration')->nullable()->comment('构建耗时(毫秒)');
|
||||
$table->string('build_url', 500)->nullable();
|
||||
$table->json('raw_data')->nullable();
|
||||
$table->boolean('notified')->default(false);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['job_name', 'build_number']);
|
||||
$table->index(['project_id', 'created_at']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jenkins_deployments');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('scheduled_tasks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->unique()->comment('任务唯一标识符');
|
||||
$table->string('command')->comment('任务命令');
|
||||
$table->string('description')->nullable()->comment('任务描述');
|
||||
$table->string('frequency')->comment('执行频率描述');
|
||||
$table->string('cron')->comment('Cron 表达式');
|
||||
$table->boolean('enabled')->default(false)->comment('是否启用');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('scheduled_tasks');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('jenkins_deployments', function (Blueprint $table) {
|
||||
$table->json('build_params')->nullable()->after('raw_data')->comment('构建参数');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('jenkins_deployments', function (Blueprint $table) {
|
||||
$table->dropColumn('build_params');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -181,6 +181,22 @@
|
||||
<input v-model="form.is_important" type="checkbox" id="is_important" class="rounded border-gray-300 text-yellow-500 focus:ring-yellow-500" />
|
||||
<label for="is_important" class="text-sm text-gray-700">标记为重要项目</label>
|
||||
</div>
|
||||
|
||||
<!-- Jenkins 配置 -->
|
||||
<div class="border-t border-gray-200 pt-4 mt-4">
|
||||
<h5 class="text-sm font-medium text-gray-700 mb-3">Jenkins 发布通知</h5>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Jenkins Job 名称</label>
|
||||
<input v-model="form.jenkins_job_name" type="text" class="w-full px-3 py-2 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500" placeholder="如: portal-be-deploy" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input v-model="form.jenkins_notify_enabled" type="checkbox" id="jenkins_notify" class="rounded border-gray-300 text-orange-500 focus:ring-orange-500" />
|
||||
<label for="jenkins_notify" class="text-sm text-gray-700">启用 Jenkins 发布通知</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">日志 App 名称 (逗号分隔)</label>
|
||||
<input v-model="form.log_app_names_text" type="text" class="w-full px-3 py-2 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500" placeholder="如: portal-api, portal-worker" />
|
||||
@@ -309,6 +325,10 @@ const ProjectCard = {
|
||||
<span class="text-gray-500">版本:</span>
|
||||
<span class="font-mono text-gray-700">{{ project.git_current_version }}</span>
|
||||
</div>
|
||||
<div v-if="project.jenkins_job_name" class="flex items-center gap-2">
|
||||
<span class="text-gray-500">Jenkins:</span>
|
||||
<span class="font-mono bg-orange-50 text-orange-700 px-1.5 py-0.5 rounded text-xs">{{ project.jenkins_job_name }}</span>
|
||||
</div>
|
||||
<div v-if="project.log_app_names?.length" class="flex items-center gap-2">
|
||||
<span class="text-gray-500">App:</span>
|
||||
<span class="text-gray-700">{{ project.log_app_names.join(', ') }}</span>
|
||||
@@ -333,6 +353,14 @@ const ProjectCard = {
|
||||
</span>
|
||||
<span class="text-xs" :class="project.auto_create_release_branch ? 'text-purple-600' : 'text-gray-500'">自动创建分支</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-1.5 cursor-pointer" @click.prevent="$emit('toggle-field', project, 'jenkins_notify_enabled')">
|
||||
<span class="relative inline-block">
|
||||
<input type="checkbox" :checked="project.jenkins_notify_enabled" class="sr-only peer" />
|
||||
<span class="block w-8 h-4 bg-gray-200 rounded-full peer peer-checked:bg-orange-500 transition-colors"></span>
|
||||
<span class="absolute left-0.5 top-0.5 w-3 h-3 bg-white rounded-full transition-transform peer-checked:translate-x-4"></span>
|
||||
</span>
|
||||
<span class="text-xs" :class="project.jenkins_notify_enabled ? 'text-orange-600' : 'text-gray-500'">Jenkins通知</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-1.5 cursor-pointer" @click.prevent="$emit('toggle-field', project, 'is_important')">
|
||||
<span class="relative inline-block">
|
||||
<input type="checkbox" :checked="project.is_important" class="sr-only peer" />
|
||||
@@ -418,6 +446,8 @@ export default {
|
||||
git_monitor_enabled: false,
|
||||
auto_create_release_branch: false,
|
||||
is_important: false,
|
||||
jenkins_job_name: '',
|
||||
jenkins_notify_enabled: false,
|
||||
log_app_names_text: '',
|
||||
log_env: 'production'
|
||||
};
|
||||
@@ -456,6 +486,8 @@ export default {
|
||||
git_monitor_enabled: project.git_monitor_enabled || false,
|
||||
auto_create_release_branch: project.auto_create_release_branch || false,
|
||||
is_important: project.is_important || false,
|
||||
jenkins_job_name: project.jenkins_job_name || '',
|
||||
jenkins_notify_enabled: project.jenkins_notify_enabled || false,
|
||||
log_app_names_text: (project.log_app_names || []).join(', '),
|
||||
log_env: project.log_env || 'production'
|
||||
};
|
||||
@@ -483,6 +515,8 @@ export default {
|
||||
git_monitor_enabled: this.form.git_monitor_enabled,
|
||||
auto_create_release_branch: this.form.auto_create_release_branch,
|
||||
is_important: this.form.is_important,
|
||||
jenkins_job_name: this.form.jenkins_job_name || null,
|
||||
jenkins_notify_enabled: this.form.jenkins_notify_enabled,
|
||||
log_app_names: this.form.log_app_names_text ? this.form.log_app_names_text.split(',').map(s => s.trim()).filter(Boolean) : null,
|
||||
log_env: this.form.log_env || null
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Http\Controllers\SqlGeneratorController;
|
||||
use App\Http\Controllers\Admin\AdminMetaController;
|
||||
use App\Http\Controllers\Admin\ConfigController;
|
||||
use App\Http\Controllers\Admin\IpUserMappingController;
|
||||
use App\Http\Controllers\Admin\JenkinsDeploymentController;
|
||||
use App\Http\Controllers\Admin\OperationLogController;
|
||||
use App\Http\Controllers\Admin\ProjectController;
|
||||
use App\Http\Controllers\Admin\ScheduledTaskController;
|
||||
@@ -94,6 +95,10 @@ Route::prefix('admin')->middleware('admin.ip')->group(function () {
|
||||
// 定时任务管理
|
||||
Route::get('/scheduled-tasks', [ScheduledTaskController::class, 'index']);
|
||||
Route::post('/scheduled-tasks/{name}/toggle', [ScheduledTaskController::class, 'toggle']);
|
||||
|
||||
// Jenkins 发布历史
|
||||
Route::get('/jenkins/deployments', [JenkinsDeploymentController::class, 'index']);
|
||||
Route::get('/jenkins/deployments/{id}', [JenkinsDeploymentController::class, 'show']);
|
||||
});
|
||||
|
||||
// 日志分析 API 路由
|
||||
|
||||
@@ -25,16 +25,16 @@ Schedule::command('git-monitor:check')
|
||||
->everyTenMinutes()
|
||||
->withoutOverlapping()
|
||||
->runInBackground()
|
||||
->name('git-monitor-check')
|
||||
->description('Git 监控 - 检查 release 分支变化')
|
||||
->description('git-monitor-check')
|
||||
->appendOutputTo(storage_path('logs/scheduled-tasks/git-monitor-' . date('Y-m-d') . '.log'))
|
||||
->when(fn() => \App\Services\ScheduledTaskService::isEnabled('git-monitor-check'));
|
||||
|
||||
// Git Monitor - 每天凌晨 2 点刷新 release 缓存
|
||||
Schedule::command('git-monitor:cache')
|
||||
->dailyAt('02:00')
|
||||
->withoutOverlapping()
|
||||
->name('git-monitor-cache')
|
||||
->description('Git 监控 - 刷新 release 缓存')
|
||||
->description('git-monitor-cache')
|
||||
->appendOutputTo(storage_path('logs/scheduled-tasks/git-monitor-' . date('Y-m-d') . '.log'))
|
||||
->when(fn() => \App\Services\ScheduledTaskService::isEnabled('git-monitor-cache'));
|
||||
|
||||
// SLS 日志分析 - 每天凌晨 2 点执行
|
||||
@@ -42,8 +42,8 @@ Schedule::command('log-analysis:run --from="-24h" --to="now" --query="ERROR or W
|
||||
->dailyAt('02:00')
|
||||
->withoutOverlapping()
|
||||
->runInBackground()
|
||||
->name('daily-log-analysis')
|
||||
->description('SLS 日志分析 - 每日分析过去 24 小时日志')
|
||||
->description('daily-log-analysis')
|
||||
->appendOutputTo(storage_path('logs/scheduled-tasks/log-analysis-' . date('Y-m-d') . '.log'))
|
||||
->when(fn() => \App\Services\ScheduledTaskService::isEnabled('daily-log-analysis'))
|
||||
->onFailure(fn() => Log::error('每日日志分析定时任务执行失败'));
|
||||
|
||||
@@ -52,7 +52,32 @@ Schedule::command('log-analysis:run --from="-6h" --to="now" --query="ERROR or WA
|
||||
->everyFourHours()
|
||||
->withoutOverlapping()
|
||||
->runInBackground()
|
||||
->name('frequent-log-analysis')
|
||||
->description('SLS 日志分析 - 定期分析过去 6 小时日志')
|
||||
->description('frequent-log-analysis')
|
||||
->appendOutputTo(storage_path('logs/scheduled-tasks/log-analysis-' . date('Y-m-d') . '.log'))
|
||||
->when(fn() => \App\Services\ScheduledTaskService::isEnabled('frequent-log-analysis'))
|
||||
->onFailure(fn() => Log::error('SLS 日志分析定时任务执行失败'));
|
||||
|
||||
// Jenkins Monitor - 每分钟检查新构建
|
||||
Schedule::command('jenkins:monitor')
|
||||
->everyMinute()
|
||||
->withoutOverlapping()
|
||||
->runInBackground()
|
||||
->description('jenkins-monitor')
|
||||
->appendOutputTo(storage_path('logs/scheduled-tasks/jenkins-monitor-' . date('Y-m-d') . '.log'))
|
||||
->when(fn() => \App\Services\ScheduledTaskService::isEnabled('jenkins-monitor'));
|
||||
|
||||
// 定时任务刷新 - 每天凌晨 3 点刷新定时任务列表
|
||||
Schedule::command('scheduled-task:refresh')
|
||||
->dailyAt('03:00')
|
||||
->withoutOverlapping()
|
||||
->description('scheduled-task-refresh')
|
||||
->appendOutputTo(storage_path('logs/scheduled-tasks/scheduled-tasks-' . date('Y-m-d') . '.log'))
|
||||
->when(fn() => \App\Services\ScheduledTaskService::isEnabled('scheduled-task-refresh'));
|
||||
|
||||
// 日志清理 - 每天凌晨 4 点清理 7 天前的定时任务日志
|
||||
Schedule::command('logs:clean-scheduled-tasks --days=7')
|
||||
->dailyAt('04:00')
|
||||
->withoutOverlapping()
|
||||
->description('logs-cleanup')
|
||||
->appendOutputTo(storage_path('logs/scheduled-tasks/scheduled-tasks-' . date('Y-m-d') . '.log'))
|
||||
->when(fn() => \App\Services\ScheduledTaskService::isEnabled('logs-cleanup'));
|
||||
|
||||
Reference in New Issue
Block a user