#feature: add Jenkins deploy monitor & log clean task
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user