Browse Source

update mon module

reghao 1 month ago
parent
commit
101c2a9cf4
19 changed files with 133 additions and 920 deletions
  1. 1 2
      mgr/src/main/java/cn/reghao/devops/mgr/admin/controller/HomeController.java
  2. 0 114
      mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/JitterAnalysisService.java
  3. 0 27
      mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/OperationReportDTO.java
  4. 0 2
      mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/PrometheusAsyncClient.java
  5. 115 478
      mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/PrometheusService.java
  6. 0 28
      mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/dto/ContainerHealthReport.java
  7. 0 43
      mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/dto/ContainerUsageDTO.java
  8. 1 1
      mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/model/ContainerInfo.java
  9. 1 1
      mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/model/ContainerReportVO.java
  10. 1 1
      mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/model/HostData.java
  11. 1 1
      mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/model/HostInfo.java
  12. 1 1
      mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/model/MetricGroup.java
  13. 1 1
      mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/model/PillarReportDTO.java
  14. 0 64
      mgr/src/main/resources/templates/container.ftl
  15. 0 62
      mgr/src/main/resources/templates/container_mem.ftl
  16. 0 0
      mgr/src/main/resources/templates/container_report_v1.ftl
  17. 0 94
      mgr/src/main/resources/templates/daily_report.ftl
  18. 11 0
      mgr/src/main/resources/templates/mem_report.ftl
  19. 0 0
      mgr/src/main/resources/templates/report_v1.ftl

+ 1 - 2
mgr/src/main/java/cn/reghao/devops/mgr/admin/controller/HomeController.java

@@ -1,8 +1,7 @@
 package cn.reghao.devops.mgr.admin.controller;
 package cn.reghao.devops.mgr.admin.controller;
 
 
-import cn.reghao.devops.mgr.admin.model.vo.DashboardData;
 import cn.reghao.devops.mgr.admin.service.HomeViewService;
 import cn.reghao.devops.mgr.admin.service.HomeViewService;
-import cn.reghao.devops.mgr.ops.srv.mon.ContainerReportVO;
+import cn.reghao.devops.mgr.ops.srv.mon.model.ContainerReportVO;
 import cn.reghao.devops.mgr.ops.srv.mon.PrometheusService;
 import cn.reghao.devops.mgr.ops.srv.mon.PrometheusService;
 import cn.reghao.jutil.web.WebResult;
 import cn.reghao.jutil.web.WebResult;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Operation;

+ 0 - 114
mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/JitterAnalysisService.java

@@ -1,114 +0,0 @@
-package cn.reghao.devops.mgr.ops.srv.mon;
-
-import cn.reghao.devops.mgr.ops.srv.mon.dto.ContainerHealthReport;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * @author reghao
- * @date 2026-03-30 15:59:35
- */
-@Slf4j
-@Service
-public class JitterAnalysisService {
-    ObjectMapper objectMapper = new ObjectMapper();
-
-    public List<ContainerHealthReport> analyzeMetrics(String cpuJson, String memJson) throws Exception {
-        // 1. 解析原始数据为 Map<Key, List<Double>>
-        Map<String, List<Double>> cpuDataMap = parseRawMatrix(cpuJson);
-        Map<String, List<Double>> memDataMap = parseRawMatrix(memJson);
-
-        List<ContainerHealthReport> reports = new ArrayList<>();
-
-        // 2. 遍历所有容器进行健康诊断
-        for (String key : cpuDataMap.keySet()) {
-            List<Double> cpuValues = cpuDataMap.get(key);
-            List<Double> memValues = memDataMap.getOrDefault(key, new ArrayList<>());
-
-            String[] parts = key.split("@");
-            ContainerHealthReport report = ContainerHealthReport.builder()
-                    .containerName(parts[0])
-                    .instanceIp(parts[1])
-                    .build();
-
-            // --- CPU 诊断逻辑 ---
-            analyzeCpuHealth(report, cpuValues);
-
-            // --- 内存 诊断逻辑 ---
-            analyzeMemHealth(report, memValues);
-
-            reports.add(report);
-        }
-        return reports;
-    }
-
-    private void analyzeCpuHealth(ContainerHealthReport report, List<Double> values) {
-        if (values.isEmpty()) return;
-
-        double avg = values.stream().mapToDouble(v -> v).average().orElse(0);
-        double max = values.stream().mapToDouble(v -> v).max().orElse(0);
-
-        // 计算标准差
-        double variance = values.stream().mapToDouble(v -> Math.pow(v - avg, 2)).average().orElse(0);
-        double stdDev = Math.sqrt(variance);
-        double cv = avg > 0.05 ? stdDev / avg : 0; // 变异系数
-
-        report.setCpuAvg(round(avg));
-        report.setCpuMax(round(max));
-        report.setCpuJitterScore(round(cv));
-
-        // 判定准则
-        if (cv > 0.6) report.setCpuStatus("⚡ 剧烈抖动");
-        else if (avg > 0.8) report.setCpuStatus("🔥 持续高负载");
-        else report.setCpuStatus("✅ 运行平稳");
-    }
-
-    private void analyzeMemHealth(ContainerHealthReport report, List<Double> values) {
-        if (values.size() < 10) return;
-
-        double start = values.get(0);
-        double end = values.get(values.size() - 1);
-        double max = values.stream().mapToDouble(v -> v).max().orElse(0);
-        double growth = (end - start) / (start > 0 ? start : 1);
-
-        report.setMemStart(round(start));
-        report.setMemEnd(round(end));
-        report.setMemMax(round(max));
-        report.setMemGrowthRate(round(growth));
-
-        // 判定准则
-        // 1. 检测内存泄漏:24h 增长超过 20% 且不回落
-        if (growth > 0.2) report.setMemStatus("📈 疑似内存泄漏");
-            // 2. 检测锯齿抖动:通过比较 Max 和 Avg 的差距(简化算法)
-        else if ((max - end) / end > 0.3) report.setMemStatus("🔄 频繁GC (锯齿波动)");
-        else report.setMemStatus("✅ 运行平稳");
-    }
-
-    private Map<String, List<Double>> parseRawMatrix(String json) throws Exception {
-        Map<String, List<Double>> map = new HashMap<>();
-        JsonNode results = objectMapper.readTree(json).path("data").path("result");
-        for (JsonNode res : results) {
-            String name = res.path("metric").path("name").asText();
-            String ip = res.path("metric").path("instance").asText().split(":")[0];
-            String key = name + "@" + ip;
-
-            List<Double> vals = new ArrayList<>();
-            for (JsonNode v : res.path("values")) {
-                vals.add(v.get(1).asDouble());
-            }
-            map.put(key, vals);
-        }
-        return map;
-    }
-
-    private double round(double val) {
-        return Math.round(val * 100.0) / 100.0;
-    }
-}

+ 0 - 27
mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/OperationReportDTO.java

