springboot配置请求日志

springboot配置请求日志

一般情况下,接口请求都需要日志记录,Java springboot中的日志记录相对复杂一点

经过实践,以下方案可行,记录一下完整过程

一、创建日志数据模型

创建实体类,也就是日志文件中要记录的数据格式

我是放在我的实体类包中,也就算pojo包下

java 复制代码
package org.example.pojo;

import lombok.Data;

@Data
public class RequestLog {
    private String url; // 请求URL
    private String method; // HTTP方法
    private long startTime; // 开始时间
    private long endTime; // 结束时间
    private String remoteIp; // 客户端IP
    private String queryString; // 查询参数
    private String requestBody; // 请求体(根据需求谨慎记录)
    private Integer status; // 响应状态码
    private Long costTime; // 耗时(ms)

    @Override
    public String toString(){
        return "{" +
                "\"url\":\"" + url + '\"' +
                ", \"method\":\"" + method + '\"' +
                ", \"remoteIp\":\"" + remoteIp + '\"' +
                ", \"status\":" + status +
                ", \"costTime\":" + costTime +
                ", \"queryString\":\"" + (queryString != null ? queryString : "") + '\"' +
                ", \"requestBody\":\"" + (requestBody != null ? requestBody : "") + '\"' +
                '}';
    }
}

这里有个小问题,requestBody似乎取不到,比较麻烦,但不影响记录

二、创建拦截器

其实就是每次请求前,读取请求信息,并添加到请求实体中去

我放在interceptor包下

java 复制代码
package org.example.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.pojo.RequestLog;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
public class RequestLoggingInterceptor implements HandlerInterceptor {
    private static final Logger logger = LoggerFactory.getLogger("REQUEST_LOG");
    private static final String ATTRIBUTE_REQUEST_LOG = "requestLog";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        RequestLog requestLog = new RequestLog();
        requestLog.setStartTime(System.currentTimeMillis());
        requestLog.setUrl(request.getRequestURI());
        requestLog.setMethod(request.getMethod());
        // request请求中的原始ip
        requestLog.setRemoteIp(request.getRemoteHost());
        // 自定义的获取ip方法,考虑了使用代理的情况
//        requestLog.setRemoteIp(getClientIp(request));
        requestLog.setQueryString(request.getQueryString());
//        下面这两行会报错
//        requestLog.setRequestBody(request.getReader().toString());
//        requestLog.setRequestBody(request.getRequestBody());

        request.setAttribute(ATTRIBUTE_REQUEST_LOG, requestLog);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,  Object handler, Exception ex) throws Exception {
        RequestLog requestLog = (RequestLog) request.getAttribute(ATTRIBUTE_REQUEST_LOG);
        if (requestLog != null) {
            requestLog.setEndTime(System.currentTimeMillis());
            requestLog.setCostTime(System.currentTimeMillis() - requestLog.getStartTime());

            requestLog.setStatus(response.getStatus());
            logger.info(requestLog.toString());
        }
    }

    /**
     * 获取客户端真实IP(考虑了代理情况)
     */
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

几个基础问题记录下:

  • Component注解,将这个方法注入到springboot项目中,使其成为项目组件
  • preHandle和afterCompletion都是HandlerInterceptor中原有的方法,重写方法时,必须保证参数完全一致,同时需要抛出异常
  • slf4j在maven中并没有显示引入,应该是springboot自带的

三、注册拦截器

需要写一个配置类,我放在config包中

java 复制代码
package org.example.config;

import org.example.interceptor.RequestLoggingInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private RequestLoggingInterceptor requestLoggingInterceptor;

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://localhost:5173", "http://localhost:5174")
                .allowedMethods("*")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(requestLoggingInterceptor)
                .addPathPatterns("/**");  // 拦截所有请求路径,都需要添加日志
    }
}

几点说明:

  • addCorsMappings用来处理跨域,重写父类方法
  • 拦截器在addInterceptors,也是重写的

四、配置 Logback 以将日志保存到文件

