springboot中使用拦截器记录日志(日志框架-logback)

https://blog.csdn.net/qq_31289187/article/details/83513290

一、在分布式项目中,会出现很多跟踪某个用户的每一次请求,定位用户请求过程中出现的问题,统计接口的响应时间、效率等。比如定位用户请求过程中出现的问题,这就需要知道用户请求的是哪个接口,即知道URI、请求参数,在接口中出现了什么问题,如果在日志中没有打印用户的请求参数、没有记录同一次请求相同的id之类的参数、那么定位问题是非常痛苦的。常见的日志框架有:log4j、log4j 2、 slf4j,Common logging 、JUL、 logback等,目前流行的主要是logback。logback当前分成三个模块:logback-core,logback- classic和logback-access。logback-core是其它两个模块的基础模块。logback-classic是log4j的一个 改良版本。logback的效率、稳定性、占用内存小都比log4j好,所以现在大多数项目都选择logback作为日志框架。

二、这个案例的目的:

1、用户一次请求,打印的日志traceId相同,即跟踪id,方便定位问题(主要内容);

2、记录响应时间、方便以后统计分析接口质量(这是后话了);

3、打印请求参数、响应结果(主要内容);

4、使用logstash或者filebeat采集日志,输入到elasticsearch,在kibana中可以快速定位问题、统计分析等。(后面的学习计划)

三、创建springboot项目

1、pom.xml

   
            xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        4.0.0
     
        com.cn.dl
        springbootlogdemo
        0.0.1-SNAPSHOT
        jar
     
        springbootlogdemo
        Demo project for Spring Boot
     
        
            org.springframework.boot
            spring-boot-starter-parent
            2.0.6.RELEASE
            
        

     
        
            UTF-8
            UTF-8
            1.8
        

     
        
            
                org.springframework.boot
                spring-boot-starter-web
            

     
            
                org.springframework.boot
                spring-boot-starter-test
                test
            

     
            
                org.projectlombok
                lombok
                1.16.22
            

     
            
                com.alibaba
                fastjson
                1.2.47
            

     
        

     
        
            
                
                    org.springframework.boot
                    spring-boot-maven-plugin
                

            

        

     
     
   

2、Controller

    package com.cn.dl.controller;
     
    import com.alibaba.fastjson.JSONObject;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
     
    import java.util.List;
     
    /**
     * Created by Tiger on 2018/10/29.
     */
    @RestController
    @RequestMapping({"log"})
    @Slf4j
    public class LogDemoController {
     
        @RequestMapping({"test"})
        public void logTest(@RequestParam("userName") String name,
                            @RequestParam("age") int age,
                            @RequestParam("interest") List interest){
            log.info("name:{},age:{},interest:{}",name,age,interest);
        }
     
        @RequestMapping({"test_two"})
        public JSONObject logTestTwo(@RequestParam("dataType") int dataType){
            JSONObject result = new JSONObject();
            if(dataType == 1){
                result.put("name","Tiger");
                result.put("age",18);
                result.put("gender","man");
            }else if (dataType == 2){
                result.put("name","WYY");
                result.put("age",17);
                result.put("gender","woman");
            }else{
                result.put("type","java");
                result.put("study","springboot2");
            }
            return result;
        }
    }

3、TraceIdInterceptor:拦截所有请求,记录traceId。MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。这里使用slf4j的MDC用来保存traceId,可以追踪用户请求,方便快捷。

    package com.cn.dl.interceptor;
     
    import com.cn.dl.common.CommonConfig;
    import org.slf4j.MDC;
    import org.springframework.lang.Nullable;
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
     
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.util.UUID;
     
    /**
     * Created by Tiger on 2018/10/29.
     */
    public class TraceIdInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            
            String traceId = UUID.randomUUID().toString().replace("-","");
            MDC.put(CommonConfig.TRACE_ID, traceId);
            return true;
        }
     
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
     
        }
     
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
     
        }
    }

