链路id封装实现及使用
现有模块在返回结果中统一封装了响应码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 ResultEntity<T> implements 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 postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
String startTime = MDC.get(MdCConstants.START_TIME);
if (StringUtils.isNotBlank(startTime)) {
long costTime = System.currentTimeMillis() - Long.valueOf(startTime);
log.info("current api cost time: {}", costTime);
MDC.put(MdCConstants.COST_TIME, String.valueOf(costTime));
}
}*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
MDC.clear();//清空 避免内存泄露
}
}
-
注册拦截器
拦截器声明后需注册进容器中,可在注册时设置拦截器需拦截的请求和排除不拦截的请求,具体提供的方法可自行查看
/**
* 注册项目中的Interceptor
* */
@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, 传递给被调用方
/**
* 用于feign调用中传递链路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标签中修改即可。示例如下:
<!-- 日志输出的文件的格式 -->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %5p %X{requestId} [%t] \(%F:%L\) - %m%n</pattern>
</layout>
封装接口返回结果
利用AOP对controller层进行增强处理,打印接口出入参,将controller层返回的数据统一封装成Result对象。
后续进行接口开发时无需将接口的返回值声明为ResultEntity,可直接返回对应的实体类, 无数据返回时可直接声明为void.
/**
* 2021.11.02改动
* 打印接口出入参 封装请求结果为ResultEntity,后续无需再controller层声明方法返回值为ResultEntity,可返回空对象或者对应的数据结构,由此处统一封装
* */
@Aspect
@Component
@Slf4j
public class WebLogAspect {
/**
* 作用于com.ab.dh.datahouse.controller包下的所有public方法
* 两个..代表所有子目录,最后括号里的两个..代表所有参数
*/
@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;
}
/**
* 获取请求入参,排除参数中request response对象
* */
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对象即可。
/**
* Controller全局异常控制
* @Description
*/
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 在Controller方法执行之前,校验一些参数不匹配,Get post方法等异常
*
* @param
* @return
* @Function handleExceptionInternal
* @Description
* @version v1.0
* @author hzx
* @see ResponseEntityExceptionHandler#handleExceptionInternal(Exception,
* Object, HttpHeaders,
* HttpStatus,
* WebRequest)
*/
@Override
protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers,
HttpStatus status, WebRequest request) {
ResultEntity<Object> resultEntity = ResultEntity.ofStatus(ResultCode.SYS_PARAM_ERROR);
if (ex instanceof MethodArgumentNotValidException) {
MethodArgumentNotValidException methodArgumentNotValidException = (MethodArgumentNotValidException) ex;
List<FieldError> fieldErrors = methodArgumentNotValidException.getBindingResult().getFieldErrors();
List<String> messages = new ArrayList<>();
for (FieldError fieldError : fieldErrors) {
messages.add(fieldError.getDefaultMessage());
}
resultEntity.setMsg("arguments not valid: \n" + JSON.toJSONString(messages));
} else if ((ex instanceof IllegalArgumentException || ex instanceof MissingServletRequestParameterException)) {
resultEntity.setCode(ResultCode.SYS_PARAM_ERROR.getCode());
resultEntity.setMsg(ex.getMessage());
} else if ((ex instanceof BusinessException)) {
BusinessException businessException = (BusinessException) ex;
resultEntity.setCode(businessException.getCode());
resultEntity.setMsg(businessException.getMessage());
} else {
if (ex.getMessage() != null && ex.getMessage().length() < 100) {
resultEntity = ResultEntity
.error("wrong request to match controller definition: \n" + processExMsg(ex));
}
}
LOGGER.error("全局异常控制:", ex);
//封装链路id
resultEntity.setRequestId(MDC.get(MdCConstants.REQUEST_ID));
return new ResponseEntity<Object>(resultEntity, HttpStatus.OK);
}
/**
* Controller内部异常控制
*
* @param
* @return
* @throws Exception
* @Function handlerException
* @Description
* @version v1.0
* @author hzx
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Object handlerException(HttpServletRequest request, HttpServletResponse response, Exception e) {
//e.printStackTrace();
LOGGER.error("global exception :", e);
// 统一返回200 http status code
response.setStatus(HttpStatus.OK.value());
ResultEntity<?> resultEntity = new ResultEntity<>();
//封装链路id
resultEntity.setRequestId(MDC.get(MdCConstants.REQUEST_ID));
if (e instanceof IllegalArgumentException) {
resultEntity.setCode(ResultCode.SYS_PARAM_ERROR.getCode());
resultEntity.setMsg(processExMsg(e));
return resultEntity;
}
if (e instanceof AuthenticationException) {
resultEntity.setCode(ResultCode.USER_ILLEGAL_TOKEN.getCode());
resultEntity.setMsg(ResultCode.USER_ILLEGAL_TOKEN.getMsg());
return resultEntity;
}
if (e instanceof BusinessException) {
BusinessException businessException = (BusinessException) e;
resultEntity.setCode(businessException.getCode());
resultEntity.setMsg(businessException.getMessage());
return resultEntity;
} else {
resultEntity.setCode(ResultCode.ERROR.getCode());
resultEntity.setMsg(ResultCode.ERROR.getMsg());
return resultEntity;
}
}
private String processExMsg(Exception e) {
if (e.getMessage() != null && e.getMessage().length() < 100) {
return e.getMessage();
}
return "";
}
}
可扩展功能点
-
返回服务器ip地址(生产慎用)
后续服务集群部署时,可在接口返回结果中返回服务器ip地址,避免查日志时需去多台机器上查找
-
线程池或者异步事件中传递链路id
后续项目中如有使用到线程池或者异步事件,可传递此链路id, 定义异步线程的日志将会方便很多