@@ -1,27 +0,0 @@
-package cn.reghao.devops.mgr.ops.srv.mon;
-
-import lombok.Data;
-
-import java.util.List;
-
-/**
- * @author reghao
- * @date 2026-03-28 01:21:21
- */
-@Data
-public class OperationReportDTO {
-    // 基础信息
-    private String startTime;
-    private String endTime;
-    private int containerCount;
-
-    // 宿主机列表 (10条)
-    private List<HostInfo> hostList;
-
-    // 容器排行 (Top 5)
-    private List<ContainerInfo> topContainers;
-
-    // 图表数据
-    private String timeLabels;    // "00:00, 04:00..."
-    private String avgCpuTrend;   // "10, 15, 40..."
-}

+ 0 - 2
mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/PrometheusAsyncClient.java

@@ -1,7 +1,5 @@
 package cn.reghao.devops.mgr.ops.srv.mon;
 package cn.reghao.devops.mgr.ops.srv.mon;
 
 
-import cn.reghao.devops.mgr.ops.srv.mon.dto.ContainerUsageDTO;
-import com.fasterxml.jackson.databind.JsonNode;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 
 
 import java.net.URI;
 import java.net.URI;

+ 115 - 478
mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/PrometheusService.java

@@ -1,11 +1,7 @@
 package cn.reghao.devops.mgr.ops.srv.mon;
 package cn.reghao.devops.mgr.ops.srv.mon;
 
 
 import cn.reghao.devops.mgr.config.AppProperties;
 import cn.reghao.devops.mgr.config.AppProperties;
-import cn.reghao.devops.mgr.ops.srv.mon.dto.ContainerHealthReport;
-import cn.reghao.devops.mgr.ops.srv.mon.model.AnalysisResult;
-import cn.reghao.devops.mgr.ops.srv.mon.model.CpuThresholdConfig;
-import cn.reghao.devops.mgr.ops.srv.mon.model.HostGroup;
-import cn.reghao.devops.mgr.ops.srv.mon.model.MetricRecord;
+import cn.reghao.devops.mgr.ops.srv.mon.model.*;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -13,7 +9,6 @@ import com.github.benmanes.caffeine.cache.Cache;
 import freemarker.template.Configuration;
 import freemarker.template.Configuration;
 import freemarker.template.Template;
 import freemarker.template.Template;
 import freemarker.template.TemplateException;
 import freemarker.template.TemplateException;
-import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
 import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
