现有模块在返回结果中统一封装了响应码code及响应信息msg, 遇到报错时通过报错信息无法快速定位到报错相关的日志信息,查看日志不是很方便
{code: “-1”, msg: “系统繁忙”, content: null}
封装链路id后,可根据此id作为关键字查找日志,日志信息中会包含整个请求中的相关日志,定位问题会方便很多
使用方式{
“code”: “1”,
“msg”: “success”,
“content”: {
“page”: 1,
“pageSize”: 10,
“total”: 0,
“data”: [],
“extend”: null
},
"requestId": “880637”,
“costTime”: 17
}
获取接口返回的数据,找到返回数据中的requestId, 登陆服务器,切换到日志路径,使用命令 grep ‘requestId’ xxx.log
也可在grep 后加参数 -B | -A rows, 日志会输出关键字前后对应行数相关的日志, 其余参数可使用 grep --help查看。
某些情况下可能拿不到requestId, 无法直接使用。此情况下可根据请求参数中的业务标识id或者返回结果中的业务数据作为关键字先查询,找到对应的requestId后再查看整个请求的日志。
实现方式代码示例:
定义接口统一返回结果及常量-
返回结果中一般会定义状态码、返回消息、接口数据、链路id、请求耗时等字段,创建Result对象
可在result对象中封装一些返回成功、失败的构造方法
@Data @ApiModel(description = "响应对象") public class ResultEntityimplements Serializable { private static final long serialVersionUID = 1L; @NonNull @ApiModelProperty(value = "应答码") private String code; private String msg; private T content; @ApiModelProperty(value = "链路id(方便查询日志 通过 grep ‘xxxx’ xxx.log 定位整个请求的日志)", example = "56895623") private String requestId;//链路id 方便查询日志 通过 grep ‘xxxx’ xxx.log 定位整个请求的日志 //@ApiModelProperty(value = "服务器ip", example = "127.0.0.1") //private String ip;//服务器ip 多机器部署时通过服务器ip直接去对应机器找对应日志 容易暴露地址 不适合在生产放开 @ApiModelProperty(value = "请求耗时(单位 毫秒)", example = "253" , hidden = true) private long costTime;//请求耗时(单位 毫秒) }
-
定义常量 MdCConstants
public class MdCConstants { public static final String REQUEST_ID = "requestId"; //链路id public static final String COST_TIME = "costTime"; //请求耗时 public static final String START_TIME = "startTime"; //请求开始时间 }
-
创建拦截器RequestAdapterInterceptor, 封装对应逻辑
拦截器中需重写前置处理方法,首先从http header中获取链路id, 没有则生成6位随机数字,以requestId作为key放入MDC中.
要从header中获取链路id是因为接口可能被其它服务调用,沿用上层服务的id方便整个请求链的追踪
在拦截器链路走完后可在afterCompletion方法中清空MDC, 避免没有及时清空导致内存泄露问题
MDC的介绍及使用:
https://www.jianshu.com/p/1dea7479eb07
https://segmentfault.com/a/1190000008315137#articleHeader12
拦截器介绍:
https://juejin.cn/post/6844904020675559432#h
@Component
@Slf4j
public class RequestAdapterInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取请求头中是否有requestId 没有则生成
String requestId = request.getHeader(MdCConstants.REQUEST_ID);
if (StringUtils.isBlank(requestId)) {
requestId = RandomStringUtils.randomNumeric(6);
log.info("generate requestId: {}", requestId);
}
MDC.put(MdCConstants.REQUEST_ID, requestId);
//MDC.put(MdCConstants.START_TIME, String.valueOf(System.currentTimeMillis())); 通过AOP封装接口耗时
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
MDC.clear();//清空 避免内存泄露
}
}
-
注册拦截器
拦截器声明后需注册进容器中,可在注册时设置拦截器需拦截的请求和排除不拦截的请求,具体提供的方法可自行查看
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Resource
private RequestAdapterInterceptor requestIdInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestIdInterceptor)
.addPathPatterns("**");//设置拦截的请求
}
}
-
feign接口中传递requestId
创建FeignRequestInterceptor类,在发起远程调用前会先进拦截器,通过此拦截器在header中添加链路id, 传递给被调用方
@Configuration
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
//在请求头中添加链路id
template.header(MdCConstants.REQUEST_ID, MDC.get(MdCConstants.REQUEST_ID));
}
}
日志增强
要在日志中打印链路id, 需要利用MDC在日志配置文件中修改日志打印格式,加入链路id参数即可。
放入MDC的参数在日志中使用 %X{参数名}
找到项目的日志配置文件logback,在layout标签中修改即可。示例如下:
%d{yyyy-MM-dd HH:mm:ss.SSS} %5p %X{requestId} [%t] (%F:%L) - %m%n
封装接口返回结果
利用AOP对controller层进行增强处理,打印接口出入参,将controller层返回的数据统一封装成Result对象。
后续进行接口开发时无需将接口的返回值声明为ResultEntity,可直接返回对应的实体类, 无数据返回时可直接声明为void.
@Aspect
@Component
@Slf4j
public class WebLogAspect {
@Pointcut("execution(public * com.ab.dh.datahouse.controller.*.*(..))")
public void logPointCut() {}
@Around("logPointCut()")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
long startTime = System.currentTimeMillis();
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//打印请求路径和入参
log.info("request URL: {}", request.getRequestURI());
log.info("request params: {}", JSON.toJSONString(getArgs(pjp.getArgs())));
//执行真正的方法调用
Object ob = pjp.proceed();
log.info("id:{}, request uri:{}, handle request time: {}", logId, request.getRequestURI(), System.currentTimeMillis() - startTime);
ResultEntity result = null;
//封装结果 将对象封装成result
if (ob instanceof ResultEntity) {
//nothing 已经是Result 无需再封装
result = (ResultEntity) ob;
} else {
result = ResultEntity.success();
result.setContent(ob);
}
//计算请求耗时 封装进Result中
result.setCostTime(System.currentTimeMillis() - startTime);
result.setRequestId(MDC.get(MdCConstants.REQUEST_ID));//封装链路id
//打印出参
log.info("request end, result info: {}", JSON.toJSONString(result));
return result;
}
private Object[] getArgs(Object[] args) {
if (args == null || args.length == 0) {
return null;
}
Object[] result = new Object[args.length];
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof ServletResponse || args[i] instanceof ServletRequest) {
continue;
}
result[i] = args[i];
}
return result;
}
}
打印接口入参时需过滤掉request、response对象,部分接口会将这俩个对象作为入参实现某些操作,如不过滤这俩个对象,在打印这俩个参数进行json解析时会遇到如下报错:
全局异常处理java.lang.IllegalStateException: getOutputStream() has already been called for this response
at org.apache.catalina.connector.Response.getWriter(Response.java:581)
at org.apache.catalina.connector.ResponseFacade.getWriter(ResponseFacade.java:227)
at com.alibaba.fastjson.serializer.ASMSerializer_2_ResponseFacade.write(Unknown Source)
at com.alibaba.fastjson.serializer.ObjectArrayCodec.write(ObjectArrayCodec.java:103)
at com.alibaba.fastjson.serializer.JSONSerializer.write(JSONSerializer.java:281)
at com.alibaba.fastjson.JSON.toJSonString(JSON.java:673)
at com.alibaba.fastjson.JSON.toJSonString(JSON.java:611)
at com.alibaba.fastjson.JSON.toJSonString(JSON.java:576)
at com.dh.manager.interceptor.WebLogAspect.doAround(WebLogAspect.java:52)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
当前项目中进行了全局的异常处理,会拦截项目内抛出的异常,将异常信息也封装成ResultEntity对象返回,在此处只需将链路requestId赋值给result对象即可。
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@Override
protected ResponseEntity
可扩展功能点
-
返回服务器ip地址(生产慎用)
后续服务集群部署时,可在接口返回结果中返回服务器ip地址,避免查日志时需去多台机器上查找
-
线程池或者异步事件中传递链路id
后续项目中如有使用到线程池或者异步事件,可传递此链路id, 定义异步线程的日志将会方便很多



