SpringBoot记录HTTP请求日志

1、需求解读

需求:

框架需要记录每一个HTTP请求的信息,包括请求路径、请求参数、响应状态、返回参数、请求耗时等信息。

需求解读:

Springboot框架提供了多种方式来拦截HTTP请求和响应,只要能够获取到对应的request和response,就可以通过相应的API来获取所需要的信息。

需要注意的是,请求参数可以分为两部分,一部分是GET请求时,请求参数通过URL拼接的方式传到后端,还有一部分是通过POST请求提交Json格式的参数,这种参数会放在request body中传到后端,通过request.getParameterMap是无法获取到的。

2、Spring Boot Actuator

2.1、介绍和使用

Spring Boot Actuator 的关键特性是在应用程序里提供众多 Web 接口,通过它们了解应用程序运行时的内部状况,且能监控和度量Spring Boot 应用程序。

要使用Spring Boot Actuator,首先需要引入依赖包

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

其次需要开启端口访问权限

management.endpoints.web.exposure.include=httptrace

Spring Boot 应用启动时可以看到控制台的信息如下,代表开启了该端口的访问

 
image-20180829094800774

浏览器访问/acutator/httptrace就能看到HTTP的请求情况

 
image-20180829100827244

2.2、默认的HttpTraceRepository

Spring Boot Actuator 默认会把最近100次的HTTP请求记录到内存中,对应的实现类是InMemoryHttpTraceRepository

public class InMemoryHttpTraceRepository implements HttpTraceRepository {

    private int capacity = 100;

    private boolean reverse = true;

    private final List<HttpTrace> traces = new LinkedList<>();

    /**
* Flag to say that the repository lists traces in reverse order.
* @param reverse flag value (default true)
*/
public void setReverse(boolean reverse) {
synchronized (this.traces) {
this.reverse = reverse;
}
} /**
* Set the capacity of the in-memory repository.
* @param capacity the capacity
*/
public void setCapacity(int capacity) {
synchronized (this.traces) {
this.capacity = capacity;
}
} @Override
public List<HttpTrace> findAll() {
synchronized (this.traces) {
return Collections.unmodifiableList(new ArrayList<>(this.traces));
}
} @Override
public void add(HttpTrace trace) {
synchronized (this.traces) {
while (this.traces.size() >= this.capacity) {
this.traces.remove(this.reverse ? this.capacity - 1 : 0);
}
if (this.reverse) {
this.traces.add(0, trace);
}
else {
this.traces.add(trace);
}
}
} }

这里add方法使用了synchronized,默认只存储最近到100条,如果并发量大的话,性能会有所影响

2.3、自定义HttpTraceRepository

我们可以自己实现HttpTraceRepository这个接口,重写add方法并记录trace日志

@Slf4j
public class RemoteHttpTraceRepository implements HttpTraceRepository { @Override
public List<HttpTrace> findAll() {
return Collections.emptyList();
} @Override
public void add(HttpTrace trace) {
String path = trace.getRequest().getUri().getPath();
String queryPara = trace.getRequest().getUri().getQuery();
String queryParaRaw = trace.getRequest().getUri().getRawQuery();
String method = trace.getRequest().getMethod();
long timeTaken = trace.getTimeTaken();
String time = trace.getTimestamp().toString();
log.info("path: {}, queryPara: {}, queryParaRaw: {}, timeTaken: {}, time: {}, method: {}", path, queryPara, queryParaRaw,
timeTaken, time, method);
}
}

将该实现类注册到Spring的容器中

@Configuration
@ConditionalOnWebApplication
@ConditionalOnProperty(prefix = "management.trace.http", name = "enabled", matchIfMissing = true)
@EnableConfigurationProperties(HttpTraceProperties.class)
@AutoConfigureBefore(HttpTraceAutoConfiguration.class)
public class TraceConfig { @Bean
@ConditionalOnMissingBean(HttpTraceRepository.class)
public RemoteHttpTraceRepository traceRepository() {
return new RemoteHttpTraceRepository();
}
}

2.4、缺点