@@ -86,102 +81,6 @@ public class PrometheusService {
     public void generateDailyReport() {
     public void generateDailyReport() {
         // 定义查询任务
         // 定义查询任务
         Map<String, String> tasks = Map.of(
         Map<String, String> tasks = Map.of(
-                "container_count", """
-                        avg_over_time((1 - avg(irate(node_cpu_seconds_total{mode="idle"}[5m])) by (instance))[24h:1m]) * 100
-                        """,
-                "top_cpu_containers", """
-                        max_over_time((1 - avg(irate(node_cpu_seconds_total{mode="idle"}[5m])) by (instance))[24h:1m]) * 100
-                        """,
-                "top_cpu_containers1", """
-                        max_over_time((1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)[24h:1m]) * 100
-                        """,
-                "top_cpu_containers2", """
-                        max_over_time(rate(container_cpu_usage_seconds_total{name!=""}[5m])[24h:1m])
-                        """,
-                "top_cpu_containers3", """
-                        max_over_time(container_memory_usage_bytes{name!=""}[24h:1m]) / 1024 / 1024
-                        """,
-                "top_cpu_containers4", """
-                        increase(container_oom_events_total[24h]) > 0
-                        """
-        );
-
-        // 异步执行
-        promClient.fetchAllMetrics(tasks).thenAccept(results -> {
-            //processResults(results);
-            System.out.println("所有数据采集完成,开始渲染报表...");
-        }).join(); // 如果是在定时任务主线程,可以用 join 等待完成
-    }
-
-    private OperationReportDTO processResults(Map<String, String> results) {
-        OperationReportDTO report = new OperationReportDTO();
-        Map<String, HostInfo> hostMap = new HashMap<>();
-        try {
-            // 1. 解析 CPU 指标
-            if (results.containsKey("node_cpu")) {
-                parseToHostMap(results.get("node_cpu"), hostMap, "cpu");
-            }
-
-            // 2. 填充内存数据
-            if (results.containsKey("node_mem")) {
-                parseToHostMap(results.get("node_mem"), hostMap, "mem");
-            }
-
-            // 3. 填充容器数量
-            if (results.containsKey("container_count")) {
-                parseToHostMap(results.get("container_count"), hostMap, "count");
-            }
-
-            // 4. 解析 Top 5 容器排行 (注意这里 Key 的对齐)
-            // 修正:使用 generateDailyReport 中定义的 "top_cpu_containers"
-            String topCpuJson = results.get("top_cpu_containers");
-            if (topCpuJson != null) {
-                List<ContainerInfo> topContainers = parseTopContainers(topCpuJson);
-                report.setTopContainers(topContainers);
-            }
-
-            report.setHostList(new ArrayList<>(hostMap.values()));
-            // 这里建议加上容器总数统计
-            report.setContainerCount(report.getTopContainers() != null ? report.getTopContainers().size() : 0);
-
-            report.setStartTime(LocalDateTime.now().minusDays(1).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
-            report.setEndTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
-
-            // 解析趋势数据
-            if (results.containsKey("cpu_trend")) {
-                parseTrendData(results.get("cpu_trend"), report);
-            }
-        } catch (Exception e) {
-        }
-
-        Map<String, Object> root = new HashMap<>();
-        root.put("hostGroupList", report);
-
-        /*try {
-            // 6. 渲染最终的复合模板
-            String templatePath = "daily_report.ftl";
-            String htmlContent = renderHtml(templatePath, root);
-            Path outputPath = Paths.get("/home/reghao/Downloads", "daily_report_" + LocalDate.now() + ".html");
-            if (Files.notExists(outputPath.getParent())) {
-                Files.createDirectories(outputPath.getParent());
-            }
-            Files.writeString(outputPath, htmlContent, StandardCharsets.UTF_8);
-            System.out.println("✅ 报表已成功保存至: " + outputPath.toAbsolutePath());
-        } catch (Exception e) {
-            log.error("{}", e.getMessage());
-        }*/
-
-        return report;
-    }
-
-    public void generateDailyReport1() {
-        // 定义查询任务
-        Map<String, String> tasks = Map.of(
-                "container_count", "count by (instance) (container_last_seen{image!=''})",
-                "top_cpu_containers", "topk(5, sum by (name) (rate(container_cpu_usage_seconds_total{image!=''}[5m]) * 100))"
-        );
-
-        tasks = Map.of(
                 "node_cpu_core", """
                 "node_cpu_core", """
                         count by(instance) (node_cpu_seconds_total{mode="idle"})
                         count by(instance) (node_cpu_seconds_total{mode="idle"})
                         """,
                         """,
@@ -226,79 +125,11 @@ public class PrometheusService {
 
 
         // 异步执行
         // 异步执行
         promClient.fetchAllMetrics(tasks).thenAccept(results -> {
         promClient.fetchAllMetrics(tasks).thenAccept(results -> {
-            processResults0(results);
-            System.out.println("所有数据采集完成,开始渲染报表...");
+            processResults(results);
         }).join(); // 如果是在定时任务主线程,可以用 join 等待完成
         }).join(); // 如果是在定时任务主线程,可以用 join 等待完成
     }
     }
 
 
-    public List<HostGroup> generateUnifiedReport(Map<String, MetricRecord> mergedMap) {
-        // 2. 建立 HostGroup 层级关系
-        Map<String, HostGroup> hostGroupMap = new LinkedHashMap<>();
-        mergedMap.values().forEach(record -> {
-            // 提取 IP (172.16.45.66:9100 -> 172.16.45.66)
-            String ip = record.getInstance().split(":")[0];
-            HostGroup group = hostGroupMap.computeIfAbsent(ip, k -> {
-                HostGroup hg = new HostGroup();
-                hg.setHostIp(ip);
-                return hg;
-            });
-
-            if (record.getContainer() == null) {
-                group.setHostRecord(record);
-            } else {
-                group.getContainerRecords().add(record);
-            }
-        });
-
-        // 3. 核心:建立宿主机与容器的量纲关联 (统一为 Node 视角)
-        hostGroupMap.values().forEach(group -> {
-            MetricRecord host = group.getHostRecord();
-            List<MetricRecord> containers = group.getContainerRecords();
-
-            if (host != null && !containers.isEmpty()) {
-                // 获取宿主机总核数 (从 node_cpu_core 指标中解析,此处假设 record 已包含该值)
-                double totalCores = host.getCpuCore() > 0 ? host.getCpuCore() : 8.0;
-
-                // 计算容器对宿主机的“实际贡献值”
-                // 容器利用率 (C_util) = (Used_Cores / Limit_Cores) * 100
-                // 贡献 Node 的百分比 = C_util * (Limit_Cores / Node_Total_Cores)
-                double totalContainerContributionToNode = containers.stream()
-                        .mapToDouble(c -> c.getAvgValue() * (c.getCpuLimit() / totalCores))
-                        .sum();
-
-                // 系统隐性损耗 = 宿主机总利用率 - 容器贡献总和
-                group.setSystemOverhead(Math.max(0, host.getAvgValue() - totalContainerContributionToNode));
-
-                for (MetricRecord container : group.getContainerRecords()) {
-                    // 1. 获取该容器的 Limit 核数 (需从 container_cpu_limit 查询中匹配)
-                    double limitCores = container.getCpuLimit();
-
-                    // 2. 计算:相对宿主机的百分比 = 相对Limit百分比 * (Limit核数 / 总核数)
-                    double relativeToHost = container.getAvgValue() * (limitCores / totalCores);
-                    double relativeToHostMax = container.getMaxValue() * (limitCores / totalCores);
-
-                    // 将这两个值存入 record 供模板使用
-                    container.setRelativeToHostAvg(relativeToHost);
-                    container.setRelativeToHostMax(relativeToHostMax);
-                }
-
-                // 诊断结论
-                StringBuilder diag = new StringBuilder();
-                if (group.getSystemOverhead() > 15.0) {
-                    diag.append(String.format("⚠️ 宿主机非容器损耗(内核/IO)高达 %.1f%%。 ", group.getSystemOverhead()));
-                }
-                if (host.getMaxValue() > 85.0) {
-                    diag.append("🚨 宿主机峰值接近瓶颈。 ");
-                }
-                group.setRelationshipDiagnosis(diag.length() == 0 ? "✅ 资源分配健康" : diag.toString());
-            }
-        });
-
-        // 4. 传给模板
-        return new ArrayList<>(hostGroupMap.values());
-    }
-
-    private void processResults0(Map<String, String> rawResults) {
+    private void processResults(Map<String, String> rawResults) {
         // 使用 Map 存储合并后的对象,Key 是 "instance:container"
         // 使用 Map 存储合并后的对象,Key 是 "instance:container"
         Map<String, MetricRecord> mergedMap = new HashMap<>();
         Map<String, MetricRecord> mergedMap = new HashMap<>();
         // 遍历所有的任务结果 (node_cpu_avg, node_cpu_max, container_cpu_avg 等)
         // 遍历所有的任务结果 (node_cpu_avg, node_cpu_max, container_cpu_avg 等)
@@ -349,14 +180,17 @@ public class PrometheusService {
             }
             }
         });
         });
         // 4. 将合并后的结果转换为 List 并执行分析
         // 4. 将合并后的结果转换为 List 并执行分析
-        List<MetricRecord> finalRecords = new ArrayList<>(mergedMap.values());
-        System.out.println("数据对齐完成,共计 " + finalRecords.size() + " 条指标记录。");
+        //List<MetricRecord> finalRecords = new ArrayList<>(mergedMap.values());
+        System.out.println("数据对齐完成,共计 " + mergedMap.size() + " 条指标记录。");
 
 
+        generateReport1(mergedMap);
+        generateReport2(mergedMap);
+    }
 
 
-        //List<HostGroup> hostGroupList0 = buildHierarchyAndAnalyze(mergedMap);
-        List<HostGroup> hostGroupList = generateUnifiedReport(mergedMap);
+    private void generateReport1(Map<String, MetricRecord> mergedMap) {
+        List<MetricRecord> finalRecords = new ArrayList<>(mergedMap.values());
         // 5. 调用之前的分析逻辑
         // 5. 调用之前的分析逻辑
-        /*List<AnalysisResult> results = finalRecords.stream()
+        List<AnalysisResult> results = finalRecords.stream()
                 .map(this::getAnalysisResult)
                 .map(this::getAnalysisResult)
                 .filter(res -> !"NORMAL".equals(res.getStatus())) // 过滤掉正常的
                 .filter(res -> !"NORMAL".equals(res.getStatus())) // 过滤掉正常的
                 .collect(Collectors.toList());
                 .collect(Collectors.toList());
@@ -375,16 +209,14 @@ public class PrometheusService {
                                     return list;
                                     return list;
                                 }
                                 }
                         )
                         )
-                ));*/
+                ));
 
 
         Map<String, Object> root = new HashMap<>();
         Map<String, Object> root = new HashMap<>();
-        //root.put("hostMap", groupMap);
         root.put("date", new SimpleDateFormat("yyyy-MM-dd").format(new Date()));
         root.put("date", new SimpleDateFormat("yyyy-MM-dd").format(new Date()));
