分布式微服务架构日志调用链路跟踪-traceId

分布式微服务架构日志调用链路跟踪-traceId

在ELK日志集成平台里(日志的写入,收集,跟踪,搜索,分析)

背景知识

在xxx(博主之前的公司),每个前端请求里面,都会在request的header区携带一个traceId 随机数值,用来跟踪在后端的调用链路栈打印.通过ES收集的日志数据,在ELK日志集成平台里,用traceId就能得到用户请求的完整链路,排查问题的效率非常高.那么具体是怎么设计的?

同样的,每个请求的用户会话信息该怎么传递?

traceId,UserId,facilityId,tenantId.

遇到的场景

有几个难题需要思考: 1.客户端调用方法中,遇到了rpc该怎么传递traceId? 2.遇到了feign接口该怎么传递traceId? 3.遇到了mq该怎么传递tranceId? 4.遇到了线程池里的新线程该怎么传递tranceId? 下面针对各个场景进行逐一的分析

xxx的实现方案:

在gateway中对sid进行校验和初步存储 gateway-GlobalFilter-SIDCheckedFilter.java gateway的原理:一系列的router和filter集合.

常见:

  • IP白名单和黑名单控制
  • SID检测
  • 请求签名检测,防止恶意构建请求
  • 用户会话检测,防止非系统用户请求
复制代码
  private static String getSID(ServerHttpRequest request) {
      String sid = request.getHeaders().getFirst(Constants.HEADER_SID);
      // 将sid放到日志的变量中
      MDC.put(Constants.HEADER_SID,sid);
      return sid;
  }
  @Override
  public Mono<Void> execute(ServerHttpRequest request, ServerWebExchange exchange, GatewayFilterChain chain) {
      String sid = getSID(request);
      if(logger.isInfoEnabled()) {
          String url = StringUtils.defaultIfBlank(request.getPath().value(), "");
          String ip = IpUtils.getIp(request);
          MDC.put(MDC_SID,sid);
          MDC.put(MDC_URI,url);
          MDC.put(MDC_IP,ip);
          MDC.put(MDC_USER,null);
          logger.info("网关请求..");
      }
      if (validSid) {
          if (StringUtils.isBlank(sid)) {
              logger.info("{} SID为空", this.getRequestUri(request));
              // 构造返回错误信息
              ApiResponse<String> responseMap = ApiResponse.failResp(RequestCode.SID_ISNULL);
              return returnError(exchange, HttpStatus.BAD_REQUEST, responseMap);
          }
          // 解决高并发状态的多次重复请求问题
          String redisSidKey = GatewayRedisKeyConstants.GATEWAY_SID_KEY_PREFIX + sid;
          boolean isSuccess = redisTemplate.opsForValue().setIfAbsent(redisSidKey, GATEWAY_SID_KEY_DEF_VAL, GATEWAY_SID_KEY_EXPIRE_TIME, TimeUnit.MINUTES);
          if (!isSuccess) {
              logger.info("SID已存在,重复请求:{}", sid);
              ApiResponse<String> responseMap = ApiResponse.failResp(RequestCode.SID_REPEATED_REQUESTS);
              return returnError(exchange, HttpStatus.BAD_REQUEST, responseMap);
          }
      }
      exchange = afterValidHandler(exchange);//验证过后处理器
      return chain.filter(exchange);
  }


  /**
   * 验证过后处理器
   *
   * @param exchange
   * @return
   */
  @Override
  public ServerWebExchange afterValidHandler(ServerWebExchange exchange) {
      ServerHttpRequest request = exchange.getRequest();
      String sid = request.getHeaders().getFirst(Constants.HEADER_SID);
      //不要验证SID的情况下,如果SI为空,默认生成一个SID,供下游系统使用
      if (StringUtils.isBlank(sid)) {
          //生成sid
          String gatewaySid = "gateway_" + DateTimeUtils.format(LocalDateTime.now(), DateTimeUtils.FORMAT_1) + RandomUtil.randomNumbers(6);
          //构造新的ServerHttpRequest
          ServerHttpRequest.Builder builder = request.mutate()
                  //往header中添加网关生成的SID
                  .header(com.ess.framework.commons.constant.Constants.HEADER_SID, gatewaySid);
          // 将sid放到日志的变量中
          MDC.put(Constants.HEADER_SID,gatewaySid);
          exchange = exchange.mutate().request(builder.build()).build();
      }
      return exchange;
  }

