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

Grails应用http.server.requests指标数据采集问题排查及解决

问题

遇到的问题:同一个应用,Spring Boot(Java)和Grails(Groovy)混合编程,常规的Spring Controller,可通过Micromete + Pushgateway,
在这里插入图片描述
采集到http.server.requests指标数据,注意下面的指标名称是点号(请忽略下面截图里的接口的uri并不是上面的截图里的)
在这里插入图片描述
在Prometheus页面,会发现指标名称已经变成下划线命名,且增加后缀_seconds_sum
在这里插入图片描述
为啥Grails的UrlMappings和controller,无法采集到http_server_requests指标数据?(请忽略下面的截图是另一个应用)
在这里插入图片描述

源码分析

一开始,我只知道MeterRegistry.registerMeterIfNecessary方法,打个断点,调试可进入断点:
在这里插入图片描述
截图如上,tag里的uri全部变成root,也就是上面截图4中看到的所有接口全变成root,不同的是method方法。

为啥会变成root呢?

只能断点调试。

断点调试的前提是熟悉框架代码。想一想,如果不知道方法调用层级关系,怎么打断点呢?

如何熟悉代码?花时间。或者反复询问ChatGPT、DeepSeek、GitHub Copilot。

总之,这里直接给出原因。

WebMvcMetricsFilter类相关方法如下:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {TimingContext timingContext = TimingContext.get(request);if (timingContext == null) {timingContext = startAndAttachTimingContext(request);}try {filterChain.doFilter(request, response);if (!request.isAsyncStarted()) {// Only record when async processing has finished or never been started.// If async was started by something further down the chain we wait until the second filter invocation (but we'll be using the TimingContext that was attached to the first)Throwable exception = fetchException(request);record(timingContext, request, response, exception);}} catch (Exception ex) {response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());record(timingContext, request, response, unwrapNestedServletException(ex));throw ex;}
}private void record(TimingContext timingContext, HttpServletRequest request, HttpServletResponse response,Throwable exception) {try {Object handler = getHandler(request);Set<Timed> annotations = getTimedAnnotations(handler);Timer.Sample timerSample = timingContext.getTimerSample();AutoTimer.apply(this.autoTimer, this.metricName, annotations,(builder) -> timerSample.stop(getTimer(builder, handler, request, response, exception)));}catch (Exception ex) {logger.warn("Failed to record timer metrics", ex);// Allow request-response exchange to continue, unaffected by metrics problem}
}private Timer getTimer(Builder builder, Object handler, HttpServletRequest request, HttpServletResponse response,Throwable exception) {return builder.description("Duration of HTTP server request handling").tags(this.tagsProvider.getTags(request, response, handler, exception)).register(this.registry);
}

DefaultWebMvcTagsProvider类的相关方法如下:

@Override
public Iterable<Tag> getTags(HttpServletRequest request, HttpServletResponse response, Object handler,Throwable exception) {Tags tags = Tags.of(WebMvcTags.method(request), WebMvcTags.uri(request, response, this.ignoreTrailingSlash),WebMvcTags.exception(exception), WebMvcTags.status(response), WebMvcTags.outcome(response));for (WebMvcTagsContributor contributor : this.contributors) {tags = tags.and(contributor.getTags(request, response, handler, exception));}return tags;
}

WebMvcTags类的相关方法如下:

// 这才是我们最终想要定位的代码行,
private static final Tag URI_ROOT = Tag.of("uri", "root");public static Tag uri(HttpServletRequest request, HttpServletResponse response, boolean ignoreTrailingSlash) {if (request != null) {String pattern = getMatchingPattern(request);if (pattern != null) {if (ignoreTrailingSlash && pattern.length() > 1) {pattern = TRAILING_SLASH_PATTERN.matcher(pattern).replaceAll("");}if (pattern.isEmpty()) {return URI_ROOT;}return Tag.of("uri", pattern);}if (response != null) {HttpStatus status = extractStatus(response);if (status != null) {if (status.is3xxRedirection()) {return URI_REDIRECTION;}if (status == HttpStatus.NOT_FOUND) {return URI_NOT_FOUND;}}}String pathInfo = getPathInfo(request);if (pathInfo.isEmpty()) {return URI_ROOT;}}return URI_UNKNOWN;
}private static String getPathInfo(HttpServletRequest request) {String pathInfo = request.getPathInfo();String uri = StringUtils.hasText(pathInfo) ? pathInfo : "/";uri = MULTIPLE_SLASH_PATTERN.matcher(uri).replaceAll("/");return TRAILING_SLASH_PATTERN.matcher(uri).replaceAll("");
}private static String getMatchingPattern(HttpServletRequest request) {PathPattern dataRestPathPattern = (PathPattern) request.getAttribute(DATA_REST_PATH_PATTERN_ATTRIBUTE);if (dataRestPathPattern != null) {return dataRestPathPattern.getPatternString();}return (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
}

如下截图所示,在HttpServletRequest类里根本就没有pathInfo字段:
在这里插入图片描述
以及
在这里插入图片描述
代码为啥会走到getPathInfo方法呢,那是因为getMatchingPattern方法返回为空。

一个常规的Spring Boot Controller接口是可以获取到pattern的:
在这里插入图片描述
但是Grails框架下的Groovy Controller接口,pattern为null:
在这里插入图片描述
继续看看getMatchingPattern方法:
在这里插入图片描述
这里面尝试从request里获取两个key都失败,都返回null:

  • org.springframework.data.rest.webmvc.RepositoryRestHandlerMapping.EFFECTIVE_REPOSITORY_RESOURCE_LOOKUP_PATH
  • org.springframework.web.servlet.HandlerMapping.bestMatchingPattern

总结一下:Spring Boot Actuator的Filter类WebMvcMetricsFilter类doFilterInternal方法,调用内部方法record,继续调用内部方法getTimer,然后调用DefaultWebMvcTagsProvider的getTags方法,然后调用WebMvcTags的uri方法,调用内部方法getMatchingPattern,获取不到接口的uri信息,则走到内部方法getPathInfo,而HttpServletRequest.getPathInfo方法,也是返回null。导致最后记录到的tag为private static final Tag URI_ROOT = Tag.of("uri", "root");

如果不熟悉框架原理,全局搜索root关键词,根本就定位不到WebMvcTags类的URI_ROOT字段。

自定义指标采集

既然Grails框架下,Micrometer采集http.server.requests数据有问题,DeepSeek等工具告诉我,可以自定义指标数据。

下面的代码片段是DeepSeek给出的:

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Component;import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** @author johnny*/
@Component
class CustomMetricsFilter implements Filter {private final MeterRegistry meterRegistry;CustomMetricsFilter(MeterRegistry meterRegistry) {this.meterRegistry = meterRegistry;}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {HttpServletRequest httpRequest = (HttpServletRequest) request;HttpServletResponse httpResponse = (HttpServletResponse) response;// 开始计时Timer.Sample sample = Timer.start(meterRegistry);try {// 继续处理请求chain.doFilter(request, response);} finally {// 结束计时并记录指标// DeepSeek给出的是http.server.requests.custom自定义名称sample.stop(meterRegistry.timer("http.server.requests","method", httpRequest.getMethod(),"uri", httpRequest.getRequestURI(),"status", String.valueOf(httpResponse.getStatus())));}}
}

FilterConfig配置类:

package com.johnny.config;import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;/*** @author johnny*/
@Configuration
public class FilterConfig {@Beanpublic FilterRegistrationBean<CustomMetricsFilter> customMetricsFilter(MeterRegistry meterRegistry) {FilterRegistrationBean<CustomMetricsFilter> bean = new FilterRegistrationBean<>();bean.setFilter(new CustomMetricsFilter(meterRegistry));bean.addUrlPatterns("/*");bean.setOrder(Ordered.HIGHEST_PRECEDENCE);return bean;}
}

我没有使用自定义名称,而是使用期望推送的指标名称,http.server.requests。通过断点调试,上面的代码是生效的,但在Prometheus页面并不能看到我请求的接口,也就是说为啥不能覆盖默认的指标名。

原因,经过分析,在Timer类的register方法上:

/*** Add the timer to a single registry, or return an existing timer in that* registry. The returned timer will be unique for each registry, but each* registry is guaranteed to only create one timer for the same combination of* name and tags.* @param registry A registry to add the timer to, if it doesn't already exist.* @return A new or existing timer.*/
public Timer register(MeterRegistry registry) {// the base unit for a timer will be determined by the monitoring system// implementationreturn registry.timer(new Meter.Id(name, tags, null, description, Type.TIMER),distributionConfigBuilder.build(),pauseDetector == null ? registry.config().pauseDetector() : pauseDetector);
}

猜测下来,对于已存在的指标名称http.server.requests,会直接返回,并不会。

既然上面的代码可以断点调试,说明逻辑没有什么问题,为了进一步验证,使用自定义的指标名称http.server.requests.custom

浏览器打开:http://localhost:8867/actuator/metrics
在这里插入图片描述
如上图,除了组件默认采集到的http.server.requests,还有一条自定义的http.server.requests.custom。
打开Prometheus,查询新增的自定义指标,PromQL为:http_server_requests_custom_seconds_sum{job="agent-document"}
在这里插入图片描述
确实有数据。

问题来了:我想要在Grafana页面查询,查询范围当然是所有的应用。

DeepSeek给出的答案:

// 移除默认的 http.server.requests 指标
meterRegistry.remove(meterRegistry.find("http.server.requests").tags().timer());
// 结束计时并记录指标
// 省略代码

确实可以解决问题。

但是,如果一段时间内没有请求,组件自带的默认指标http.server.requests还是会覆盖我推送的。
在这里插入图片描述
代码里定时将数据通过Pushgateway推送到Prometheus(已经保存下来),Grafana可以查询到数据,哪怕被覆盖也没有问题??

另一方面,前面刚刚使用meterRegistry.remove()方法移除,后一脚又采集meterRegistry.timer("http.server.requests")数据,感觉怪怪的。

那能不能禁用默认的http.server.requests指标呢?

Grails

Grails框架下对HttpServletRequest做了各种不知道的封装。
在这里插入图片描述
主要是下面这个:
在这里插入图片描述
以及GrailsDispatcherServlet:
在这里插入图片描述
看到上面这么多Grails的Jar包,是不是要疯掉。

禁用默认指标

management:metrics:enable:http.server.requests: falsehttp: false

不管是http: false,还是http.server.requests: false,并不能将Micrometer默认的http.server.requests指标给屏蔽掉。

真正可以实现屏蔽的配置如下:

management:metrics:web:server:request:autotime:enabled: false

重启应用,请求http://localhost:8867/actuator/metrics,再随便请求一个其他接口,发现不再有http.server.requests指标,即实现禁用。

方案

最终的方案:禁用默认指标,加上CustomMetricsFilter,和FilterConfig配置类。

写在最后

本文如果行文思路还算清晰的话,请一定不要以为排查问题的过程也是思路清晰的。

实际上,在排查问题时,由于对Micrometer组件的源码不熟悉,浪费不少时间。

参考

  • GitHub Copilot
  • DeepSeek
  • ChatGPT

相关文章:

Grails应用http.server.requests指标数据采集问题排查及解决

问题 遇到的问题&#xff1a;同一个应用&#xff0c;Spring Boot(Java)和Grails(Groovy)混合编程&#xff0c;常规的Spring Controller&#xff0c;可通过Micromete Pushgateway&#xff0c; 采集到http.server.requests指标数据&#xff0c;注意下面的指标名称是点号&#…...

开源临床试验软件OpenClinica的安装

本文是为帮网友 A萤火虫 解决安装问题做的记录&#xff1b; 简介 什么是 OpenClinica &#xff1f; OpenClinica 是世界上第一个商业开源临床试验软件&#xff0c;主要用于电子数据捕获&#xff08;EDC&#xff09;和临床数据管理&#xff08;CDM&#xff09;。它的设计旨在优…...

网络安全 | 网络安全法规:GDPR、CCPA与中国网络安全法

网络安全 | 网络安全法规&#xff1a;GDPR、CCPA与中国网络安全法 一、前言二、欧盟《通用数据保护条例》&#xff08;GDPR&#xff09;2.1 背景2.2 主要内容2.3 特点2.4 实施效果与影响 三、美国《加利福尼亚州消费者隐私法案》&#xff08;CCPA&#xff09;3.1 背景3.2 主要内…...

深入学习 Python 爬虫:从基础到实战

深入学习 Python 爬虫&#xff1a;从基础到实战 前言 Python 爬虫是一个强大的工具&#xff0c;可以帮助你从互联网上抓取各种数据。无论你是数据分析师、机器学习工程师&#xff0c;还是对网络数据感兴趣的开发者&#xff0c;爬虫都是一个非常实用的技能。在本文中&#xff…...

element plus 使用 upload 组件达到上传数量限制时隐藏上传按钮

最近在重构项目&#xff0c;使用了 element plus UI框架&#xff0c;有个功能是实现图片上传&#xff0c;且限制只能上传一张图片&#xff0c;结果&#xff0c;发现&#xff0c;可以限制只上传一张图片&#xff0c;但是上传按钮还在&#xff0c;如图&#xff1a; 解决办法&…...

音频DSP的发展历史

音频数字信号处理&#xff08;DSP&#xff09;的发展历史是电子技术、计算机科学和音频工程共同进步的结果。这个领域的进展不仅改变了音乐制作、音频后期制作和通信的方式&#xff0c;也影响了音频设备的设计和功能。以下是对音频DSP发展历史的概述&#xff1a; 早期概念和理论…...

2025低代码与人工智能AI新篇

在当今数字化浪潮汹涌澎湃的时代&#xff0c;低代码开发与人工智能&#xff08;AI&#xff09;犹如两颗璀璨的星辰&#xff0c;正逐渐交汇融合&#xff0c;为企业解锁前所未有的智能业务解决方案。今天&#xff0c;咱们就深入探讨一下低代码平台是如何集成 AI 技术&#xff0c;…...

【HarmonyOS Next NAPI 深度探索1】Node.js 和 CC++ 原生扩展简介

【HarmonyOS Next NAPI 深度探索1】Node.js 和 CC 原生扩展简介 如果你用过 Node.js&#xff0c;应该知道它强大的地方在于能处理各种场景&#xff0c;速度还很快。但你有没有想过&#xff0c;Node.js 的速度秘密是什么&#xff1f;今天我们来聊聊其中一个幕后英雄——原生扩展…...

redis的学习(四)

13. 渐进式遍历 通过渐进式遍历能够获取当前所有的key&#xff0c;又不会讲当前的服务器卡死。不是一个命令将所有的key获取&#xff0c;而是每执行一次命令&#xff0c;只获取到其中的一部分。所以想要获取到所有的key就需要多次遍历&#xff0c;即化整为零的思想。 渐进式遍历…...

C# winform 多线程 UI更新数据 报错:无法访问已释放的对象。

System.ObjectDisposedException HResult0x80131622 Message无法访问已释放的对象。 ObjectDisposed_ObjectName_Name SourceSystem.Windows.Forms StackTrace: at System.Windows.Forms.Control.MarshaledInvoke(Control caller, Delegate method, Object[] args, …...

error: linker `link.exe` not found

开始学习rust&#xff0c;安装好rust的环境&#xff0c;开始从hello world开始&#xff0c;结果用在win10环境下&#xff0c;使用vs code或cmd窗口编译rust报错&#xff1a; PS E:\study_codes\rust-demo\chart01> rustc hello.rs error: linker link.exe not found| note:…...

Vue.js组件开发-如何使用moment.js

在Vue.js组件开发中&#xff0c;需要处理日期和时间&#xff0c;moment.js 是一个非常有用的库。moment.js 提供了丰富的API来解析、验证、操作和显示日期和时间。 步骤&#xff1a; 1. 安装moment.js 首先&#xff0c;需要通过npm或yarn安装moment.js。在项目根目录下运行以…...

Linux第二课:LinuxC高级 学习记录day01

0、大纲 0.1、Linux 软件安装&#xff0c;用户管理&#xff0c;进程管理&#xff0c;shell 命令&#xff0c;硬链接和软连接&#xff0c;解压和压缩&#xff0c;功能性语句&#xff0c;结构性语句&#xff0c;分文件&#xff0c;make工具&#xff0c;shell脚本 0.2、C高级 …...

《DOM NodeList》

《DOM NodeList》 介绍 DOM&#xff08;文档对象模型&#xff09;是HTML和XML文档的编程接口&#xff0c;它允许开发者在JavaScript等编程语言中操作文档的结构、样式和内容。在DOM中&#xff0c;NodeList是一个重要的接口&#xff0c;它表示一个包含节点&#xff08;如元素、…...

Nginx代理同域名前后端分离项目的完整步骤

前后端分离项目&#xff0c;前后端共用一个域名。通过域名后的 url 前缀来区别前后端项目。 以 vue php 项目为例。直接上 server 模块的 nginx 配置。 server{ listen 80; #listen [::]:80 default_server ipv6onlyon; server_name demo.com;#二配置项目域名 index index.ht…...

uniapp页面高度设置(铺满可视区域、顶部状态栏高度、底部导航栏高度)

这里说几种在uniapp开发中,关于页面设置高度的几种情况。宽度就不说了哈,宽度设置百分比都会生效。 首先我们要知道平时开发中,如果说没在uniapp做特殊处理,即正常情况下,所有的页面(.vue文件)中都是没有高度的(和vue一样),也就是说给最外层的的view标签设置高度为1…...

解锁 RAG 技术:从原理、论文研读走向实战应用RAG

亲爱的小伙伴们&#x1f618;&#xff0c;在求知的漫漫旅途中&#xff0c;若你对深度学习的奥秘、Java 与 Python 的奇妙世界&#xff0c;亦或是读研论文的撰写攻略有所探寻&#x1f9d0;&#xff0c;那不妨给我一个小小的关注吧&#x1f970;。我会精心筹备&#xff0c;在未来…...

HTML5实现好看的中秋节网页源码

HTML5实现好看的中秋节网页源码 前言一、设计来源1.1 网站首页界面1.2 登录注册界面1.3 节日由来界面1.4 节日习俗界面1.5 节日文化界面1.6 节日美食界面1.7 节日故事界面1.8 节日民谣界面1.9 联系我们界面 二、效果和源码2.1 动态效果2.2 源代码 源码下载结束语 HTML5实现好看…...

数字孪生笔记 1 工业数字孪生的意义

什么是工业数字孪生&#xff1f; 很多在做这个工作研究的同学最开始都想问的一个问题。到底什么才是数字孪生&#xff1f;我在五年前做数字孪生的时候也在思考这个问题。五年时间从数字孪生兴起&#xff0c;到元宇宙爆发&#xff0c;再到数字孪生和元宇宙没人提起&#xff0c;…...

013:深度学习之神经网络

本文为合集收录&#xff0c;欢迎查看合集/专栏链接进行全部合集的系统学习。 合集完整版请参考这里。 深度学习是机器学习中重要的一个学科分支&#xff0c;它的特点就在于需要构建多层且“深度”的神经网络。 人们在探索人工智能初期&#xff0c;就曾设想构建一个用数学方式…...

Opencv中的addweighted函数

一.addweighted函数作用 addweighted&#xff08;&#xff09;是OpenCV库中用于图像处理的函数&#xff0c;主要功能是将两个输入图像&#xff08;尺寸和类型相同&#xff09;按照指定的权重进行加权叠加&#xff08;图像融合&#xff09;&#xff0c;并添加一个标量值&#x…...

家政维修平台实战20:权限设计

目录 1 获取工人信息2 搭建工人入口3 权限判断总结 目前我们已经搭建好了基础的用户体系&#xff0c;主要是分成几个表&#xff0c;用户表我们是记录用户的基础信息&#xff0c;包括手机、昵称、头像。而工人和员工各有各的表。那么就有一个问题&#xff0c;不同的角色&#xf…...

土地利用/土地覆盖遥感解译与基于CLUE模型未来变化情景预测;从基础到高级,涵盖ArcGIS数据处理、ENVI遥感解译与CLUE模型情景模拟等

&#x1f50d; 土地利用/土地覆盖数据是生态、环境和气象等诸多领域模型的关键输入参数。通过遥感影像解译技术&#xff0c;可以精准获取历史或当前任何一个区域的土地利用/土地覆盖情况。这些数据不仅能够用于评估区域生态环境的变化趋势&#xff0c;还能有效评价重大生态工程…...

解读《网络安全法》最新修订,把握网络安全新趋势

《网络安全法》自2017年施行以来&#xff0c;在维护网络空间安全方面发挥了重要作用。但随着网络环境的日益复杂&#xff0c;网络攻击、数据泄露等事件频发&#xff0c;现行法律已难以完全适应新的风险挑战。 2025年3月28日&#xff0c;国家网信办会同相关部门起草了《网络安全…...

全面解析数据库:从基础概念到前沿应用​

在数字化时代&#xff0c;数据已成为企业和社会发展的核心资产&#xff0c;而数据库作为存储、管理和处理数据的关键工具&#xff0c;在各个领域发挥着举足轻重的作用。从电商平台的商品信息管理&#xff0c;到社交网络的用户数据存储&#xff0c;再到金融行业的交易记录处理&a…...

【安全篇】金刚不坏之身:整合 Spring Security + JWT 实现无状态认证与授权

摘要 本文是《Spring Boot 实战派》系列的第四篇。我们将直面所有 Web 应用都无法回避的核心问题&#xff1a;安全。文章将详细阐述认证&#xff08;Authentication) 与授权&#xff08;Authorization的核心概念&#xff0c;对比传统 Session-Cookie 与现代 JWT&#xff08;JS…...

6️⃣Go 语言中的哈希、加密与序列化:通往区块链世界的钥匙

Go 语言中的哈希、加密与序列化:通往区块链世界的钥匙 一、前言:离区块链还有多远? 区块链听起来可能遥不可及,似乎是只有密码学专家和资深工程师才能涉足的领域。但事实上,构建一个区块链的核心并不复杂,尤其当你已经掌握了一门系统编程语言,比如 Go。 要真正理解区…...

加密通信 + 行为分析:运营商行业安全防御体系重构

在数字经济蓬勃发展的时代&#xff0c;运营商作为信息通信网络的核心枢纽&#xff0c;承载着海量用户数据与关键业务传输&#xff0c;其安全防御体系的可靠性直接关乎国家安全、社会稳定与企业发展。随着网络攻击手段的不断升级&#xff0c;传统安全防护体系逐渐暴露出局限性&a…...

【大模型】RankRAG:基于大模型的上下文排序与检索增强生成的统一框架

文章目录 A 论文出处B 背景B.1 背景介绍B.2 问题提出B.3 创新点 C 模型结构C.1 指令微调阶段C.2 排名与生成的总和指令微调阶段C.3 RankRAG推理&#xff1a;检索-重排-生成 D 实验设计E 个人总结 A 论文出处 论文题目&#xff1a;RankRAG&#xff1a;Unifying Context Ranking…...

Linux操作系统共享Windows操作系统的文件

目录 一、共享文件 二、挂载 一、共享文件 点击虚拟机选项-设置 点击选项&#xff0c;设置文件夹共享为总是启用&#xff0c;点击添加&#xff0c;可添加需要共享的文件夹 查询是否共享成功 ls /mnt/hgfs 如果显示Download&#xff08;这是我共享的文件夹&#xff09;&…...