-        root.put("hostGroupList", hostGroupList);
-
+        root.put("hostMap", groupMap);
         try {
         try {
             // 6. 渲染最终的复合模板(左右布局那个)
             // 6. 渲染最终的复合模板(左右布局那个)
-            String templatePath = "host_group_report.ftl";
+            String templatePath = "report.ftl";
             String htmlContent = renderHtml(templatePath, root);
             String htmlContent = renderHtml(templatePath, root);
             Path outputPath = Paths.get("/home/reghao/Downloads", "report_" + LocalDate.now() + ".html");
             Path outputPath = Paths.get("/home/reghao/Downloads", "report_" + LocalDate.now() + ".html");
             if (Files.notExists(outputPath.getParent())) {
             if (Files.notExists(outputPath.getParent())) {
@@ -397,59 +229,39 @@ public class PrometheusService {
         }
         }
     }
     }
 
 
-    private List<HostGroup> buildHostGroups(Map<String, MetricRecord> mergedMap) {
-        Map<String, HostGroup> hostGroups = new HashMap<>();
-
-        // 1. 第一次遍历:初始化宿主机
-        mergedMap.values().stream().filter(r -> r.getContainer() == null).forEach(r -> {
-            HostGroup g = new HostGroup();
-            g.setHostIp(r.getInstance());
-            g.setHostRecord(r);
-            hostGroups.put(r.getInstance(), g);
-        });
+    private void generateReport2(Map<String, MetricRecord> mergedMap) {
+        List<HostGroup> hostGroupList = generateUnifiedReport(mergedMap);
+        Map<String, Object> root = new HashMap<>();
+        root.put("date", new SimpleDateFormat("yyyy-MM-dd").format(new Date()));
+        root.put("hostGroupList", hostGroupList);
 
 
-        // 2. 第二次遍历:挂载容器并折算贡献度
-        mergedMap.values().stream().filter(r -> r.getContainer() != null).forEach(r -> {
-            HostGroup g = hostGroups.get(r.getInstance());
-            if (g != null) {
-                g.getContainerRecords().add(r);
-                // 关键:计算该容器对宿主机的实际贡献 = (容器利用率 * 容器配额核心数) / 宿主机总核数
-                // 但因为我们没有直接查配额核心数,目前最稳妥的办法是仅做展示,
-                // 损耗诊断建议基于:HostAvg - Sum(Container核心数)/TotalCores
-                // 此处为了简化,我们假设容器利用率是相对于 Limit 的。
+        try {
+            // 6. 渲染最终的复合模板(左右布局那个)
+            String templatePath = "report_v1.ftl";
+            String htmlContent = renderHtml(templatePath, root);
+            Path outputPath = Paths.get("/home/reghao/Downloads", "report_v1_" + LocalDate.now() + ".html");
+            if (Files.notExists(outputPath.getParent())) {
+                Files.createDirectories(outputPath.getParent());
             }
             }
-        });
-
-        // 3. 执行关系分析
-        hostGroups.values().forEach(this::analyzeRelationship);
-        return new ArrayList<>(hostGroups.values());
-    }
-
-    private void analyzeRelationship(HostGroup group) {
-        MetricRecord host = group.getHostRecord();
-        if (host == null) return;
-
-        double totalCores = host.getCpuCore() > 0 ? host.getCpuCore() : 1.0;
-
-        // 这里需要注意:因为容器 avg 是相对于 limit 的百分比
-        // 如果没有采集到 limit 核心数,sum(avg) 是没有物理意义的。
-        // 建议:在实际运维中,我们关注的是宿主机整体水位。
-
-        StringBuilder sb = new StringBuilder();
-        if (host.getAvgValue() > 80) {
-            sb.append("🚨 宿主机整体负载极高,请检查资源分配。");
-        } else {
-            sb.append("✅ 节点运行状态平稳。");
+            Files.writeString(outputPath, htmlContent, StandardCharsets.UTF_8);
+            System.out.println("✅ 报表已成功保存至: " + outputPath.toAbsolutePath());
+        } catch (Exception e) {
+            log.error("{}", e.getMessage());
         }
         }
-        group.setRelationshipDiagnosis(sb.toString());
     }
     }
 
 
-    public List<HostGroup> buildHierarchyAndAnalyze0(Map<String, MetricRecord> mergedMap) {
-        // 1. 初步按 instance 分组
-        Map<String, HostGroup> groups = new HashMap<>();
-
+    public List<HostGroup> generateUnifiedReport(Map<String, MetricRecord> mergedMap) {
+        // 2. 建立 HostGroup 层级关系
+        Map<String, HostGroup> hostGroupMap = new LinkedHashMap<>();
         mergedMap.values().forEach(record -> {
         mergedMap.values().forEach(record -> {
-            HostGroup group = groups.computeIfAbsent(record.getInstance(), k -> new HostGroup());
+            // 提取 IP (172.16.45.66:9100 -> 172.16.45.66)
+            String ip = record.getInstance().split(":")[0];
+            HostGroup group = hostGroupMap.computeIfAbsent(ip, k -> {
+                HostGroup hg = new HostGroup();
+                hg.setHostIp(ip);
+                return hg;
+            });
+
             if (record.getContainer() == null) {
             if (record.getContainer() == null) {
                 group.setHostRecord(record);
                 group.setHostRecord(record);
             } else {
             } else {
@@ -457,136 +269,54 @@ public class PrometheusService {
             }
             }
         });
         });
 
 
-        // 2. 深度分析每一组的关系
-        groups.values().forEach(group -> {
+        // 3. 核心:建立宿主机与容器的量纲关联 (统一为 Node 视角)
+        hostGroupMap.values().forEach(group -> {
             MetricRecord host = group.getHostRecord();
             MetricRecord host = group.getHostRecord();
             List<MetricRecord> containers = group.getContainerRecords();
             List<MetricRecord> containers = group.getContainerRecords();
 
 
             if (host != null && !containers.isEmpty()) {
             if (host != null && !containers.isEmpty()) {
-                // 计算容器均值总和
-                double sumContainerAvg = containers.stream().mapToDouble(MetricRecord::getAvgValue).sum();
-                // 计算系统损耗 (宿主机利用率 - 容器利用率总和)
-                group.setSystemOverhead(Math.max(0, host.getAvgValue() - sumContainerAvg));
-
-                // 计算峰值共振:宿主机峰值 / 容器峰值总和
-                double sumContainerMax = containers.stream().mapToDouble(MetricRecord::getMaxValue).sum();
-                group.setPeakCohesion(sumContainerMax > 0 ? host.getMaxValue() / sumContainerMax : 0);
-
-                // 3. 自动生成诊断结论
-                StringBuilder diagnosis = new StringBuilder();
-                if (group.getSystemOverhead() > 20.0) {
-                    diagnosis.append(String.format("⚠️ 系统隐性损耗过高(%.1f%%),请检查宿主机原生进程。 ", group.getSystemOverhead()));
-                }
-                if (host.getMaxValue() > 80.0 && group.getPeakCohesion() > 0.8) {
-                    diagnosis.append("🚨 探测到明显的容器并发冲撞,建议交错执行高负载任务。 ");
-                }
-                if (diagnosis.length() == 0) diagnosis.append("✅ 宿主与容器负载分配比例健康。");
-
-                group.setRelationshipDiagnosis(diagnosis.toString());
-            }
-        });
-
-        return new ArrayList<>(groups.values());
-    }
-    public List<HostGroup> buildHierarchyAndAnalyze(Map<String, MetricRecord> mergedMap) {
-        // 1. 使用纯 IP (不带端口) 作为分组的 Key
-        Map<String, HostGroup> groups = new HashMap<>();
-
-        mergedMap.values().forEach(record -> {
-            // 提取 IP 部分,例如 "192.168.1.10:9100" -> "192.168.1.10"
-            String rawInstance = record.getInstance();
-            String ipAddress = rawInstance.contains(":") ? rawInstance.split(":")[0] : rawInstance;
-
-            HostGroup group = groups.computeIfAbsent(ipAddress, k -> {
-                HostGroup newGroup = new HostGroup();
-                newGroup.setHostIp(ipAddress); // 建议在 HostGroup 中增加该字段
-                return newGroup;
-            });
+                // 获取宿主机总核数 (从 node_cpu_core 指标中解析,此处假设 record 已包含该值)
+                double totalCores = host.getCpuCore() > 0 ? host.getCpuCore() : 8.0;
 
 
-            // 判定归属:根据 container 字段是否为空
-            if (record.getContainer() == null) {
-                // 来自 node_exporter 的宿主机数据
-                group.setHostRecord(record);
-            } else {
-                // 来自 cadvisor 的容器数据
-                group.getContainerRecords().add(record);
-            }
-        });
+                // 计算容器对宿主机的“实际贡献值”
+                // 容器利用率 (C_util) = (Used_Cores / Limit_Cores) * 100
+                // 贡献 Node 的百分比 = C_util * (Limit_Cores / Node_Total_Cores)
+                double totalContainerContributionToNode = containers.stream()
+                        .mapToDouble(c -> c.getAvgValue() * (c.getCpuLimit() / totalCores))
+                        .sum();
 
 
-        // 2. 深度分析每一组的关系
-        groups.values().forEach(group -> {
-            MetricRecord host = group.getHostRecord();
-            List<MetricRecord> containers = group.getContainerRecords();
+                // 系统隐性损耗 = 宿主机总利用率 - 容器贡献总和
+                group.setSystemOverhead(Math.max(0, host.getAvgValue() - totalContainerContributionToNode));
 
 
-            // 只有当宿主机数据存在时才计算损耗
-            if (host != null) {
-                double sumContainerAvg = containers.stream().mapToDouble(MetricRecord::getAvgValue).sum();
+                for (MetricRecord container : group.getContainerRecords()) {
+                    // 1. 获取该容器的 Limit 核数 (需从 container_cpu_limit 查询中匹配)
+                    double limitCores = container.getCpuLimit();
 
 
-                // 计算系统隐性损耗:宿主机总量 - 容器总量
-                // 注意:如果容器很多,sum 可能略大于 host(采样时间差),需用 Math.max(0, ...)
-                group.setSystemOverhead(Math.max(0, host.getAvgValue() - sumContainerAvg));
+                    // 2. 计算:相对宿主机的百分比 = 相对Limit百分比 * (Limit核数 / 总核数)
+                    double relativeToHost = container.getAvgValue() * (limitCores / totalCores);
+                    double relativeToHostMax = container.getMaxValue() * (limitCores / totalCores);
 
 
-                // 诊断逻辑
-                StringBuilder diagnosis = new StringBuilder();
-                if (group.getSystemOverhead() > 20.0) {
-                    diagnosis.append(String.format("⚠️ 宿主机非容器损耗较高(%.1f%%)。 ", group.getSystemOverhead()));
+                    // 将这两个值存入 record 供模板使用
+                    container.setRelativeToHostAvg(relativeToHost);
+                    container.setRelativeToHostMax(relativeToHostMax);
                 }
                 }
 
 
-                if (containers.isEmpty()) {
-                    diagnosis.append("ℹ️ 该节点当前未发现运行中的业务容器。");
-                } else if (diagnosis.length() == 0) {
-                    diagnosis.append("✅ 宿主与容器负载分配正常。");
+                // 诊断结论
+                StringBuilder diag = new StringBuilder();
+                if (group.getSystemOverhead() > 15.0) {
+                    diag.append(String.format("⚠️ 宿主机非容器损耗(内核/IO)高达 %.1f%%。 ", group.getSystemOverhead()));
+                }
+                if (host.getMaxValue() > 85.0) {
+                    diag.append("🚨 宿主机峰值接近瓶颈。 ");
                 }
                 }
-                group.setRelationshipDiagnosis(diagnosis.toString());
+                group.setRelationshipDiagnosis(diag.length() == 0 ? "✅ 资源分配健康" : diag.toString());
             }
             }
         });
         });
 
 
-        return new ArrayList<>(groups.values());
+        // 4. 传给模板
+        return new ArrayList<>(hostGroupMap.values());
     }
     }
 
 
-    /*public String analyze(MetricRecord record) {
-        StringBuilder report = new StringBuilder();
-        boolean isContainer = record.getContainer() != null;
-
-        // 1. 统一单位描述
-        String unit = isContainer ? "核" : "%";
-        String displayName = isContainer ? "容器:" + record.getContainer() : "宿主机:" + record.getInstance();
-
-        report.append(String.format("【%s 报告】均值: %.2f%s, 峰值: %.2f%s\n",
-                displayName, record.getAvgValue(), unit, record.getMaxValue(), unit));
-
-        // 2. 针对性设定“防零处理”的底数 (Silence Threshold)
-        // 宿主机分母至少 1%;容器分母至少 0.1 核
-        double silenceThreshold = isContainer ? 0.1 : 1.0;
-        double targetThreshold = isContainer ? CpuThresholdConfig.CONTAINER_THRESHOLD : CpuThresholdConfig.NODE_THRESHOLD;
-
-        // 3. 判定:容量不足 (注意容器的 threshold 应该是它的 CPU Limit 核心数)
-        if (record.getAvgValue() >= targetThreshold) {
-            report.append(" -> [严重异常] 均值已触及水位线,资源严重不足!\n");
-        }
-
-        // 4. 判定:毛刺率 (Spike Rate)
-        else {
-            double ratio = record.getMaxValue() / Math.max(record.getAvgValue(), silenceThreshold);
-
-            // 判定阈值:如果峰值本身很小(比如宿主机 < 5% 或 容器 < 0.2核),则忽略毛刺
-            double significantPeak = isContainer ? 0.2 : 5.0;
-
-            if (record.getMaxValue() > significantPeak && ratio > 5.0) {
-                report.append(String.format(" -> [预警] 瞬时毛刺严重(%.1fx)。", ratio));
-                if (isContainer) {
-                    report.append("建议检查容器内部是否有突发短查询或频繁GC。\n");
-                } else {
-                    report.append("建议检查宿主机是否有系统级任务或IO等待引起的CPU飙升。\n");
-                }
-            } else {
-                report.append(" -> [正常] 运行平稳。\n");
-            }
-        }
-
-        return report.toString();
-    }*/
-
     public AnalysisResult getAnalysisResult(MetricRecord record) {
     public AnalysisResult getAnalysisResult(MetricRecord record) {
         AnalysisResult res = new AnalysisResult();
         AnalysisResult res = new AnalysisResult();
         // 基础信息设置
         // 基础信息设置
@@ -618,139 +348,6 @@ public class PrometheusService {
         return res;
         return res;
     }
     }
 
 
-    /**
-     * 模拟解析 Prometheus 返回的 JSON 结构
-     * 生产环境建议使用 Jackson 或 Fastjson 遍历 data.result 数组
-     */
-    private Map<String, List<MetricRecord>> parsePrometheusJson(Map<String, String> results) {
-        Map<String, List<MetricRecord>> parsedData = new HashMap<>();
-        results.forEach((taskName, jsonContent) -> {
-            List<MetricRecord> records = new ArrayList<>();
-            try {
-                JsonNode root = objectMapper.readTree(jsonContent);
-                // Prometheus 标准响应路径: data -> result
-                JsonNode resultNodes = root.path("data").path("result");
-
-                if (resultNodes.isArray()) {
-                    for (JsonNode node : resultNodes) {
-                        MetricRecord record = new MetricRecord();
-
-                        // 1. 解析标签 (Metric Labels)
-                        JsonNode metric = node.path("metric");
-                        record.setInstance(metric.path("instance").asText("unknown"));
-                        // container 在 cAdvisor 中通常对应 'name' 标签,宿主机指标则没有此标签
-                        if (metric.has("name")) {
-                            record.setContainer(metric.path("name").asText());
-                        }
-
-                        // 2. 解析数值 (Value)
-                        // Prometheus value 格式为 [timestamp, "value_string"]
-                        JsonNode valueNode = node.path("value");
-                        if (valueNode.isArray() && valueNode.size() >= 2) {
-                            // 注意:Prometheus 返回的数值是字符串形式,需要转换
-                            double val = valueNode.get(1).asDouble();
-
-                            // 根据任务名决定填充到哪个字段(暂时存入,后续 mergeMetrics 会处理)
-                            if (taskName.contains("avg")) {
-                                record.setAvgValue(val);
-                            } else if (taskName.contains("max")) {
-                                record.setMaxValue(val);
-                            }
-                            // 设置指标名称便于识别
-                            record.setName(taskName);
-                        }
-
-                        records.add(record);
-                    }
-                }
-                parsedData.put(taskName, records);
-            } catch (Exception e) {
-                System.err.println("解析任务 [" + taskName + "] 失败: " + e.getMessage());
-                parsedData.put(taskName, Collections.emptyList());
-            }
-        });
-
-        return parsedData;
-    }
-
-    private void parseToHostMap(String json, Map<String, HostInfo> hostMap, String type) throws Exception {
-        JsonNode root = objectMapper.readTree(json);
-        JsonNode resultList = root.path("data").path("result");
-
-        for (JsonNode node : resultList) {
-            // 关键点:提取 IP (例如从 192.168.1.10:9100 提取 192.168.1.10)
-            String rawInstance = node.path("metric").path("instance").asText();
-            String ip = rawInstance.contains(":") ? rawInstance.split(":")[0] : rawInstance;
-
-            double value = node.path("value").get(1).asDouble();
-
-            // 如果 Map 里没有该 IP,则新建
-            HostInfo host = hostMap.computeIfAbsent(ip, k -> {
-                HostInfo h = new HostInfo();
-                h.setName(k);
-                h.setIp(k);
-                return h;
-            });
-
-            // 根据类型赋值
-            switch (type) {
-                case "cpu" -> host.setCpuUsage(formatDouble(value));
-                case "mem" -> host.setMemUsage(formatDouble(value));
-                case "count" -> host.setContainerCount((int) value);
-            }
-        }
-    }
-
-    private List<ContainerInfo> parseTopContainers(String json) throws Exception {
-        List<ContainerInfo> list = new ArrayList<>();
-        JsonNode resultList = objectMapper.readTree(json).path("data").path("result");
-
-        for (JsonNode node : resultList) {
-            ContainerInfo c = new ContainerInfo();
-            c.setName(node.path("metric").path("name").asText());
-            c.setHostIp(node.path("metric").path("instance").asText().split(":")[0]);
-            c.setCpu(formatDouble(node.path("value").get(1).asDouble()));
-            list.add(c);
-        }
-        return list;
-    }
-
-    private double formatDouble(double val) {
-        return Math.round(val * 10.0) / 10.0; // 保留一位小数
-    }
-
-    private void parseTrendData(String json, OperationReportDTO report) {
-        try {
-            JsonNode root = objectMapper.readTree(json);
-            // 趋势数据在 data.result[0].values 中
-            JsonNode valuesNode = root.path("data").path("result").get(0).path("values");
-
-            List<String> labels = new ArrayList<>();
-            List<String> trends = new ArrayList<>();
-            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm");
-
-            for (JsonNode node : valuesNode) {
-                // node 是一个数组: [1672531200, "15.5"]
-                long timestamp = node.get(0).asLong();
-                double value = node.get(1).asDouble();
-
-                // 转换时间戳为 HH:mm 格式
-                String timeLabel = LocalDateTime.ofInstant(Instant.ofEpochSecond(timestamp), ZoneId.systemDefault())
-                        .format(formatter);
-
-                labels.add("'" + timeLabel + "'"); // 加引号是为了符合 JS 数组格式
-                trends.add(String.valueOf(formatDouble(value)));
-            }
-
-            // 将 List 转为逗号分隔的字符串,直接交给 FreeMarker 渲染进 JS 数组
-            report.setTimeLabels(String.join(",", labels));
-            report.setAvgCpuTrend(String.join(",", trends));
-
-        } catch (Exception e) {
-            log.error("趋势数据解析失败", e);
-        }
-    }
-
     /**
     /**
      * 辅助方法:构建 query_range 的完整 URL
      * 辅助方法:构建 query_range 的完整 URL
      */
      */
@@ -1026,8 +623,8 @@ public class PrometheusService {
         model.put("timeLabels", String.join(",", timeLabels));
         model.put("timeLabels", String.join(",", timeLabels));
 
 
         // 6. 渲染与输出
         // 6. 渲染与输出
-        String htmlContent = renderHtml("container_report_v2.ftl", model);
-        Path outputPath = Paths.get("/home/reghao/Downloads", "container_report_v2_" + LocalDate.now() + ".html");
+        String htmlContent = renderHtml("container_report_v1.ftl", model);
+        Path outputPath = Paths.get("/home/reghao/Downloads", "container_report_v1_" + LocalDate.now() + ".html");
         Files.writeString(outputPath, htmlContent, StandardCharsets.UTF_8);
         Files.writeString(outputPath, htmlContent, StandardCharsets.UTF_8);
         System.out.println("✅ 报表生成成功: " + outputPath.toAbsolutePath());
         System.out.println("✅ 报表生成成功: " + outputPath.toAbsolutePath());
     }
     }
@@ -1111,11 +708,51 @@ public class PrometheusService {
         return report;
         return report;
     }
     }
 
 
+    public void generateMemReport() {
+        // 定义查询任务
+        Map<String, String> tasks = Map.of(
+                "node_mem_avg", """
+                        avg_over_time(container_memory_working_set_bytes{name!=""}[24h]) / 1024 / 1024
+                        """,
+                "node_mem_max", """
+                        avg_over_time(container_memory_working_set_bytes{name!=""}[24h]) / 1024 / 1024
+                        """,
+                "container_mem_avg", """
+                        avg_over_time(container_memory_working_set_bytes{name!=""}[24h]) / 1024 / 1024
+                        """,
+                "container_mem_max", """
+                        max_over_time(container_memory_working_set_bytes{name!=""}[24h]) / 1024 /1024
+                        """
+        );
+
+        // 异步执行
+        promClient.fetchAllMetrics(tasks).thenAccept(results -> {
+            processResults0(results);
+        }).join(); // 如果是在定时任务主线程,可以用 join 等待完成
+    }
+
+    private void processResults0(Map<String, String> rawResults) {
+        Map<String, Object> root = new HashMap<>();
+        try {
+            // 6. 渲染最终的复合模板(左右布局那个)
+            String templatePath = "mem_report.ftl";
+            String htmlContent = renderHtml(templatePath, root);
+            Path outputPath = Paths.get("/home/reghao/Downloads", "mem_report_" + LocalDate.now() + ".html");
+            if (Files.notExists(outputPath.getParent())) {
+                Files.createDirectories(outputPath.getParent());
+            }
+            Files.writeString(outputPath, htmlContent, StandardCharsets.UTF_8);
+            System.out.println("✅ 报表已成功保存至: " + outputPath.toAbsolutePath());
+        } catch (Exception e) {
+            log.error("{}", e.getMessage());
+        }
+    }
+
     public static void main(String[] args) throws Exception {
     public static void main(String[] args) throws Exception {
         PrometheusService prometheusService = new PrometheusService();
         PrometheusService prometheusService = new PrometheusService();
         prometheusService.generateContainerReport1();
         prometheusService.generateContainerReport1();
         prometheusService.generateContainerReport();
         prometheusService.generateContainerReport();
         prometheusService.generatePillarReport();
         prometheusService.generatePillarReport();
-        //prometheusService.generateDailyReport();
+        prometheusService.generateDailyReport();
     }
     }
 }
 }