定义个framework-boot的依赖包

这个依赖包内使用interceptor拦截器对web请求做拦截,拿到traceId,通过threadLocal对象放入一个Context对象中.每个请求对应一个ThreadLocal对象. 每个服务的全局拦截器

复制代码
  @RefreshScope
  public abstract class BootWebConfigurer implements WebMvcConfigurer {
      protected final Logger logger = LoggerFactory.getLogger(this.getClass());

      @Autowired
      private RequestMappingHandlerAdapter requestMappingHandlerAdapter;

      @Bean
      @RefreshScope
      public GatewayInterceptor getGatewayInterceptor() {
          return new GatewayInterceptor();
      }

      @Bean
      @RefreshScope
      public SIDInterceptor getSIDInterceptor() {
          return new SIDInterceptor();
      }

      @Bean
      @RefreshScope
      public InnerInterceptor getInnerInterceptor() {
          return new InnerInterceptor();
      }

      @Bean
      public TokenInterceptor getTokenInterceptor() {
          return new TokenInterceptor();
      }

      @Bean
      public LogMdcInterceptor getLogMdcInterceptor() {
          return new LogMdcInterceptor();
      }

      @Bean
      public UnionInterceptor getUnionInterceptor() {
          return new UnionInterceptor();
      }

      @Override
      public void addInterceptors(InterceptorRegistry registry) {
          registry.addInterceptor(getSIDInterceptor()).addPathPatterns("/**").excludePathPatterns(ResouceConstants.EXCLUDE_STATIC_RESOURCE);
          registry.addInterceptor(getGatewayInterceptor()).addPathPatterns("/**").excludePathPatterns(ResouceConstants.EXCLUDE_STATIC_RESOURCE);
          registry.addInterceptor(getInnerInterceptor()).addPathPatterns("/**").excludePathPatterns(ResouceConstants.EXCLUDE_STATIC_RESOURCE);
          registry.addInterceptor(getTokenInterceptor()).addPathPatterns("/**").excludePathPatterns(ResouceConstants.EXCLUDE_STATIC_RESOURCE);
          registry.addInterceptor(getLogMdcInterceptor()).addPathPatterns("/**").excludePathPatterns(ResouceConstants.EXCLUDE_STATIC_RESOURCE);
          // 添加联盟拦截器
          registry.addInterceptor(getUnionInterceptor()).addPathPatterns("/**").excludePathPatterns(ResouceConstants.EXCLUDE_STATIC_RESOURCE);
      }

      /**
       * 初始化SIDReturnValueHandler  对所有的响应返回SID字段
       */
      @PostConstruct
      public void initSidHandler() {
          final List<HandlerMethodReturnValueHandler> originalHandlers = new ArrayList<>(requestMappingHandlerAdapter.getReturnValueHandlers());
          final int deferredPos = obtainValueHandlerPosition(originalHandlers, DeferredResultMethodReturnValueHandler.class);
          SIDReturnValueHandler decorator = null;
          for (HandlerMethodReturnValueHandler handler : originalHandlers) {
              if (handler instanceof RequestResponseBodyMethodProcessor) {
                  decorator = new SIDReturnValueHandler((RequestResponseBodyMethodProcessor) handler);
                  break;
              }
          }
          originalHandlers.add(deferredPos + 1, decorator);
          requestMappingHandlerAdapter.setReturnValueHandlers(originalHandlers);
      }

      private int obtainValueHandlerPosition(final List<HandlerMethodReturnValueHandler> originalHandlers, Class<?> handlerClass) {
          for (int i = 0; i < originalHandlers.size(); i++) {
              final HandlerMethodReturnValueHandler valueHandler = originalHandlers.get(i);
              if (handlerClass.isAssignableFrom(valueHandler.getClass())) {
                  return i;
              }
          }
          return -1;
      }

      @Override
      public void addResourceHandlers(ResourceHandlerRegistry registry) {
          registry.addResourceHandler("/");
          registry.addResourceHandler("/favicon.ico").addResourceLocations("classpath:/favicon.ico");
          registry.addResourceHandler("app.html").addResourceLocations("classpath:/");
          registry.addResourceHandler("/error");
          // Swagger2配置
          registry.addResourceHandler("/v2/**");
          registry.addResourceHandler("/swagger-resources/**");
          registry.addResourceHandler("/csrf");
          registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
          registry.addResourceHandler("/swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
          registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
      }

      /**
       * 自定义扩展消息转换器
       *
       * @param converters
       */
      @Override
      public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
          //定义fastjson转换消息对象
          FastJsonHttpMessageConverter fastJsonConverter = new FastJsonHttpMessageConverter();
          //添加fastjson全局配置
          FastJsonConfig fastJsonConfig = new FastJsonConfig();
          fastJsonConfig.setSerializerFeatures(
                  // 排序配置
                  SerializerFeature.SortField,
                  SerializerFeature.MapSortField,
                  // 避免对象重复引用
                  SerializerFeature.DisableCircularReferenceDetect,
                  // 格式化输出
                  SerializerFeature.PrettyFormat
          );
          fastJsonConfig.setCharset(Charset.forName("UTF-8"));
          fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
          //默认json会对属性里的json字符串值进行排序,加了这个Feature.OrderedField则会禁止排序
          fastJsonConfig.setFeatures(Feature.OrderedField);
          fastJsonConverter.setFastJsonConfig(fastJsonConfig);
          //添加支持的MediaType类型
          fastJsonConverter.setSupportedMediaTypes(Lists.newArrayList(MediaType.APPLICATION_JSON));
          converters.set(0, fastJsonConverter);

          //BigDecimal格式化
          SerializeConfig serializeConfig = SerializeConfig.getGlobalInstance();
          serializeConfig.put(BigDecimal.class, BigDecimalConfigure.instance);
          fastJsonConfig.setSerializeConfig(serializeConfig);

          //字节数组消息转换器(供文件下载使用)
          converters.add(new ByteArrayHttpMessageConverter());

          Map<String, HttpMessageConverter<?>> convertersMap = new LinkedHashMap<>();
          //转换器去重,并设置所有转换器的默认编码
          for (HttpMessageConverter converter : converters) {
              String name = converter.getClass().getSimpleName();
              if (converter instanceof StringHttpMessageConverter) {
                  //设置StringHttpMessageConverter的默认编码为:UTF-8
                  ((StringHttpMessageConverter) converter).setDefaultCharset(Charset.forName("UTF-8"));
              }
              if (!convertersMap.containsKey(name)) {
                  convertersMap.put(name, converter);
              }
          }
          converters.clear();
          converters.addAll(Lists.newArrayList(convertersMap.values()));
      }


      @Bean
      public Converter<String, LocalDate> localDateConverter() {
          return new Converter<String, LocalDate>() {
              @Override
              public LocalDate convert(String source) {
                  return LocalDate.parse(source, Constants.DATE_FORMATTER);
              }
          };
      }

      @Bean
      public Converter<String, LocalDateTime> localDateTimeConverter() {
          return new Converter<String, LocalDateTime>() {
              @Override
              public LocalDateTime convert(String source) {
                  return LocalDateTime.parse(source, Constants.DATE_TIME_FORMATTER);
              }
          };
      }

      /**
       * Java常用时间类型序列化和反序列化
       * LocalDateTime、LocalDate、LocalTime、java.util.Date、java.sql.Date、Calendar、Timestamp
       * 解决通过Feign调用时,客户端是以上类型会报错问题。
       */
      @Bean
      public ObjectMapper objectMapper() {
          ObjectMapper objectMapper = new ObjectMapper();
          JavaTimeModule javaTimeModule = new JavaTimeModule();
          //定义序列化日期时间类型转换器
          javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeUtils.DEFAULT_FORMATTER));
          javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeUtils.YEAR_MONTH_DAY_FORMATTER));
          javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeUtils.HOUR_MINUTE_SECOND_FORMATTER));
          javaTimeModule.addSerializer(Date.class, new DateSerializer(true, new SimpleDateFormat(DateTimeUtils.DEFAULT_FORMAT)));
          javaTimeModule.addSerializer(java.sql.Date.class, new SqlDateSerializer().withFormat(true, new SimpleDateFormat(DateTimeUtils.DEFAULT_FORMAT)));
          javaTimeModule.addSerializer(Calendar.class, new CalendarSerializer(true, new SimpleDateFormat(DateTimeUtils.DEFAULT_FORMAT)));
          //定义反序列化日期时间类型转换器
          javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeUtils.DEFAULT_FORMATTER));
          javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeUtils.YEAR_MONTH_DAY_FORMATTER));
          javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeUtils.HOUR_MINUTE_SECOND_FORMATTER));
          javaTimeModule.addDeserializer(Date.class, new DateDeserializers.DateDeserializer(DateDeserializers.DateDeserializer.instance,
                  new SimpleDateFormat(DateTimeUtils.DEFAULT_FORMAT), DateTimeUtils.DEFAULT_FORMAT));
          javaTimeModule.addDeserializer(java.sql.Date.class, new DateDeserializers.SqlDateDeserializer(new DateDeserializers.SqlDateDeserializer(),
                  new SimpleDateFormat(DateTimeUtils.DEFAULT_FORMAT), DateTimeUtils.DEFAULT_FORMAT));
          javaTimeModule.addDeserializer(Calendar.class, new DateDeserializers.CalendarDeserializer(new DateDeserializers.CalendarDeserializer(),
                  new SimpleDateFormat(DateTimeUtils.DEFAULT_FORMAT), DateTimeUtils.DEFAULT_FORMAT));
          javaTimeModule.addDeserializer(Timestamp.class, new DateDeserializers.TimestampDeserializer(new DateDeserializers.TimestampDeserializer(),
                  new SimpleDateFormat(DateTimeUtils.DEFAULT_FORMAT), DateTimeUtils.DEFAULT_FORMAT));
          objectMapper.registerModule(javaTimeModule);

          objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

          return objectMapper;
      }
  }

