微服务请求日志统一处理方案

问题:在微服务中如何对请求日志统一输出?

新建日志组件,日志组件对请求进行拦截处理,输出请求入参、出参。其他各微服务引用日志组件,对日志统一输出

日志组件如下:
工具类

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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容