+ 0 - 28
mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/dto/ContainerHealthReport.java

@@ -1,28 +0,0 @@
-package cn.reghao.devops.mgr.ops.srv.mon.dto;
-
-import lombok.Builder;
-import lombok.Data;
-
-/**
- * @author reghao
- * @date 2026-03-30 15:59:53
- */
-@Data
-@Builder
-public class ContainerHealthReport {
-    private String containerName;
-    private String instanceIp;
-
-    // CPU 维度
-    private Double cpuAvg;
-    private Double cpuMax;
-    private Double cpuJitterScore; // 变异系数
-    private String cpuStatus;      // 正常 / 频繁抖动 / 长期高负载
-
-    // 内存维度
-    private Double memStart;       // 24h前数值
-    private Double memEnd;         // 当前数值
-    private Double memMax;
-    private Double memGrowthRate;  // (End - Start) / Start
-    private String memStatus;      // 正常 / 疑似泄漏 / 锯齿形抖动 (GC)
-}

+ 0 - 43
mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/dto/ContainerUsageDTO.java

@@ -1,43 +0,0 @@
-package cn.reghao.devops.mgr.ops.srv.mon.dto;
-
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-/**
- * @author reghao
- * @date 2026-03-30 10:41:39
- */
-@Data
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class ContainerUsageDTO {
-    private String containerName;  // 容器名称 (name 标签)
-    private String nodeIp;         // 宿主机 IP (instance 标签)
-
-    // CPU 使用率相关
-    private Double cpuUsage;       // 当前 CPU 使用率 (百分比,如 15.42)
-
-    // CPU 限流相关
-    private Double throttleRatio;  // 限流时间占比 (如 1.62)
-    private String throttleStatus; // 状态描述:正常、警告、严重
-
-    /**
-     * 获取格式化的限流百分比字符串
-     */
-    public String getThrottlePercent() {
-        if (throttleRatio == null) return "0%";
-        return Math.round(throttleRatio * 100) + "%";
-    }
-
-    /**
-     * 根据限流比例自动判定状态颜色
-     */
-    public String getStatusColor() {
-        if (throttleRatio == null || throttleRatio < 0.1) return "#52c41a"; // 绿色
-        if (throttleRatio < 0.5) return "#fa8c16"; // 橙色
-        return "#f5222d"; // 红色
-    }
-}