sid拦截器

复制代码
  package com.ess.framework.boot.interceptor;

  import com.ess.framework.commons.constant.Constants;
  import com.ess.framework.commons.response.RequestCode;
  import com.ess.framework.commons.utils.EssContextHolder;
  import com.ess.framework.commons.utils.ExceptionUtils;
  import org.apache.commons.lang3.StringUtils;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  import org.springframework.beans.factory.annotation.Value;
  import org.springframework.cloud.context.config.annotation.RefreshScope;
  import org.springframework.web.servlet.HandlerInterceptor;

  import javax.servlet.http.HttpServletRequest;
  import javax.servlet.http.HttpServletResponse;

  /**
   * SID 拦截器 ,验证SID是否为空
   */
  @RefreshScope
  public class SIDInterceptor implements HandlerInterceptor {
      protected final Logger logger = LoggerFactory.getLogger(this.getClass());

      /**
       * 是否验证SID
       */
      @Value("${valid.sid:false}")
      private boolean validSid = false;

      @Override
      public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
          String sid = request.getHeader(Constants.HEADER_SID);
          // 验证sid是否为空
          if (validSid && StringUtils.isBlank(sid)) {
              logger.warn("{} {}", request.getRequestURI(), RequestCode.SID_ISNULL.message());
              ExceptionUtils.throwBusiness(RequestCode.SID_ISNULL);
          }
          // 设置SID到线程变量 这是传输的关键
          if (StringUtils.isNotBlank(sid)) {
              EssContextHolder.setSID(sid);
          } else {
              EssContextHolder.setSID(null);
          }
          return true;
      }
  }

