Jelajahi Sumber

update PrometheusService

reghao 21 jam lalu
induk
melakukan
7dcde1c452

+ 59 - 3
mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/PrometheusAsyncClient.java

@@ -7,7 +7,9 @@ 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.time.Duration;
+import java.time.Instant;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
@@ -46,10 +48,9 @@ public class PrometheusAsyncClient {
                         .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
     }
 
-    private CompletableFuture<Map.Entry<String, String>> fetchSingleMetric(String alias, String query) {
-        String encodedUrl0 = prometheusBaseUrl + "/api/v1/query?query=" +
+    public CompletableFuture<Map.Entry<String, String>> fetchSingleMetric(String alias, String query) {
+        String encodedUrl = prometheusBaseUrl + "/api/v1/query?query=" +
                 URLEncoder.encode(query, java.nio.charset.StandardCharsets.UTF_8);
-        String encodedUrl = prometheusBaseUrl + URLEncoder.encode(query, java.nio.charset.StandardCharsets.UTF_8);
 
         HttpRequest request = HttpRequest.newBuilder()
                 .uri(URI.create(encodedUrl))
@@ -110,4 +111,59 @@ public class PrometheusAsyncClient {
                     return Map.entry(entry.getKey(), "{}");
                 });
     }
+
+    public CompletableFuture<String> query(String promql) {
+        String promql0 = URLEncoder.encode(promql, StandardCharsets.UTF_8);
+        double time = System.currentTimeMillis() / 1000.0;
+        String url = String.format("%s/api/v1/query?query=%s&time=%s", prometheusBaseUrl, promql0, time);
+
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create(url))
+                .GET()
+                .build();
+        return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
+                .thenApply(response -> {
+                    if (response.statusCode() != 200) {
+                        throw new RuntimeException("Prometheus error: " + response.body());
+                    }
+                    return response.body();
+                });
+    }
+
+    public CompletableFuture<String> queryRange(String promql, long start, long end, String step) {
+        String promql0 = URLEncoder.encode(promql, StandardCharsets.UTF_8);
+        double time = System.currentTimeMillis() / 1000.0;
+        String url1 = String.format("%s/api/v1/query?query=%s&time=%s", prometheusBaseUrl, promql0, time);
+        String url = String.format("%s/api/v1/query_range?query=%s&start=%d&end=%d&step=%s",
+                prometheusBaseUrl, promql0, start, end, step);
+
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create(url))
+                .GET()
+                .build();
+        return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
+                .thenApply(response -> {
+                    if (response.statusCode() != 200) {
+                        throw new RuntimeException("Prometheus error: " + response.body());
+                    }
+                    return response.body();
+                });
+    }
+
+    public static void main(String[] args) {
+        String baseUrl = "http://prometheus.iquizoo.cn";
+        PrometheusAsyncClient client = new PrometheusAsyncClient(baseUrl);
+        String promql = "topk(5, increase(container_cpu_cfs_throttled_periods_total{name!=''}[24h]))";
+        //String respBody = client.query(promql).join();
+
+        String cpuQuery = "1 - avg(irate(node_cpu_seconds_total{mode='idle'}[5m])) by (instance)";
+        // 1. 计算时间范围:昨天 00:00:00 到 23:59:59
+        // 也可以根据需求改为:当前时间向前推 24 小时
+        long end = Instant.now().getEpochSecond();
+        long start = end - (24 * 3600);
+        // 30分钟一个采样点,适合 24h 趋势图
+        String step = "30m";
+        String respBody1 = client.queryRange(cpuQuery, start, end, step).join();
+        System.out.println();
+    }
 }

+ 27 - 30
mgr/src/main/java/cn/reghao/devops/mgr/ops/srv/mon/PrometheusService.java

@@ -2,12 +2,15 @@ package cn.reghao.devops.mgr.ops.srv.mon;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import freemarker.template.Configuration;
 import freemarker.template.Template;
+import freemarker.template.TemplateException;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
 import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
 
+import java.io.IOException;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
@@ -27,7 +30,7 @@ import java.util.*;
 @Slf4j
 @Service
 public class PrometheusService {
-    private String baseUrl = "http://prometheus.reghao.cn";
+    private String baseUrl = "http://prometheus.iquizoo.cn";
     private ObjectMapper objectMapper = new ObjectMapper();
     private final PrometheusAsyncClient promClient = new PrometheusAsyncClient(baseUrl);
 
@@ -190,15 +193,12 @@ public class PrometheusService {
         }
     }
 
