分布式微服务架构日志调用链路跟踪-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
    秒杀 : 做一个完善的全链路日志实现方案有多简单

相关推荐

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

    2023-12-16 08:50:03       47 阅读
  2. 分布式微服务 - 3.服务调用 - 1.概念

    2023-12-16 08:50:03       16 阅读
  3. springboot单体项目日志跟踪及接口耗时

    2023-12-16 08:50:03       18 阅读
  4. 分布式微服务 - 总概

    2023-12-16 08:50:03       20 阅读

最近更新

  1. TCP协议是安全的吗?

    2023-12-16 08:50:03       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2023-12-16 08:50:03       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2023-12-16 08:50:03       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2023-12-16 08:50:03       20 阅读

热门阅读

  1. Zlmediakit 接收到 rtc包后的处理流程

    2023-12-16 08:50:03       37 阅读
  2. Vue2面试题:说一下对vuex的理解?

    2023-12-16 08:50:03       39 阅读
  3. c语言 关于逻辑运算符

    2023-12-16 08:50:03       38 阅读
  4. 2312d,d语言作为胶水,用C++调用rust

    2023-12-16 08:50:03       41 阅读
  5. Linux 服务器使用 ssh 密钥登录

    2023-12-16 08:50:03       39 阅读
  6. Spring Mybatis随记

    2023-12-16 08:50:03       39 阅读
  7. Mac下ERROR: Cannot connect to the Docker daemon

    2023-12-16 08:50:03       43 阅读
  8. BOOST_MP11_VERSION宏用法的测试程序

    2023-12-16 08:50:03       45 阅读