关于feign接口的传递tranceId

复制代码
  package com.ess.framework.boot.interceptor;

  import com.ess.framework.commons.constant.Constants;
  import com.ess.framework.commons.utils.EssContextHolder;
  import com.ess.framework.commons.utils.IpUtils;
  import feign.RequestInterceptor;
  import feign.RequestTemplate;
  import lombok.extern.slf4j.Slf4j;
  import org.apache.commons.lang3.StringUtils;
  import org.slf4j.MDC;
  import org.springframework.web.context.request.RequestContextHolder;
  import org.springframework.web.context.request.ServletRequestAttributes;

  import javax.servlet.http.HttpServletRequest;

  /**
   * @ClassName FeignRequestInterceptor
   * @Description feign拦截器
   * @Date 2021/5/27 21:56
   * @Version
   */
  @Slf4j
  public class FeignRequestInterceptor implements RequestInterceptor {
      private final static String HEADER_ESS_FEIGN_IP = "ess-feign-ip";
      private final static String HEADER_IP = "ip";

      @Override
      public void apply(RequestTemplate requestTemplate) {
          try {
              String ip = null;
              String sid = null;
              String token = null;
              String unionId = null;
              /* ExecuteTaskUtils 线程池处理方案**/
              String threadName = Thread.currentThread().getName();
              if (StringUtils.startsWith(threadName, "ess-task-pool") || StringUtils.startsWith(threadName, "ess-async-pool")) {
                  sid = MDC.get(Constants.HEADER_SID);
                  ip = MDC.get(HEADER_IP);
                  unionId  = EssContextHolder.getUnionId();
                  token = EssContextHolder.getToken();
              } else {
                  ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                  if (attributes == null) {
                      return;
                  }
                  HttpServletRequest request = attributes.getRequest();
                  token = request.getHeader(Constants.HEADER_TOKEN);
                  sid = request.getHeader(Constants.HEADER_SID);
                  ip = IpUtils.getIPAddress(request);
                  unionId  = request.getHeader(Constants.HEADER_UNIONID);
              }
              requestTemplate.header(Constants.HEADER_TOKEN, token);
              requestTemplate.header(Constants.HEADER_SID, sid);
              requestTemplate.header(Constants.HEADER_UNIONID, unionId);
              requestTemplate.header(HEADER_ESS_FEIGN_IP, ip);
              // feign调用都设置网关标识设置为true,防止被拦截
              requestTemplate.header(Constants.PASS_GATEWAY, "true");
          } catch (Exception e) {
              log.error("拦截器异常", e);
          }
      }
  }

