mirror of
https://github.com/201206030/novel-plus.git
synced 2026-02-07 00:22:31 +08:00
feat(novel-crawl): 新增磁盘保护机制,支持邮件告警与自动熔断
- 实现定时检测磁盘使用率,避免爬虫耗尽磁盘资源 - 当磁盘使用率达到 85%、90%、95% 时,分别发送告警邮件 - 当使用率达到 95% 时,强制停止当前爬虫进程
This commit is contained in:
@@ -37,6 +37,10 @@
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.java2nb.novel.core.bean;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author xiongxiaoyang
|
||||
* @date 2025/10/24
|
||||
*/
|
||||
@Data
|
||||
public class DiskInfo {
|
||||
|
||||
private String path;
|
||||
private double totalGB;
|
||||
private double freeGB;
|
||||
private double usage;
|
||||
|
||||
public DiskInfo(String path, long totalBytes, long freeBytes, double usage) {
|
||||
this.path = path;
|
||||
this.totalGB = bytesToGB(totalBytes);
|
||||
this.freeGB = bytesToGB(freeBytes);
|
||||
this.usage = usage;
|
||||
}
|
||||
|
||||
private double bytesToGB(long bytes) {
|
||||
return bytes / (1024.0 * 1024.0 * 1024.0);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.java2nb.novel.core.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author xiongxiaoyang
|
||||
* @date 2025/10/24
|
||||
*/
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "disk-monitor")
|
||||
@Data
|
||||
public class DiskMonitorProperties {
|
||||
|
||||
private boolean enabled;
|
||||
private int intervalMinutes;
|
||||
private double warningThreshold;
|
||||
private double severeThreshold;
|
||||
private double criticalThreshold;
|
||||
private List<String> recipients;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.java2nb.novel.core.schedule;
|
||||
|
||||
import com.java2nb.novel.core.bean.DiskInfo;
|
||||
import com.java2nb.novel.core.config.DiskMonitorProperties;
|
||||
import com.java2nb.novel.service.EmailService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* @author xiongxiaoyang
|
||||
* @date 2025/10/24
|
||||
*/
|
||||
@ConditionalOnProperty(prefix = "disk-monitor", name = "enabled", havingValue = "true")
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class DiskMonitorSchedule {
|
||||
|
||||
private final DiskMonitorProperties properties;
|
||||
|
||||
private final EmailService emailService;
|
||||
|
||||
private final AtomicBoolean criticalAlertSent = new AtomicBoolean(false);
|
||||
|
||||
@Scheduled(fixedDelayString = "#{1000 * 60 * ${disk-monitor.interval-minutes}}")
|
||||
public void checkDiskUsage() {
|
||||
log.info("🔍 开始检查磁盘使用情况...");
|
||||
|
||||
File[] roots = File.listRoots();
|
||||
List<DiskInfo> diskInfos = new ArrayList<>();
|
||||
boolean criticalDetected = false;
|
||||
|
||||
for (File root : roots) {
|
||||
String path = root.getAbsolutePath().trim();
|
||||
if (path.isEmpty()) continue;
|
||||
long total = root.getTotalSpace();
|
||||
if (total == 0) continue;
|
||||
long free = root.getFreeSpace();
|
||||
double usage = (double)(total - free) / total * 100;
|
||||
diskInfos.add(new DiskInfo(path, total, free, usage));
|
||||
if (usage >= properties.getCriticalThreshold()) {
|
||||
criticalDetected = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (criticalDetected) {
|
||||
if (criticalAlertSent.compareAndSet(false, true)) {
|
||||
sendAlertEmail("CRITICAL", diskInfos);
|
||||
log.error("🚨 磁盘使用率 ≥ 95%,10秒后终止进程...");
|
||||
try {
|
||||
Thread.sleep(10000);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
System.exit(1);
|
||||
}
|
||||
} else {
|
||||
criticalAlertSent.set(false);
|
||||
double maxUsage = diskInfos.stream().mapToDouble(DiskInfo::getUsage).max().orElse(0);
|
||||
if (maxUsage >= properties.getSevereThreshold() && maxUsage < properties.getCriticalThreshold()) {
|
||||
sendAlertEmail("WARNING", diskInfos);
|
||||
} else if (maxUsage >= properties.getWarningThreshold() && maxUsage < properties.getSevereThreshold()) {
|
||||
sendAlertEmail("INFO", diskInfos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sendAlertEmail(String level, List<DiskInfo> diskInfos) {
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put("diskInfos", diskInfos);
|
||||
model.put("alertLevel", getAlertLevelText(level));
|
||||
model.put("icon", getIcon(level));
|
||||
model.put("actionText", getActionText(level));
|
||||
model.put("levelColor", getLevelColor(level));
|
||||
|
||||
String subject = getSubject(level);
|
||||
emailService.sendHtmlEmail(subject, "disk_alert", model, properties.getRecipients());
|
||||
}
|
||||
|
||||
private String getAlertLevelText(String level) {
|
||||
return switch (level) {
|
||||
case "CRITICAL" -> "严重";
|
||||
case "WARNING" -> "警告";
|
||||
case "INFO" -> "提示";
|
||||
default -> "通知";
|
||||
};
|
||||
}
|
||||
|
||||
private String getIcon(String level) {
|
||||
return switch (level) {
|
||||
case "CRITICAL" -> "🚨";
|
||||
case "WARNING" -> "⚠️";
|
||||
case "INFO" -> "💡";
|
||||
default -> "📢";
|
||||
};
|
||||
}
|
||||
|
||||
private String getActionText(String level) {
|
||||
return switch (level) {
|
||||
case "CRITICAL" ->
|
||||
"⚠️ 检测到任一磁盘使用率 ≥ 95%,为防止系统崩溃,系统已自动<strong style='color:#d32f2f'>关闭爬虫程序</strong>。";
|
||||
case "WARNING" ->
|
||||
"📌 建议:<strong style='color:#f57c00'>请立即暂停爬虫程序</strong>,防止磁盘写满导致服务中断。";
|
||||
case "INFO" ->
|
||||
"📌 提示:磁盘使用率已较高,请关注爬虫数据写入情况。";
|
||||
default -> "";
|
||||
};
|
||||
}
|
||||
|
||||
private String getLevelColor(String level) {
|
||||
return switch (level) {
|
||||
case "CRITICAL" -> "#d32f2f";
|
||||
case "WARNING" -> "#f57c00";
|
||||
case "INFO" -> "#388e3c";
|
||||
default -> "#1976d2";
|
||||
};
|
||||
}
|
||||
|
||||
private String getSubject(String level) {
|
||||
return switch (level) {
|
||||
case "CRITICAL" -> "🚨 严重告警:磁盘使用率超 95%,爬虫已关闭";
|
||||
case "WARNING" -> "⚠️ 警告:磁盘使用率超 90%,请暂停爬虫";
|
||||
case "INFO" -> "💡 提示:磁盘使用率超 85%";
|
||||
default -> "磁盘使用率告警";
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.java2nb.novel.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author xiongxiaoyang
|
||||
* @date 2025/10/24
|
||||
*/
|
||||
public interface EmailService {
|
||||
|
||||
void sendHtmlEmail(String subject, String templateName, Map<String, Object> templateModel, List<String> to);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.java2nb.novel.service.impl;
|
||||
|
||||
import com.java2nb.novel.service.EmailService;
|
||||
import jakarta.mail.internet.InternetAddress;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.thymeleaf.TemplateEngine;
|
||||
import org.thymeleaf.context.Context;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author xiongxiaoyang
|
||||
* @date 2025/10/24
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class EmailServiceImpl implements EmailService {
|
||||
|
||||
private final JavaMailSender javaMailSender;
|
||||
private final TemplateEngine templateEngine;
|
||||
|
||||
@Value("${spring.mail.username}")
|
||||
private String fromEmail;
|
||||
|
||||
@Value("${spring.mail.nickname}")
|
||||
private String nickName;
|
||||
|
||||
@Override
|
||||
public void sendHtmlEmail(String subject, String templateName, Map<String, Object> templateModel, List<String> to) {
|
||||
try {
|
||||
MimeMessage message = javaMailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(
|
||||
message,
|
||||
MimeMessageHelper.MULTIPART_MODE_MIXED_RELATED,
|
||||
StandardCharsets.UTF_8.name());
|
||||
|
||||
Context context = new Context();
|
||||
context.setVariables(templateModel);
|
||||
|
||||
String htmlBody = templateEngine.process(templateName, context);
|
||||
|
||||
helper.setFrom(new InternetAddress(fromEmail, nickName, "UTF-8"));
|
||||
helper.setTo(to.toArray(new String[0]));
|
||||
helper.setSubject(subject);
|
||||
helper.setText(htmlBody, true);
|
||||
|
||||
javaMailSender.send(message);
|
||||
} catch (Exception e) {
|
||||
log.error("发送邮件失败");
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,41 @@ crawl:
|
||||
max: 500
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
#邮箱服务器
|
||||
spring:
|
||||
mail:
|
||||
host: smtp.163.com
|
||||
#发件人昵称
|
||||
nickname: novel-plus
|
||||
#邮箱账户
|
||||
username: xxyopen@163.com
|
||||
#邮箱第三方授权码
|
||||
password: novel123456
|
||||
#编码类型
|
||||
default-encoding: UTF-8
|
||||
port: 465
|
||||
properties:
|
||||
mail:
|
||||
smtp:
|
||||
auth: true
|
||||
starttls:
|
||||
enable: true
|
||||
required: rue
|
||||
socketFactory:
|
||||
port: 465
|
||||
class: javax.net.ssl.SSLSocketFactory
|
||||
fallback: false
|
||||
|
||||
# 磁盘监控配置
|
||||
disk-monitor:
|
||||
enabled: false
|
||||
interval-minutes: 5
|
||||
warning-threshold: 85.0
|
||||
severe-threshold: 90.0
|
||||
critical-threshold: 95.0
|
||||
# 告警邮件接收人列表(支持多个邮箱)
|
||||
recipients: xxyopen@foxmail.com,12345678@qq.com
|
||||
|
||||
|
||||
|
||||
44
novel-crawl/src/main/resources/templates/disk_alert.html
Normal file
44
novel-crawl/src/main/resources/templates/disk_alert.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; color: #333; background: #f4f6f9; }
|
||||
.container { max-width: 800px; margin: 20px auto; padding: 20px; background: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||
.header { padding: 15px; border-radius: 6px; text-align: center; color: white; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
|
||||
th { background: #1976d2; color: white; padding: 12px; text-align: center; }
|
||||
td { padding: 10px; border: 1px solid #ddd; text-align: center; }
|
||||
.action { margin: 20px 0; font-size: 16px; line-height: 1.6; }
|
||||
.footer { margin-top: 30px; font-size: 12px; color: #777; text-align: center; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header" th:style="'background:' + ${levelColor}">
|
||||
<h2 th:text="${icon} + ' ' + ${alertLevel} + ' 告警:磁盘使用率过高'"></h2>
|
||||
</div>
|
||||
|
||||
<p><strong>时间:</strong><span th:text="${#dates.format(#dates.createNow(), 'yyyy-MM-dd HH:mm:ss')}"></span></p>
|
||||
|
||||
<div class="action" th:utext="${actionText}"></div>
|
||||
|
||||
<h3>📊 磁盘使用详情</h3>
|
||||
<table>
|
||||
<tr style="background:#1976d2; color:white;">
|
||||
<th>磁盘路径</th><th>总空间 (GB)</th><th>空闲空间 (GB)</th><th>使用率</th>
|
||||
</tr>
|
||||
<tr th:each="disk : ${diskInfos}" th:style="'background-color:#f9f9f9;'">
|
||||
<td th:text="${disk.path}"></td>
|
||||
<td th:text="${#numbers.formatDecimal(disk.totalGB, 2, 2)}"></td>
|
||||
<td th:text="${#numbers.formatDecimal(disk.freeGB, 2, 2)}"></td>
|
||||
<td th:text="${#numbers.formatDecimal(disk.usage, 2, 2)} + '%'"
|
||||
th:style="'color:' + ${disk.usage >= 95 ? '#d32f2f' : (disk.usage >= 90 ? '#f57c00' : '#388e3c')} + '; font-weight:bold;'">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="footer">此邮件由磁盘监控系统自动发送,请及时处理。</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user