-    public static FreeMarkerConfigurer createConfigurer() {
+    public static Configuration getTemplateConfiguration() throws TemplateException, IOException {
         FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
-
         // 1. 设置模板存放路径 (通常在 resources/templates 下)
         configurer.setTemplateLoaderPath("classpath:/templates/");
-
         // 2. 设置默认编码
         configurer.setDefaultEncoding("UTF-8");
-
         // 3. 配置 FreeMarker 的原生属性
         Properties settings = new Properties();
         settings.setProperty("template_update_delay", "0"); // 检查模板更新延迟
@@ -206,24 +206,27 @@ public class PrometheusService {
         settings.setProperty("number_format", "0.##");      // 数字格式化,防止 1000 变 1,000
         settings.setProperty("datetime_format", "yyyy-MM-dd HH:mm:ss");
         configurer.setFreemarkerSettings(settings);
-
-        try {
-            // 重要:必须调用此方法来初始化内部的 Configuration 对象
-            configurer.afterPropertiesSet();
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-
-        return configurer;
+        // 重要:必须调用此方法来初始化内部的 Configuration 对象
+        configurer.afterPropertiesSet();
+        return configurer.getConfiguration();
     }
 
     /**
-     * 生成最终的 HTML 字符串
+     * @param templatePath 相对于 src/main/resources/templates/ 的路径
+     * @return
+     * @date 2026-03-29 00:03:145
      */
+    public String renderHtml(String templatePath, Map<String, Object> root) throws Exception {
+        // 2. 加载模板文件
+        // 默认路径:src/main/resources/templates/pillar_report.ftl
+        Template template = getTemplateConfiguration().getTemplate(templatePath);
+        // 渲染并返回 HTML 字符串, FreeMarkerTemplateUtils 会自动处理异常并转换为 String
+        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);
@@ -236,11 +239,10 @@ public class PrometheusService {
         root.put("startTime", reportData.getStartTime());
         root.put("endTime", reportData.getEndTime());
         root.put("containerCount", reportData.getContainerCount());
+        String templatePath = "daily_report.ftl";
 
-        // 3. 加载模板 (确保文件位于 src/main/resources/templates/daily_report.ftl)
-        Template template = createConfigurer().getConfiguration().getTemplate("daily_report.ftl");
-        // 4. 合并数据与模板生成字符串
-        return FreeMarkerTemplateUtils.processTemplateIntoString(template, root);
+        String htmlContent = renderHtml(templatePath, root);
+        return htmlContent;
     }
 
     /**
@@ -249,9 +251,8 @@ public class PrometheusService {
     public Map<String, String> fetchFromPrometheus() {
         // 1. 计算时间范围:昨天 00:00:00 到 23:59:59
         // 也可以根据需求改为:当前时间向前推 24 小时
-        long now = Instant.now().getEpochSecond();
-        long start = now - (24 * 3600);
-        long end = now;
+        long end = Instant.now().getEpochSecond();
+        long start = end - (24 * 3600);
         String step = "30m"; // 30分钟一个采样点,适合 24h 趋势图
 
         // 2. 定义 PromQL 查询语句
@@ -373,14 +374,10 @@ public class PrometheusService {
         // 在模板中可以通过 ${report.reportDate} 或直接 ${reportDate} 访问
         Map<String, Object> model = new HashMap<>();
         model.put("report", dto);
+        String templatePath = "pillar_report.ftl";
 
-        // 2. 加载模板文件
-        // 默认路径:src/main/resources/templates/pillar_report.ftl
-        Template template = createConfigurer().getConfiguration().getTemplate("pillar_report.ftl");
-
-        // 3. 执行渲染并返回 HTML 字符串
-        // FreeMarkerTemplateUtils 会自动处理异常并转换为 String
-        return FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
+        String htmlContent = renderHtml(templatePath,  model);
+        return htmlContent;
     }
 
     public static void main(String[] args) throws Exception {

+ 64 - 20
mgr/src/main/resources/templates/pillar_report.ftl

@@ -40,60 +40,104 @@
 </div>
 
 <script>
-    // 通用配置生成逻辑
-    const commonOption = (title, unit, threshold) => ({
-        animation: false, // 禁用动画确保截图完整
-        tooltip: { trigger: 'axis' },
-        legend: { bottom: 0, type: 'scroll', itemWidth: 10, textStyle: { fontSize: 10 } },
-        grid: { top: 40, left: '3%', right: '4%', bottom: '15%', containLabel: true },
+    // 基础配置保持不变...
+    const commonOption = (title, unit) => ({
+        animation: false,
+        tooltip: { trigger: 'axis', confine: true },
+        legend: { bottom: 0, type: 'scroll', itemWidth: 10 },
+        grid: { top: 40, left: '5%', right: '5%', bottom: '15%', containLabel: true },
         xAxis: { type: 'category', boundaryGap: false, data: [${report.timeLabels}] },
         yAxis: { type: 'value', axisLabel: { formatter: '{value}' + unit } }
     });
 
-    // 1. CPU Chart
+    // 辅助函数:生成水位线配置
+    const getWatermarkLines = (warn, crit) => ({
+        symbol: ['none', 'none'], // 不显示箭头
+        silent: true,            // 鼠标悬停不触发事件
+        data: [
+            {
+                yAxis: warn,
+                name: '70% Warning',
+                lineStyle: { color: '#fa8c16', type: 'dashed', width: 1, opacity: 0.6 },
+                label: { position: 'end', formatter: warn + '%', fontSize: 10, color: '#fa8c16' }
+            },
+            {
+                yAxis: crit,
+                name: '80% Critical',
+                lineStyle: { color: '#ff4d4f', type: 'dashed', width: 1.5, opacity: 0.8 },
+                label: { position: 'end', formatter: crit + '%', fontSize: 10, color: '#ff4d4f' }
+            }
+        ]
+    });
+
+    // --- CPU Chart ---
     const cpuChart = echarts.init(document.getElementById('cpuChart'));
     cpuChart.setOption({
-        ...commonOption('CPU', '%'),
+        ...commonOption('CPU Usage', '%'),
         series: [
             <#list report.cpuSeries?keys as ip>
-            { name: '${ip}', type: 'line', smooth: true, symbol: 'none', data: [${report.cpuSeries[ip]?join(",")}] }<#if ip_has_next>,</#if>
+            {
+                name: '${ip}',
+                type: 'line',
+                smooth: true,
+                symbol: 'none',
+                lineStyle: { width: 1.2 },
+                data: [${report.cpuSeries[ip]?join(",")}],
+                <#if ip_index == 0> // 仅在第一条曲线上绘制水位线,避免重复绘制渲染开销
+                markLine: getWatermarkLines(70, 80)
+                </#if>
+            }<#if ip_has_next>,</#if>
             </#list>
-        ],
-        visualMap: { show: false, pieces: [{ gt: 0, lte: 80, color: '#1890ff' }, { gt: 80, color: '#ff4d4f' }] }
+        ]
     });
 
-    // 2. Memory Chart
+    // --- Memory Chart ---
     const memChart = echarts.init(document.getElementById('memChart'));
     memChart.setOption({
-        ...commonOption('Memory', '%'),
+        ...commonOption('Memory Usage', '%'),
         series: [
             <#list report.memSeries?keys as ip>
-            { name: '${ip}', type: 'line', smooth: true, symbol: 'none', data: [${report.memSeries[ip]?join(",")}] }<#if ip_has_next>,</#if>
+            {
+                name: '${ip}',
+                type: 'line',
+                smooth: true,
+                symbol: 'none',
+                data: [${report.memSeries[ip]?join(",")}],
+                <#if ip_index == 0>
+                markLine: getWatermarkLines(70, 85) // 内存可以稍微高一点,设为 70/85
+                </#if>
+            }<#if ip_has_next>,</#if>
             </#list>
         ]
     });
 
-    // 3. Disk Chart (包含 90% 警戒线)
+    // --- Disk Chart ---
     const diskChart = echarts.init(document.getElementById('diskChart'));
     diskChart.setOption({
-        ...commonOption('Disk', '%'),
+        ...commonOption('Disk I/O', '%'),
         series: [
             <#list report.diskSeries?keys as ip>
             {
-                name: '${ip}', type: 'line', smooth: true, symbol: 'none', data: [${report.diskSeries[ip]?join(",")}],
-                markLine: { symbol: 'none', data: [{ yAxis: 90, lineStyle: { color: 'red', type: 'dashed' } }] }
+                name: '${ip}',
+                type: 'line',
+                smooth: true,
+                symbol: 'none',
+                data: [${report.diskSeries[ip]?join(",")}],
+                <#if ip_index == 0>
+                markLine: getWatermarkLines(80, 90) // 磁盘水位线通常设为 80/90
+                </#if>
             }<#if ip_has_next>,</#if>
             </#list>
         ]
     });
 
-    // 4. Network Chart
+    // --- Network Chart (通常不设水位线,除非有固定带宽限制) ---
     const netChart = echarts.init(document.getElementById('netChart'));
     netChart.setOption({
         ...commonOption('Network', 'MB/s'),
         series: [
             <#list report.netSeries?keys as ip>
-            { name: '${ip}', type: 'line', smooth: true, symbol: 'none', data: [${report.netSeries[ip]?join(",")}] }<#if ip_has_next>,</#if>
+            { name: '${ip}', type: 'line', smooth: true, symbol: 'none', data: [${report.netSeries[ip]?join(", ")}] }<#if ip_has_next>,</#if>
             </#list>
         ]
     });