由此可见,feign请求的本质就是requestTemplate,而原理上,就是把sid重新塞到request的header里面.

线程池sid的传递

那么怎么传递到另外的线程里呢? ==答案就是threadLocal作为管道存储对象.

复制代码
  package com.ess.framework.commons.utils;

  /**
   * 线程变量上下文
   */
  public class EssContextHolder {

      private EssContextHolder() {
      }

      /**
       * sid
       */
      private final static ThreadLocal<String> SID = new ThreadLocal<>();

      /**
       * token
       */
      private final static ThreadLocal<String> TOKEN = new ThreadLocal<>();

      /**
       * 联盟code
       */
      private final static ThreadLocal<String> UNION_CODE = new ThreadLocal<>();

      /**
       * 联盟unionId
       */
      private final static ThreadLocal<String> UNION_ID = new ThreadLocal<>();

      /**
       * 设置SID
       *
       * @param sid
       */
      public static void setSID(String sid) {
          EssContextHolder.SID.set(sid);
      }

      /**
       * 获取SID
       */
      public static String getSID() {
          return EssContextHolder.SID.get();
      }

      /**
       * 设置TOKEN
       *
       * @param token
       */
      public static void setToken(String token) {
          EssContextHolder.TOKEN.set(token);
      }

      /**
       * 获取TOKEN
       */
      public static String getToken() {
          return EssContextHolder.TOKEN.get();
      }

      /**
       * 设置unionCode
       */
      public static void setUnionCode(String unionCode) {
          EssContextHolder.UNION_CODE.set(unionCode);
      }

      /**
       * 获取unionCode
       */
      public static String getUnionCode() {
          return EssContextHolder.UNION_CODE.get();
      }


      /**
       * 设置unionId
       */
      public static void setUnionId(String unionId) {
          EssContextHolder.UNION_ID.set(unionId);
      }

      /**
       * 获取联盟unionId
       */
      public static String getUnionId() {
          return EssContextHolder.UNION_ID.get();
      }
  }

