当前位置: 首页 > news >正文

prometheus应用demo(一)接口监控

完整代码

一、统计API请求(次数、响应码等)

filter实现,监控API接口的调用次数、错误码等,提前发现问题。

1、代码

使用Cursor自动生成代码

关键代码:

(1)自定义指标DTO
package com.demo.dto;import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Map;@Data
public class APIMetricsDTO {private String url;private String httpMethod;private String apiName;private Map<String, String[]> parameters;private String responseBody;private int statusCode;private long responseTime;private String time;private String clientIp;
}
(2)filter拦截HTTP请求并上报数据 
package com.demo.filter;import com.demo.dto.APIMetricsDTO;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingResponseWrapper;import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;@Component
@Slf4j
@Order(1)
public class UrlMetricsFilter extends OncePerRequestFilter {@Autowiredprivate MeterRegistry meterRegistry;private final ObjectMapper objectMapper = new ObjectMapper();@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 过滤掉 actuator 相关的监控端点if (shouldSkipFiltering(request)) {log.info("忽略上报");filterChain.doFilter(request, response);return;}log.info("进入UrlMetricsFilter - API: {} {}", request.getMethod(), request.getRequestURI());long startTime = System.currentTimeMillis();Timer.Sample sample = Timer.start(meterRegistry);// 使用ContentCachingResponseWrapper来捕获响应内容ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);try {// 执行请求filterChain.doFilter(request, responseWrapper);// 计算响应时间long responseTime = System.currentTimeMillis() - startTime;sample.stop(Timer.builder("api_requests_duration_seconds").description("API request duration in seconds").tag("uri", request.getRequestURI()).tag("method", request.getMethod()).tag("status", String.valueOf(responseWrapper.getStatus())).register(meterRegistry));// 收集API指标数据APIMetricsDTO metricsDTO = collectMetrics(request, responseWrapper, responseTime);// 上报指标reportMetrics(metricsDTO);// 记录API调用次数Counter.builder("api_request_total").description("Total number of API requests").tag("uri", request.getRequestURI()).tag("method", request.getMethod()).tag("status", String.valueOf(responseWrapper.getStatus())).register(meterRegistry).increment();} catch (Exception e) {log.error("上报指标error", e);// 记录错误次数Counter.builder("api_requests_errors_total").description("Total number of API request errors").tag("uri", request.getRequestURI()).tag("method", request.getMethod()).register(meterRegistry).increment();} finally {// 确保响应内容被写回responseWrapper.copyBodyToResponse();}}private APIMetricsDTO collectMetrics(HttpServletRequest request, ContentCachingResponseWrapper response, long responseTime) {APIMetricsDTO metricsDTO = new APIMetricsDTO();// 基本信息metricsDTO.setUrl(request.getRequestURL().toString());metricsDTO.setHttpMethod(request.getMethod());metricsDTO.setApiName(request.getRequestURI());SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");metricsDTO.setTime(simpleDateFormat.format(new Date()));metricsDTO.setResponseTime(responseTime);metricsDTO.setStatusCode(response.getStatus());// 客户端IPString clientIp = getClientIpAddress(request);metricsDTO.setClientIp(clientIp);// 请求参数metricsDTO.setParameters(request.getParameterMap());// 响应内容try {byte[] responseBody = response.getContentAsByteArray();if (responseBody.length > 0) {String responseContent = new String(responseBody, response.getCharacterEncoding());// 限制响应内容长度,避免过大的日志if (responseContent.length() > 1000) {responseContent = responseContent.substring(0, 1000) + "...";}metricsDTO.setResponseBody(responseContent);}} catch (Exception e) {log.warn("获取响应内容失败");metricsDTO.setResponseBody("获取响应内容失败: " + e.getMessage());}return metricsDTO;}private void reportMetrics(APIMetricsDTO metricsDTO) {try {// 记录详细的API调用日志log.info("API指标上报: {}", objectMapper.writeValueAsString(metricsDTO));// 可以在这里添加其他上报逻辑,比如发送到监控系统// 例如:发送到Kafka、写入数据库、发送到外部监控服务等} catch (Exception e) {log.error("上报API指标失败", e);}}private String getClientIpAddress(HttpServletRequest request) {String clientIp = request.getHeader("X-Forwarded-For");if (clientIp == null || clientIp.isEmpty() || "unknown".equalsIgnoreCase(clientIp)) {clientIp = request.getHeader("X-Real-IP");}if (clientIp == null || clientIp.isEmpty() || "unknown".equalsIgnoreCase(clientIp)) {clientIp = request.getHeader("Proxy-Client-IP");}if (clientIp == null || clientIp.isEmpty() || "unknown".equalsIgnoreCase(clientIp)) {clientIp = request.getHeader("WL-Proxy-Client-IP");}if (clientIp == null || clientIp.isEmpty() || "unknown".equalsIgnoreCase(clientIp)) {clientIp = request.getRemoteAddr();}// 处理多个IP的情况,取第一个if (clientIp != null && clientIp.contains(",")) {clientIp = clientIp.split(",")[0].trim();}return clientIp;}/*** 判断是否应该跳过过滤* @param request HTTP请求* @return true表示跳过,false表示需要过滤*/private boolean shouldSkipFiltering(HttpServletRequest request) {String requestURI = request.getRequestURI();// 过滤掉 actuator 相关的监控端点if (requestURI.contains("/actuator/")) {log.debug("跳过监控端点: {}", requestURI);return true;}// 可以添加更多需要跳过的路径// 例如:静态资源、健康检查等String[] skipPaths = {"/favicon.ico","/static/","/css/","/js/","/images/"};for (String skipPath : skipPaths) {if (requestURI.contains(skipPath)) {log.debug("跳过静态资源: {}", requestURI);return true;}}return false;}
}

