Pre Merge pull request !568 from AprilWind/dev-monitor

This commit is contained in:
AprilWind 2024-08-01 07:15:56 +00:00 committed by Gitee
commit 951b550f9f
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
6 changed files with 402 additions and 0 deletions

View File

@ -50,6 +50,11 @@
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mail</artifactId>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,90 @@
package org.dromara.monitor.admin.config.properties;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 通知配置
*
* @author AprilWind
*/
@Data
@ConfigurationProperties(prefix = "notify")
@Configuration
public class NotifyProperties {
/**
* 邮件设置
*/
private Mail mail;
/**
* WebHooks 设置
*/
private WebHook webHook;
@Data
@NoArgsConstructor
public static class Mail {
/**
* 发送开关
*/
private Boolean enabled = false;
/**
* 收件人
*/
private String to;
/**
* 主题
*/
private String subject;
/**
* 邮件模版
*/
private String template;
}
@Data
@NoArgsConstructor
public static class WebHook {
/**
* 发送开关
*/
private Boolean enabled = false;
/**
* 0默认 1密码 2签名密钥
*/
private String type = "0";
/**
* 签名密钥
*/
private String secret;
/**
* 关键词
*/
private String keywords;
/**
* Post地址
*/
private String url;
/**
* WebHook发送模版
*/
private String template;
}
}

View File

@ -0,0 +1,44 @@
package org.dromara.monitor.admin.event;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 通知事件
*
* @author AprilWind
*/
@Data
public class NotifierEvent implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 实例注册名称
*/
private String registName;
/**
* 实例状态名称
*/
private String statusName;
/**
* 实例ID
*/
private String instanceId;
/**
* 实例状态
*/
private String status;
/**
* 服务URL
*/
private String serviceUrl;
}

View File

@ -6,6 +6,8 @@ import de.codecentric.boot.admin.server.domain.events.InstanceEvent;
import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent;
import de.codecentric.boot.admin.server.notify.AbstractEventNotifier;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.monitor.admin.event.NotifierEvent;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@ -49,6 +51,13 @@ public class CustomNotifier extends AbstractEventNotifier {
};
log.info("Instance Status Change: 状态名称【{}】, 注册名称【{}】, 实例ID【{}】, 状态【{}】, 服务URL【{}】",
statusName, registName, instanceId, status, serviceUrl);
NotifierEvent notifier = new NotifierEvent();
notifier.setRegistName(registName);
notifier.setStatusName(statusName);
notifier.setInstanceId(instanceId);
notifier.setStatus(status);
notifier.setServiceUrl(serviceUrl);
SpringUtils.context().publishEvent(notifier);
}
});
}

View File