目前这种实现可以记录到请求路径、请求耗时、响应状态、请求Header、响应Header等信息,没有办法记录请求参数和响应参数。有人在github上提了个issue,作者回复说这样的设计是为了兼容Spring MVC和WebFlux两种模式,具体可以参考:https://github.com/spring-projects/spring-boot/issues/12953#issuecomment-383830749

3、Spring Boot Filter

3.1、HttpTraceFilter

既然httptrace无法满足现有的需求,我们可以顺着InMemoryHttpTraceRepository这个默认实现往上找,看看谁调用了这个实现类。结果可以发现是被HttpTraceFilter这个拦截器(servlet模式下)进行了调用。

public class HttpTraceFilter extends OncePerRequestFilter implements Ordered {

    // Not LOWEST_PRECEDENCE, but near the end, so it has a good chance of catching all
// enriched headers, but users can add stuff after this if they want to
private int order = Ordered.LOWEST_PRECEDENCE - 10; private final HttpTraceRepository repository; private final HttpExchangeTracer tracer; /**
* Create a new {@link HttpTraceFilter} instance.
* @param repository the trace repository
* @param tracer used to trace exchanges
*/
public HttpTraceFilter(HttpTraceRepository repository, HttpExchangeTracer tracer) {
this.repository = repository;
this.tracer = tracer;
} @Override
public int getOrder() {
return this.order;
} public void setOrder(int order) {
this.order = order;
} @Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!isRequestValid(request)) {
filterChain.doFilter(request, response);
return;
}
TraceableHttpServletRequest traceableRequest = new TraceableHttpServletRequest(
request);
HttpTrace trace = this.tracer.receivedRequest(traceableRequest);
int status = HttpStatus.INTERNAL_SERVER_ERROR.value();
try {
filterChain.doFilter(request, response);
status = response.getStatus();
}
finally {
TraceableHttpServletResponse traceableResponse = new TraceableHttpServletResponse(
(status != response.getStatus())
? new CustomStatusResponseWrapper(response, status)
: response);
this.tracer.sendingResponse(trace, traceableResponse,
request::getUserPrincipal, () -> getSessionId(request));
this.repository.add(trace);
}
}
...省略部分代码
}

tracer中会记录HTTP的请求耗时

3.2、自定义HttpTraceFilter获取请求参数

HttpTraceFilter继承了OncePerRequestFilter,我们可以仿照这个过滤器,定义自己的过滤器去继承OncePerRequestFilter,在doFilterInternal这个方法中获取到HttpServletRequestHttpServletResponse,这样就可以获取到对应的请求参数和返回参数了。

GET请求时的参数可以通过以下方式进行获取:

String parameterMap = request.getParameterMap()

POST请求会将参数放入request body中,用以下方式进行获取:

String requestBody = IOUtils.toString(request.getInputStream(), Charsets.UTF_8);

很不幸,代码运行会抛出异常

 
image-20180829111619987

原因是:body里字符的传输是通过HttpServletRequest中的字节流getInputStream()获得的;而这个字节流在读取了一次之后就不复存在了。

解决方法:利用ContentCachingRequestWrapperHttpServletRequest的请求包一层,该类会将inputstream中的copy一份到自己的字节数组中,这样就不会报错了。读取完body后,需要调用

wrappedResponse.copyBodyToResponse();

将请求还原。

3.3、完整的自定义HttpTraceFilter

