瀏覽代碼

Admin控制台添加服务状态变更告警功能

zhaojinyu 18 小時之前
父節點
當前提交
df59a8b80c

+ 11 - 0
base-components/monitor/pom.xml

@@ -22,6 +22,12 @@
             <artifactId>spring-boot-admin-starter-server</artifactId>
             <version>${spring-boot-admin.version}</version>
         </dependency>
+
+        <!--增加安全防护,防止别人随便进-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
 		
         <!-- SpringCloud Alibaba Nacos -->
         <dependency>
@@ -52,6 +58,11 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-security</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-webflux</artifactId>
+        </dependency>
 		
     </dependencies>
 

+ 67 - 0
base-components/monitor/src/main/java/com/ruoyi/modules/monitor/notify/DingTalkNotifier.java

@@ -0,0 +1,67 @@
+package com.ruoyi.modules.monitor.notify;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.BodyInserters;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+public class DingTalkNotifier {
+
+    @Value("${dingtalk.webhook}")
+    private String webhook;
+
+    @Value("${dingtalk.secret:}")
+    private String secret;
+
+    private final WebClient webClient = WebClient.create();
+
+    public Mono<Void> sendMarkdown(String title, String content) {
+        String url = buildUrlWithSign();
+
+        Map<String, Object> markdownMsg = new HashMap<>();
+        markdownMsg.put("msgtype", "markdown");
+
+        Map<String, String> markdown = new HashMap<>();
+        markdown.put("title", title);
+        markdown.put("text", content);
+
+        markdownMsg.put("markdown", markdown);
+
+        return webClient.post()
+                .uri(url)
+                .contentType(MediaType.APPLICATION_JSON)
+                .body(BodyInserters.fromValue(markdownMsg))
+                .retrieve()
+                .bodyToMono(String.class)
+                .then();
+    }
+
+    private String buildUrlWithSign() {
+        if (secret == null || secret.trim().isEmpty()) {
+            return webhook;
+        }
+        long timestamp = Instant.now().toEpochMilli();
+        String stringToSign = timestamp + "\n" + secret;
+        try {
+            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(new String(Base64.getEncoder().encode(signData)), String.valueOf(StandardCharsets.UTF_8));
+            return webhook + "&timestamp=" + timestamp + "&sign=" + sign;
+        } catch (Exception e) {
+            throw new RuntimeException("生成钉钉签名失败", e);
+        }
+    }
+}

+ 100 - 0
base-components/monitor/src/main/java/com/ruoyi/modules/monitor/notify/ServiceChangeNotifier.java

@@ -0,0 +1,100 @@
+package com.ruoyi.modules.monitor.notify;
+
+import de.codecentric.boot.admin.server.domain.entities.Instance;
+import de.codecentric.boot.admin.server.domain.entities.InstanceRepository;
+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.AbstractStatusChangeNotifier;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+@Component
+public class ServiceChangeNotifier extends AbstractStatusChangeNotifier {
+
+    private final DingTalkNotifier dingTalkNotifier;
+    private final InstanceRepository repository;
+
+    /* 新增字段 */
+    private final ConcurrentHashMap<String, Instance> buffer = new ConcurrentHashMap<>();
+    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
+
+
+    public ServiceChangeNotifier(InstanceRepository repository,
+                                 DingTalkNotifier dingTalkNotifier) {
+        super(repository);
+        this.repository = repository;
+        this.dingTalkNotifier = dingTalkNotifier;
+
+        /* 每 5 秒聚合发送一次(可按需调整) */
+        scheduler.scheduleWithFixedDelay(this::flush, 5, 5, TimeUnit.SECONDS);
+    }
+
+    @Override
+    protected boolean shouldNotify(InstanceEvent event, Instance instance) {
+        return event instanceof InstanceStatusChangedEvent;
+    }
+
+    @Override
+    protected Mono<Void> doNotify(InstanceEvent event, Instance instance) {
+        /* 仅把实例放进 buffer,不做实际推送 */
+        buffer.put(String.valueOf(instance.getId()), instance);
+        return Mono.empty();
+    }
+
+    private void flush() {
+        if (buffer.isEmpty()) {
+            return;
+        }
+
+        /* 拿到此刻的快照,然后清空 buffer */
+        List<Instance> snapshot = new ArrayList<>(buffer.values());
+        buffer.clear();
+
+        /* 计算每个实例的 total,并排序 */
+        Map<Instance, Long> totalMap = new HashMap<>();
+        for (Instance inst : snapshot) {
+            long total = repository.findAll().collectList().block().size(); // 这里简单起见直接 block
+            totalMap.put(inst, total);
+        }
+
+        snapshot.sort(Comparator.comparing(totalMap::get));
+
+        /* 依次发送钉钉 */
+        for (Instance instance : snapshot) {
+            sendOne(instance);
+        }
+    }
+
+    private void sendOne(Instance instance) {
+        String serviceName = instance.getRegistration().getName();
+        String newStatus   = instance.getStatusInfo().getStatus();
+        String serviceUrl  = instance.getRegistration().getServiceUrl();
+        String time        = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
+
+        List<Instance> all = repository.findAll().collectList().block();
+        long total   = all.size();
+        long online  = all.stream().filter(i -> "UP".equals(i.getStatusInfo().getStatus())).count();
+        long offline = total - online;
+
+        /* 根据状态决定颜色 */
+        String icon = "UP".equalsIgnoreCase(newStatus)
+                ? "<font color=\"#00ff00\">✅</font>"
+                : "<font color=\"#ff0000\">🚨</font>";
+
+        String markdown = "### " + icon + " 服务状态变更告警\n" +
+                "- **服务名**: " + serviceName + "  \n" +
+                "- **当前状态**: " + newStatus + "  \n" +
+                "- **地址**: " + serviceUrl + "  \n" +
+                "- **时间**: " + time + "  \n" +
+                "- **统计**: 总 " + total + ",在线 " + online + ",离线 " + offline;
+
+        dingTalkNotifier.sendMarkdown("服务状态变更", markdown).subscribe();
+    }
+}