4、NetUtils:从request中获取用户ip

    package com.cn.dl.utils;
     
    import javax.servlet.http.HttpServletRequest;
     
    /**
     * 获取用户真实IP
     * Created by Tiger on 2018/10/29.
     */
    public class NetUtils {
        private static final String[] HEADERS_TO_TRY = {
                "X-Forwarded-For",
                "Proxy-Client-IP",
                "WL-Proxy-Client-IP",
                "HTTP_X_FORWARDED_FOR",
                "HTTP_X_FORWARDED",
                "HTTP_X_CLUSTER_CLIENT_IP",
                "HTTP_CLIENT_IP",
                "HTTP_FORWARDED_FOR",
                "HTTP_FORWARDED",
                "HTTP_VIA",
                "REMOTE_ADDR"
        };
        /**
         * 获取用户真实IP地址
         * @param request
         * @return
         */
        public static String getClientIpAddress(HttpServletRequest request) {
            String rip = request.getRemoteAddr();
            for (String header : HEADERS_TO_TRY) {
                String ip = request.getHeader(header);
                if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
                    rip = ip;
                    break;
                }
            }
            int pos = rip.indexOf(',');
            if (pos >= 0) {
                rip = rip.substring(0, pos);
            }
            return rip;
        }
    }

5、CommonConfig:公共配置,主要是个人编码习惯,为了定义统一的规范、代码整洁、方便维护等,这些公共配置是非常有必要的,这里有一个配置:public static final String LOG_PREFIX = "logData==",在使用logstash采集日志的时候,就知道它的作用了。

    package com.cn.dl.common;
     
    /**
     * Created by Tiger on 2018/10/29.
     */
    public interface CommonConfig {
        String START_TIME = "startTime";
        String IP = "ip";
        String CONSUME_TIME = "consumeTime";
        String REQ_PATH = "reqPath";
        String RES_BODY = "resBody";
        String LOG_PREFIX = "logData==";
        String TRACE_ID = "tarceId";
        String LOG_TYPE = "logType";
        String START = "start";
        String END = "end";
    }

6、LogDemoInterceptor:从request中获取ip、请求参数、记录请求开始的时间戳、在请求接口之前输出日志。

    package com.cn.dl.interceptor;
     
    import com.alibaba.fastjson.JSONObject;
    import com.cn.dl.common.CommonConfig;
    import com.cn.dl.utils.NetUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.slf4j.MDC;
    import org.springframework.lang.Nullable;
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
     
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.util.Map;
     
    /**
     * Created by Tiger on 2018/10/29.
     */
    public class LogDemoInterceptor implements HandlerInterceptor {
     
        private Logger logger = LoggerFactory.getLogger("outToFile");
     
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            Long startTime = System.currentTimeMillis();
            JSONObject json = new JSONObject();
            //用户ip
            json.put(CommonConfig.IP, NetUtils.getClientIpAddress(request));
            //请求路径
            json.put(CommonConfig.REQ_PATH,request.getRequestURI());
            //请求参数
            Map map = request.getParameterMap();
            map.forEach((key,value) -> {
                json.put(key,request.getParameter(key));
            });
            //记录请求开始时间
            request.setAttribute(CommonConfig.START_TIME,startTime);
            //traceId
            json.put(CommonConfig.TRACE_ID, MDC.get(CommonConfig.TRACE_ID));
     
            json.put(CommonConfig.LOG_TYPE,CommonConfig.START);
     
            logger.info(CommonConfig.LOG_PREFIX + json.toJSONString());
            return true;
        }
     
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
     
        }
     
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
            JSONObject json = new JSONObject();
            Long startTime = (Long) request.getAttribute(CommonConfig.START_TIME);
            //请求耗时
            json.put(CommonConfig.CONSUME_TIME,System.currentTimeMillis() - startTime);
            //traceId
            json.put(CommonConfig.TRACE_ID,MDC.get(CommonConfig.TRACE_ID));
            //响应Data
            json.put(CommonConfig.RES_BODY,request.getAttribute(CommonConfig.RES_BODY));
            //日志类型
            json.put(CommonConfig.LOG_TYPE,CommonConfig.END);
     
            logger.info(CommonConfig.LOG_PREFIX + json.toJSONString());
        }
    }

7、ControllerConfiger:配置需要拦截的请求以及需要做哪些事情。

    package com.cn.dl.configuration;
     
    import com.cn.dl.interceptor.LogDemoInterceptor;
    import com.cn.dl.interceptor.TraceIdInterceptor;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
     
    /**
     * Created by Tiger on 2018/10/29.
     */
    @Configuration
    public class ControllerConfiger implements WebMvcConfigurer {
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            // TODO: 2018/10/29 这两个拦截的顺序可以变化吗?不可以,因为需要先获取traceId,
            // TODO: 2018/10/29  不然LogDemoInterceptor -> preHandle方法打印的日志没有traceId
            /**
             * 同一次请求traceId相同
             * */
            registry.addInterceptor(new TraceIdInterceptor()).addPathPatterns("/**");
            /**
             * 打印请求路径、请求参数已经返回的参数等等
             * */
            registry.addInterceptor(new LogDemoInterceptor()).addPathPatterns("/**");
        }
    }