@Slf4j
public class HttpTraceLogFilter extends OncePerRequestFilter implements Ordered { private static final String NEED_TRACE_PATH_PREFIX = "/api";
private static final String IGNORE_CONTENT_TYPE = "multipart/form-data"; private final MeterRegistry registry; public HttpTraceLogFilter(MeterRegistry registry) {
this.registry = registry;
} @Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 10;
} @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!isRequestValid(request)) {
filterChain.doFilter(request, response);
return;
}
if (!(request instanceof ContentCachingRequestWrapper)) {
request = new ContentCachingRequestWrapper(request);
}
if (!(response instanceof ContentCachingResponseWrapper)) {
response = new ContentCachingResponseWrapper(response);
}
int status = HttpStatus.INTERNAL_SERVER_ERROR.value();
long startTime = System.currentTimeMillis();
try {
filterChain.doFilter(request, response);
status = response.getStatus();
} finally {
String path = request.getRequestURI();
if (path.startsWith(NEED_TRACE_PATH_PREFIX) && !Objects.equals(IGNORE_CONTENT_TYPE, request.getContentType())) { String requestBody = IOUtils.toString(request.getInputStream(), Charsets.UTF_8);
log.info(requestBody);
//1. 记录日志
HttpTraceLog traceLog = new HttpTraceLog();
traceLog.setPath(path);
traceLog.setMethod(request.getMethod());
long latency = System.currentTimeMillis() - startTime;
traceLog.setTimeTaken(latency);
traceLog.setTime(LocalDateTime.now().toString());
traceLog.setParameterMap(JsonMapper.INSTANCE.toJson(request.getParameterMap()));
traceLog.setStatus(status);
traceLog.setRequestBody(getRequestBody(request));
traceLog.setResponseBody(getResponseBody(response));
log.info("Http trace log: {}", JsonMapper.INSTANCE.toJson(traceLog));
}
updateResponse(response);
}
} private boolean isRequestValid(HttpServletRequest request) {
try {
new URI(request.getRequestURL().toString());
return true;
} catch (URISyntaxException ex) {
return false;
}
} private String getRequestBody(HttpServletRequest request) {
String requestBody = "";
ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
if (wrapper != null) {
try {
requestBody = IOUtils.toString(wrapper.getContentAsByteArray(), wrapper.getCharacterEncoding());
} catch (IOException e) {
// NOOP
}
}
return requestBody;
} private String getResponseBody(HttpServletResponse response) {
String responseBody = "";
ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
if (wrapper != null) {
try {
responseBody = IOUtils.toString(wrapper.getContentAsByteArray(), wrapper.getCharacterEncoding());
} catch (IOException e) {
// NOOP
}
}
return responseBody;
} private void updateResponse(HttpServletResponse response) throws IOException {
ContentCachingResponseWrapper responseWrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
Objects.requireNonNull(responseWrapper).copyBodyToResponse();
} @Data
private static class HttpTraceLog { private String path;
private String parameterMap;
private String method;
private Long timeTaken;
private String time;
private Integer status;
private String requestBody;
private String responseBody;
} }
​@Configuration
@ConditionalOnWebApplication
public class HttpTraceConfiguration { @ConditionalOnWebApplication(type = Type.SERVLET)
static class ServletTraceFilterConfiguration { @Bean
public HttpTraceLogFilter httpTraceLogFilter(MeterRegistry registry) {
return new HttpTraceLogFilter(registry);
} } }

4、Spring AOP

使用Spring AOP的方式需要自定义注解,并且每个controller的方法上都需要加上这个注解才能进行拦截,对业务代码对编写有强制性的要求,所以没有采用这种方式。

作者:eaglewa
链接:https://www.jianshu.com/p/29459bcf6e6a
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