重载线程池实现细节,这样springboot在使用自定义线程池时,就会初始化个性化的实现细节,把sid等会话状态传递到线程的ThreadLocal里.

复制代码
  package com.ess.framework.boot.asynctask;

  import com.ess.framework.commons.utils.EssContextHolder;
  import org.slf4j.MDC;

  import java.util.concurrent.*;

  /**
   * @ClassName ttt
   * @Description TODO
   * @Author shengfq
   * @Date 2021/5/28 0028 上午 10:53
   * @Version
   */
  public class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {


      public ThreadPoolExecutorMdcWrapper(AsyncTaskThreadPoolConfig config,ThreadFactory threadFactory,RejectedExecutionHandler handler ) {
          this(config.getCorePoolSize(), config.getMaxPoolSize(), config.getKeepAliveSecond(), TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(config.getQueueCapacity()),threadFactory,handler);
      }

      public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                          BlockingQueue<Runnable> workQueue) {
          super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
      }

      public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                          BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
          super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
      }

      public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                          BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
          super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
      }

      public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                          BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
                                          RejectedExecutionHandler handler) {
          super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
      }

      @Override
      public void execute(Runnable task) {
          String sid =   EssContextHolder.getSID();
          String token = EssContextHolder.getToken();
          String unionId = EssContextHolder.getUnionId();
          super.execute(ThreadMdcUtil.wrap(sid,token,unionId,task, MDC.getCopyOfContextMap()));
      }

      @Override
      public <T> Future<T> submit(Runnable task, T result) {
          String sid =   EssContextHolder.getSID();
          String token = EssContextHolder.getToken();
          String unionId = EssContextHolder.getUnionId();
          return super.submit(ThreadMdcUtil.wrap(sid,token,unionId,task, MDC.getCopyOfContextMap()), result);
      }

      @Override
      public <T> Future<T> submit(Callable<T> task) {
          String sid =   EssContextHolder.getSID();
          String token = EssContextHolder.getToken();
          String unionId = EssContextHolder.getUnionId();
          return super.submit(ThreadMdcUtil.wrap(sid,token,unionId,task, MDC.getCopyOfContextMap()));
      }

      @Override
      public Future<?> submit(Runnable task) {
          String sid =   EssContextHolder.getSID();
          String token = EssContextHolder.getToken();
          String unionId = EssContextHolder.getUnionId();
          return super.submit(ThreadMdcUtil.wrap(sid,token,unionId,task, MDC.getCopyOfContextMap()));
      }
  }

mq消息的sid传递

如何传递给mq的消费者呢?实际上有了EssContextHolder对象,在Thread里获取sid很方便,如何通过mq传递sid,可以把sid放入message的header区,在消费端取出来,存入消费端线程的ThreadLocal对象中,就能起到传递的作用.

消息发送

消息接收

