一、在分布式项目中,会出现很多跟踪某个用户的每一次请求,定位用户请求过程中出现的问题,统计接口的响应时间、效率等。比如定位用户请求过程中出现的问题,这就需要知道用户请求的是哪个接口,即知道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
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实时查询打印的日志。