spring boot actuator扩展httptrace的记录的更多相关文章

  1. spring boot actuator端点高级进阶metris指标详解、git配置详解、自定义扩展详解

    https://www.cnblogs.com/duanxz/p/3508267.html 前言 接着上一篇<Springboot Actuator之一:执行器Actuator入门介绍>a ...

  2. spring boot actuator专题

    spring-boot-starter-actuator模块的实现对于实施微服务的中小团队来说,可以有效地减少监控系统在采集应用指标时的开发量.当然,它也并不是万能的,有时候我们也需要对其做一些简单的 ...

  3. springboot(十九):使用Spring Boot Actuator监控应用

    微服务的特点决定了功能模块的部署是分布式的,大部分功能模块都是运行在不同的机器上,彼此通过服务调用进行交互,前后台的业务流会经过很多个微服务的处理和传递,出现了异常如何快速定位是哪个环节出现了问题? ...

  4. 使用Spring Boot Actuator、Jolokia和Grafana实现准实时监控

    由于最近在做监控方面的工作,因此也读了不少相关的经验分享.其中有这样一篇文章总结了一些基于Spring Boot的监控方案,因此翻译了一下,希望可以对大家有所帮助. 原文:Near real-time ...

  5. (转)Spring Boot (十九):使用 Spring Boot Actuator 监控应用

    http://www.ityouknow.com/springboot/2018/02/06/spring-boot-actuator.html 微服务的特点决定了功能模块的部署是分布式的,大部分功能 ...

  6. 朱晔和你聊Spring系列S1E7:简单好用的Spring Boot Actuator

    阅读PDF版本 本文会来看一下Spring Boot Actuator提供给我们的监控端点Endpoint.健康检查Health和打点指标Metrics等所谓的Production-ready(生产环 ...

  7. Spring Boot Actuator 使用

    转载于:https://www.jianshu.com/p/af9738634a21 Spring Boot 的 Actuator 提供了很多生产级的特性,比如监控和度量Spring Boot 应用程 ...

  8. Spring Boot Actuator监控应用

    微服务的特点决定了功能模块的部署是分布式的,大部分功能模块都是运行在不同的机器上,彼此通过服务调用进行交互,前后台的业务流会经过很多个微服务的处理和传递,出现了异常如何快速定位是哪个环节出现了问题? ...

  9. spring Boot(十九):使用Spring Boot Actuator监控应用

    spring Boot(十九):使用Spring Boot Actuator监控应用 微服务的特点决定了功能模块的部署是分布式的,大部分功能模块都是运行在不同的机器上,彼此通过服务调用进行交互,前后台 ...

随机推荐

  1. [256个管理学理论]003.鳄鱼法则(Alligator Principle)

    鳄鱼法则(Alligator Principle) 来自于大洋彼岸的让你看不懂的解释: 这是经济学交易技术法则之一,也叫“鳄鱼效应”,它的意思是:假定一只鳄鱼咬住你的脚,如果你用手去试图挣脱你的脚,鳄 ...

  2. [Objective-C] Xcode中常用的快捷键操作与插件

    古人云“工欲善其事必先利其器”,打造和熟悉一个强大的开发环境,是每个程序员必须的! 在Xcode 6中有许多快捷键的设定可以使得你的编程工作更为高效,对于在代码文件中快速导航.定位Bug以及新增应用特 ...

  3. win10系统下计算器界面变成英文的解决方法

    标题: win10系统下计算器界面变成英文的解决方法 作者: 梦幻之心星 347369787@QQ.com 标签: [win10, 计算器, 英文] 目录: 软件 日期: 2019-04-20 目录 ...

  4. 50个SQL语句(MySQL版) 建表 插入数据

    本学期正在学习数据库,前段时间老师让我们做一下50个经典SQL语句,当时做的比较快,有一些也是百度的,自我感觉理解的不是很透彻. 所以从本篇随笔开始,我将进行50个经典SQL语句的复盘,加深理解. 答 ...

  5. Mysql安装与设置用户名、密码

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 关于MySQL程序中数据库调度的流程图解: ​ 1-MySql数据库的安装 ​ 安装前要注意,看看当前系 ...

  6. Java实现 LeetCode 206 反转链表

    206. 反转链表 反转一个单链表. 示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL ...

  7. Java实现 蓝桥杯VIP 算法提高 身份证排序

    算法提高 身份证排序 时间限制:1.0s 内存限制:256.0MB 问题描述 安全局搜索到了一批(n个)身份证号码,希望按出生日期对它们进行从大到小排序,如果有相同日期,则按身份证号码大小进行排序.身 ...

  8. Java实现 洛谷 P1008 三连击

    public class Main { public static void main(String[] args){ for(int i = 123; i <= 329; i++){ int[ ...

  9. Java实现第八届蓝桥杯方格分割

    方格分割 题目描述 6x6的方格,沿着格子的边线剪开成两部分. 要求这两部分的形状完全相同. 如图:p1.png, p2.png, p3.png 就是可行的分割法. 试计算: 包括这3种分法在内,一共 ...

  10. spring Cloud负载均衡Ribbon

    Ribbon饥饿加载 默认情况下Ribbon是懒加载的.当服务起动好之后,第一次请求是非常慢的,第二次之后就快很多. 解决方式:开启饥饿加载 ribbon: eager-load: enabled: ...