复制代码
  import java.io.IOException;
  import java.nio.charset.StandardCharsets;
  import java.util.Map;
  import java.util.function.Consumer;

  import org.hzero.core.exception.OptimisticLockException;
  import org.hzero.core.message.MessageAccessor;
  import org.springframework.amqp.core.Message;

  import com.rabbitmq.client.Channel;
  import com..mom.me.common.constants.MeBaseConstants;
  import com..mom.me.common.mq.MessageHandler;

  import cn.hutool.core.util.ObjectUtil;
  import cn.hutool.json.JSONObject;
  import cn.hutool.json.JSONUtil;

  import io.choerodon.core.exception.CommonException;
  import io.choerodon.core.oauth.CustomUserDetails;
  import io.choerodon.core.oauth.DetailsHelper;

  import lombok.extern.slf4j.Slf4j;


  /**
   * <p>
   * 消息处理类
   * </p>
   */
  @Slf4j
  public abstract class AbstractMessageHandlers implements MessageHandler {
      private AbstractMessageHandlers() {}

      public static void handleMessage(Message msg, Map<String, Object> headers, Channel channel, Consumer<String> fn) {
          String consumerQueue = msg.getMessageProperties().getConsumerQueue();
          try {
              log.trace("消息队列[{}]处理, headers={}", consumerQueue, headers);
              fn.accept(new String(msg.getBody(), StandardCharsets.UTF_8));
          } catch (CommonException e) {
              log.error("消息队列[{}], 消息处理失败业务异常->{}", consumerQueue,
                              MessageAccessor.getMessage(e.getCode(), e.getParameters(), e.getMessage()).desc());
          } catch (IllegalArgumentException e) {
              log.error("消息队列[{}], 消息处理失败业务异常: {}", consumerQueue, e.getMessage());
          } catch (OptimisticLockException e) {
              // 如果乐观锁异常 则触发重试
              log.warn("消息队列[{}], 数据处理乐观锁异常触发重试", consumerQueue);
              throw e;
          } catch (Exception e) {
              log.error("消息队列[{}], 消息处理失败系统异常", consumerQueue, e);
          }
      }

      /**
       * 携带用户会话信息 -> 消息头获取用户会话信息
       * 
       * 经AbstractMessageSender 发送MQ消息会在消息头附加用户会话信息
       * 
       * JSON示例如下
       * 
       * @param msg
       * @param headers
       * @param channel
       * @param fn
       */
      public static void handleWithUser(Message msg, Map<String, Object> headers, Channel channel, Consumer<String> fn) {
          try {
              // 生产执行发出的mq消息头带有用户会话信息 切勿用于其他系统、模块的mq消息处理
              Object userDetails = headers.get(Constants.HEADER_SID);
              if (ObjectUtil.isNull(userDetails)) {
                  throw new CommonException("MQ消息头获取用户会话信息失败!!!");
              }
              JSONObject jsonObject = JSONUtil.parseObj(userDetails);
              log.trace("设置用户会话信息,传入JSONObject信息: {}", jsonObject);

              CustomUserDetails details = new CustomUserDetails(jsonObject.getStr("username"), MeBaseConstants.DEFAULT);
              details.setTenantId(jsonObject.getLong("tenantId"));
              details.setUserId(jsonObject.getLong("userId"));
              details.setOrganizationId(jsonObject.getLong("organizationId"));
              details.setRealName(jsonObject.getStr("realName"));
              details.setLanguage(jsonObject.getStr("language"));
              details.setAdditionInfo(jsonObject.getJSONObject("additionInfo"));
              DetailsHelper.setCustomUserDetails(details);
              // 设置用户会话异常
          } catch (Exception e) {
              log.error("消息队列[{}], 消息处理失败", msg.getMessageProperties().getConsumerQueue(), e);
              return;
          }
          handleMessage(msg, headers, channel, fn);
      }

      public static void handleWithNack(Message msg, Map<String, Object> headers, Channel channel, Consumer<String> fn) {
          String consumerQueue = msg.getMessageProperties().getConsumerQueue();
          try {
              log.trace("消息队列[{}]处理, headers={}", consumerQueue, headers);
              fn.accept(new String(msg.getBody(), StandardCharsets.UTF_8));
              ack(msg, channel);
          } catch (CommonException e) {
              log.error("消息队列[{}], 消息处理失败业务异常->{}", consumerQueue,
                              MessageAccessor.getMessage(e.getCode(), e.getParameters(), e.getMessage()).desc());
              nack(msg, channel);
          } catch (IllegalArgumentException e) {
              log.error("消息队列[{}], 消息处理失败业务异常: {}", consumerQueue, e.getMessage());
              nack(msg, channel);
          } catch (Exception e) {
              log.error("消息队列[{}], 消息处理失败系统异常:{}", consumerQueue, e);
              nack(msg, channel);
          }
      }

      public static void handleWithUserThrowEx(Message msg, Map<String, Object> headers, Channel channel,
                      Consumer<String> fn) {
          String consumerQueue = msg.getMessageProperties().getConsumerQueue();

          try {
              // 生产执行发出的mq消息头带有用户会话信息 切勿用于其他系统、模块的mq消息处理
              Object userDetails = headers.get(MeBaseConstants.USER_DETAILS_KEY);
              if (ObjectUtil.isNull(userDetails)) {
                  throw new CommonException("MQ消息头获取用户会话信息失败!!!");
              }
              JSONObject jsonObject = JSONUtil.parseObj(userDetails);
              log.trace("设置用户会话信息,传入JSONObject信息: {}", jsonObject);

              CustomUserDetails details = new CustomUserDetails(jsonObject.getStr("username"), MeBaseConstants.DEFAULT);
              details.setTenantId(jsonObject.getLong("tenantId"));
              details.setUserId(jsonObject.getLong("userId"));
              details.setOrganizationId(jsonObject.getLong("organizationId"));
              details.setRealName(jsonObject.getStr("realName"));
              details.setLanguage(jsonObject.getStr("language"));
              details.setAdditionInfo(jsonObject.getJSONObject("additionInfo"));
              DetailsHelper.setCustomUserDetails(details);

              log.trace("消息队列[{}]处理, headers={}", consumerQueue, headers);
              fn.accept(new String(msg.getBody(), StandardCharsets.UTF_8));
          } catch (CommonException e) {
              log.error("消息队列[{}], 消息处理失败业务异常->{}, 详细信息:", consumerQueue,
                              MessageAccessor.getMessage(e.getCode(), e.getParameters(), e.getMessage()).desc(), e);
              throw e;
          } catch (Exception e) {
              log.error("消息队列[{}], 消息处理失败系统异常:{}", consumerQueue, e);
              throw e;
          }
      }

      private static void ack(Message msg, Channel channel) {
          try {
              long deliverTag = msg.getMessageProperties().getDeliveryTag();
              channel.basicAck(deliverTag, false);
          } catch (IOException ioe) {
              log.error("消息处理ack异常{}", ioe.getMessage(), ioe);
          }
      }

      private static void nack(Message msg, Channel channel) {
          try {
              long deliverTag = msg.getMessageProperties().getDeliveryTag();
              channel.basicNack(deliverTag, false, true);
          } catch (IOException ioe) {
              log.error("消息处理nack异常{}", ioe.getMessage(), ioe);
          }
      }

  }