8、ControllerReponseAdvice:为了在响应体返回之前记录数据、修改数据、加密(邮箱、电话号码等隐私信息,例如:158****0215,实现这种小功能)等。

    package com.cn.dl.advice;
     
    import com.alibaba.fastjson.JSON;
    import com.cn.dl.common.CommonConfig;
    import org.springframework.core.MethodParameter;
    import org.springframework.http.MediaType;
    import org.springframework.http.server.ServerHttpRequest;
    import org.springframework.http.server.ServerHttpResponse;
    import org.springframework.http.server.ServletServerHttpRequest;
    import org.springframework.lang.Nullable;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
     
    import javax.servlet.http.HttpServletRequest;
     
    /**
     * Created by Tiger on 2018/10/29.
     */
    @ControllerAdvice
    public class ControllerReponseAdvice implements ResponseBodyAdvice {
        @Override
        public boolean supports(MethodParameter methodParameter, Class aClass) {
            return true;
        }
     
        @Override
        public Object beforeBodyWrite(@Nullable Object obj, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
            if (obj != null) {
                try {
                    HttpServletRequest servletRequest = ((ServletServerHttpRequest) serverHttpRequest).getServletRequest();
                    servletRequest.setAttribute(CommonConfig.RES_BODY, JSON.toJSONString(obj));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return obj;
        }
    }

10、logback.xml

   
   
     
     
     
       
          %-15(%d{HH:mm:ss.SSS}) [%thread] %-5level %logger{80}[%line] -%msg%n
       

     

     
     
        ${logData}/logDataDemo.log
       
          ${logData}/logDataDemo-%d{yyyy-MM-dd}.log
          30
       

       
         
            %-20(%d{yyyy-MMM-dd HH:mm:ss} [%thread]) %-5level %logger{80} [%line] -%msg%n
         

          UTF-8
       

     

     
     
       
     

     
     
       
     

   

四、测试

1、预期打印的日志同一次请求traceId是相同的,请求接口时打印出请求参数、用户ip,接口返回时有返回参数、请求耗时等信息。

    2018-十月-29 11:58:25 [http-nio-8080-exec-1] INFO  outToFile [39] -logData=={"interest1":"篮球,足球","ip":"127.0.0.1","name":"Tiger","reqPath":"/log/test","interest2":"123,23","age":"24"}
    2018-十月-29 11:58:26 [http-nio-8080-exec-2] INFO  outToFile [39] -logData=={"interest1":"篮球,足球","ip":"127.0.0.1","name":"Tiger","reqPath":"/log/test","interest2":"123,23","age":"24"}
    2018-十月-29 11:58:27 [http-nio-8080-exec-3] INFO  outToFile [39] -logData=={"interest1":"篮球,足球","ip":"127.0.0.1","name":"Tiger","reqPath":"/log/test","interest2":"123,23","age":"24"}
    2018-十月-29 12:03:23 [http-nio-8080-exec-1] INFO  outToFile [41] -logData=={"interest1":"篮球,足球","ip":"127.0.0.1","name":"Tiger","reqPath":"/log/test","interest2":"123,23","age":"24","tarceId":"b27527bc9a284a7496f11d2a4843a5ca"}
    2018-十月-29 12:03:24 [http-nio-8080-exec-2] INFO  outToFile [41] -logData=={"interest1":"篮球,足球","ip":"127.0.0.1","name":"Tiger","reqPath":"/log/test","interest2":"123,23","age":"24","tarceId":"72f4f8a697d74a1298105b7afcb8b37b"}
    2018-十月-29 12:03:24 [http-nio-8080-exec-3] INFO  outToFile [41] -logData=={"interest1":"篮球,足球","ip":"127.0.0.1","name":"Tiger","reqPath":"/log/test","interest2":"123,23","age":"24","tarceId":"92b4129de3b44ae19136d75a8ee4d4b0"}
    2018-十月-29 12:03:25 [http-nio-8080-exec-4] INFO  outToFile [41] -logData=={"interest1":"篮球,足球","ip":"127.0.0.1","name":"Tiger","reqPath":"/log/test","interest2":"123,23","age":"24","tarceId":"59314d66d8554ec0bfc8deeca506e408"}
    2018-十月-29 13:03:15 [http-nio-8080-exec-1] INFO  outToFile [42] -logData=={"logType":"start","ip":"127.0.0.1","reqPath":"/log/test_two","tarceId":"8efe4b8a28f648eca94ec560639a07b3"}
    2018-十月-29 13:03:15 [http-nio-8080-exec-1] INFO  outToFile [42] -logData=={"logType":"start","ip":"127.0.0.1","reqPath":"/error","tarceId":"9c4521d0d8ba4ddaadd461dde1184bfa"}
    2018-十月-29 13:03:23 [http-nio-8080-exec-2] INFO  outToFile [42] -logData=={"logType":"start","ip":"127.0.0.1","dataType":"1","reqPath":"/log/test_two","tarceId":"d409a8f851f3460c88e055f92752046a"}
    2018-十月-29 13:04:21 [http-nio-8080-exec-3] INFO  outToFile [42] -logData=={"logType":"start","ip":"127.0.0.1","dataType":"1","reqPath":"/log/test_two","tarceId":"f3be19fa4fd44d89a88ec304e901a821"}
    2018-十月-29 13:05:37 [http-nio-8080-exec-1] INFO  outToFile [42] -logData=={"logType":"start","ip":"127.0.0.1","dataType":"1","reqPath":"/log/test_two","tarceId":"185419a3d1444ead8d8b604cf524ce4f"}
    2018-十月-29 13:07:43 [http-nio-8080-exec-10] INFO  outToFile [42] -logData=={"logType":"start","ip":"127.0.0.1","dataType":"1","reqPath":"/log/test_two","tarceId":"7c063a6c6ca84e63b9d984d8c4490020"}
    2018-十月-29 13:07:43 [http-nio-8080-exec-10] INFO  outToFile [60] -logData=={"logType":"end","consumeTime":105,"tarceId":"7c063a6c6ca84e63b9d984d8c4490020"}
    2018-十月-29 13:09:01 [http-nio-8080-exec-1] INFO  outToFile [42] -logData=={"logType":"start","ip":"127.0.0.1","dataType":"1","reqPath":"/log/test_two","tarceId":"75b5edb346be4f7fa5ddd1236d7632a3"}
    2018-十月-29 13:09:01 [http-nio-8080-exec-1] INFO  outToFile [59] -logData=={"logType":"end","consumeTime":137,"tarceId":"75b5edb346be4f7fa5ddd1236d7632a3"}
    2018-十月-29 13:09:02 [http-nio-8080-exec-2] INFO  outToFile [42] -logData=={"logType":"start","ip":"127.0.0.1","dataType":"1","reqPath":"/log/test_two","tarceId":"21b38c96334448e49e28f720a7001381"}
    2018-十月-29 13:09:02 [http-nio-8080-exec-2] INFO  outToFile [59] -logData=={"logType":"end","consumeTime":2,"tarceId":"21b38c96334448e49e28f720a7001381"}
    2018-十月-29 13:10:37 [http-nio-8080-exec-1] INFO  outToFile [42] -logData=={"logType":"start","ip":"127.0.0.1","dataType":"1","reqPath":"/log/test_two","tarceId":"42cfca81a86444d9aa811b54af513482"}
    2018-十月-29 13:10:37 [http-nio-8080-exec-1] INFO  outToFile [59] -logData=={"logType":"end","consumeTime":106,"tarceId":"42cfca81a86444d9aa811b54af513482"}
    2018-十月-29 13:12:04 [http-nio-8080-exec-1] INFO  outToFile [42] -logData=={"logType":"start","ip":"127.0.0.1","dataType":"1","reqPath":"/log/test_two","tarceId":"81a13f3d36d74d4fa3c840c1a46282b7"}
    2018-十月-29 13:12:04 [http-nio-8080-exec-1] INFO  outToFile [59] -logData=={"logType":"end","consumeTime":125,"resBody":"{\"gender\":\"man\",\"name\":\"Tiger\",\"age\":18}","tarceId":"81a13f3d36d74d4fa3c840c1a46282b7"}
    2018-十月-29 13:15:01 [http-nio-8080-exec-1] INFO  outToFile [42] -logData=={"logType":"start","ip":"127.0.0.1","dataType":"1","reqPath":"/","tarceId":"268a0c984e1f4093aae2ae4adf205747"}
    2018-十月-29 13:15:01 [http-nio-8080-exec-1] INFO  outToFile [59] -logData=={"logType":"end","consumeTime":59,"tarceId":"268a0c984e1f4093aae2ae4adf205747"}
    2018-十月-29 13:15:01 [http-nio-8080-exec-1] INFO  outToFile [42] -logData=={"logType":"start","ip":"127.0.0.1","dataType":"1","reqPath":"/error","tarceId":"1890a30a82644abc8abb7fb86c5c8d01"}
    2018-十月-29 13:15:01 [http-nio-8080-exec-1] INFO  outToFile [59] -logData=={"logType":"end","consumeTime":77,"resBody":"{\"timestamp\":1540790101767,\"status\":404,\"error\":\"Not Found\",\"message\":\"No message available\",\"path\":\"/\"}","tarceId":"1890a30a82644abc8abb7fb86c5c8d01"}
    2018-十月-29 13:15:15 [http-nio-8080-exec-2] INFO  outToFile [42] -logData=={"logType":"start","ip":"127.0.0.1","dataType":"1","reqPath":"/","tarceId":"47ef2b0d4f304065a363a2dff8cfaf71"}
    2018-十月-29 13:15:15 [http-nio-8080-exec-2] INFO  outToFile [59] -logData=={"logType":"end","consumeTime":1,"tarceId":"47ef2b0d4f304065a363a2dff8cfaf71"}
    2018-十月-29 13:15:15 [http-nio-8080-exec-2] INFO  outToFile [42] -logData=={"logType":"start","ip":"127.0.0.1","dataType":"1","reqPath":"/error","tarceId":"159cd790d1574e6cb3b11a647857e24d"}
    2018-十月-29 13:15:15 [http-nio-8080-exec-2] INFO  outToFile [59] -logData=={"logType":"end","consumeTime":3,"resBody":"{\"timestamp\":1540790115376,\"status\":404,\"error\":\"Not Found\",\"message\":\"No message available\",\"path\":\"/\"}","tarceId":"159cd790d1574e6cb3b11a647857e24d"}
    2018-十月-29 13:18:35 [http-nio-8080-exec-10] INFO  outToFile [44] -logData=={"logType":"start","ip":"127.0.0.1","dataType":"1","reqPath":"/"}
    2018-十月-29 13:18:35 [http-nio-8080-exec-10] INFO  outToFile [66] -logData=={"logType":"end","consumeTime":57,"tarceId":"f06737292396431e9c523da820d322fb"}
    2018-十月-29 13:18:35 [http-nio-8080-exec-10] INFO  outToFile [44] -logData=={"logType":"start","ip":"127.0.0.1","dataType":"1","reqPath":"/error","tarceId":"f06737292396431e9c523da820d322fb"}
    2018-十月-29 13:18:35 [http-nio-8080-exec-10] INFO  outToFile [66] -logData=={"logType":"end","consumeTime":63,"resBody":"{\"timestamp\":1540790315526,\"status\":404,\"error\":\"Not Found\",\"message\":\"No message available\",\"path\":\"/\"}","tarceId":"5792b7de44174dc4a27bd56edccc27f8"}
    2018-十月-29 13:18:44 [http-nio-8080-exec-9] INFO  outToFile [44] -logData=={"logType":"start","ip":"127.0.0.1","dataType":"1","reqPath":"/log/test_two"}
    2018-十月-29 13:18:44 [http-nio-8080-exec-9] INFO  outToFile [66] -logData=={"logType":"end","consumeTime":18,"resBody":"{\"gender\":\"man\",\"name\":\"Tiger\",\"age\":18}","tarceId":"1e33c92d7bef44eba71a2a21a9d9a573"}
    2018-十月-29 13:18:57 [http-nio-8080-exec-8] INFO  outToFile [44] -logData=={"logType":"start","ip":"127.0.0.1","dataType":"1","reqPath":"/log/test_two"}
    2018-十月-29 13:18:57 [http-nio-8080-exec-8] INFO  outToFile [66] -logData=={"logType":"end","consumeTime":2,"resBody":"{\"gender\":\"man\",\"name\":\"Tiger\",\"age\":18}","tarceId":"5a921e9e59304a92902e7ef1b8e1b625"}

五、案例不足之处

1、未处理接口中打印的日志;

2、未记录接口中出现的异常日志。

六、下次目标

1、使用filebeat处理打印的日志文件;

2、使用logback将日志输出到kafka中;

3、本地搭建elasticsearch、kibana,可以在kibana实时查询打印的日志。

 
 

你可能感兴趣的:(微服务~~sprintboot)