+ 1 - 1
mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/ContainerInfo.java → mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/model/ContainerInfo.java

@@ -1,4 +1,4 @@
-package cn.reghao.devops.mgr.ops.srv.mon;
+package cn.reghao.devops.mgr.ops.srv.mon.model;
 
 
 import lombok.Data;
 import lombok.Data;
 
 

+ 1 - 1
mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/ContainerReportVO.java → mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/model/ContainerReportVO.java

@@ -1,4 +1,4 @@
-package cn.reghao.devops.mgr.ops.srv.mon;
+package cn.reghao.devops.mgr.ops.srv.mon.model;
 
 
 import lombok.Data;
 import lombok.Data;
 
 

+ 1 - 1
mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/HostData.java → mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/model/HostData.java

@@ -1,4 +1,4 @@
-package cn.reghao.devops.mgr.ops.srv.mon;
+package cn.reghao.devops.mgr.ops.srv.mon.model;
 
 
 import lombok.Data;
 import lombok.Data;
 
 

+ 1 - 1
mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/HostInfo.java → mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/model/HostInfo.java

@@ -1,4 +1,4 @@
-package cn.reghao.devops.mgr.ops.srv.mon;
+package cn.reghao.devops.mgr.ops.srv.mon.model;
 
 
 import lombok.Data;
 import lombok.Data;
 
 

