|
|
@@ -1,10 +1,15 @@
|
|
|
package cn.reghao.devops.mgr.ops.srv.mon;
|
|
|
|
|
|
+import cn.reghao.devops.mgr.config.AppProperties;
|
|
|
+import cn.reghao.devops.mgr.ops.srv.mon.dto.ContainerHealthReport;
|
|
|
+import com.fasterxml.jackson.core.JsonProcessingException;
|
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import com.github.benmanes.caffeine.cache.Cache;
|
|
|
import freemarker.template.Configuration;
|
|
|
import freemarker.template.Template;
|
|
|
import freemarker.template.TemplateException;
|
|
|
+import lombok.Data;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
|
|
|
@@ -16,12 +21,10 @@ import java.nio.charset.StandardCharsets;
|
|
|
import java.nio.file.Files;
|
|
|
import java.nio.file.Path;
|
|
|
import java.nio.file.Paths;
|
|
|
-import java.time.Instant;
|
|
|
-import java.time.LocalDate;
|
|
|
-import java.time.LocalDateTime;
|
|
|
-import java.time.ZoneId;
|
|
|
+import java.time.*;
|
|
|
import java.time.format.DateTimeFormatter;
|
|
|
import java.util.*;
|
|
|
+import java.util.concurrent.CompletableFuture;
|
|
|
|
|
|
/**
|
|
|
* @author reghao
|
|
|
@@ -30,41 +33,26 @@ import java.util.*;
|
|
|
@Slf4j
|
|
|
@Service
|
|
|
public class PrometheusService {
|
|
|
- private String baseUrl = "http://prometheus.iquizoo.cn";
|
|
|
private ObjectMapper objectMapper = new ObjectMapper();
|
|
|
- private final PrometheusAsyncClient promClient = new PrometheusAsyncClient(baseUrl);
|
|
|
+ private final PrometheusAsyncClient promClient;
|
|
|
+ private final Cache<String, Object> cache;
|
|
|
|
|
|
- public OperationReportDTO getAggregatedData() {
|
|
|
- // 定义 24 小时范围
|
|
|
- long now = Instant.now().getEpochSecond();
|
|
|
- Map<String, String> tasks = Map.of(
|
|
|
- "node_cpu", "100 - (avg by (instance) (irate(node_cpu_seconds_total{mode='idle'}[5m])) * 100)",
|
|
|
- "node_mem", "(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100",
|
|
|
- "container_count", "count by (instance) (container_last_seen{image!=''})",
|
|
|
- "top_cpu_containers", "topk(5, sum by (name, instance) (rate(container_cpu_usage_seconds_total{image!=''}[5m]) * 100))",
|
|
|
- "cpu_trend", "avg(100 - (irate(node_cpu_seconds_total{mode='idle'}[5m]) * 100))[24h:30m]"
|
|
|
- );
|
|
|
-
|
|
|
- // 异步抓取并解析
|
|
|
- return promClient.fetchAllMetrics(tasks)
|
|
|
- .thenApply(this::processResults) // 这里的 processResults 就是你之前写的 Jackson 解析逻辑
|
|
|
- .join();
|
|
|
+ public PrometheusService(AppProperties appProperties, Cache<String, Object> cache) {
|
|
|
+ this.promClient = new PrometheusAsyncClient(appProperties.getPrometheusBaseUrl());
|
|
|
+ this.cache = cache;
|
|
|
}
|
|
|
|
|
|
public void generateDailyReport() {
|
|
|
// 定义查询任务
|
|
|
Map<String, String> tasks = Map.of(
|
|
|
- "node_cpu", "100 - (avg by (instance) (irate(node_cpu_seconds_total{mode='idle'}[5m])) * 100)",
|
|
|
- "node_mem", "(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100",
|
|
|
"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))",
|
|
|
- "cpu_trend", "avg(100 - (irate(node_cpu_seconds_total{mode='idle'}[5m]) * 100))[24h:30m]"
|
|
|
+ "top_cpu_containers", "topk(5, sum by (name) (rate(container_cpu_usage_seconds_total{image!=''}[5m]) * 100))"
|
|
|
);
|
|
|
|
|
|
// 异步执行
|
|
|
promClient.fetchAllMetrics(tasks).thenAccept(results -> {
|
|
|
// 在这里解析 JSON 并填充到 DTO
|
|
|
- OperationReportDTO operationReportDTO = processResults(results);
|
|
|
+ processResults(results);
|
|
|
System.out.println("所有数据采集完成,开始渲染报表...");
|
|
|
}).join(); // 如果是在定时任务主线程,可以用 join 等待完成
|
|
|
}
|
|
|
@@ -224,35 +212,35 @@ public class PrometheusService {
|
|
|
return FreeMarkerTemplateUtils.processTemplateIntoString(template, root);
|
|
|
}
|
|
|
|
|
|
- public String generateHtmlReport() throws Exception {
|
|
|
- // 1. 获取聚合后的数据 DTO
|
|
|
- OperationReportDTO reportData = getAggregatedData();
|
|
|
- // 2. 准备 FreeMarker 数据模型 (Root Map)
|
|
|
- Map<String, Object> root = new HashMap<>();
|
|
|
- root.put("report", reportData);
|
|
|
- // 这样在模板中可以使用 ${report.startTime}
|
|
|
- // 或者为了匹配你之前的模板写法,直接放入 list 和 trend
|
|
|
- root.put("hostList", reportData.getHostList());
|
|
|
- root.put("topContainers", reportData.getTopContainers());
|
|
|
- root.put("timeLabels", reportData.getTimeLabels());
|
|
|
- root.put("avgCpuTrend", reportData.getAvgCpuTrend());
|
|
|
- root.put("startTime", reportData.getStartTime());
|
|
|
- root.put("endTime", reportData.getEndTime());
|
|
|
- root.put("containerCount", reportData.getContainerCount());
|
|
|
- String templatePath = "daily_report.ftl";
|
|
|
-
|
|
|
- String htmlContent = renderHtml(templatePath, root);
|
|
|
- return htmlContent;
|
|
|
- }
|
|
|
-
|
|
|
/**
|
|
|
- * 获取四大支柱的原始 JSON 数据
|
|
|
+ * 辅助方法:构建 query_range 的完整 URL
|
|
|
*/
|
|
|
- public Map<String, String> fetchFromPrometheus() {
|
|
|
+ private String buildRangeUrl(String query, long start, long end, String step) {
|
|
|
+ return String.format("/api/v1/query_range?query=%s&start=%d&end=%d&step=%s",
|
|
|
+ URLEncoder.encode(query, StandardCharsets.UTF_8),
|
|
|
+ start,
|
|
|
+ end,
|
|
|
+ step);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void generatePillarReport() throws Exception {
|
|
|
// 1. 计算时间范围:昨天 00:00:00 到 23:59:59
|
|
|
// 也可以根据需求改为:当前时间向前推 24 小时
|
|
|
long end = Instant.now().getEpochSecond();
|
|
|
long start = end - (24 * 3600);
|
|
|
+ // 1. 获取今天的凌晨 03:00:00 (基于系统默认时区)
|
|
|
+ ZonedDateTime today3AM = LocalDate.now()
|
|
|
+ .atTime(3, 0, 0)
|
|
|
+ .atZone(ZoneId.systemDefault());
|
|
|
+ // 2. 如果当前时间还没到 3 点,LocalDate.now() 拿到的 3 点其实是“未来”,
|
|
|
+ // 为了保证逻辑稳健(拿已经过去的完整 24h),可以加个判断:
|
|
|
+ if (ZonedDateTime.now().isBefore(today3AM)) {
|
|
|
+ today3AM = today3AM.minusDays(1);
|
|
|
+ }
|
|
|
+ // 3. 计算时间戳
|
|
|
+ end = today3AM.toEpochSecond(); // 今天凌晨 03:00:00
|
|
|
+ start = end - (24 * 3600); // 昨天凌晨 03:00:00
|
|
|
+
|
|
|
String step = "30m"; // 30分钟一个采样点,适合 24h 趋势图
|
|
|
|
|
|
// 2. 定义 PromQL 查询语句
|
|
|
@@ -269,58 +257,14 @@ public class PrometheusService {
|
|
|
"disk", buildRangeUrl(diskQuery, start, end, step),
|
|
|
"net", buildRangeUrl(netQuery, start, end, step)
|
|
|
);
|
|
|
-
|
|
|
log.info("开始并行抓取 Prometheus 四大支柱数据...");
|
|
|
|
|
|
// 4. 并行执行并阻塞等待结果(join)
|
|
|
- return promClient.fetchAllMetrics0(tasks).join();
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 辅助方法:构建 query_range 的完整 URL
|
|
|
- */
|
|
|
- private String buildRangeUrl(String query, long start, long end, String step) {
|
|
|
- return String.format("/api/v1/query_range?query=%s&start=%d&end=%d&step=%s",
|
|
|
- URLEncoder.encode(query, StandardCharsets.UTF_8),
|
|
|
- start,
|
|
|
- end,
|
|
|
- step);
|
|
|
- }
|
|
|
-
|
|
|
- private String buildRangeUrl1(String query, long start, long end, String step) {
|
|
|
- String encodedQuery = URLEncoder.encode(query, StandardCharsets.UTF_8);
|
|
|
- return String.format("/api/v1/query_range?query=%s&start=%d&end=%d&step=%s",
|
|
|
- encodedQuery, start, end, step);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 构建 query_range 完整的 URL
|
|
|
- * @param query PromQL 语句
|
|
|
- * @param hours 查询过去多少小时的数据(如 24)
|
|
|
- * @param step 采样步长(如 "30m", "15m")
|
|
|
- */
|
|
|
- public String buildRangeUrl2(String query, int hours, String step) {
|
|
|
- // 1. 获取当前时间戳(秒)作为结束时间
|
|
|
- long end = Instant.now().getEpochSecond();
|
|
|
- // 2. 计算开始时间
|
|
|
- long start = end - (hours * 3600L);
|
|
|
-
|
|
|
- // 3. 对 PromQL 进行 URL 编码,防止特殊字符(如 { } [ ] +)导致请求失败
|
|
|
- String encodedQuery = URLEncoder.encode(query, StandardCharsets.UTF_8);
|
|
|
-
|
|
|
- // 4. 拼装 Prometheus 标准 API 格式
|
|
|
- return String.format("/api/v1/query_range?query=%s&start=%d&end=%d&step=%s",
|
|
|
- encodedQuery, start, end, step);
|
|
|
- }
|
|
|
+ Map<String, String> rawResults = promClient.fetchAllMetrics0(tasks).join();
|
|
|
|
|
|
- public PillarReportDTO generatePillarReport() throws Exception {
|
|
|
- // 假设 rawResults 是通过 PrometheusAsyncClient 拿到的 Map<String, String>
|
|
|
- Map<String, String> rawResults = fetchFromPrometheus();
|
|
|
PillarReportDTO dto = new PillarReportDTO();
|
|
|
-
|
|
|
// 设置基础信息
|
|
|
dto.setReportDate(LocalDate.now().minusDays(1).toString());
|
|
|
-
|
|
|
// 解析四大指标
|
|
|
dto.setCpuSeries(parseMatrix(rawResults.get("cpu"), true));
|
|
|
dto.setMemSeries(parseMatrix(rawResults.get("mem"), true));
|
|
|
@@ -330,7 +274,20 @@ public class PrometheusService {
|
|
|
// 提取 X 轴标签(取任意一个结果的 values 即可)
|
|
|
dto.setTimeLabels(extractTimeLabels(rawResults.get("cpu")));
|
|
|
|
|
|
- return dto;
|
|
|
+ // 1. 准备数据模型 (Root Map)
|
|
|
+ // 在模板中可以通过 ${report.reportDate} 或直接 ${reportDate} 访问
|
|
|
+ Map<String, Object> model = new HashMap<>();
|
|
|
+ model.put("report", dto);
|
|
|
+ String templatePath = "pillar_report.ftl";
|
|
|
+ // 3. 渲染 HTML (FreeMarker)
|
|
|
+ String htmlContent = renderHtml(templatePath, model);
|
|
|
+
|
|
|
+ Path outputPath = Paths.get("/home/reghao/Downloads", "pillar_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());
|
|
|
}
|
|
|
|
|
|
private String extractTimeLabels(String json) throws Exception {
|
|
|
@@ -362,35 +319,367 @@ public class PrometheusService {
|
|
|
return map;
|
|
|
}
|
|
|
|
|
|
- public String executeFullProcess() throws Exception {
|
|
|
- PillarReportDTO dto = generatePillarReport();
|
|
|
- // 3. 渲染 HTML (FreeMarker)
|
|
|
- String html = generateHtml(dto);
|
|
|
- return html;
|
|
|
+ private List<String> getCpuTimeLabels(String usageJson) throws JsonProcessingException {
|
|
|
+ List<String> timeLabels = new ArrayList<>();
|
|
|
+ JsonNode results = objectMapper.readTree(usageJson).path("data").path("result");
|
|
|
+ boolean labelsExtracted = false;
|
|
|
+ for (JsonNode res : results) {
|
|
|
+ JsonNode valuesNode = res.path("values");
|
|
|
+ for (JsonNode v : valuesNode) {
|
|
|
+ if (!labelsExtracted) {
|
|
|
+ String timeLabel = Instant.ofEpochSecond(v.get(0).asLong())
|
|
|
+ .atZone(ZoneId.systemDefault())
|
|
|
+ .format(DateTimeFormatter.ofPattern("HH:mm"));
|
|
|
+ timeLabels.add("'" + timeLabel + "'");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ labelsExtracted = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return timeLabels;
|
|
|
}
|
|
|
|
|
|
- public String generateHtml(PillarReportDTO dto) throws Exception {
|
|
|
- // 1. 准备数据模型 (Root Map)
|
|
|
- // 在模板中可以通过 ${report.reportDate} 或直接 ${reportDate} 访问
|
|
|
+ private Map<String, Map<String, List<Double>>> parseCpuJson(String usageJson) throws JsonProcessingException {
|
|
|
+ // 结构:Instance -> (ContainerName -> List<Double>)
|
|
|
+ Map<String, Map<String, List<Double>>> groupedMap = new TreeMap<>();
|
|
|
+ List<String> timeLabels = new ArrayList<>();
|
|
|
+ JsonNode results = objectMapper.readTree(usageJson).path("data").path("result");
|
|
|
+ boolean labelsExtracted = false;
|
|
|
+ for (JsonNode res : results) {
|
|
|
+ String containerName = res.path("metric").path("name").asText();
|
|
|
+ String instance = res.path("metric").path("instance").asText().split(":")[0];
|
|
|
+
|
|
|
+ // 获取或创建该节点的容器 Map
|
|
|
+ Map<String, List<Double>> containerMap = groupedMap.computeIfAbsent(instance, k -> new TreeMap<>());
|
|
|
+
|
|
|
+ List<Double> values = new ArrayList<>();
|
|
|
+ JsonNode valuesNode = res.path("values");
|
|
|
+ for (JsonNode v : valuesNode) {
|
|
|
+ if (!labelsExtracted) {
|
|
|
+ String timeLabel = Instant.ofEpochSecond(v.get(0).asLong())
|
|
|
+ .atZone(ZoneId.systemDefault())
|
|
|
+ .format(DateTimeFormatter.ofPattern("HH:mm"));
|
|
|
+ timeLabels.add("'" + timeLabel + "'");
|
|
|
+ }
|
|
|
+ values.add(Math.round(v.get(1).asDouble() * 100.0) / 100.0);
|
|
|
+ }
|
|
|
+ labelsExtracted = true;
|
|
|
+ containerMap.put(containerName, values);
|
|
|
+ }
|
|
|
+
|
|
|
+ return groupedMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Map<String, List<Double>>> parseMemJson(String usageJson) throws JsonProcessingException {
|
|
|
+ Map<String, Map<String, List<Double>>> groupedMap = new TreeMap<>();
|
|
|
+ JsonNode results = objectMapper.readTree(usageJson).path("data").path("result");
|
|
|
+ for (JsonNode res : results) {
|
|
|
+ String containerName = res.path("metric").path("name").asText();
|
|
|
+ String instance = res.path("metric").path("instance").asText().split(":")[0];
|
|
|
+
|
|
|
+ Map<String, List<Double>> containerMap = groupedMap.computeIfAbsent(instance, k -> new TreeMap<>());
|
|
|
+
|
|
|
+ List<Double> values = new ArrayList<>();
|
|
|
+ for (JsonNode v : res.path("values")) {
|
|
|
+ // 内存数值:MB
|
|
|
+ values.add(Math.round(v.get(1).asDouble() * 100.0) / 100.0);
|
|
|
+ }
|
|
|
+ containerMap.put(containerName, values);
|
|
|
+ }
|
|
|
+
|
|
|
+ return groupedMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void generateContainerReport() throws Exception {
|
|
|
+ // 1. 定义查询语句
|
|
|
+ String cpuQuery = """
|
|
|
+ sum(
|
|
|
+ irate(container_cpu_usage_seconds_total{name!=""}[5m])
|
|
|
+ ) by (name, instance) * 100
|
|
|
+ """;
|
|
|
+ String memQuery = """
|
|
|
+ sum(container_memory_working_set_bytes{name!=''}) by (name, instance) / 1024 / 1024
|
|
|
+ """;
|
|
|
+
|
|
|
+ // 2. 时间范围计算
|
|
|
+ long end = Instant.now().getEpochSecond();
|
|
|
+ long start = end - (24 * 3600);
|
|
|
+ // 1. 获取今天的凌晨 03:00:00 (基于系统默认时区)
|
|
|
+ ZonedDateTime today3AM = LocalDate.now()
|
|
|
+ .atTime(3, 0, 0)
|
|
|
+ .atZone(ZoneId.systemDefault());
|
|
|
+ // 2. 如果当前时间还没到 3 点,LocalDate.now() 拿到的 3 点其实是“未来”,
|
|
|
+ // 为了保证逻辑稳健(拿已经过去的完整 24h),可以加个判断:
|
|
|
+ if (ZonedDateTime.now().isBefore(today3AM)) {
|
|
|
+ today3AM = today3AM.minusDays(1);
|
|
|
+ }
|
|
|
+ // 3. 计算时间戳
|
|
|
+ end = today3AM.toEpochSecond(); // 今天凌晨 03:00:00
|
|
|
+ start = end - (24 * 3600); // 昨天凌晨 03:00:00
|
|
|
+
|
|
|
+ String step = "30m";
|
|
|
+
|
|
|
+ // 3. 并行抓取数据(推荐异步 join,提高效率)
|
|
|
+ CompletableFuture<String> cpuFuture = promClient.queryRange(cpuQuery, start, end, step);
|
|
|
+ CompletableFuture<String> memFuture = promClient.queryRange(memQuery, start, end, step);
|
|
|
+ String cpuJson = cpuFuture.join();
|
|
|
+ String memJson = memFuture.join();
|
|
|
+ // 4. 解析数据
|
|
|
+ List<String> timeLabels = getCpuTimeLabels(cpuJson);
|
|
|
+ // 分别解析 CPU 和 内存 到不同的 GroupedMap
|
|
|
+ Map<String, Map<String, List<Double>>> cpuGroupedMap = parseCpuJson(cpuJson);
|
|
|
+ // 内存解析时复用 CPU 的 timeLabels 即可,不再重复抓取标签
|
|
|
+ Map<String, Map<String, List<Double>>> memGroupedMap = parseMemJson(memJson);
|
|
|
+
|
|
|
+ // 5. 构建统一的 Model
|
|
|
Map<String, Object> model = new HashMap<>();
|
|
|
- model.put("report", dto);
|
|
|
- String templatePath = "pillar_report.ftl";
|
|
|
+ model.put("cpuGroupedMap", cpuGroupedMap);
|
|
|
+ model.put("memGroupedMap", memGroupedMap);
|
|
|
+ model.put("timeLabels", String.join(",", timeLabels));
|
|
|
+
|
|
|
+ // 6. 渲染最终的复合模板(左右布局那个)
|
|
|
+ String templatePath = "container_report.ftl";
|
|
|
+ String htmlContent = renderHtml(templatePath, model);
|
|
|
+ Path outputPath = Paths.get("/home/reghao/Downloads", "container_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());
|
|
|
+ }
|
|
|
|
|
|
- String htmlContent = renderHtml(templatePath, model);
|
|
|
- return htmlContent;
|
|
|
+ public void generateContainerReport1() throws Exception {
|
|
|
+ // 1. 定义查询语句 (修正后的 PromQL)
|
|
|
+ String cpuQuery = "sum(irate(container_cpu_usage_seconds_total{name!=''}[5m])) by (name, instance) * 100";
|
|
|
+ String cpuQueryOld = "sum(irate(container_cpu_usage_seconds_total{name!=''}[5m] offset 1d)) by (name, instance) * 100";
|
|
|
+ String memQuery = "sum(container_memory_working_set_bytes{name!=''}) by (name, instance) / 1024 / 1024";
|
|
|
+ String memQueryOld = "sum(container_memory_working_set_bytes{name!='' } offset 1d) by (name, instance) / 1024 / 1024";
|
|
|
+
|
|
|
+ // 2. 时间范围计算 (今日凌晨 03:00)
|
|
|
+ ZonedDateTime today3AM = LocalDate.now().atTime(3, 0, 0).atZone(ZoneId.systemDefault());
|
|
|
+ if (ZonedDateTime.now().isBefore(today3AM)) today3AM = today3AM.minusDays(1);
|
|
|
+
|
|
|
+ long end = today3AM.toEpochSecond();
|
|
|
+ long start = end - (24 * 3600);
|
|
|
+ String step = "30m";
|
|
|
+
|
|
|
+ // 3. 并行抓取
|
|
|
+ CompletableFuture<String> cpuFuture = promClient.queryRange(cpuQuery, start, end, step);
|
|
|
+ CompletableFuture<String> cpuOldFuture = promClient.queryRange(cpuQueryOld, start, end, step);
|
|
|
+ CompletableFuture<String> memFuture = promClient.queryRange(memQuery, start, end, step);
|
|
|
+ CompletableFuture<String> memOldFuture = promClient.queryRange(memQueryOld, start, end, step);
|
|
|
+
|
|
|
+ CompletableFuture.allOf(cpuFuture, cpuOldFuture, memFuture, memOldFuture).join();
|
|
|
+
|
|
|
+ // 4. 解析数据
|
|
|
+ List<String> timeLabels = getCpuTimeLabels(cpuFuture.get());
|
|
|
+ Map<String, Map<String, List<Double>>> cpuToday = parseCpuJson(cpuFuture.get());
|
|
|
+ Map<String, Map<String, List<Double>>> cpuYesterday = parseCpuJson(cpuOldFuture.get());
|
|
|
+ Map<String, Map<String, List<Double>>> memToday = parseMemJson(memFuture.get());
|
|
|
+ Map<String, Map<String, List<Double>>> memYesterday = parseMemJson(memOldFuture.get());
|
|
|
+
|
|
|
+ // 5. 组装 Model (这里的 Key 必须与 FTL 里的变量名严格一致)
|
|
|
+ Map<String, Object> model = new HashMap<>();
|
|
|
+ model.put("cpuToday", cpuToday);
|
|
|
+ model.put("cpuYesterday", cpuYesterday);
|
|
|
+ model.put("memToday", memToday);
|
|
|
+ model.put("memYesterday", memYesterday);
|
|
|
+ model.put("timeLabels", String.join(",", timeLabels));
|
|
|
+
|
|
|
+ // 6. 渲染与输出
|
|
|
+ /*String htmlContent = renderHtml("container_report_v2.ftl", model);
|
|
|
+ Path outputPath = Paths.get("/home/reghao/Downloads", "container_report_v2_" + LocalDate.now() + ".html");
|
|
|
+ Files.writeString(outputPath, htmlContent, StandardCharsets.UTF_8);
|
|
|
+ System.out.println("✅ 报表生成成功: " + outputPath.toAbsolutePath());*/
|
|
|
+ System.out.println();
|
|
|
}
|
|
|
|
|
|
- public static void main(String[] args) throws Exception {
|
|
|
- PrometheusService prometheusService = new PrometheusService();
|
|
|
- //prometheusService.generateDailyReport();
|
|
|
- String htmlContent = prometheusService.executeFullProcess();
|
|
|
+ public ContainerReportVO getReportData() {
|
|
|
+ String cacheKey = "CONT_REPORT:" + LocalDate.now();
|
|
|
+ // Caffeine 的 get 方法天然支持并发锁,防止击穿
|
|
|
+ Object result = cache.get(cacheKey, key -> {
|
|
|
+ try {
|
|
|
+ return getContainerReportData();
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("Failed to generate report", e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return (ContainerReportVO) result;
|
|
|
+ }
|
|
|
|
|
|
- //Path outputPath = Paths.get("/home/reghao/Downloads", "daily_report_" + LocalDate.now() + ".html");
|
|
|
- Path outputPath = Paths.get("/home/reghao/Downloads", "pillar_report_" + LocalDate.now() + ".html");
|
|
|
+ public ContainerReportVO getContainerReportData() throws Exception {
|
|
|
+ // 1. 定义查询语句 (修正后的 PromQL)
|
|
|
+ String cpuQuery = "sum(irate(container_cpu_usage_seconds_total{name=~'.*-prod'}[5m])) by (name, instance) * 100";
|
|
|
+ String cpuQueryOld = "sum(irate(container_cpu_usage_seconds_total{name=~'.*-prod'}[5m] offset 1d)) by (name, instance) * 100";
|
|
|
+ String memQuery = "sum(container_memory_working_set_bytes{name=~'.*-prod'}) by (name, instance) / 1024 / 1024";
|
|
|
+ String memQueryOld = "sum(container_memory_working_set_bytes{name=~'.*-prod' } offset 1d) by (name, instance) / 1024 / 1024";
|
|
|
+
|
|
|
+ // 2. 时间范围计算 (今日凌晨 03:00)
|
|
|
+ ZonedDateTime today3AM = LocalDate.now().atTime(3, 0, 0).atZone(ZoneId.systemDefault());
|
|
|
+ if (ZonedDateTime.now().isBefore(today3AM)) today3AM = today3AM.minusDays(1);
|
|
|
+
|
|
|
+ long end = today3AM.toEpochSecond();
|
|
|
+ long start = end - (24 * 3600);
|
|
|
+ String step = "30m";
|
|
|
+
|
|
|
+ // 3. 并行抓取
|
|
|
+ CompletableFuture<String> cpuFuture = promClient.queryRange(cpuQuery, start, end, step);
|
|
|
+ CompletableFuture<String> cpuOldFuture = promClient.queryRange(cpuQueryOld, start, end, step);
|
|
|
+ CompletableFuture<String> memFuture = promClient.queryRange(memQuery, start, end, step);
|
|
|
+ CompletableFuture<String> memOldFuture = promClient.queryRange(memQueryOld, start, end, step);
|
|
|
+ CompletableFuture.allOf(cpuFuture, cpuOldFuture, memFuture, memOldFuture).join();
|
|
|
+
|
|
|
+ // 4. 解析原始数据 (假设解析出的结构依然是 Map<Instance, Map<Container, List<Double>>>)
|
|
|
+ Map<String, Map<String, List<Double>>> cpuT = parseCpuJson(cpuFuture.get());
|
|
|
+ Map<String, Map<String, List<Double>>> cpuY = parseCpuJson(cpuOldFuture.get());
|
|
|
+ Map<String, Map<String, List<Double>>> memT = parseMemJson(memFuture.get());
|
|
|
+ Map<String, Map<String, List<Double>>> memY = parseMemJson(memOldFuture.get());
|
|
|
+
|
|
|
+ // 5. 核心:按实例(Instance)维度聚合数据
|
|
|
+ // 获取所有出现过的实例名并去重
|
|
|
+ Set<String> allInstanceNames = new HashSet<>();
|
|
|
+ allInstanceNames.addAll(cpuT.keySet());
|
|
|
+ allInstanceNames.addAll(cpuY.keySet());
|
|
|
+ allInstanceNames.addAll(memT.keySet());
|
|
|
+ allInstanceNames.addAll(memY.keySet());
|
|
|
+
|
|
|
+ List<HostData> instanceList = new ArrayList<>();
|
|
|
+
|
|
|
+ for (String instName : allInstanceNames) {
|
|
|
+ HostData instData = new HostData();
|
|
|
+ instData.setName(instName);
|
|
|
+
|
|
|
+ // 组装 CPU 组
|
|
|
+ MetricGroup cpuGroup = new MetricGroup();
|
|
|
+ cpuGroup.setToday(cpuT.getOrDefault(instName, new HashMap<>()));
|
|
|
+ cpuGroup.setYesterday(cpuY.getOrDefault(instName, new HashMap<>()));
|
|
|
+ instData.setCpu(cpuGroup);
|
|
|
+
|
|
|
+ // 组装内存组
|
|
|
+ MetricGroup memGroup = new MetricGroup();
|
|
|
+ memGroup.setToday(memT.getOrDefault(instName, new HashMap<>()));
|
|
|
+ memGroup.setYesterday(memY.getOrDefault(instName, new HashMap<>()));
|
|
|
+ instData.setMem(memGroup);
|
|
|
+
|
|
|
+ instanceList.add(instData);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. 返回最终结果
|
|
|
+ ContainerReportVO report = new ContainerReportVO();
|
|
|
+ report.setTimeLabels(getCpuTimeLabels(cpuFuture.get()));
|
|
|
+ report.setInstances(instanceList);
|
|
|
+
|
|
|
+ return report;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void detect() throws Exception {
|
|
|
+ // 1. 计算时间范围:昨天 00:00:00 到 23:59:59
|
|
|
+ // 也可以根据需求改为:当前时间向前推 24 小时
|
|
|
+ long end = Instant.now().getEpochSecond();
|
|
|
+ long start = end - (24 * 3600);
|
|
|
+ String step = "5m"; // 30分钟一个采样点,适合 24h 趋势图
|
|
|
+
|
|
|
+ // 2. 定义 PromQL 查询语句
|
|
|
+ String cpuQuery = "sum(irate(container_cpu_usage_seconds_total{name!=\"\"}[5m])) by (name, instance)";
|
|
|
+ String memQuery = "sum(container_memory_working_set_bytes{name!=\"\"}) by (name, instance) / 1024 / 1024";
|
|
|
+ String diskQuery = "max(rate(node_disk_io_time_seconds_total[5m])) by (instance)";
|
|
|
+ String netQuery = "sum(irate(node_network_receive_bytes_total[5m])) by (instance) / 1024 / 1024";
|
|
|
+
|
|
|
+ // 3. 构造异步任务 Map
|
|
|
+ // 注意:这里调用的是 query_range 接口
|
|
|
+ Map<String, String> tasks = Map.of(
|
|
|
+ "cpu", buildRangeUrl(cpuQuery, start, end, step),
|
|
|
+ "mem", buildRangeUrl(memQuery, start, end, step)
|
|
|
+ );
|
|
|
+ log.info("开始并行抓取 Prometheus 四大支柱数据...");
|
|
|
+
|
|
|
+ // 4. 并行执行并阻塞等待结果(join)
|
|
|
+ Map<String, String> rawResults = promClient.fetchAllMetrics0(tasks).join();
|
|
|
+ jitter2(rawResults);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void jitter1(Map<String, String> rawResults) throws Exception {
|
|
|
+ JitterAnalysisService jitterAnalysisService = new JitterAnalysisService();
|
|
|
+ List<ContainerHealthReport> list = jitterAnalysisService.analyzeMetrics(rawResults.get("cpu"), rawResults.get("mem"));
|
|
|
+ Map<String, Object> model = new HashMap<>();
|
|
|
+ // 关键:这里的 key "healthReports" 必须与模板中的 <#list healthReports> 匹配
|
|
|
+ model.put("healthReports", list);
|
|
|
+
|
|
|
+ String templatePath = "risk_dashboard.ftl";
|
|
|
+ String htmlContent = renderHtml(templatePath, model);
|
|
|
+ // 后续可以调用 playwright 截图
|
|
|
+ // screenshotService.capture(htmlContent, "container_report.png");
|
|
|
+
|
|
|
+ Path outputPath = Paths.get("/home/reghao/Downloads", "risk_dashboard_" + LocalDate.now() + ".html");
|
|
|
if (Files.notExists(outputPath.getParent())) {
|
|
|
Files.createDirectories(outputPath.getParent());
|
|
|
}
|
|
|
Files.writeString(outputPath, htmlContent, StandardCharsets.UTF_8);
|
|
|
System.out.println("✅ 报表已成功保存至: " + outputPath.toAbsolutePath());
|
|
|
}
|
|
|
+
|
|
|
+ @Data
|
|
|
+ public class InstanceData {
|
|
|
+ private Map<String, List<Double>> cpuSeries = new TreeMap<>();
|
|
|
+ private Map<String, List<Double>> memSeries = new TreeMap<>();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void parseToMap(String json, Map<String, InstanceData> groupedMap,
|
|
|
+ List<String> timeLabels, boolean isCpu) throws Exception {
|
|
|
+ JsonNode results = objectMapper.readTree(json).path("data").path("result");
|
|
|
+ boolean labelsExtracted = (timeLabels == null);
|
|
|
+
|
|
|
+ for (JsonNode res : results) {
|
|
|
+ String name = res.path("metric").path("name").asText();
|
|
|
+ String instance = res.path("metric").path("instance").asText().split(":")[0];
|
|
|
+
|
|
|
+ InstanceData data = groupedMap.computeIfAbsent(instance, k -> new InstanceData());
|
|
|
+ Map<String, List<Double>> targetSeries = isCpu ? data.getCpuSeries() : data.getMemSeries();
|
|
|
+
|
|
|
+ List<Double> values = new ArrayList<>();
|
|
|
+ for (JsonNode v : res.path("values")) {
|
|
|
+ if (!labelsExtracted) {
|
|
|
+ String time = Instant.ofEpochSecond(v.get(0).asLong())
|
|
|
+ .atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("HH:mm"));
|
|
|
+ timeLabels.add("'" + time + "'");
|
|
|
+ }
|
|
|
+ values.add(Math.round(v.get(1).asDouble() * 100.0) / 100.0);
|
|
|
+ }
|
|
|
+ labelsExtracted = true;
|
|
|
+ targetSeries.put(name, values);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void jitter2(Map<String, String> rawResults) throws Exception {
|
|
|
+ Map<String, InstanceData> groupedMap = new TreeMap<>();
|
|
|
+ List<String> timeLabels = new ArrayList<>();
|
|
|
+
|
|
|
+ // 1. 解析 CPU 数据
|
|
|
+ parseToMap(rawResults.get("cpu"), groupedMap, timeLabels, true);
|
|
|
+ // 2. 解析内存数据 (不再重复提取 timeLabels)
|
|
|
+ parseToMap(rawResults.get("mem"), groupedMap, null, false);
|
|
|
+
|
|
|
+ Map<String, Object> model = new HashMap<>();
|
|
|
+ model.put("groupedMap", groupedMap);
|
|
|
+ model.put("timeLabels", String.join(",", timeLabels));
|
|
|
+
|
|
|
+ String templatePath = "jitter.ftl";
|
|
|
+ String htmlContent = renderHtml(templatePath, model);
|
|
|
+ Path outputPath = Paths.get("/home/reghao/Downloads", "jitter_" + LocalDate.now() + ".html");
|
|
|
+ if (Files.notExists(outputPath.getParent())) {
|
|
|
+ Files.createDirectories(outputPath.getParent());
|
|
|
+ }
|
|
|
+ Files.writeString(outputPath, htmlContent, StandardCharsets.UTF_8);
|
|
|
+ System.out.println("✅ 报表已成功保存至: " + outputPath.toAbsolutePath());
|
|
|
+ }
|
|
|
+
|
|
|
+ public static void main(String[] args) throws Exception {
|
|
|
+ //PrometheusService prometheusService = new PrometheusService();
|
|
|
+ //prometheusService.generateContainerReport1();
|
|
|
+ //prometheusService.getContainerReportData();
|
|
|
+ //prometheusService.generatePillarReport();
|
|
|
+ //prometheusService.generateDailyReport();
|
|
|
+ //prometheusService.detect();
|
|
|
+ }
|
|
|
}
|