src/main/resources目录下创建或修改 logback-spring.xml文件,为请求日志配置一个独立的追加器 (Appender),并将其输出到文件

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 定义日志文件存储路径和文件名 -->
    <property name="LOG_PATH" value="./logs" />
    <property name="REQUEST_LOG_FILE" value="${LOG_PATH}/request.log" />

    <!-- 控制台输出配置(可选) -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 为请求日志配置独立的滚动文件Appender -->
    <appender name="REQUEST_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${REQUEST_LOG_FILE}</file> <!-- 当前活动的日志文件 -->
        <encoder>
            <!-- 配置输出格式,因为我们在Interceptor中已输出JSON字符串,这里只需消息本身 -->
            <pattern>%msg%n</pattern> 
        </encoder>
        <!-- 滚动策略:按日期和大小滚动 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 滚动后的文件命名模式:按天归档,超过大小则递增,并自动压缩 -->
            <fileNamePattern>${REQUEST_LOG_FILE}.%d{yyyy-MM-dd}.%i.gz</fileNamePattern>
            <maxFileSize>50MB</maxFileSize> <!-- 每个文件最大大小 -->
            <maxHistory>30</maxHistory> <!-- 保留30天的历史日志 -->
            <totalSizeCap>5GB</totalSizeCap> <!-- 所有请求日志文件总大小上限 -->
        </rollingPolicy>
    </appender>

    <!-- 专门为我们的请求日志logger配置,指向独立的文件Appender,并不再向上传递(additivity=false) -->
    <logger name="REQUEST_LOG" level="INFO" additivity="false">
        <appender-ref ref="REQUEST_FILE" />
        <!-- 如果需要同时在控制台看到,可以加上 <appender-ref ref="CONSOLE"/> -->
    </logger>

    <!-- 根日志记录器(其他日志的输出配置) -->
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

五、日志文件

bash 复制代码
{"url":"/api/layout/positions", "method":"POST", "remoteIp":"0:0:0:0:0:0:0:1", "status":200, "costTime":310, "queryString":""}
{"url":"/api/user/login", "method":"POST", "remoteIp":"0:0:0:0:0:0:0:1", "status":200, "costTime":19, "queryString":""}
{"url":"/api/user/login", "method":"POST", "remoteIp":"0:0:0:0:0:0:0:1", "status":200, "costTime":10, "queryString":"", "requestBody":"{"}
{"url":"/error", "method":"POST", "remoteIp":"0:0:0:0:0:0:0:1", "status":500, "costTime":27, "queryString":"", "requestBody":"    "username": "huangg","}
{"url":"/api/user/login", "method":"POST", "remoteIp":"0:0:0:0:0:0:0:1", "status":200, "costTime":283, "queryString":"", "requestBody":""}
{"url":"/api/user/login", "method":"POST", "remoteIp":"0:0:0:0:0:0:0:1", "status":200, "costTime":10, "queryString":"", "requestBody":"org.apache.catalina.connector.CoyoteReader@6906fda1"}
{"url":"/error", "method":"POST", "remoteIp":"0:0:0:0:0:0:0:1", "status":500, "costTime":27, "queryString":"", "requestBody":"org.apache.catalina.connector.CoyoteReader@6906fda1"}
{"url":"/api/user/login", "method":"POST", "remoteIp":"0:0:0:0:0:0:0:1", "status":200, "costTime":301, "queryString":"", "requestBody":""}
{"url":"/api/user/login", "method":"POST", "remoteIp":"127.0.0.1", "status":200, "costTime":3, "queryString":"", "requestBody":""}
{"url":"/api/user/login", "method":"POST", "remoteIp":"192.168.0.94", "status":200, "costTime":4, "queryString":"", "requestBody":""}
  • 如果本机上发起请求,请求域名用的是localhost,则remoteIp是ipv6地址,0:0:0:0:0:0:0:1
  • 如果本机上发起请求,请求域名用的是127.0.0.1,则remoteIp是127.0.0.1
  • 如果使用真实ip地址192.168.0.94,则remoteIp是真实IP192.168.0.94
相关推荐
七夜zippoe2 小时前
分布式事务性能优化:从故障现场到方案落地的实战手记(二)
java·分布式·性能优化
番薯大佬2 小时前
Python学习-day8 元组tuple
java·python·学习
何似在人间5752 小时前
Go语言快速入门教程(JAVA转go)——1 概述
java·开发语言·golang
疯子@1232 小时前
nacos1.3.2 ARM 版容器镜像制作
java·linux·docker·容器
Swift社区2 小时前
如何解决 Spring Bean 循环依赖
java·后端·spring
我真的是大笨蛋3 小时前
从源码和设计模式深挖AQS(AbstractQueuedSynchronizer)
java·jvm·设计模式
爱吃烤鸡翅的酸菜鱼3 小时前
【Redis】常用数据结构之Hash篇:从常用命令到使用场景详解
数据结构·数据库·redis·后端·缓存·哈希算法
bobz9653 小时前
calico vxlan 模式如何实现和公有云一样的 VPC 功能?
后端
空山新雨(大队长)3 小时前
Java第五课:输入输出
java·开发语言