问题:在微服务中如何对请求日志统一输出?
新建日志组件,日志组件对请求进行拦截处理,输出请求入参、出参。其他各微服务引用日志组件,对日志统一输出
日志组件如下:
工具类
1、新建TimeCostEnum 请求耗时类,用于对请求处理耗时进行耗时级别定义
package com.jhjcn.common.logger;
/**
* @Deacription TODO
* @Author jianhua.hong
* @Date 2020/4/3 10:39
**/
public enum TimeCostEnum {
M1(0, 20, "M1"),
M2(20, 40, "M1"),
M3(40, 60, "M1"),
M4(60, 80, "M1"),
M5(80, 100, "M1"),
M6(100, 150, "M1"),
M7(150, 200, "M1"),
M8(200, 300, "M1"),
M9(300, 999999999, "M1"),
;
private int beginValue;
private int endValue;
private String costMark;
TimeCostEnum(int beginValue, int endValue, String costMark) {
this.beginValue = beginValue;
this.endValue = endValue;
this.costMark = costMark;
}
public int getBeginValue() {
return beginValue;
}
public int getEndValue() {
return endValue;
}
private String getCostMark() {
return costMark;
}
public static String costMark(long costTime) {
String mark = "M0";
for (TimeCostEnum timeCostEnum : TimeCostEnum.values()) {
long beginValue = timeCostEnum.getBeginValue();
long endValue = timeCostEnum.getEndValue();
if (beginValue < costTime && costTime <= endValue) {
mark = timeCostEnum.getCostMark();
break;
}
}
return mark;
}
}
2、新建LogComponentConstant 日志组件常量类
package com.jhjcn.common.logger;
/**
* @Deacription TODO
* @Author jianhua.hong
* @Date 2020/4/3 10:32
**/
public class LogComponentConstant {
public static final String TRACE_ID = "traceId";
}
核心组件
1、新搭建xxxx-common-logger 工程,其pom文件如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<groupId>com.jhjcn</groupId>
<artifactId>jhjcn-common-logger</artifactId>
<packaging>jar</packaging>
<description>日志组件</description>
<properties>
<javax.servlet-api.version>4.0.1</javax.servlet-api.version>
<commons-lang3.version>3.9</commons-lang3.version>
<feign-core.version>10.2.3</feign-core.version>
<spring-cloud-openfeign-core.version>2.1.2.RELEASE</spring-cloud-openfeign-core.version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.61</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${javax.servlet-api.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
<version>10.2.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
<version>2.1.7.RELEASE</version>
<exclusions>
<exclusion>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
2、新建MethodLogInfo类,用于封装请求信息
package com.jhjcn.common.logger;
import lombok.Data;
import java.io.Serializable;
/**
* @Deacription TODO
* @Author jianhua.hong
* @Date 2020/4/3 14:31
**/
@Data
public class MethodLogInfo implements Serializable {
private String path;
private String className;
private String method;
private String methodName;
private String paramsStr;
private boolean multiFileMark;
}
3、新建RequestLogContext 请求日志处理上下文类,用于对请求设置链路ID
package com.jhjcn.common.logger;
/**
* @Deacription TODO
* @Author jianhua.hong
* @Date 2020/4/2 18:29
**/
public final class RequestLogContext {
private final static ThreadLocal<String> TRACE_ID_THREADLOCAL = new ThreadLocal<>();
private final static ThreadLocal<String> SPAN_ID_THREADLOCAL = new ThreadLocal<>();
private final static ThreadLocal<String> PARENT_SPAN_ID_THREADlOCAL = new ThreadLocal<>();
public static void addTraceId(String id) {
TRACE_ID_THREADLOCAL.set(id);
}
public static String getTraceId() {
return TRACE_ID_THREADLOCAL.get();
}
public static void removeTraceId() {
TRACE_ID_THREADLOCAL.remove();
}
public static void addSpanId(String id) {
SPAN_ID_THREADLOCAL.set(id);
}
public static String getSpanId() {
return SPAN_ID_THREADLOCAL.get();
}
public static void removeSpanId() {
SPAN_ID_THREADLOCAL.remove();
}
public static void addParentSpanId(String id) {
PARENT_SPAN_ID_THREADlOCAL.set(id);
}
public static String getParentSpanId() {
return PARENT_SPAN_ID_THREADlOCAL.get();
}
public static void removeParentSpanId() {
PARENT_SPAN_ID_THREADlOCAL.remove();
}
}
4、新建AbstractLogHandler日志处理抽象类,用于封装日志处理公用方法
package com.jhjcn.common.logger;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
/**
* @Deacription TODO
* @Author jianhua.hong
* @Date 2020/4/2 18:33
**/
public abstract class AbstractLogHandler {
protected HttpServletRequest getRequest() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
return request;
}
protected void processRequestTraceId() {
HttpServletRequest request = this.getRequest();
String traceId = request.getHeader(LogComponentConstant.TRACE_ID);
if (StringUtils.isBlank(traceId)) {
traceId = RequestSeqHelper.nextId();
}
RequestLogContext.addTraceId(traceId);
}
protected boolean isOutPutLog(Object[] params) {
boolean outputMark = this.isMultilFileMark(params);
if (outputMark) {
return false;
} else {
return true;
}
}
protected boolean isMultilFileMark(Object[] params) {
boolean multilFileMark = false;
for (Object param : params) {
if (param instanceof MultipartFile) {
multilFileMark = true;
break;
}
}
return multilFileMark;
}
protected String getTimeCostFlag(long cost) {
String costFlag = TimeCostEnum.costMark(cost);
return costFlag;
}
protected MethodLogInfo buildMethodInfo(JoinPoint point) {
HttpServletRequest request = this.getRequest();
String path = request.getRequestURL().toString();
String className = point.getSignature().getDeclaringTypeName();
String method = request.getMethod();
String methodName = point.getSignature().getName();
Object[] params = point.getArgs();
boolean multilFileMark = this.isMultilFileMark(params);
MethodLogInfo requestLogInfo = new MethodLogInfo();
requestLogInfo.setPath(path);
requestLogInfo.setClassName(className);
requestLogInfo.setMethod(method);
requestLogInfo.setMethodName(methodName);
if (!multilFileMark) {
String paramsStr = JSONObject.toJSONString(params);
requestLogInfo.setParamsStr(paramsStr);
} else {
requestLogInfo.setMultiFileMark(true);
}
return requestLogInfo;
}
}
5、新建RequestLogHandler类,用于处理请求controller层日志输出
package com.jhjcn.common.logger;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import java.text.MessageFormat;
/**
* @Deacription TODO
* @Author jianhua.hong
* @Date 2020/4/2 17:40
**/
@Slf4j
@Aspect
public class RequestLogHandler extends AbstractLogHandler {
private static final String REQUEST_MESSAGE_FORMAT = "URL=[{0}],method=[{1}],handle class=[{2}],handle method=[{3}],Param=[{4}],Response=[{5}],cost time=[{6}ms-({7})]";
private static final String REQUEST_MESSAGE_ERROR_FORMAT = "URL=[{0}],method=[{1}],handle class=[{2}],handle method=[{3}],Param=[{4}]";
private static final String REQUEST_MESSAGE_MULIT_FORMAT = "URL=[{0}],request method=[{1}],handle class=[{2}],handle method=[{3}]";
@Pointcut("@within(org.springframework.web.bind.annotation.RestController) && execution(public * *(..) )")
public void controllerPoincut() {
}
@Around("controllerPoincut()")
public Object doControllerAround(ProceedingJoinPoint point) throws Throwable {
Long startTime = System.currentTimeMillis();
super.processRequestTraceId();
String traceId = RequestLogContext.getTraceId();
MethodLogInfo methodLogInfo = super.buildMethodInfo(point);
Object controllerResp = point.proceed();
Long endTime = System.currentTimeMillis();
Long cost = endTime - startTime;
String path = methodLogInfo.getPath();
String className = methodLogInfo.getClassName();
String method = methodLogInfo.getMethod();
String methodName = methodLogInfo.getMethodName();
if (!methodLogInfo.isMultiFileMark()) {
String paramsStr = methodLogInfo.getParamsStr();
String timeFlag = getTimeCostFlag(cost);
String controllerRespStr = JSONObject.toJSONString(controllerResp);
Object[] paramsArray = new Object[]{
path, method, className, methodName, paramsStr, controllerRespStr, cost, timeFlag
};
log.info("[request-log-around-{}]{}", traceId, MessageFormat.format(REQUEST_MESSAGE_FORMAT, paramsArray));
log.info("[timec-log-{}],class=[{}],method=[{}],cost time=[{}-({})]", traceId, className, methodName, cost, timeFlag);
} else {
Object[] paramsArray = new Object[]{
path, method, className, methodName
};
log.info("[request-log-around-{}]{}", traceId, MessageFormat.format(REQUEST_MESSAGE_MULIT_FORMAT, paramsArray));
}
return controllerResp;
}
@AfterThrowing(pointcut = "controllerPoincut()", throwing = "e")
public void handle(JoinPoint point, Exception e) {
super.processRequestTraceId();
String traceId = RequestLogContext.getTraceId();
MethodLogInfo methodLogInfo = super.buildMethodInfo(point);
String path = methodLogInfo.getPath();
String className = methodLogInfo.getClassName();
String method = methodLogInfo.getMethod();
String methodName = methodLogInfo.getMethodName();
String paramsStr = methodLogInfo.getParamsStr();
Object[] paramsArray = new Object[]{
path, method, className, methodName, paramsStr
};
log.info("[request-log-pre-{}]{}", traceId, MessageFormat.format(REQUEST_MESSAGE_ERROR_FORMAT, paramsArray));
log.info("[request-log-after-{}],handle class=[{}],handle method=[{}]异常", traceId, className, methodName);
}
}
6、新建ServiceLogHandler 类,用于对@servcie层日志处理,记录入参、出参
package com.jhjcn.common.logger;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import java.text.MessageFormat;
/**
* @Deacription TODO
* @Author jianhua.hong
* @Date 2020/4/2 20:34
**/
@Slf4j
@Aspect
public class ServiceLogHandler extends AbstractLogHandler {
private static final String SERVICE_MESSAGE_FORMAT = "handle class=[{0}],handle method=[{1}],Param=[{2}],Response=[{3}],cost time=[{4}ms-({5})]";
private static final String SERVICE_MESSAGE_ERROR_FORMAT = "handle class=[{0}],handle method=[{1}],Param=[{2}]";
@Pointcut("@within(org.springframework.stereotype.Service) && execution(public * *(..))")
public void servicePointcut() {
}
@Around("servicePointcut()")
public Object doControllerAround(ProceedingJoinPoint point) throws Throwable {
super.processRequestTraceId();
String traceId = RequestLogContext.getTraceId();
Long startTime = System.currentTimeMillis();
String methodName = point.getSignature().getName();
String className = point.getSignature().getDeclaringTypeName();
Object[] params = point.getArgs();
boolean outputLogMark = super.isOutPutLog(params);
Object serviceResp = point.proceed();
Long endTime = System.currentTimeMillis();
Long costTime = endTime - startTime;
if (outputLogMark) {
String paramsStr = JSONObject.toJSONString(params, SerializerFeature.IgnoreNonFieldGetter);
String rspStr = JSONObject.toJSONString(serviceResp, SerializerFeature.IgnoreNonFieldGetter);
String timeFlag = getTimeCostFlag(costTime);
Object[] paramsArray = new Object[]{
className, methodName, paramsStr, rspStr, costTime, timeFlag
};
log.info("[service-log-around-{}]{}", traceId, MessageFormat.format(SERVICE_MESSAGE_FORMAT, paramsArray));
log.info("[times-log-{}] class=[{}],method=[{}],cost time=[{}-({})]", traceId, className, methodName, costTime, timeFlag);
}
return serviceResp;
}
@AfterThrowing(pointcut = "servicePointcut()", throwing = "e")
public void handle(JoinPoint point, Exception e) {
super.processRequestTraceId();
String traceId = RequestLogContext.getTraceId();
String methodName = point.getSignature().getName();
String className = point.getSignature().getDeclaringTypeName();
Object[] params = point.getArgs();
String paramsStr = JSONObject.toJSONString(params, SerializerFeature.IgnoreNonFieldGetter);
Object[] paramsArray = new Object[]{
className, methodName, paramsStr
};
log.info("[service-log-pre-{}]{}", traceId, MessageFormat.format(SERVICE_MESSAGE_ERROR_FORMAT, paramsArray));
log.info("[service-log-after-{}],handle class=[{}],handle method=[{}]异常", traceId, className, methodName);
}
}
7、新建LogHandlerConfig类,用于注入RequestLogHandler 、ServiceLogHandler 请求日志处理组件
package com.jhjcn.common.logger;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Deacription TODO
* @Author jianhua.hong
* @Date 2020/4/3 9:37
**/
@Slf4j
@Configuration
public class LogHandlerConfig {
@Bean
public RequestLogHandler requestLogHandler() {
log.info("RequestLogHandler component init");
return new RequestLogHandler();
}
@Bean
public ServiceLogHandler serviceLogHandler() {
log.info("ServiceLogHandler component init");
return new ServiceLogHandler();
}
}
8、新建FeginRemoteInterceptor类,用于对微服务Fegin调用设置请求链路ID
package com.jhjcn.common.logger;
import feign.RequestInterceptor;
import feign.RequestTemplate;
/**
* @Deacription TODO
* @Author jianhua.hong
* @Date 2020/4/3 10:04
**/
public class FeginRemoteInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header(LogComponentConstant.TRACE_ID, RequestLogContext.getTraceId());
}
}
9、新建FeginRemoteConfig类,用于注入FeginRemoteInterceptor 拦截器、
package com.jhjcn.common.logger;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Deacription TODO
* @Author jianhua.hong
* @Date 2020/4/3 10:30
**/
@Slf4j
@Configuration
public class FeginRemoteConfig {
@Bean
FeginRemoteInterceptor feginRemoteInterceptor() {
log.info("FeginRemoteInterceptor component init");
return new FeginRemoteInterceptor();
}
}
组件使用如下
1、微服务工程pom文件中引用日志组件
<dependency>
<groupId>com.xxxx</groupId>
<artifactId>xxxx-common-logger</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
2、微服务启动类加上日志组件扫描路径
企业微信截图_15859019508763.png