启动项目发现在自动上报

2025-08-04T11:24:15.461+08:00  INFO 33060 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 3 endpoint(s) beneath base path '/actuator'
2025-08-04T11:24:15.500+08:00  INFO 33060 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 3333 (http) with context path '/prometheusDemo'
2025-08-04T11:24:15.507+08:00  INFO 33060 --- [           main] com.demo.PrometheusApplication           : Started PrometheusApplication in 1.362 seconds (process running for 1.908)
2025-08-04T11:24:15.755+08:00  INFO 33060 --- [-10.100.168.183] o.a.c.c.C.[.[.[/prometheusDemo]          : Initializing Spring DispatcherServlet 'dispatcherServlet'
2025-08-04T11:24:15.755+08:00  INFO 33060 --- [-10.100.168.183] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2025-08-04T11:24:15.756+08:00  INFO 33060 --- [-10.100.168.183] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
2025-08-04T11:24:17.861+08:00  INFO 33060 --- [nio-3333-exec-1] com.demo.filter.UrlMetricsFilter         : 忽略上报
2025-08-04T11:24:22.775+08:00  INFO 33060 --- [nio-3333-exec-2] com.demo.filter.UrlMetricsFilter         : 忽略上报
2025-08-04T11:24:27.775+08:00  INFO 33060 --- [nio-3333-exec-3] com.demo.filter.UrlMetricsFilter         : 忽略上报
2、后台监控查看

启动本地Prometheus ,访问 http://localhost:9090/ 进入Prometheus  UI页面。

(1)输入api_request_total可以看到总览

 (2)输入sort_desc(sum by (uri) (api_request_total))可以看到按接口访问次数倒序排列情况:

(3)输入topk(2, sum by (uri) (api_request_total)) # 按URI分组,显示访问次数最多的前2个

更多预览方法参考官方说明。

3、结合Grafana

对于项目上报的数据,如果需要监控多个维度,可以在Prometheus UI新建多个query,但是很不直观。下面结合Grafana展示自定义报表。

二、统计Repository接口的入参出参数量

AOP实现,监控Repository接口的调用总量、入参个数、出参个数,防止慢查询。

1、代码
(1)bean定义
package com.demo.metrics;import lombok.Data;
import lombok.extern.slf4j.Slf4j;import java.util.concurrent.atomic.LongAdder;/*** Repository方法统计指标Bean*/
@Data
@Slf4j
public class RepositoryMetricsBean {// 方法标识private String className;private String methodName;// 统计字段private int callCount;private long totalElements;private long averageElements;private long maxElements = -1; // 初始值为-1,表示还没有数据// 内部累加器private transient final LongAdder callCountAdder = new LongAdder();private transient final LongAdder totalElementsAdder = new LongAdder();public RepositoryMetricsBean(String className, String methodName) {this.className = className;this.methodName = methodName;}/*** 记录一次方法调用* @param elementCount List参数的元素个数*/public void record(int elementCount) {// 1. 增加调用次数callCountAdder.add(1);callCount = callCountAdder.intValue();// 2. 累加元素总数totalElementsAdder.add(elementCount);totalElements = totalElementsAdder.longValue();// 3. 比较并更新最大值if (elementCount > maxElements) {maxElements = elementCount;log.debug("📈 更新最大值: {}#{} -> {}", className, methodName, maxElements);} else {log.debug("📊 当前值不大于最大值: {}#{} {} <= {}", className, methodName, elementCount, maxElements);}}/*** 聚合计算平均值*/public void aggregate() {if (callCount > 0) {averageElements = totalElements / callCount;}}/*** 获取方法的唯一标识*/public String getMethodKey() {return className + "#" + methodName;}
}
(2)AOP 
package com.demo.aop;import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import com.demo.metrics.RepositoryMetricsBean;import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;@Aspect
@Component
@Slf4j
public class RepositoryMonitoringAspect {@Autowiredprivate MeterRegistry meterRegistry;// 使用ConcurrentHashMap存储每个方法的统计Beanprivate final ConcurrentHashMap<String, RepositoryMetricsBean> methodMetricsBeans = new ConcurrentHashMap<>();/** 接口调用量 repository_method_calls_total* List元素个数最大值  repository_method_list_elements_max* List元素个数平均值  repository_method_list_elements_avg** @param joinPoint*/@After("execution(* com.demo.repository.*.*(..))")public void monitorRepositoryMethod(JoinPoint joinPoint) {String className = joinPoint.getTarget().getClass().getSimpleName();String methodName = joinPoint.getSignature().getName();log.info("🔍 AOP拦截到Repository方法: {}#{}", className, methodName);// 获取List参数中的元素个数Object[] args = joinPoint.getArgs();int listElementCount = extractListElementCount(args);// 1. 统计调用量 - 先查找已存在的,不存在才创建Counter callsCounter = meterRegistry.find("repository_method_calls_total").tag("class", className).tag("method", methodName).counter();if (callsCounter == null) {try {callsCounter = Counter.builder("repository_method_calls_total").description("Repository method call count").tag("class", className).tag("method", methodName).register(meterRegistry);} catch (IllegalArgumentException e) {callsCounter = meterRegistry.find("repository_method_calls_total").tag("class", className).tag("method", methodName).counter();}}if (callsCounter != null) {callsCounter.increment();}// 2. 记录List元素总数 - 先查找已存在的,不存在才创建Counter sumCounter = meterRegistry.find("repository_method_list_elements_sum").tag("class", className).tag("method", methodName).counter();if (sumCounter == null) {try {sumCounter = Counter.builder("repository_method_list_elements_sum").description("Total number of list elements").tag("class", className).tag("method", methodName).register(meterRegistry);} catch (IllegalArgumentException e) {sumCounter = meterRegistry.find("repository_method_list_elements_sum").tag("class", className).tag("method", methodName).counter();}}if (sumCounter != null) {sumCounter.increment(listElementCount);}// 3. 记录List元素统计 - 使用独立的MetricsBean处理recordListElementMetrics(className, methodName, listElementCount);// 4. 从Prometheus获取数据并计算平均值,repository_method_list_elements_avgcalculateAndReportAverage(className, methodName);log.info("📊 Repository监控统计: {}#{} 调用,List元素个数: {}", className, methodName, listElementCount);}/*** 提取方法参数中List的元素个数* @param args 方法参数数组* @return List中元素的总个数*/private int extractListElementCount(Object[] args) {if (args == null || args.length == 0) {return 0;}int totalElements = 0;for (Object arg : args) {if (arg instanceof Collection) {Collection<?> collection = (Collection<?>) arg;totalElements += collection.size();log.debug("发现Collection参数,元素个数: {}", collection.size());}}return totalElements;}/*** 记录List元素统计 - 使用独立的MetricsBean处理* @param className 类名* @param methodName 方法名  * @param listElementCount 当前List元素个数*/private void recordListElementMetrics(String className, String methodName, int listElementCount) {// 1. 查找或创建对应方法的MetricsBeanRepositoryMetricsBean metricsBean = findOrCreateMetricsBean(className, methodName);// 2. 记录当前调用数据metricsBean.record(listElementCount);// 3. 聚合计算平均值metricsBean.aggregate();// 4. 确保Prometheus指标已注册ensurePrometheusMetricsRegistered(metricsBean);}/*** 查找或创建方法的MetricsBean*/private RepositoryMetricsBean findOrCreateMetricsBean(String className, String methodName) {String methodKey = className + "#" + methodName;return methodMetricsBeans.computeIfAbsent(methodKey, key -> {log.debug("✅ 创建新的MetricsBean: {}", key);return new RepositoryMetricsBean(className, methodName);});}/*** 确保Prometheus指标已注册*/private void ensurePrometheusMetricsRegistered(RepositoryMetricsBean metricsBean) {String className = metricsBean.getClassName();String methodName = metricsBean.getMethodName();// 注册最大值GaugeGauge maxGauge = meterRegistry.find("repository_method_list_elements_max").tag("class", className).tag("method", methodName).gauge();if (maxGauge == null) {try {Gauge.builder("repository_method_list_elements_max", () -> (double) metricsBean.getMaxElements()).description("Maximum number of list elements from MetricsBean").tag("class", className).tag("method", methodName).register(meterRegistry);log.debug("✅ 注册最大值Gauge: {}#{}", className, methodName);} catch (IllegalArgumentException e) {log.debug("Gauge已存在,跳过注册: {}#{}", className, methodName);}}}/*** 从Prometheus获取指标数据,计算平均值并上报* @param className 类名* @param methodName 方法名*/private void calculateAndReportAverage(String className, String methodName) {// 使用安全的获取或创建方式try {// 捕获当前的className和methodName,避免闭包问题final String finalClassName = className;final String finalMethodName = methodName;Gauge.builder("repository_method_list_elements_avg", () -> calculateAverage(finalClassName, finalMethodName)).description("Average number of list elements (calculated from Prometheus data)").tag("class", finalClassName).tag("method", finalMethodName).register(meterRegistry);log.debug("🎯 注册平均值Gauge: {}#{}", finalClassName, finalMethodName);} catch (IllegalArgumentException e) {// 如果已存在,什么都不做,Gauge会继续工作log.debug("Gauge已存在,跳过注册: {}#{}", className, methodName);}}/*** 从MeterRegistry中获取指标值并计算平均值* @param className 类名* @param methodName 方法名* @return 平均值*/private double calculateAverage(String className, String methodName) {try {// 从MeterRegistry获取调用次数Counter callsCounter = meterRegistry.find("repository_method_calls_total").tag("class", className).tag("method", methodName).counter();// 从MeterRegistry获取元素总数Counter sumCounter = meterRegistry.find("repository_method_list_elements_sum").tag("class", className).tag("method", methodName).counter();if (callsCounter != null && sumCounter != null) {double calls = callsCounter.count();double sum = sumCounter.count();if (calls > 0) {double average = sum / calls;log.debug("📊 计算平均值: {}#{} = {} / {} = {}", className, methodName, sum, calls, average);return average;}}log.debug("📊 暂无足够数据计算平均值: {}#{}", className, methodName);return 0.0;} catch (Exception e) {log.error("❌ 计算平均值失败: {}#{}, 错误: {}", className, methodName, e.getMessage());return 0.0;}}
}
2、测试

(1)调用一次

(2)调用两次localhost:3333/prometheusDemo/user/batchQueryByUserIds

一次传16个参数,一次传17个参数。

(3)看下UI统计

① 调用量

② 最大个数
③ 平均个数
3、重启项目测试

访问一次localhost:3333/prometheusDemo/user/batchQueryByUserIds带18个参数,值是重新计算了吗?

 

http://www.dtcms.com/a/315203.html

相关文章:

  • 【MySQL04】:基础查询
  • 初识SpringBoot
  • Java计算机网络面试题
  • 【BUUCTF系列】[SUCTF 2019]EasySQL1
  • script标签放在header里和放在body底部里有什么区别?
  • 鸿蒙开发元组
  • 单点登录(SSO)全面解析:原理、实现与应用
  • 中标喜讯 | 安畅检测成功中标海南工信大脑(二期)软件测评服务
  • 基于SpringBoot的OA办公系统的设计与实现
  • docker-compose一键部署Springboot+Vue前后端分离项目
  • 映射公式解常微分方程,偏微分方程
  • JVM-自动内存管理-运行时数据区域
  • createAsyncThunk
  • 结构体数组2-单向链表
  • MySQL详解(一)
  • SAP_MMBASIS模块-选择屏幕变式添加动态字段赋值
  • 如何在AD中快速定位器件?J+C
  • AWS服务分类
  • 人员检测识别中漏检率↓76%:陌讯动态特征融合算法实战解析
  • C++入门自学Day6-- STL简介(初识)
  • AI产品经理手册(Ch6-8)AI Product Manager‘s Handbook学习笔记
  • Vue3+TypeScript项目实战day1——项目的创建及环境配置
  • pytorch 学习笔记(2)-实现一个线性回归模型
  • sqli-labs通关笔记-第30关GET字符注入(WAF绕过 双引号闭合 手工注入+脚本注入两种方法)
  • QCustomplot极坐标系绘制
  • Qt项目模板全解析:选择最适合你的开发起点
  • Gitee:本土化DevOps平台如何助力中国企业实现高效研发协作
  • 水面垃圾清扫船cad【6张】三维图+设计说明书
  • C语言实现Elasticsearch增删改查API
  • OpenCV学习 day4