Spring Cloud Gateway implementa el seguimiento completo de registros de enlaces a través de traceId

pregunta:

En el proceso de desarrollo diario, si se utiliza la arquitectura de microservicio, entonces la consulta de registro es un problema. Por ejemplo, el servicio A llama al servicio B y el servicio B llama al servicio C. En este momento, el servicio C informa un error, lo que hace que la solicitud completa para fallar anormalmente Si desea solucionar este problema, si no hay integración de registro, será muy problemático para nosotros solucionar la causa del problema

solución:

Cuando el servicio de la puerta de enlace recibe una solicitud, se genera un traceId y luego se pasa el traceId entre cada servicio. Al mismo tiempo, el traceId se imprime junto cuando se imprime el registro, de modo que al usar ELK para consultar el registro, usted solo necesita buscar un traceId. Puede consultar la información de registro de enlace completa de toda la solicitud.

Preparar:

1: agregar un interceptor personalizado al servicio de puerta de enlace
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
@Component
public 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;
    }

}

En el código anterior, solo puede mirar la parte en la figura a continuación, y otras demostraciones, como la autorización de interceptación, pueden ignorarse.
inserte la descripción de la imagen aquí
A través del interceptor personalizado anterior, la solicitud se mejora y se agrega un traceId al encabezado. El valor es una cadena aleatoria generada por UUID
Al mismo tiempo, el traceId se pone en funcionamiento usando MDC
A continuación, usaremos MDC para realizar operaciones relacionadas con la impresión de registros.

2: archivo de configuración de puerta de enlace
logging:
  file:
    path: /opt/log/gateway
  config: classpath:logbak-conf.xml
3: Logbak del archivo de configuración del registro de la puerta de enlace
<?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>

[%X{traceId}] en el archivo de configuración anterior puede traer el valor que establecimos a través de la operación MDC.put, de modo que el traceId se pueda imprimir en el registro.

4: El aspecto aop del servicio de entrada de interfaz recibe 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
@Aspect
public 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;
    }
}

La parte más importante del código anterior es la parte inicial, como se muestra en la siguiente figura
inserte la descripción de la imagen aquí

  1. Obtenga el traceId agregado por el servicio de puerta de enlace del encabezado de la solicitud
  2. Establezca traceId en MDC (para que el registro de servicio imprima traceId, también se requiere logbak.xml)
  3. Establezca traceId en RpcContext de dubbo (usado para pasar el traceId al siguiente servicio, y el enlace subsiguiente entre microservicios pasará el traceId a través de RpcContext)
5: unifique el valor de retorno global, agregue el campo traceId en el valor de retorno

Todas las interfaces se devuelven mediante la entidad de respuesta global y el traceId se establece automáticamente en el valor de retorno a través de MDC al devolver

package com.weibo.common.resp;

import com.weibo.common.enums.BizExceptionEnum;
import lombok.Data;
import org.slf4j.MDC;

import java.io.Serializable;

@Data
public 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;
    }

}
6: Encapsule la respuesta global capturando excepciones globalmente a través de aop

inserte la descripción de la imagen aquí
Una vez que ocurre una excepción cuando aop ejecuta el método, capturará la excepción, luego encapsulará el objeto de respuesta global y lo devolverá al front-end, sin filtrar la excepción.Si es una excepción comercial personalizada, el código y el mensaje de la excepción la información será devuelta al front-end de la misma manera.

7: el servicio de usuario obtiene traceId a través de RpcContext
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;
   }
}

Efecto:

Registro de servicio de puerta de enlace:
traceId: fd6f8174714745f4a1ac7dada1b3949b
inserte la descripción de la imagen aquí
Registro de servicio de ingreso:
traceId: fd6f8174714745f4a1ac7dada1b3949b inserte la descripción de la imagen aquí
Valor de retorno:
traceId: fd6f8174714745f4a1ac7dada1b3949b
inserte la descripción de la imagen aquí
para que el valor de retorno de la solicitud pueda ser Obtenga el traceId en el , una vez que haya una excepción o un error, puede usar el traceId devuelto para la búsqueda de registros y la solución de problemas.

Observación:

  1. Excepto el servicio de puerta de enlace, otros servicios deben agregar el archivo logbak.xml para imprimir traceId.
  2. El RpcContext de dubbo se usa para transferir entre microservicios rpc.

En resumen, puede consultar los registros de todo el enlace a través de un traceId.

Supongo que te gusta

Origin blog.csdn.net/Ellis_li/article/details/127083428
Recomendado
Clasificación