增加ssh链接框架

This commit is contained in:
dengqichen 2025-12-07 23:29:36 +08:00
parent e5bd51b4b5
commit 03f2146546
26 changed files with 603 additions and 135 deletions

View File

@ -1,6 +1,6 @@
package com.qqchen.deploy.backend.deploy.dto; package com.qqchen.deploy.backend.deploy.dto;
import com.qqchen.deploy.backend.deploy.enums.AlertTypeEnum; import com.qqchen.deploy.backend.framework.enums.MonitorMetricEnum;
import com.qqchen.deploy.backend.framework.dto.BaseDTO; import com.qqchen.deploy.backend.framework.dto.BaseDTO;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*; import jakarta.validation.constraints.*;
@ -25,9 +25,9 @@ public class ServerAlertRuleDTO extends BaseDTO {
@Size(max = 100, message = "规则名称长度不能超过100个字符") @Size(max = 100, message = "规则名称长度不能超过100个字符")
private String ruleName; private String ruleName;
@Schema(description = "告警类型: CPU/MEMORY/DISK", required = true) @Schema(description = "监控指标类型: CPU/MEMORY/DISK/NETWORK", required = true)
@NotNull(message = "告警类型不能为空") @NotNull(message = "监控指标类型不能为空")
private AlertTypeEnum alertType; private MonitorMetricEnum alertType;
@Schema(description = "警告阈值(%)", required = true, example = "80.00") @Schema(description = "警告阈值(%)", required = true, example = "80.00")
@NotNull(message = "警告阈值不能为空") @NotNull(message = "警告阈值不能为空")
@ -48,13 +48,6 @@ public class ServerAlertRuleDTO extends BaseDTO {
@Schema(description = "是否启用") @Schema(description = "是否启用")
private Boolean enabled; private Boolean enabled;
@Schema(description = "通知方式: EMAIL/SMS/WEBHOOK")
@Size(max = 50, message = "通知方式长度不能超过50个字符")
private String notifyMethod;
@Schema(description = "通知联系人JSON格式")
private String notifyContacts;
@Schema(description = "规则描述") @Schema(description = "规则描述")
@Size(max = 500, message = "描述长度不能超过500个字符") @Size(max = 500, message = "描述长度不能超过500个字符")
private String description; private String description;

View File

@ -36,6 +36,9 @@ public class ServerMonitorDataDTO {
@Schema(description = "各分区磁盘使用率") @Schema(description = "各分区磁盘使用率")
private List<DiskUsageInfo> diskUsage; private List<DiskUsageInfo> diskUsage;
@Schema(description = "网络使用率(MB/s)")
private BigDecimal networkUsage;
@Schema(description = "采集时间") @Schema(description = "采集时间")
private LocalDateTime collectTime; private LocalDateTime collectTime;
} }

View File

@ -1,7 +1,7 @@
package com.qqchen.deploy.backend.deploy.entity; package com.qqchen.deploy.backend.deploy.entity;
import com.qqchen.deploy.backend.deploy.enums.AlertLevelEnum; import com.qqchen.deploy.backend.framework.enums.MonitorAlertLevelEnum;
import com.qqchen.deploy.backend.deploy.enums.AlertTypeEnum; import com.qqchen.deploy.backend.framework.enums.MonitorMetricEnum;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
@ -39,18 +39,18 @@ public class ServerAlertLog {
private Long ruleId; private Long ruleId;
/** /**
* 告警类型 * 监控指标类型
*/ */
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@Column(name = "alert_type", nullable = false, length = 20) @Column(name = "alert_type", nullable = false, length = 20)
private AlertTypeEnum alertType; private MonitorMetricEnum alertType;
/** /**
* 告警级别 * 告警级别
*/ */
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@Column(name = "alert_level", nullable = false, length = 20) @Column(name = "alert_level", nullable = false, length = 20)
private AlertLevelEnum alertLevel; private MonitorAlertLevelEnum alertLevel;
/** /**
* 当前值 * 当前值

View File

@ -1,7 +1,7 @@
package com.qqchen.deploy.backend.deploy.entity; package com.qqchen.deploy.backend.deploy.entity;
import com.qqchen.deploy.backend.deploy.enums.AlertTypeEnum;
import com.qqchen.deploy.backend.framework.annotation.LogicDelete; import com.qqchen.deploy.backend.framework.annotation.LogicDelete;
import com.qqchen.deploy.backend.framework.enums.MonitorMetricEnum;
import com.qqchen.deploy.backend.framework.domain.Entity; import com.qqchen.deploy.backend.framework.domain.Entity;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@ -36,11 +36,11 @@ public class ServerAlertRule extends Entity<Long> {
private String ruleName; private String ruleName;
/** /**
* 告警类型 * 监控指标类型
*/ */
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@Column(name = "alert_type", nullable = false, length = 20) @Column(name = "alert_type", nullable = false, length = 20)
private AlertTypeEnum alertType; private MonitorMetricEnum alertType;
/** /**
* 警告阈值(%) * 警告阈值(%)
@ -66,18 +66,6 @@ public class ServerAlertRule extends Entity<Long> {
@Column(name = "enabled") @Column(name = "enabled")
private Boolean enabled = true; private Boolean enabled = true;
/**
* 通知方式: EMAIL/SMS/WEBHOOK
*/
@Column(name = "notify_method", length = 50)
private String notifyMethod;
/**
* 通知联系人JSON格式
*/
@Column(name = "notify_contacts", columnDefinition = "JSON")
private String notifyContacts;
/** /**
* 规则描述 * 规则描述
*/ */

View File

@ -1,29 +0,0 @@
package com.qqchen.deploy.backend.deploy.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 告警类型枚举
*/
@Getter
@AllArgsConstructor
public enum AlertTypeEnum {
/**
* CPU告警
*/
CPU("CPU", "CPU使用率"),
/**
* 内存告警
*/
MEMORY("MEMORY", "内存使用率"),
/**
* 磁盘告警
*/
DISK("DISK", "磁盘使用率");
private final String code;
private final String description;
}

View File

@ -1,7 +1,7 @@
package com.qqchen.deploy.backend.deploy.integration.impl; package com.qqchen.deploy.backend.deploy.integration.impl;
import com.qqchen.deploy.backend.deploy.entity.ExternalSystem; import com.qqchen.deploy.backend.deploy.entity.ExternalSystem;
import com.qqchen.deploy.backend.framework.util.SensitiveDataEncryptor; import com.qqchen.deploy.backend.framework.utils.SensitiveDataEncryptor;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;

View File

@ -0,0 +1,52 @@
package com.qqchen.deploy.backend.deploy.monitor;
import com.qqchen.deploy.backend.deploy.dto.ServerMonitorDataDTO;
import com.qqchen.deploy.backend.framework.enums.MonitorMetricEnum;
import com.qqchen.deploy.backend.framework.monitor.ServerThresholdCheckerFactory;
import com.qqchen.deploy.backend.framework.monitor.impl.CpuThresholdChecker;
import com.qqchen.deploy.backend.framework.monitor.impl.DiskThresholdChecker;
import com.qqchen.deploy.backend.framework.monitor.impl.MemoryThresholdChecker;
import com.qqchen.deploy.backend.framework.monitor.impl.NetworkThresholdChecker;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
/**
* 服务器监控阈值检测器工厂
* Deploy 业务层使用 Framework 提供的检测器能力
*
* @author qqchen
* @since 2025-12-07
*/
@Component
public class ServerMonitorThresholdCheckerFactory extends ServerThresholdCheckerFactory<ServerMonitorDataDTO> {
@PostConstruct
@Override
protected void registerCheckers() {
// 注册 CPU 检测器
checkers.put(MonitorMetricEnum.CPU,
new CpuThresholdChecker<>(ServerMonitorDataDTO::getCpuUsage));
// 注册内存检测器
checkers.put(MonitorMetricEnum.MEMORY,
new MemoryThresholdChecker<>(ServerMonitorDataDTO::getMemoryUsage));
// 注册磁盘检测器取所有分区中使用率最高的
checkers.put(MonitorMetricEnum.DISK,
new DiskThresholdChecker<>(data -> {
if (data == null || data.getDiskUsage() == null || data.getDiskUsage().isEmpty()) {
return null;
}
return data.getDiskUsage().stream()
.map(disk -> disk.getUsagePercent())
.max(BigDecimal::compareTo)
.orElse(null);
}));
// 注册网络检测器
checkers.put(MonitorMetricEnum.NETWORK,
new NetworkThresholdChecker<>(ServerMonitorDataDTO::getNetworkUsage));
}
}

View File

@ -1,6 +1,6 @@
package com.qqchen.deploy.backend.deploy.query; package com.qqchen.deploy.backend.deploy.query;
import com.qqchen.deploy.backend.deploy.enums.AlertTypeEnum; import com.qqchen.deploy.backend.framework.enums.MonitorMetricEnum;
import com.qqchen.deploy.backend.framework.annotation.QueryField; import com.qqchen.deploy.backend.framework.annotation.QueryField;
import com.qqchen.deploy.backend.framework.enums.QueryType; import com.qqchen.deploy.backend.framework.enums.QueryType;
import com.qqchen.deploy.backend.framework.query.BaseQuery; import com.qqchen.deploy.backend.framework.query.BaseQuery;
@ -24,9 +24,9 @@ public class ServerAlertRuleQuery extends BaseQuery {
@QueryField(field = "ruleName", type = QueryType.LIKE) @QueryField(field = "ruleName", type = QueryType.LIKE)
private String ruleName; private String ruleName;
@Schema(description = "告警类型") @Schema(description = "监控指标类型")
@QueryField(field = "alertType") @QueryField(type = QueryType.EQUAL)
private AlertTypeEnum alertType; private MonitorMetricEnum alertType;
@Schema(description = "是否启用") @Schema(description = "是否启用")
@QueryField(field = "enabled") @QueryField(field = "enabled")

View File

@ -52,32 +52,44 @@ public class ServerMonitorScheduler {
* 采集所有在线服务器的监控数据 * 采集所有在线服务器的监控数据
* 此方法由定时任务管理系统调用 * 此方法由定时任务管理系统调用
* *
* @param config 通知配置可选为null则只记录数据不发通知 * @param notificationChannelId 通知渠道ID可选为null则不发送通知
* @param serverOfflineTemplateId 服务器离线通知模板ID可选
* @param resourceAlertTemplateId 资源告警通知模板ID可选
*/ */
public void collectServerMetrics(ServerMonitorNotificationConfig config) { public void collectServerMetrics(Long notificationChannelId,
if (config != null) { Long serverOfflineTemplateId,
Long resourceAlertTemplateId) {
// 构建通知配置对象
ServerMonitorNotificationConfig config = null;
if (notificationChannelId != null) {
config = new ServerMonitorNotificationConfig();
config.setNotificationChannelId(notificationChannelId);
config.setServerOfflineTemplateId(serverOfflineTemplateId);
config.setResourceAlertTemplateId(resourceAlertTemplateId);
log.info("========== 开始采集服务器监控数据 ========== channelId={}, offlineTemplateId={}, alertTemplateId={}", log.info("========== 开始采集服务器监控数据 ========== channelId={}, offlineTemplateId={}, alertTemplateId={}",
config.getNotificationChannelId(), config.getServerOfflineTemplateId(), config.getResourceAlertTemplateId()); notificationChannelId, serverOfflineTemplateId, resourceAlertTemplateId);
} else { } else {
log.info("========== 开始采集服务器监控数据(不发送通知) =========="); log.info("========== 开始采集服务器监控数据(不发送通知) ==========");
} }
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
try { try {
// 1. 查询所有应该在线的服务器状态为ONLINE // 1. 查询所有服务器不管当前状态准备检测在线状态
List<Server> shouldBeOnlineServers = serverRepository List<Server> allServers = serverRepository.findAll();
.findByStatusAndDeleted(ServerStatusEnum.ONLINE, false);
if (shouldBeOnlineServers.isEmpty()) { if (allServers.isEmpty()) {
log.debug("没有需要监控的服务器,跳过监控采集"); log.debug("没有需要监控的服务器,跳过监控采集");
return; return;
} }
log.info("发现 {} 台应在线服务器,开始检测状态", shouldBeOnlineServers.size()); log.info("发现 {} 台服务器,开始检测在线状态并采集监控数据", allServers.size());
// 2. 并发检测服务器连接状态并采集监控数据 // 2. 并发检测所有服务器的连接状态并采集监控数据
// - 连接失败 发送离线通知
// - 连接成功 采集数据检查阈值告警
final ServerMonitorNotificationConfig finalConfig = config; final ServerMonitorNotificationConfig finalConfig = config;
List<CompletableFuture<ServerMonitorDataDTO>> futures = shouldBeOnlineServers.stream() List<CompletableFuture<ServerMonitorDataDTO>> futures = allServers.stream()
.map(server -> CompletableFuture.supplyAsync(() -> .map(server -> CompletableFuture.supplyAsync(() ->
collectSingleServerWithStatusCheck(server, finalConfig))) collectSingleServerWithStatusCheck(server, finalConfig)))
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -95,8 +107,8 @@ public class ServerMonitorScheduler {
.collect(Collectors.toList()); .collect(Collectors.toList());
long duration = System.currentTimeMillis() - startTime; long duration = System.currentTimeMillis() - startTime;
log.info("========== 监控数据采集完成: 成功={}/{}, 耗时={}ms ==========", log.info("========== 监控数据采集完成: 在线={}/{}, 耗时={}ms ==========",
monitorDataList.size(), shouldBeOnlineServers.size(), duration); monitorDataList.size(), allServers.size(), duration);
// 5. 批量保存监控数据到数据库 // 5. 批量保存监控数据到数据库
if (!monitorDataList.isEmpty()) { if (!monitorDataList.isEmpty()) {
@ -152,7 +164,8 @@ public class ServerMonitorScheduler {
Map<String, Object> templateParams = new HashMap<>(); Map<String, Object> templateParams = new HashMap<>();
templateParams.put("serverName", server.getServerName()); templateParams.put("serverName", server.getServerName());
templateParams.put("serverIp", server.getHostIp()); templateParams.put("serverIp", server.getHostIp());
templateParams.put("offlineTime", LocalDateTime.now().toString()); templateParams.put("offlineTime", LocalDateTime.now().format(
java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
// 2. 构建SendNotificationRequest // 2. 构建SendNotificationRequest
SendNotificationRequest request = new SendNotificationRequest(); SendNotificationRequest request = new SendNotificationRequest();

View File

@ -14,14 +14,13 @@ import com.qqchen.deploy.backend.deploy.service.IJenkinsBuildService;
import com.qqchen.deploy.backend.deploy.service.IJenkinsJobService; import com.qqchen.deploy.backend.deploy.service.IJenkinsJobService;
import com.qqchen.deploy.backend.deploy.service.IJenkinsViewService; import com.qqchen.deploy.backend.deploy.service.IJenkinsViewService;
import com.qqchen.deploy.backend.system.enums.ExternalSystemAuthTypeEnum; import com.qqchen.deploy.backend.system.enums.ExternalSystemAuthTypeEnum;
import com.qqchen.deploy.backend.system.enums.ExternalSystemSyncStatusEnum;
import com.qqchen.deploy.backend.system.enums.ExternalSystemTypeEnum; import com.qqchen.deploy.backend.system.enums.ExternalSystemTypeEnum;
import com.qqchen.deploy.backend.framework.annotation.ServiceType; import com.qqchen.deploy.backend.framework.annotation.ServiceType;
import com.qqchen.deploy.backend.framework.enums.ResponseCode; import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.exception.BusinessException; import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.framework.exception.UniqueConstraintException; import com.qqchen.deploy.backend.framework.exception.UniqueConstraintException;
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
import com.qqchen.deploy.backend.framework.util.SensitiveDataEncryptor; import com.qqchen.deploy.backend.framework.utils.SensitiveDataEncryptor;
import com.qqchen.deploy.backend.deploy.integration.IExternalSystemIntegration; import com.qqchen.deploy.backend.deploy.integration.IExternalSystemIntegration;
import com.qqchen.deploy.backend.system.model.ExternalSystemDTO; import com.qqchen.deploy.backend.system.model.ExternalSystemDTO;
import com.qqchen.deploy.backend.deploy.repository.IExternalSystemRepository; import com.qqchen.deploy.backend.deploy.repository.IExternalSystemRepository;

View File

@ -5,8 +5,8 @@ import com.qqchen.deploy.backend.deploy.dto.ServerMonitorNotificationConfig;
import com.qqchen.deploy.backend.deploy.entity.Server; import com.qqchen.deploy.backend.deploy.entity.Server;
import com.qqchen.deploy.backend.deploy.entity.ServerAlertLog; import com.qqchen.deploy.backend.deploy.entity.ServerAlertLog;
import com.qqchen.deploy.backend.deploy.entity.ServerAlertRule; import com.qqchen.deploy.backend.deploy.entity.ServerAlertRule;
import com.qqchen.deploy.backend.deploy.enums.AlertLevelEnum; import com.qqchen.deploy.backend.framework.enums.MonitorAlertLevelEnum;
import com.qqchen.deploy.backend.deploy.enums.AlertTypeEnum; import com.qqchen.deploy.backend.framework.enums.MonitorMetricEnum;
import com.qqchen.deploy.backend.deploy.repository.IServerAlertLogRepository; import com.qqchen.deploy.backend.deploy.repository.IServerAlertLogRepository;
import com.qqchen.deploy.backend.deploy.repository.IServerAlertRuleRepository; import com.qqchen.deploy.backend.deploy.repository.IServerAlertRuleRepository;
import com.qqchen.deploy.backend.deploy.repository.IServerRepository; import com.qqchen.deploy.backend.deploy.repository.IServerRepository;
@ -98,14 +98,14 @@ public class ServerAlertServiceImpl implements IServerAlertService {
} }
// 判断告警级别 // 判断告警级别
AlertLevelEnum alertLevel = null; MonitorAlertLevelEnum alertLevel = null;
BigDecimal threshold = null; BigDecimal threshold = null;
if (currentValue.compareTo(rule.getCriticalThreshold()) >= 0) { if (currentValue.compareTo(rule.getCriticalThreshold()) >= 0) {
alertLevel = AlertLevelEnum.CRITICAL; alertLevel = MonitorAlertLevelEnum.CRITICAL;
threshold = rule.getCriticalThreshold(); threshold = rule.getCriticalThreshold();
} else if (currentValue.compareTo(rule.getWarningThreshold()) >= 0) { } else if (currentValue.compareTo(rule.getWarningThreshold()) >= 0) {
alertLevel = AlertLevelEnum.WARNING; alertLevel = MonitorAlertLevelEnum.WARNING;
threshold = rule.getWarningThreshold(); threshold = rule.getWarningThreshold();
} }
@ -117,6 +117,7 @@ public class ServerAlertServiceImpl implements IServerAlertService {
/** /**
* 检查磁盘告警 * 检查磁盘告警
* 策略计算所有分区的总使用率 = (所有已用空间总和) / (所有总容量) * 100%
*/ */
private void checkDiskAlert(Long serverId, ServerMonitorDataDTO monitorData, ServerAlertRule rule, private void checkDiskAlert(Long serverId, ServerMonitorDataDTO monitorData, ServerAlertRule rule,
ServerMonitorNotificationConfig config) { ServerMonitorNotificationConfig config) {
@ -125,34 +126,50 @@ public class ServerAlertServiceImpl implements IServerAlertService {
return; return;
} }
for (DiskUsageInfo diskUsage : diskUsageList) { // 计算所有分区的总容量和已用空间
BigDecimal usagePercent = diskUsage.getUsagePercent(); BigDecimal totalCapacity = BigDecimal.ZERO; // 总容量GB
if (usagePercent == null) { BigDecimal totalUsed = BigDecimal.ZERO; // 已用空间GB
continue;
for (DiskUsageInfo disk : diskUsageList) {
if (disk.getTotalSize() != null && disk.getUsedSize() != null) {
totalCapacity = totalCapacity.add(disk.getTotalSize());
totalUsed = totalUsed.add(disk.getUsedSize());
}
} }
AlertLevelEnum alertLevel = null; if (totalCapacity.compareTo(BigDecimal.ZERO) == 0) {
return;
}
// 计算总磁盘使用率
BigDecimal totalUsagePercent = totalUsed
.multiply(new BigDecimal(100))
.divide(totalCapacity, 2, BigDecimal.ROUND_HALF_UP);
// 判断是否超过阈值
MonitorAlertLevelEnum alertLevel = null;
BigDecimal threshold = null; BigDecimal threshold = null;
if (usagePercent.compareTo(rule.getCriticalThreshold()) >= 0) { if (totalUsagePercent.compareTo(rule.getCriticalThreshold()) >= 0) {
alertLevel = AlertLevelEnum.CRITICAL; alertLevel = MonitorAlertLevelEnum.CRITICAL;
threshold = rule.getCriticalThreshold(); threshold = rule.getCriticalThreshold();
} else if (usagePercent.compareTo(rule.getWarningThreshold()) >= 0) { } else if (totalUsagePercent.compareTo(rule.getWarningThreshold()) >= 0) {
alertLevel = AlertLevelEnum.WARNING; alertLevel = MonitorAlertLevelEnum.WARNING;
threshold = rule.getWarningThreshold(); threshold = rule.getWarningThreshold();
} }
// 触发告警
if (alertLevel != null) { if (alertLevel != null) {
String resourceInfo = "磁盘[" + diskUsage.getMountPoint() + "]"; String resourceInfo = String.format("总磁盘(%d个分区总容量%.0fGB,已用%.0fGB",
triggerAlert(serverId, rule, alertLevel, usagePercent, threshold, resourceInfo, config); diskUsageList.size(), totalCapacity.doubleValue(), totalUsed.doubleValue());
} triggerAlert(serverId, rule, alertLevel, totalUsagePercent, threshold, resourceInfo, config);
} }
} }
/** /**
* 触发告警 * 触发告警
*/ */
private void triggerAlert(Long serverId, ServerAlertRule rule, AlertLevelEnum level, private void triggerAlert(Long serverId, ServerAlertRule rule, MonitorAlertLevelEnum level,
BigDecimal currentValue, BigDecimal threshold, String resourceInfo, BigDecimal currentValue, BigDecimal threshold, String resourceInfo,
ServerMonitorNotificationConfig config) { ServerMonitorNotificationConfig config) {
// 1. 记录告警日志到数据库 // 1. 记录告警日志到数据库
@ -199,7 +216,7 @@ public class ServerAlertServiceImpl implements IServerAlertService {
/** /**
* 发送告警通知 * 发送告警通知
*/ */
private void sendAlertNotification(Long serverId, ServerAlertRule rule, AlertLevelEnum level, private void sendAlertNotification(Long serverId, ServerAlertRule rule, MonitorAlertLevelEnum level,
BigDecimal currentValue, BigDecimal threshold, String resourceInfo, BigDecimal currentValue, BigDecimal threshold, String resourceInfo,
ServerMonitorNotificationConfig config) { ServerMonitorNotificationConfig config) {
try { try {
@ -220,7 +237,8 @@ public class ServerAlertServiceImpl implements IServerAlertService {
templateParams.put("resourceInfo", resourceInfo); templateParams.put("resourceInfo", resourceInfo);
templateParams.put("currentValue", String.format("%.2f", currentValue)); templateParams.put("currentValue", String.format("%.2f", currentValue));
templateParams.put("threshold", String.format("%.2f", threshold)); templateParams.put("threshold", String.format("%.2f", threshold));
templateParams.put("alertTime", LocalDateTime.now().toString()); templateParams.put("alertTime", LocalDateTime.now().format(
java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
// 3. 构建SendNotificationRequest // 3. 构建SendNotificationRequest
SendNotificationRequest request = new SendNotificationRequest(); SendNotificationRequest request = new SendNotificationRequest();

View File

@ -1,14 +1,18 @@
package com.qqchen.deploy.backend.deploy.enums; package com.qqchen.deploy.backend.framework.enums;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
/** /**
* 告警级别枚举 * 监控告警级别枚举
* Framework 提供的服务器监控告警级别
*
* @author qqchen
* @since 2025-12-07
*/ */
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public enum AlertLevelEnum { public enum MonitorAlertLevelEnum {
/** /**
* 警告 * 警告
*/ */

View File

@ -0,0 +1,43 @@
package com.qqchen.deploy.backend.framework.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 监控指标枚举
* Framework 提供的服务器监控指标类型
*
* @author qqchen
* @since 2025-12-07
*/
@Getter
@AllArgsConstructor
public enum MonitorMetricEnum {
/**
* CPU告警百分比
*/
CPU("CPU", "CPU使用率", "%"),
/**
* 内存告警百分比
*/
MEMORY("MEMORY", "内存使用率", "%"),
/**
* 磁盘告警百分比
*/
DISK("DISK", "磁盘使用率", "%"),
/**
* 网络告警MB/s
*/
NETWORK("NETWORK", "网络使用率", "MB/s");
private final String code;
private final String description;
/**
* 度量单位
*/
private final String unit;
}

View File

@ -0,0 +1,37 @@
package com.qqchen.deploy.backend.framework.monitor;
import com.qqchen.deploy.backend.framework.enums.MonitorAlertLevelEnum;
import java.math.BigDecimal;
/**
* 服务器阈值检测器抽象基类
* 提供通用的阈值检测逻辑
*
* @author qqchen
* @since 2025-12-07
*/
public abstract class AbstractServerThresholdChecker<T> implements IServerThresholdChecker<T> {
@Override
public MonitorAlertLevelEnum checkThreshold(BigDecimal currentValue,
BigDecimal warningThreshold,
BigDecimal criticalThreshold) {
if (currentValue == null) {
return null;
}
// 检查是否超过严重阈值
if (criticalThreshold != null && currentValue.compareTo(criticalThreshold) >= 0) {
return MonitorAlertLevelEnum.CRITICAL;
}
// 检查是否超过警告阈值
if (warningThreshold != null && currentValue.compareTo(warningThreshold) >= 0) {
return MonitorAlertLevelEnum.WARNING;
}
// 未超过阈值
return null;
}
}

View File

@ -0,0 +1,43 @@
package com.qqchen.deploy.backend.framework.monitor;
import com.qqchen.deploy.backend.framework.enums.MonitorAlertLevelEnum;
import java.math.BigDecimal;
/**
* 服务器阈值检测器接口
* Framework 提供的服务器监控指标阈值检测能力
*
* 每种监控指标CPU/内存/磁盘/网络实现自己的阈值检测逻辑
*
* @author qqchen
* @since 2025-12-07
*/
public interface IServerThresholdChecker<T> {
/**
* 获取当前指标值
*
* @param monitorData 监控数据
* @return 当前指标值
*/
BigDecimal getCurrentValue(T monitorData);
/**
* 检查是否超过阈值并触发告警
*
* @param currentValue 当前值
* @param warningThreshold 警告阈值
* @param criticalThreshold 严重阈值
* @return 告警级别null表示未超过阈值
*/
MonitorAlertLevelEnum checkThreshold(BigDecimal currentValue, BigDecimal warningThreshold, BigDecimal criticalThreshold);
/**
* 获取资源信息描述用于通知消息
*
* @param monitorData 监控数据
* @return 资源描述 "CPU使用率", "内存使用率", "磁盘(/data)使用率"
*/
String getResourceInfo(T monitorData);
}

View File

@ -0,0 +1,55 @@
package com.qqchen.deploy.backend.framework.monitor;
import com.qqchen.deploy.backend.framework.enums.MonitorAlertLevelEnum;
import com.qqchen.deploy.backend.framework.enums.MonitorMetricEnum;
import com.qqchen.deploy.backend.framework.monitor.dto.ThresholdCheckResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
/**
* 服务器监控服务
* Framework 提供的阈值检测能力
*
* SSH采集由业务层自行处理ServerMonitorScheduler
*
* @author qqchen
* @since 2025-12-07
*/
@Slf4j
@Service
public class ServerMonitorService {
/**
* 检查阈值
* 使用检测器检查指标是否超过阈值
*
* @param metric 监控指标类型
* @param monitorData 监控数据
* @param warningThreshold 警告阈值
* @param criticalThreshold 严重阈值
* @param checker 阈值检测器
* @return 检测结果
*/
public <T> ThresholdCheckResult checkThreshold(MonitorMetricEnum metric,
T monitorData,
BigDecimal warningThreshold,
BigDecimal criticalThreshold,
IServerThresholdChecker<T> checker) {
// 获取当前值
BigDecimal currentValue = checker.getCurrentValue(monitorData);
// 检查阈值
MonitorAlertLevelEnum alertLevel = checker.checkThreshold(
currentValue, warningThreshold, criticalThreshold);
// 构建结果
return ThresholdCheckResult.builder()
.metric(metric)
.currentValue(currentValue)
.alertLevel(alertLevel)
.resourceInfo(checker.getResourceInfo(monitorData))
.build();
}
}

View File

@ -0,0 +1,39 @@
package com.qqchen.deploy.backend.framework.monitor;
import com.qqchen.deploy.backend.framework.enums.MonitorMetricEnum;
import java.util.HashMap;
import java.util.Map;
/**
* 服务器阈值检测器工厂抽象类
* Framework 提供的工厂能力
*
* @author qqchen
* @since 2025-12-07
*/
public abstract class ServerThresholdCheckerFactory<T> {
protected Map<MonitorMetricEnum, IServerThresholdChecker<T>> checkers = new HashMap<>();
/**
* 获取指定监控指标的检测器
*
* @param metric 监控指标类型
* @return 对应的检测器
* @throws IllegalArgumentException 如果找不到对应的检测器
*/
public IServerThresholdChecker<T> getChecker(MonitorMetricEnum metric) {
IServerThresholdChecker<T> checker = checkers.get(metric);
if (checker == null) {
throw new IllegalArgumentException("未找到监控指标的检测器: " + metric);
}
return checker;
}
/**
* 注册检测器
* 子类需要实现此方法来注册所有检测器
*/
protected abstract void registerCheckers();
}

View File

@ -0,0 +1,44 @@
package com.qqchen.deploy.backend.framework.monitor.dto;
import com.qqchen.deploy.backend.framework.enums.MonitorAlertLevelEnum;
import com.qqchen.deploy.backend.framework.enums.MonitorMetricEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
* 阈值检测结果
* Framework 提供的检测结果数据结构
*
* @author qqchen
* @since 2025-12-07
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ThresholdCheckResult {
/**
* 监控指标类型
*/
private MonitorMetricEnum metric;
/**
* 当前值
*/
private BigDecimal currentValue;
/**
* 告警级别null表示未超过阈值
*/
private MonitorAlertLevelEnum alertLevel;
/**
* 资源信息描述
*/
private String resourceInfo;
}

View File

@ -0,0 +1,35 @@
package com.qqchen.deploy.backend.framework.monitor.impl;
import com.qqchen.deploy.backend.framework.monitor.AbstractServerThresholdChecker;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
/**
* CPU阈值检测器
* Framework 提供的CPU使用率检测能力
*
* 通过函数式接口传入取值逻辑实现通用性
*
* @author qqchen
* @since 2025-12-07
*/
@Slf4j
public class CpuThresholdChecker<T> extends AbstractServerThresholdChecker<T> {
private final java.util.function.Function<T, BigDecimal> valueExtractor;
public CpuThresholdChecker(java.util.function.Function<T, BigDecimal> valueExtractor) {
this.valueExtractor = valueExtractor;
}
@Override
public BigDecimal getCurrentValue(T monitorData) {
return monitorData != null ? valueExtractor.apply(monitorData) : null;
}
@Override
public String getResourceInfo(T monitorData) {
return "CPU使用率";
}
}

View File

@ -0,0 +1,35 @@
package com.qqchen.deploy.backend.framework.monitor.impl;
import com.qqchen.deploy.backend.framework.monitor.AbstractServerThresholdChecker;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
/**
* 磁盘阈值检测器
* Framework 提供的磁盘使用率检测能力
*
* 通过函数式接口传入取值逻辑实现通用性
*
* @author qqchen
* @since 2025-12-07
*/
@Slf4j
public class DiskThresholdChecker<T> extends AbstractServerThresholdChecker<T> {
private final java.util.function.Function<T, BigDecimal> valueExtractor;
public DiskThresholdChecker(java.util.function.Function<T, BigDecimal> valueExtractor) {
this.valueExtractor = valueExtractor;
}
@Override
public BigDecimal getCurrentValue(T monitorData) {
return monitorData != null ? valueExtractor.apply(monitorData) : null;
}
@Override
public String getResourceInfo(T monitorData) {
return "磁盘使用率";
}
}

View File

@ -0,0 +1,35 @@
package com.qqchen.deploy.backend.framework.monitor.impl;
import com.qqchen.deploy.backend.framework.monitor.AbstractServerThresholdChecker;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
/**
* 内存阈值检测器
* Framework 提供的内存使用率检测能力
*
* 通过函数式接口传入取值逻辑实现通用性
*
* @author qqchen
* @since 2025-12-07
*/
@Slf4j
public class MemoryThresholdChecker<T> extends AbstractServerThresholdChecker<T> {
private final java.util.function.Function<T, BigDecimal> valueExtractor;
public MemoryThresholdChecker(java.util.function.Function<T, BigDecimal> valueExtractor) {
this.valueExtractor = valueExtractor;
}
@Override
public BigDecimal getCurrentValue(T monitorData) {
return monitorData != null ? valueExtractor.apply(monitorData) : null;
}
@Override
public String getResourceInfo(T monitorData) {
return "内存使用率";
}
}

View File

@ -0,0 +1,35 @@
package com.qqchen.deploy.backend.framework.monitor.impl;
import com.qqchen.deploy.backend.framework.monitor.AbstractServerThresholdChecker;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
/**
* 网络阈值检测器
* Framework 提供的网络使用率检测能力
*
* 通过函数式接口传入取值逻辑实现通用性
*
* @author qqchen
* @since 2025-12-07
*/
@Slf4j
public class NetworkThresholdChecker<T> extends AbstractServerThresholdChecker<T> {
private final java.util.function.Function<T, BigDecimal> valueExtractor;
public NetworkThresholdChecker(java.util.function.Function<T, BigDecimal> valueExtractor) {
this.valueExtractor = valueExtractor;
}
@Override
public BigDecimal getCurrentValue(T monitorData) {
return monitorData != null ? valueExtractor.apply(monitorData) : null;
}
@Override
public String getResourceInfo(T monitorData) {
return "网络使用率";
}
}

View File

@ -1,4 +1,4 @@
package com.qqchen.deploy.backend.framework.util; package com.qqchen.deploy.backend.framework.utils;
import org.springframework.security.crypto.encrypt.Encryptors; import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.security.crypto.encrypt.TextEncryptor; import org.springframework.security.crypto.encrypt.TextEncryptor;

View File

@ -1,4 +1,4 @@
package com.qqchen.deploy.backend.framework.util; package com.qqchen.deploy.backend.framework.utils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;

View File

@ -40,6 +40,8 @@ public class NotificationChannelServiceImpl extends BaseServiceImpl<Notification
@Resource @Resource
private INotificationChannelRepository notificationChannelRepository; private INotificationChannelRepository notificationChannelRepository;
@Resource
private INotificationSendService notificationSendService;
@Resource @Resource
private NotificationChannelAdapterFactory adapterFactory; private NotificationChannelAdapterFactory adapterFactory;
@ -59,30 +61,58 @@ public class NotificationChannelServiceImpl extends BaseServiceImpl<Notification
NotificationChannel channel = notificationChannelRepository.findById(id) NotificationChannel channel = notificationChannelRepository.findById(id)
.orElseThrow(() -> new BusinessException(ResponseCode.DATA_NOT_FOUND)); .orElseThrow(() -> new BusinessException(ResponseCode.DATA_NOT_FOUND));
log.info("开始测试通知渠道连接: id={}, name={}, type={}", log.info("开始测试通知渠道: id={}, name={}, type={}",
id, channel.getName(), channel.getChannelType()); id, channel.getName(), channel.getChannelType());
// 2. 获取对应的适配器 // 2. 创建测试消息
INotificationChannelAdapter adapter; BaseSendNotificationRequest testRequest = createTestRequest(channel);
try {
adapter = adapterFactory.getAdapter(channel.getChannelType());
} catch (IllegalArgumentException e) {
log.error("获取通知渠道适配器失败: {}", e.getMessage());
throw new BusinessException(ResponseCode.ERROR, new Object[] {"不支持的渠道类型: " + channel.getChannelType()});
}
// 3. 转换配置 // 3. 发送测试消息复用 send 逻辑测试完整的发送流程
BaseNotificationConfig config = convertConfig(channel);
// 4. 执行连接测试
try { try {
adapter.testConnection(config); notificationSendService.send(channel, testRequest);
log.info("通知渠道连接测试成功: id={}, name={}", id, channel.getName()); log.info("通知渠道测试成功: id={}, name={}", id, channel.getName());
return true; return true;
} catch (Exception e) { } catch (Exception e) {
log.error("通知渠道连接测试失败: id={}, name={}, 错误: {}", log.error("通知渠道测试失败: id={}, name={}, 错误: {}",
id, channel.getName(), e.getMessage(), e); id, channel.getName(), e.getMessage(), e);
throw new BusinessException(ResponseCode.ERROR, new Object[] {"连接测试失败: " + e.getMessage()}); throw new BusinessException(ResponseCode.ERROR, new Object[] {"测试失败: " + e.getMessage()});
}
}
/**
* 创建测试消息
*/
private BaseSendNotificationRequest createTestRequest(NotificationChannel channel) {
String testContent = String.format(
"【通知渠道测试】\n" +
"渠道名称:%s\n" +
"渠道类型:%s\n" +
"测试时间:%s\n\n" +
"如果您收到此消息,说明通知渠道配置正确!",
channel.getName(),
channel.getChannelType().getDescription(),
java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
);
switch (channel.getChannelType()) {
case WEWORK -> {
com.qqchen.deploy.backend.notification.dto.WeworkSendNotificationRequest request =
new com.qqchen.deploy.backend.notification.dto.WeworkSendNotificationRequest();
request.setContent(testContent);
request.setTitle("【测试消息】通知渠道连接测试");
request.setMessageType(com.qqchen.deploy.backend.notification.enums.WeworkMessageTypeEnum.TEXT);
return request;
}
case EMAIL -> {
com.qqchen.deploy.backend.notification.dto.EmailSendNotificationRequest request =
new com.qqchen.deploy.backend.notification.dto.EmailSendNotificationRequest();
request.setContent(testContent);
request.setTitle("【测试消息】通知渠道连接测试");
request.setToReceivers(java.util.Arrays.asList("admin@company.com"));
return request;
}
default -> throw new BusinessException(ResponseCode.ERROR,
new Object[]{"不支持的渠道类型: " + channel.getChannelType()});
} }
} }

View File

@ -1212,10 +1212,6 @@ CREATE TABLE deploy_server_alert_rule
-- 是否启用 -- 是否启用
enabled BOOLEAN DEFAULT TRUE COMMENT '是否启用', enabled BOOLEAN DEFAULT TRUE COMMENT '是否启用',
-- 通知方式
notify_method VARCHAR(50) NULL COMMENT '通知方式: EMAIL/SMS/WEBHOOK',
notify_contacts JSON NULL COMMENT '通知联系人',
-- 描述 -- 描述
description VARCHAR(500) NULL COMMENT '规则描述', description VARCHAR(500) NULL COMMENT '规则描述',