定制化开发SpringCloudGateway通过traceId实现全链路日志追踪

问题:

定制化开发在日常开发过程中,定制化开发如果使用架构,定制化开发那么日志查询就是一个问题,比如A定制化开发定制化开发服务调用了B服务,B服务调用了C服务,这个时候C定制化开发服务报错了,定制化开发导致整个请求异常失败,定制化开发如果想排查这个问题,定制化开发没有日志整合的话,定制化开发我们排查问题原因就变的很麻烦

解决方案:

定制化开发在网关服务接收到请求定制化开发的时候生成一个traceId,然后将traceId在每个服务间传递,同时日志打印的时候将traceId一起打印出来,这样在使用ELK去查询日志的时候,只需要搜索一个traceId,就可以查询的到整个请求的全链路日志信息了。

准备:

1:网关服务添加自定义拦截器
import cn.hutool.core.lang.UUID;import cn.hutool.core.util.ObjectUtil;import cn.hutool.core.util.StrUtil;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;import com.auth0.jwt.JWT;import lombok.extern.slf4j.Slf4j;import org.slf4j.MDC;import org.springframework.cloud.gateway.filter.GatewayFilterChain;import org.springframework.cloud.gateway.filter.GlobalFilter;import org.springframework.core.Ordered;import org.springframework.http.HttpHeaders;import org.springframework.http.HttpMethod;import org.springframework.http.HttpStatus;import org.springframework.http.server.reactive.ServerHttpRequest;import org.springframework.http.server.reactive.ServerHttpResponse;import org.springframework.stereotype.Component;import org.springframework.util.AntPathMatcher;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Flux;import reactor.core.publisher.Mono;import java.nio.charset.StandardCharsets;import java.util.ArrayList;import java.util.Base64;import java.util.HashMap;import java.util.List;@Slf4j@Componentpublic class AuthorizeFilter implements GlobalFilter, Ordered {    private static final String TRACE_ID = "traceId";    private static final AntPathMatcher matcher = new AntPathMatcher();    @Override    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {        ServerHttpRequest request = exchange.getRequest();        ServerHttpResponse response = exchange.getResponse();        //对请求对象request进行增强        ServerHttpRequest req = request.mutate().headers(httpHeaders -> {            //httpHeaders 封装了所有的请求头            String traceId = UUID.randomUUID().toString(true);            MDC.put(TRACE_ID, traceId);            httpHeaders.set(TRACE_ID, traceId);        }).build();        //设置增强的request到exchange对象中        exchange.mutate().request(req);        String url = request.getURI().getPath();        log.info("接收到请求:{}", url);        // 跨域放行        if (request.getMethod() == HttpMethod.OPTIONS) {            response.setStatusCode(HttpStatus.OK);            return Mono.empty();        }        // 不需要拦截的接口直接放行        if (needLogin(request.getPath().toString())) {            log.info("不拦截放行");            return chain.filter(exchange);        }        // 授权验证        if (!this.auth(exchange)) {            return this.responseBody(exchange, 406, "请先登录");        }        log.info("认证成功,放行");        return chain.filter(exchange);    }    /**     * 是否需要登录     *     * @param uri 请求URI     * @return boolean     */    public static boolean needLogin(String uri) {        // test        List<String> uriList = new ArrayList<>();        uriList.add("/user/login");        uriList.add("/demo/**");        uriList.add("/**");        for (String pattern : uriList) {            if (matcher.match(pattern, uri)) {                // 不需要拦截                return true;            }        }        return false;    }    /**     * 认证拦截     */    private boolean auth(ServerWebExchange exchange) {        String token = this.getToken(exchange.getRequest());        log.info("token:{}", token);        if (StrUtil.isBlank(token)) {            return false;        }        JSONObject userInfo = getUserInfo(token);        return !ObjectUtil.isNull(userInfo);    }    private JSONObject getUserInfo(String token) {        JSONObject jsonObject;        String tokenNew = token.substring(7);        String ss = JWT.decode(tokenNew).getPayload();        Base64.Decoder decoder = Base64.getDecoder();        jsonObject = JSON.parseObject(new String(decoder.decode(ss)));        return jsonObject;    }    /**     * 获取token     */    public String getToken(ServerHttpRequest request) {        String token = request.getHeaders().getFirst("Authorization");        if (StrUtil.isBlank(token)) {            return request.getQueryParams().getFirst("Authorization");        }        return token;    }    /**     * 设置响应体     **/    public Mono<Void> responseBody(ServerWebExchange exchange, Integer code, String msg) {        HashMap<Object, Object> hashMap = new HashMap<>();        hashMap.put("code", code);        hashMap.put("msg", msg);        String message = JSON.toJSONString(hashMap);        byte[] bytes = message.getBytes(StandardCharsets.UTF_8);        return this.responseHeader(exchange).getResponse()                .writeWith(Flux.just(exchange.getResponse().bufferFactory().wrap(bytes)));    }    /**     * 设置响应体的请求头     */    public ServerWebExchange responseHeader(ServerWebExchange exchange) {        ServerHttpResponse response = exchange.getResponse();        response.getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/json");        return exchange.mutate().response(response).build();    }    @Override    public int getOrder() {        return 0;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153

上述代码中可以只看下图部分,其他拦截授权等demo可以无视

通过上面我们自定义拦截器,对request进行了增强,在header中添加了一个traceId,值呢就是用UUID生成出来的随机字符串
同时使用MDC将traceId进行了put操作
下面我们会用MDC进行日志打印相关操作

2:网关配置文件
logging:  file:    path: /opt/log/gateway  config: classpath:logbak-conf.xml
  • 1
  • 2
  • 3
  • 4
3:网关日志配置文件logbak
<?xml version="1.0" encoding="UTF-8" ?><configuration debug="false">    <!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->    <property name="LOG_HOME"              value="${LOG_PATH:-.}"/>    <!-- 控制台输出设置 -->    <!-- 彩色日志格式,magenta:洋红,boldMagenta:粗红,yan:青色,·⊱══> -->    <property name="CONSOLE_LOG_PATTERN"              value="%boldMagenta([%d{yyyy-MM-dd HH:mm:ss.SSS}]) %cyan([%X{traceId}]) %boldMagenta(%-5level) %blue(%logger{15}) %magenta(==>) %cyan(%msg%n)"/>    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">        <encoder>            <pattern>${CONSOLE_LOG_PATTERN}</pattern>            <charset>utf8</charset>        </encoder>    </appender>    <!-- 按天输出日志设置 -->    <appender name="DAY_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">            <!-- 日志文件输出的文件名 -->            <FileNamePattern>${LOG_HOME}/%d{yyyy-MM-dd}_ms_gateway.%i.log            </FileNamePattern>            <!-- 日志文件保留天数 -->            <MaxHistory>7</MaxHistory>            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">                <maxFileSize>50MB</maxFileSize>            </timeBasedFileNamingAndTriggeringPolicy>        </rollingPolicy>        <filter class="ch.qos.logback.classic.filter.LevelFilter">            <level>INFO</level>             <!-- 设置拦截的对象为INFO级别日志 -->            <onMatch>ACCEPT</onMatch>       <!-- 当遇到了INFO级别时,启用改段配置 -->            <onMismatch>DENY</onMismatch>   <!-- 没有遇到INFO级别日志时,屏蔽改段配置 -->        </filter>        <encoder                class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">            <!-- 格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->            <pattern>%cyan([%X{traceId}]) %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>        </encoder>    </appender>    <!-- 按天输出ERROR级别日志设置 -->    <appender name="DAY_ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">            <!-- 日志文件输出的文件名 -->            <FileNamePattern>${LOG_HOME}/%d{yyyy-MM-dd}_ms_gateway_error.%i.log            </FileNamePattern>            <!-- 日志文件保留天数 -->            <MaxHistory>7</MaxHistory>            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">                <maxFileSize>50MB</maxFileSize>            </timeBasedFileNamingAndTriggeringPolicy>        </rollingPolicy>        <filter class="ch.qos.logback.classic.filter.LevelFilter">            <level>ERROR</level>            <!-- 设置拦截的对象为ERROR级别日志 -->            <onMatch>ACCEPT</onMatch>       <!-- 当遇到了ERROR级别时,启用改段配置 -->            <onMismatch>DENY</onMismatch>   <!-- 没有遇到ERROR级别日志时,屏蔽改段配置 -->        </filter>        <encoder                class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">            <!-- 格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->            <pattern>%cyan([%X{traceId}]) %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>        </encoder>    </appender>    <!-- 日志输出级别,OFF level > FATAL > ERROR > WARN > INFO > DEBUG > ALL level -->    <logger name="com.sand" level="INFO"/>    <logger name="com.apache.ibatis" level="INFO"/>    <logger name="java.sql.Statement" level="INFO"/>    <logger name="java.sql.Connection" level="INFO"/>    <logger name="java.sql.PreparedStatement" level="INFO"/>    <logger name="org.springframework" level="WARN"/>    <logger name="com.baomidou.mybatisplus" level="WARN"/>    <!-- 开发环境:打印控制台和输出到文件 -->    <springProfile name="dev">        <root level="INFO">            <appender-ref ref="CONSOLE"/>            <appender-ref ref="DAY_FILE"/>            <appender-ref ref="DAY_ERROR_FILE"/>        </root>    </springProfile>    <!-- 生产环境:打印控制台和输出到文件 -->    <springProfile name="pro">        <root level="INFO">            <appender-ref ref="CONSOLE"/>            <appender-ref ref="DAY_FILE"/>            <appender-ref ref="DAY_ERROR_FILE"/>        </root>    </springProfile></configuration>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85

上述配置文件中的 [%X{traceId}] 可以将我们通过MDC.put操作设置的值带入进来,这样就可以将traceId打印到日志里了。

4:接口入口服务aop切面接收traceId
package com.weibo.platform.aop;import com.alibaba.fastjson.JSON;import com.fasterxml.jackson.databind.ObjectMapper;import com.weibo.common.enums.BizExceptionEnum;import com.weibo.common.exception.BizException;import com.weibo.common.resp.ApiResponse;import org.apache.dubbo.rpc.RpcContext;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.reflect.MethodSignature;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.slf4j.MDC;import org.springframework.stereotype.Component;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;import java.lang.reflect.Method;/** * 日志aop * 记录对外,依赖服务的请求数据 */@Component@Aspectpublic class LogAspect {    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);    private static final String TRACE_ID = "traceId";    ObjectMapper mapper = new ObjectMapper();    /**     * 外部接口调用的日志监控     *     * @param joinPoint 连接点     * @return {@link Object}     */    @Around(value = "execution(* com.weibo.platform.controller..*.* (..))")    public Object doRequestAround(ProceedingJoinPoint joinPoint) throws Throwable {        try {            // 日志链路            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();            assert attributes != null;            HttpServletRequest request = attributes.getRequest();            String traceId = request.getHeader(TRACE_ID);            MDC.put(TRACE_ID, traceId);            RpcContext.getContext().setAttachment(TRACE_ID, traceId);            // 参数打印            Object result;            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();            Method method = methodSignature.getMethod();            String name = method.getName();            Object[] args = joinPoint.getArgs();            Object object = joinPoint.getTarget();            log.info("class :{}, method :{}, param :{}", object.getClass().getName(), name, mapper.writeValueAsString(args));            result = joinPoint.proceed();            log.info("class :{}, method :{}, result :{}", object.getClass().getName(), name, genResultString(result));            return result;        } catch (Exception e) {            log.error("Error :", e);            if (!(e instanceof BizException)) {                return new ApiResponse<>(BizExceptionEnum.SYS_ERROR);            } else {                return new ApiResponse<>(((BizException) e).getCode(), ((BizException) e).getMsg());            }        }    }    /**     * 创结果字符串     *     * @param result 结果     * @return {@link String}     */    private String genResultString(Object result) {        //如果结果为空,只直接返回        if (result == null) {            return null;        }        String val = JSON.toJSONString(result);        if (val.length() > 1024) {            return val.substring(0, 1023);        }        return val;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93

上述代码中比较重要的部分是开头部分,入下图

  1. 从请求头中获取网关服务添加的traceId
  2. 将traceId设置到MDC中(用于该服务日志打印traceId,也需要logbak.xml)
  3. 将traceId设置到dubbo的RpcContext中(用于将traceId传递到下个服务,后续微服务间联动都将traceId通过RpcContext传递)
5:统一全局返回值,返回值中加traceId字段

所有的接口均使用全局响应实体返回,返回的时候通过MDC自动将traceId设置到返回值中

package com.weibo.common.resp;import com.weibo.common.enums.BizExceptionEnum;import lombok.Data;import org.slf4j.MDC;import java.io.Serializable;@Datapublic class ApiResponse<T> implements Serializable {    private static final long serialVersionUID = -6025817568658364567L;    private static final String TRACE_ID = "traceId";    private Integer code;    private String msg;    private T data;    private String traceId;    public ApiResponse(Integer code, String msg) {        this.traceId = MDC.get(TRACE_ID);        this.code = code;        this.msg = msg;        this.data = null;    }    public ApiResponse(Integer code, String msg, T data) {        this.traceId = MDC.get(TRACE_ID);        this.code = code;        this.msg = msg;        this.data = data;    }    public ApiResponse(T data) {        this.traceId = MDC.get(TRACE_ID);        this.code = BizExceptionEnum.SUCCESS.getCode();        this.msg = BizExceptionEnum.SUCCESS.getMsg();        this.data = data;    }    public ApiResponse(BizExceptionEnum enums) {        this.traceId = MDC.get(TRACE_ID);        this.code = enums.getCode();        this.msg = enums.getMsg();        this.data = null;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
6:通过aop全局捕获异常封装全局响应


aop执行方法时一旦发生异常,将捕获异常,然后封装全局响应对象返回给前端,不将异常外漏,如果是自定义业务异常,同样的道理将异常信息的code和msg返回给前端。

7:用户服务通过RpcContext获取traceId
package com.weibo.user.filter;import org.apache.dubbo.rpc.RpcContext;import org.slf4j.MDC;import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;public class RpcFilter implements HandlerInterceptor {   private static final String TRACE_ID = "traceId";   /**    * 目标方法执行前    * 该方法在控制器处理请求方法前执行,其返回值表示是否中断后续操作    * 返回 true 表示继续向下执行,返回 false 表示中断后续操作    *    * @param request  请求    * @param response 响应    * @param handler  处理程序    * @return boolean    */   @Override   public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {       String traceId = RpcContext.getContext().getAttachment(TRACE_ID);       MDC.put(TRACE_ID, traceId);       return true;   }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

效果:

网关服务日志:
traceId:fd6f8174714745f4a1ac7dada1b3949b

入口服务日志:
traceId:fd6f8174714745f4a1ac7dada1b3949b
返回值:
traceId:fd6f8174714745f4a1ac7dada1b3949b

这样就可以在请求的返回值中获取traceId,一旦有异常或者错误,可以通过返回的这个traceId进行日志搜索、问题排查。

备注:

  1. 除了网关服务外,其他服务均需要添加logbak.xml文件实现打印traceId。
  2. 各rpc微服务间通过dubbo的RpcContext来进行传递。

综上就可以实现通过一个traceId查询到全链路的日志了。

网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发