总结

综上所述,sid的设置,拦截,传递,存储都有对应的机制.那么在日志系统中的存储则是通过org.slf4j.MDC;这个对象来实现的.

这个对象的实现机制不在本文中展开细节,后续日志的入库,检索,都是通过slf4j这个日志框架和ELK日志系统进行.本文着重讲解了在应用服务中一个跟踪请求的链路id是如何传递,存储的.

属于抛砖引玉的内容,内容较为主观,希望能对自己和其他开发者在分布式日志调用链路上有点启发.谢谢

参考

SpringBoot + MDC 实现全链路调用日志跟踪
微服务-网关Spring Gateway进阶-日志跟踪唯一ID
日志全链路追踪之MDC
秒杀 : 做一个完善的全链路日志实现方案有多简单

相关推荐
mghio6 小时前
Dubbo 中的集群容错
java·微服务·dubbo
数据智能老司机13 小时前
CockroachDB权威指南——CockroachDB SQL
数据库·分布式·架构
数据智能老司机14 小时前
CockroachDB权威指南——开始使用
数据库·分布式·架构
数据智能老司机14 小时前
CockroachDB权威指南——CockroachDB 架构
数据库·分布式·架构
IT成长日记14 小时前
【Kafka基础】Kafka工作原理解析
分布式·kafka
州周16 小时前
kafka副本同步时HW和LEO
分布式·kafka
爱的叹息18 小时前
主流数据库的存储引擎/存储机制的详细对比分析,涵盖关系型数据库、NoSQL数据库和分布式数据库
数据库·分布式·nosql
千层冷面19 小时前
RabbitMQ 发送者确认机制详解
分布式·rabbitmq·ruby
ChinaRainbowSea19 小时前
3. RabbitMQ 的(Hello World) 和 RabbitMQ 的(Work Queues)工作队列
java·分布式·后端·rabbitmq·ruby·java-rabbitmq
敖正炀19 小时前
基于RocketMQ的可靠消息最终一致性分布式事务解决方案
分布式