+ 1 - 1
mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/MetricGroup.java → mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/model/MetricGroup.java

@@ -1,4 +1,4 @@
-package cn.reghao.devops.mgr.ops.srv.mon;
+package cn.reghao.devops.mgr.ops.srv.mon.model;
 
 
 import lombok.Data;
 import lombok.Data;
 
 

+ 1 - 1
mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/PillarReportDTO.java → mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/model/PillarReportDTO.java

@@ -1,4 +1,4 @@
-package cn.reghao.devops.mgr.ops.srv.mon;
+package cn.reghao.devops.mgr.ops.srv.mon.model;
 
 
 import lombok.Data;
 import lombok.Data;
 
 

+ 0 - 64
mgr/src/main/resources/templates/container.ftl

@@ -1,64 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-<meta charset="UTF-8">
-    <title>Container CPU Report</title>
-    <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
-    <style>
-        body { font-family: sans-serif; padding: 20px; background-color: #f4f7f6; }
-.card { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
-.chart-container { width: 100%; height: 450px; }
-h2 { color: #2c3e50; border-left: 5px solid #3498db; padding-left: 15px; }
-.card {
-background: #fff;
-border: 1px solid #e8e8e8;
-border-radius: 4px;
-padding: 15px;
-page-break-inside: avoid;
-}
-</style>
-</head>
-<body>
-
-<#-- 遍历每一个节点 -->
-<#list groupedMap?keys as instance>
-<div class="card" style="margin-bottom: 30px;">
-    <h2 style="color: #1890ff; border-bottom: 1px solid #eee; padding-bottom: 10px;">
-        🖥️ 节点实例: ${instance}
-    </h2>
-    <div id="chart_${instance?replace('.', '_')}" style="width: 100%; height: 350px;"></div>
-</div>
-
-<script type="text/javascript">
-(function() {
-var chartDom = document.getElementById('chart_${instance?replace('.', '_')}');
-    var myChart = echarts.init(chartDom);
-    var option = {
-animation: false,
-title: { text: '容器 CPU 消耗趋势 (%)', left: 'center', textStyle: {fontSize: 12} },
-        tooltip: { trigger: 'axis', confine: true },
-        legend: { type: 'scroll', bottom: 0 },
-        grid: { top: 40, left: '3%', right: '4%', bottom: '15%', containLabel: true },
-        xAxis: { type: 'category', boundaryGap: false, data: [${timeLabels}] },
-        yAxis: { type: 'value', min: 0, axisLabel: { formatter: '{value}%' } },
-        series: [
-            <#-- 遍历该节点下的所有容器 -->
-            <#assign containerMap = groupedMap[instance]>
-            <#list containerMap?keys as cName>
-            {
-name: '${cName}',
-                type: 'line',
-                smooth: true,
-                symbol: 'none',
-                data: [${containerMap[cName]?join(",")}]
-            }<#if cName_has_next>,</#if>
-            </#list>
-        ]
-    };
-    myChart.setOption(option);
-})();
-</script>
-</#list>
-
-</body>
-</html>

+ 0 - 62
mgr/src/main/resources/templates/container_mem.ftl

@@ -1,62 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-<meta charset="UTF-8">
-    <title>Container CPU Report</title>
-    <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
-    <style>
-        body { font-family: sans-serif; padding: 20px; background-color: #f4f7f6; }
-.card { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
-.chart-container { width: 100%; height: 450px; }
-h2 { color: #2c3e50; border-left: 5px solid #3498db; padding-left: 15px; }
-.card {
-background: #fff;
-border: 1px solid #e8e8e8;
-border-radius: 4px;
-padding: 15px;
-page-break-inside: avoid;
-}
-</style>
-</head>
-
-<body>
-<#list memGroupedMap?keys as instance>
-<div class="card" style="margin-top: 20px;">
-    <h2 style="color: #52c41a; border-bottom: 1px solid #eee; padding-bottom: 10px;">
-        内存监控 - 节点: ${instance}
-    </h2>
-    <div id="mem_chart_${instance?replace('.', '_')}" style="width: 100%; height: 350px;"></div>
-</div>
-
-<script type="text/javascript">
-(function() {
-var chartDom = document.getElementById('mem_chart_${instance?replace('.', '_')}');
-    var myChart = echarts.init(chartDom);
-    var option = {
-animation: false,
-title: { text: '容器内存占用 (MB)', left: 'center', textStyle: {fontSize: 12} },
-        tooltip: { trigger: 'axis', confine: true },
-        legend: { type: 'scroll', bottom: 0 },
-        grid: { top: 40, left: '3%', right: '4%', bottom: '15%', containLabel: true },
-        xAxis: { type: 'category', data: [${timeLabels}] },
-        yAxis: { type: 'value', axisLabel: { formatter: '{value} MB' } },
-        series: [
-            <#assign containerMap = memGroupedMap[instance]>
-            <#list containerMap?keys as cName>
-            {
-name: '${cName}',
-                type: 'line',
-                smooth: true,
-                symbol: 'none',
-                data: [${containerMap[cName]?join(",")}]
-            }<#if cName_has_next>,</#if>
-            </#list>
-        ]
-    };
-    myChart.setOption(option);
-})();
-</script>
-</#list>
-
-</body>
-</html>

+ 0 - 0
mgr/src/main/resources/templates/container_report_v2.ftl → mgr/src/main/resources/templates/container_report_v1.ftl


+ 0 - 94
mgr/src/main/resources/templates/daily_report.ftl

@@ -1,94 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <meta charset="UTF-8">
-    <title>运维日报</title>
-    <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
-    <style>
-        body { font-family: Arial, sans-serif; background: #f4f7f6; padding: 20px; }
-        .card { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }
-        .host-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; }
-        .host-item { padding: 10px; border: 1px solid #eee; border-radius: 4px; text-align: center; }
-        /* 动态背景色 */
-        .bg-ok { background-color: #f6ffed; border-color: #b7eb8f; }
-        .bg-warn { background-color: #fff7e6; border-color: #ffd591; }
-        .bg-crit { background-color: #fff1f0; border-color: #ffa39e; }
-        table { width: 100%; border-collapse: collapse; }
-        th, td { border: 1px solid #eee; padding: 8px; text-align: left; }
-        th { background: #fafafa; }
-    </style>
-</head>
-<body>
-
-<div class="card">
-    <h2 style="text-align:center;">基础架构运维日报</h2>
-    <p style="text-align:center; color:#888;">周期: ${startTime} 至 ${endTime}</p>
-
-    <h3>🖥️ 宿主机健康度 (10 Nodes)</h3>
-    <div class="host-grid">
-        <#list hostList as host>
-            <div class="host-item
-                <#if host.cpuUsage gt 80 || host.memUsage gt 85>bg-crit
-                <#elseif host.cpuUsage gt 60>bg-warn
-                <#else>bg-ok</#if>">
-                <strong>${host.name}</strong><br/>
-                <small>CPU: ${host.cpuUsage}%</small><br/>
-                <small>MEM: ${host.memUsage}%</small>
-            </div>
-        </#list>
-    </div>
-</div>
-
-<div class="card">
-    <h3>📈 集群 CPU 负载趋势 (24h)</h3>
-    <div id="cpuChart" style="width: 100%; height: 300px;"></div>
-</div>
-
-<div class="card">
-    <h3>🚀 高负载容器 Top 5</h3>
-    <table>
-        <thead>
-            <tr>
-                <th>容器名称</th>
-                <th>所属主机</th>
-                <th>CPU 使用率</th>
-            </tr>
-        </thead>
-        <tbody>
-            <#list topContainers as container>
-            <tr>
-                <td>${container.name}</td>
-                <td>${container.hostIp}</td>
-                <td style="color: <#if container.cpu gt 80>red<#else>black</#if>;">
-                    ${container.cpu}%
-                </td>
-            </tr>
-            </#list>
-        </tbody>
-    </table>
-</div>
-
-<script>
-    // 渲染 ECharts
-    var chartDom = document.getElementById('cpuChart');
-    var myChart = echarts.init(chartDom);
-    var option = {
-        animation: false, // 必须禁用动画,否则 Playwright 截图时可能是空白
-        xAxis: {
-            type: 'category',
-            data: [${timeLabels!""}] // 使用 !"" 防止 null 报错
-        },
-        yAxis: { type: 'value', axisLabel: { formatter: '{value}%' } },
-        series: [{
-            data: [${avgCpuTrend!""}],
-            type: 'line',
-            smooth: true,
-            areaStyle: { color: 'rgba(24, 144, 255, 0.2)' },
-            itemStyle: { color: '#1890ff' }
-        }]
-    };
-    myChart.setOption(option);
-</script>
-
-</body>
-</html>

+ 11 - 0
mgr/src/main/resources/templates/mem_report.ftl

@@ -0,0 +1,11 @@
+<style>
+/* 增加分组样式 */
+.host-group-header { background-color: #f0f7ff !important; font-weight: bold; }
+.container-row { padding-left: 20px; color: #555; }
+.indent { margin-left: 15px; color: #999; }
+</style>
+
+<h2>🚀 运维巡检日报</h2>
+
+<table class="report-table">
+</table>

+ 0 - 0
mgr/src/main/resources/templates/host_group_report.ftl → mgr/src/main/resources/templates/report_v1.ftl