@ -0,0 +1,206 @@
package org.dromara.monitor.admin.notifier;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mail.utils.MailUtils;
import org.dromara.monitor.admin.config.properties.NotifyProperties;
import org.dromara.monitor.admin.event.NotifierEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
/**
* 信息通知
*
* @author AprilWind
*/
@RequiredArgsConstructor
@Slf4j
@Service
public class InfoNotifier {
private final NotifyProperties notifyProperties;
/**
* 处理服务通知事件
* <p>
* 此方法会在服务状态发生变化时触发并且会异步地发送 WebHook 通知
* 适用于服务的正常状态变更不包括异常情况
*
* @param notifier 通知事件对象包含服务的状态和其他相关信息
*/
@Async
@EventListener
public void infoNotification(NotifierEvent notifier) {
sendWebHook(notifier);
}
/**
* 处理服务下线离线或异常的通知事件
* <p>
* 当服务的状态为OFFLINEDOWNUNKNOWN会触发此方法
* 该方法会异步发送邮件通知以便及时处理服务问题
*
* @param notifier 通知事件对象包含服务的状态和其他相关信息
*/
@Async
@EventListener(condition = "#notifier.status == 'OFFLINE' || #notifier.status == 'DOWN' || #notifier.status == 'UNKNOWN'")
public void errNotification(NotifierEvent notifier) {
sendMail(notifier);
}
/**
* 发送邮件通知
*
* @param notifier 包含通知信息的对象
*/
public void sendMail(NotifierEvent notifier) {
NotifyProperties.Mail mail = notifyProperties.getMail();
if (mail.getEnabled()) {
String message = StringUtils.format(mail.getTemplate(), notifier.getRegistName(), notifier.getInstanceId(),
notifier.getStatusName(), notifier.getStatus(), notifier.getServiceUrl(), DateUtils.getTime());
try {
MailUtils.sendHtml(mail.getTo(), notifier.getRegistName() + notifier.getStatusName(), message);
log.info("邮件已发送至: {}", mail.getTo());
} catch (Exception e) {
log.error("邮件发送失败: ", e);
}
}
}
/**
* 发送WebHook通知
*
* @param notifier 包含通知信息的对象
*/
public void sendWebHook(NotifierEvent notifier) {
NotifyProperties.WebHook webHook = notifyProperties.getWebHook();
if (webHook.getEnabled()) {
String title = notifier.getRegistName() + notifier.getStatusName();
String message = StringUtils.format(webHook.getTemplate(), title, notifier.getRegistName(),
notifier.getInstanceId(), notifier.getStatusName(), notifier.getStatus(), notifier.getServiceUrl(), DateUtils.getTime());
try {
sendWebHookMessage(webHook, title, message);
log.info("WebHook消息已发送至: {}", webHook.getUrl());
} catch (Exception e) {
log.error("WebHook消息发送失败: ", e);
}
}
}
/**
* 发送WebHook消息
*
* @param webHook WebHook配置信息
* @param title 消息标题
* @param markdownMessage 消息内容
* @throws Exception 处理过程中可能抛出的异常
*/
private void sendWebHookMessage(NotifyProperties.WebHook webHook, String title, String markdownMessage) throws Exception {
String jsonMessage = createJsonMessage(title, markdownMessage);
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = createHttpRequest(webHook, jsonMessage);
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
handleResponse(response);
}
/**
* 创建消息体的JSON字符串
*
* @param title 消息标题
* @param markdownMessage 消息内容
* @return JSON格式的消息体
* @throws Exception 处理过程中可能抛出的异常
*/
private String createJsonMessage(String title, String markdownMessage) throws Exception {
Map<String, Object> messageBody = new HashMap<>();
messageBody.put("msgtype", "markdown");
Map<String, String> markdownContent = new HashMap<>();
markdownContent.put("title", title + "通知");
markdownContent.put("text", markdownMessage);
messageBody.put("markdown", markdownContent);
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.writeValueAsString(messageBody);
}
/**
* 根据WebHook配置创建HTTP请求
*
* @param webHook WebHook配置信息
* @param jsonMessage JSON格式的消息体
* @return 创建的HTTP请求
* @throws Exception 处理过程中可能抛出的异常
*/
private HttpRequest createHttpRequest(NotifyProperties.WebHook webHook, String jsonMessage) throws Exception {
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonMessage));
if ("2".equals(webHook.getType())) {
String signedUrl = generateSignedUrl(webHook);
requestBuilder.uri(new URI(signedUrl));
} else {
requestBuilder.uri(new URI(webHook.getUrl()));
}
return requestBuilder.build();
}
/**
* 生成带签名的URL
*
* @param webHook WebHook配置信息
* @return 带签名的URL
* @throws Exception 处理过程中可能抛出的异常
*/
private String generateSignedUrl(NotifyProperties.WebHook webHook) throws Exception {
long timestamp = System.currentTimeMillis();
String secret = webHook.getSecret();
String stringToSign = timestamp + "\n" + secret;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
String sign = URLEncoder.encode(Base64.getEncoder().encodeToString(signData), StandardCharsets.UTF_8);
return String.format("%s&timestamp=%d&sign=%s", webHook.getUrl(), timestamp, sign);
}
/**
* 处理HTTP响应
*
* @param response HTTP响应
*/
private void handleResponse(HttpResponse<String> response) {
try {
// 解析响应体
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(response.body());
int errcode = jsonNode.has("errcode") ? jsonNode.get("errcode").asInt() : -1;
String errmsg = jsonNode.has("errmsg") ? jsonNode.get("errmsg").asText() : "Unknown error";
if (errcode == 0) {
log.info("WebHook消息发送成功");
} else {
log.error("WebHook消息发送失败: errcode={}, errmsg={}", errcode, errmsg);
}
} catch (Exception e) {
log.error("处理WebHook响应时发生异常: {}", e.getMessage());
}
}
}

View File

@ -46,3 +46,51 @@ spring.boot.admin.client:
userpassword: ${spring.boot.admin.client.password}
username: ruoyi
password: 123456
--- # mail 邮件发送
mail:
enabled: false
host: smtp.163.com
port: 465
# 是否需要用户名密码验证
auth: true
# 发送方遵循RFC-822标准
from: xxx@163.com
# 用户名注意如果使用foxmail邮箱此处user为qq号
user: xxx@163.com
# 密码注意某些邮箱需要为SMTP服务单独设置密码详情查看相关帮助
pass: xxxxxxxxxx
# 使用 STARTTLS安全连接STARTTLS是对纯文本通信协议的扩展。
starttlsEnable: true
# 使用SSL安全连接
sslEnable: true
# SMTP超时时长单位毫秒缺省值不超时
timeout: 0
# Socket连接超时值单位毫秒缺省值不超时
connectionTimeout: 0
# 服务通知
notify:
mail:
enabled: false
to:
subject: 系统通知
template: "<html><body>
<p><b>服务名称:</b> {}</p >
<p><b>实例编号:</b> {}</p >
<p><b>服务状态:</b> {}({})</p >
<p><b>服务地址:</b> {}</p >
<p><b>发送时间:</b> {}</p >
</body></html>"
web-hook:
enabled: true
type: 2
secret: SEC4d035df24951f00445ea16022f0db2d442fdc83b34c6ebe15a427572264f6798
url: https://oapi.dingtalk.com/robot/send?access_token=81a122dbbfbcbe237acdd9e20e421059fc2e0d80bf83ded4099b0fef9f9800ef
template: |
#### **{}**
- **服务名称**: {}
- **实例编号**: {}
- **服务状态**: {}({})
- **服务地址**: {}
- **发送时间**: {}