ccframe系统的链路追踪,用户ID追踪的实现

news2024/12/26 23:07:56

需求

之前ccframe cloud V1用的是springcloud微服务,只需要在header将jwttoken一直传下去就没事,最近弄V2转dubbo发现用户id没有自动保存进数据库表。于是开始研究dubbo如何追踪,顺便把链路追踪ID的问题给一并解决掉。

理论

MDC

MDC(Mapped Diagnostic Context,映射调试上下文)是Slf4j(提供了接口定义和核心实现,日志库负责适配器的实现)提供的一种方便在多线程条件下记录日志的功能。

MDC 可以看成是一个与当前线程绑定的Map,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。

简而言之,MDC就是日志框架提供的一个InheritableThreadLocal,项目代码中可以将键值对放入其中,然后使用指定方式取出打印即可。

RpcContext

RpcContext 本质上是一个使用 ThreadLocal 实现的临时状态记录器,RPC请求时会自动给传递给下游服务,当RPC请求结束的时候,当前线程的RpcContex会清空

实现

原理图

生成追踪信息

了解了MDC的原理可知,MDC数据是线程级别的,那么我们可以放在一次线程调用最靠前的位置。对于当前的ccframe系统,我们使用了spring security,而spring security的filter在请求比较靠前的位置,因此我们可以在JwtHeadFilter进行对应的处理。在解析完用户的信息后,将链路ID及会员ID放入MDC。链路ID这里采用Htool的短NanoId形式,因为本身请求会有时间属性,我们只需要在短时间(一次链路请求时间范围),例如几分钟内,不重复即可方便关联日志。这里采用2个记录量:

  1. userId 记录操作人
  2. traceId 记录一次链路请求
package org.ccframe.commons.auth;

import cn.hutool.core.lang.id.NanoId;
import com.alibaba.fastjson.JSON;
import io.jsonwebtoken.JwtException;
import org.apache.dubbo.common.utils.StringUtils;
import org.apache.dubbo.rpc.RpcContext;
import org.apache.http.HttpStatus;
import org.ccframe.commons.util.IpUtil;
import org.ccframe.commons.util.JwtUtil;
import org.ccframe.commons.util.UUIDUtil;
import org.ccframe.config.GlobalEx;
import org.ccframe.subsys.core.domain.code.RoleCodeEnum;
import org.ccframe.subsys.core.dto.Result;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.StringCodec;
import org.slf4j.MDC;
import org.springframework.context.MessageSource;
import org.springframework.http.MediaType;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.LocaleResolver;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;

public class JwtHeadFilter extends OncePerRequestFilter {

	private final RedissonClient redissonClient;

	private final LocaleResolver localeResolver;

	private final MessageSource messageSource;

	public JwtHeadFilter(RedissonClient redissonClient, MessageSource messageSource, LocaleResolver localeResolver) {
		this.redissonClient = redissonClient;
		this.localeResolver = localeResolver;
		this.messageSource = messageSource;
	}

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		String traceId = NanoId.randomNanoId(8);
		MDC.put(GlobalEx.TRACE_ID, traceId);
		RpcContext.getContext().setAttachment(GlobalEx.TRACE_ID, traceId); // 同时放入RPC上下文

		String uri = request.getRequestURI();
		String token = null;
		boolean adminUrl = false;
		if(uri.startsWith("/" + GlobalEx.ADMIN_URI_PERFIX)) { //后端用户
			token = request.getHeader(GlobalEx.ADMIN_TOKEN);
			adminUrl = true;
		}else if(uri.startsWith("/" + GlobalEx.API_URI_PERFIX)) { //前端用户
			token = request.getHeader(GlobalEx.API_TOKEN);
		}else {
			filterChain.doFilter(request, response);
			return;
		}
		if (!adminUrl && StringUtils.isEmpty(token)){ // 前台的直接退出了
			filterChain.doFilter(request,response);
			return;
		}

		List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
		TokenUser tokenUser = null;

		if(StringUtils.isNotEmpty(token)){ //有token情况
			try {
				tokenUser = JwtUtil.decodeData(token, TokenUser.class);
				authorityList = tokenUser.getRoleIds().stream().map(item->new SimpleGrantedAuthority("ROLE_"+item)).collect(Collectors.toList());
			}catch(IllegalArgumentException|JwtException e) { //构造无权访问的json返回信息
				response.setStatus(HttpServletResponse.SC_FORBIDDEN);
				response.setContentType(MediaType.APPLICATION_JSON_VALUE);
				response.setCharacterEncoding("UTF-8");
				response.setHeader("Cache-Control", "no-cache, must-revalidate");
				try {
					Locale currentLocale = localeResolver.resolveLocale(request);
					String message = messageSource.getMessage("errors.auth.noAuth", GlobalEx.EMPTY_ARGS, currentLocale); // 未登陆/无权限
					response.getWriter().write(JSON.toJSONString(Result.error(HttpStatus.SC_FORBIDDEN, message, "errors.auth.noAuth", e.getMessage())));
					logger.error(e.getMessage());
					response.flushBuffer();
					return;
				} catch (IOException ioe) {
					logger.error("与客户端通讯异常:" + e.getMessage(), e);
					e.printStackTrace();
				}
			}
		}
		if(adminUrl) { // 后台,尝试匹配IP白名单
			String remoteIp = IpUtil.getRemoteIp(request);
			RBucket<String> whiteIpBucket = redissonClient.getBucket(GlobalEx.CACHKEY_WHITE_IP, StringCodec.INSTANCE);
			String whiteIp = whiteIpBucket.get();
			if(StringUtils.isNotEmpty(remoteIp) && StringUtils.isNotEmpty(whiteIp) && remoteIp.equals(whiteIp)){ // 后台操作匹配白名单权限
				authorityList.add(new SimpleGrantedAuthority("ROLE_" + RoleCodeEnum.WHITE_IP.toCode()));
			}
		}
		if(!authorityList.isEmpty()){
			//认证,根据ROLE集合解析即可
			JwtAuthenticationToken jwtAuthenticationToken =  new JwtAuthenticationToken(
					tokenUser, // 如果只是IP白名单直接访问,tokenUser可能为Null
					authorityList
			);
			MDC.put(GlobalEx.TRACE_USER_ID, tokenUser.getUserId().toString());
			RpcContext.getContext().setAttachment(GlobalEx.TRACE_USER_ID, tokenUser.getUserId().toString()); // 同时放入RPC上下文

			jwtAuthenticationToken.setDetails(new WebAuthenticationDetails(request));
			SecurityContextHolder.getContext().setAuthentication(jwtAuthenticationToken); //授权对象放入上下文
		}
		filterChain.doFilter(request,response);
	}
}

传递信息

生成MDC信息后,我们需要在dubbo服务请求时进行传递。dubbo的RpcContext机制能够保证数据传递到下游的处理服务。唯一要做的是把RpcContext放入到MDC,这样下游服务里的所有日志都具备链路信息了。dubbo有一个扩展机制,可以在消费服务的时候进行切面处理。定义对应的Bean即可

package org.ccframe.commons.helper;

import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
import org.ccframe.config.GlobalEx;
import org.slf4j.MDC;

@Activate(group = {"provider"})
public class TraceProviderFilter implements Filter {
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        String traceId = RpcContext.getContext().getAttachment(GlobalEx.TRACE_ID);
        if (traceId != null) {
            MDC.put(GlobalEx.TRACE_ID, traceId);
        };
        String traceUserId = RpcContext.getContext().getAttachment(GlobalEx.TRACE_USER_ID);
        if (traceId != null) {
            MDC.put(GlobalEx.TRACE_USER_ID, traceId);
        };
        try {
            return invoker.invoke(invocation);
        } finally {
            MDC.remove(GlobalEx.TRACE_ID);
            MDC.remove(GlobalEx.TRACE_USER_ID);
        }
    }
}

关联操作数据

这个是原来就实现的自动记录操作人的方案。原理是采用JPA的EntityListeners方案持久化监听器,原方案是在springsecurity的上下文里获取,由于微服务后请求跨机器,因此改从MDC里获取(前面已经把RpcContext放入到MDC了)

这个是数据库操作基类,EntityListeners注解定义

package org.ccframe.commons.base;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.ccframe.commons.helper.EntityOperationListener;
import org.ccframe.config.GlobalEx;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

import javax.persistence.*;
import java.util.Date;


/**
 * SAAS数据操作基础类
 * @author JIM
 *
 */
@MappedSuperclass
@Setter
@Getter
@EntityListeners({EntityOperationListener.class})
public abstract class BaseEntity implements IBaseEntity {

	private static final long serialVersionUID = -5656014821398158846L;

	public static final String CREATE_TIME = "createTime";
	
	public static final String UPDATE_TIME = "updateTime";
	
	public static final String CREATE_USER_ID = "createUserId"; //创建人
	
	public static final String UPDATE_USER_ID = "updateUserId"; //最后修改人

	public static final String TENANT_ID = "tenantId"; // 租户,所有资源按照租户隔离,提供检测annotation

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "CREATE_TIME", nullable = false, length = 0, updatable = false)
	//elasticsearch
    @Field(type = FieldType.Date, format = DateFormat.custom, pattern = GlobalEx.ES_DATE_PATTERN)
    @JsonFormat (shape = JsonFormat.Shape.STRING, pattern = GlobalEx.STANDERD_DATE_FORMAT, timezone = GlobalEx.TIMEZONE)
    private Date createTime;
	
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "UPDATE_TIME", nullable = false, length = 0)
	//elasticsearch
    @Field(type = FieldType.Date, format = DateFormat.custom, pattern = GlobalEx.ES_DATE_PATTERN)
    @JsonFormat (shape = JsonFormat.Shape.STRING, pattern = GlobalEx.STANDERD_DATE_FORMAT, timezone = GlobalEx.TIMEZONE)
	private Date updateTime;
	
    @Column(name = "CREATE_USER_ID", nullable = true, length = 10, updatable = false)
    @Field(type = FieldType.Integer)
	private Integer createUserId;

	@Column(name = "UPDATE_USER_ID", nullable = true, length = 10)
	@Field(type = FieldType.Integer)
	private Integer updateUserId;

	@Column(name = "TENANT_ID", nullable = true, length = 10)
	@Field(type = FieldType.Integer)
	private Integer tenantId;

	@Override
	public boolean equals(Object obj) {
		if(!(getClass().isInstance(obj))){
			return false;
		}
		if(this == obj){
			return true;
		}
		BaseEntity other = (BaseEntity)obj;
		return new EqualsBuilder()
		.append(getId(),other.getId())
		.isEquals();
	}
	
	@Override
	public int hashCode() {
		return new HashCodeBuilder()
		.append(getId())
		.toHashCode();
	}

}

然后EntityListeners里实现持久化新增和更新逻辑

package org.ccframe.commons.helper;

import org.ccframe.commons.auth.TokenUser;
import org.ccframe.commons.base.BaseEntity;
import org.ccframe.commons.base.IBaseEntity;
import org.ccframe.config.GlobalEx;
import org.slf4j.MDC;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

import javax.persistence.PrePersist;
import javax.persistence.PreRemove;
import javax.persistence.PreUpdate;
import java.util.Date;


public class EntityOperationListener {

	private ICcTransactionHelper ccTransactionHelper;

	private ICcTransactionHelper getCcTransactionHelper(){
		if(ccTransactionHelper == null){
			ccTransactionHelper = SpringContextHelper.getBean(ICcTransactionHelper.class);
		}
		return ccTransactionHelper;
	}

	@PrePersist
	protected void onCreate(Object baseEntity) {
			IBaseEntity targetEntity = (IBaseEntity)baseEntity;
		//采用dubbo的MDC透传操作用户userId及请求ID
		String userId = MDC.get(GlobalEx.TRACE_USER_ID);
		if(userId != null) { // MDC上下文里有
			targetEntity.setCreateUserId(Integer.valueOf(userId));
		}
		Date now = new Date();
		targetEntity.setCreateTime(now);
		targetEntity.setUpdateTime(now);

		getCcTransactionHelper().pushSave(targetEntity);
	}

	@PreUpdate
	protected void onUpdate(Object baseEntity) {
		IBaseEntity targetEntity = (IBaseEntity)baseEntity;
		//采用dubbo的MDC透传操作用户userId及请求ID
		String userId = MDC.get(GlobalEx.TRACE_USER_ID);
		if(userId != null) { // MDC上下文里有
			targetEntity.setUpdateUserId(Integer.valueOf(userId));
		}
		targetEntity.setUpdateTime(new Date());

		getCcTransactionHelper().pushSave(targetEntity);
	}
	
	@PreRemove
	protected void onRemove(BaseEntity baseEntity) {
		getCcTransactionHelper().pushDelete(baseEntity);
	}
}

新增和修改操作一下,看到对应的新增用户和修改用户记录都写入了。这样每个表都具备了简单操作记录

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1591735.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

TSINGSEE青犀AI智能分析网关V4吸烟/抽烟检测算法介绍及应用

抽烟检测AI算法是一种基于计算机视觉和深度学习技术的先进工具&#xff0c;旨在准确识别并监测个体是否抽烟。该算法通过训练大量图像数据&#xff0c;使模型能够识别出抽烟行为的关键特征&#xff0c;如烟雾、手部动作和口部形态等。 在原理上&#xff0c;抽烟检测AI算法主要…

跟TED演讲学英文:The dark side of competition in AI by Liv Boeree

The dark side of competition in AI Link: https://www.ted.com/talks/liv_boeree_the_dark_side_of_competition_in_ai Speaker:Liv Boeree Date: October 2023 文章目录 The dark side of competition in AIIntroductionVocabularyTranscriptSummary后记 Introduction Co…

微服务项目——谷粒商城

文章目录 一、项目简介&#xff08;一&#xff09;完整的微服务架构微服务划分图&#xff08;二&#xff09;电商模式1.B2B 模式2.B2C 模式3.C2B 模式4.C2C 模式5.o2o 模式2.谷粒商城 &#xff08;三&#xff09;项目技术&特色&#xff08;四&#xff09;项目前置要求 二、…

Vue3(二):报错调试,vue3响应式原理、computed和watch,ref,props,接口

一、准备工作调试 跟着张天禹老师看前几集的时候可能会遇到如下问题&#xff1a; 1.下载插件&#xff1a;Vue Language Features (Volar)或者直接下载vue-offical 2.npm run serve时运行时出现错误&#xff1a;Error: vitejs/plugin-vue requires vue (&#xff1e;3.2.13) …

基于java+springboot+vue实现的居家养老健康管理系统(文末源码+Lw)23-313

摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装智慧社区居家养老健康管理系统软件来发挥其高效地信息处理…

Filebeat+Kafka+ELK 的服务部署

一. kafka 架构深入 1.1 Kafka 工作流程及文件存储机制 Kafka 中消息是以 topic 进行分类的&#xff0c;生产者生产消息&#xff0c;消费者消费消息&#xff0c;都是面向 topic 的。 topic 是逻辑上的概念&#xff0c;而 partition 是物理上的概念&#xff0c;每个 partit…

B004-表达式 类型转换 运算符

目录 表达式数据类型转换自动转换强制转换 运算符数学运算符自增自减运算符i与 i的区别 赋值运算符比较运算符位运算符(了解)逻辑运算符三目运算符 表达式 /*** 表达式定义&#xff1a;由常量 变量 运算符 括号组成的算式&#xff0c;为了按照一定的运算规则计算出结果值* 括…

二叉搜索树--搜索二维矩阵 II

题目描述 编写一个高效的算法来搜索 m * n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性&#xff1a; 每行的元素从左到右升序排列。每列的元素从上到下升序排列。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,…

基于springboot实现桂林旅游景点导游平台管理系统【项目源码+论文说明】计算机毕业设计

基于springboot实现桂林旅游景点导游平台管理系统演示 摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了桂林旅游景点导游平台的开发全过程。通过分析桂林旅游景点导游平台管理的不足&#xff0c;创建了一个计算…

洛谷P1109 学生分组

#先看题目 题目描述 有 n 组学生&#xff0c;给出初始时每组中的学生个数&#xff0c;再给出每组学生人数的上界 R 和下界 L (L≤R)&#xff0c;每次你可以在某组中选出一个学生把他安排到另外一组中&#xff0c;问最少要多少次才可以使 N 组学生的人数都在 [L,R] 中。 输入…

echarts坐标轴、轴线、刻度、刻度标签

坐标轴 x、y轴 x 轴和 y 轴都由轴线、刻度、刻度标签、轴标题四个部分组成。部分图表中还会有网格线来帮助查看和计算数据 普通的二维数据坐标系都有x轴和y轴&#xff0c;通常情况下&#xff0c;x轴显示在图表底部&#xff0c;y轴显示在左侧&#xff0c;一般配置如下&#xf…

[面向对象] 单例模式与工厂模式

单例模式 是一种创建模式&#xff0c;保证一个类只有一个实例&#xff0c;且提供访问实例的全局节点。 工厂模式 面向对象其中的三大原则&#xff1a; 单一职责&#xff1a;一个类只有一个职责&#xff08;Game类负责什么时候创建英雄机&#xff0c;而不需要知道创建英雄机要…

【Linux学习笔记】安卓运行C可执行文件No such file or directory

文章目录 开发环境运行失败现象解决办法方法一&#xff1a;使用静态库方法二&#xff1a;使用动态库创建lib查找依赖库复制需要注意的事情 开发环境 开发板&#xff1a;正点原子RK3568开发板安卓版本&#xff1a;11可执行程序命名&#xff1a;ledApp需加载模块&#xff1a;dts…

npm命令卡在reify:eslint: timing reifyNode:node_modules/webpack Completed in 475ms不动

1.现象 执行npm install命令时&#xff0c;没有报错&#xff0c;卡在reify:eslint: timing reifyNode:node_modules/webpack Completed in 475ms不动 2.解决办法 &#xff08;1&#xff09;更换淘宝镜像源 原淘宝 npm 域名http://npm.taobao.org 和 http://registry.npm.ta…

Day 39:动态规划 LeedCode 62.不同路径 63. 不同路径 II

62. 不同路径 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish” &#xff09;。 问总共有多少条不同的路径&#…

C语言:约瑟夫环问题详解

前言 哈喽&#xff0c;宝子们&#xff01;本期为大家带来一道C语言循环链表的经典算法题&#xff08;约瑟夫环&#xff09;。 目录 1.什么是约瑟夫环2.解决方案思路3.创建链表头结点4.创建循环链表5.删除链表6.完整代码实现 1.什么是约瑟夫环 据说著名历史学家Josephus有过以下…

Ribbon-负载均衡原理解析(案例)

简介&#xff1a;负载均衡&#xff0c;英文名称为Load Balance&#xff0c;其含义就是指将负载&#xff08;工作任务&#xff09;进行平衡、分摊到多个操作单元上进行运行&#xff0c;例如FTP服务器、Web服务器、企业核心应用服务器和其它主要任务服务器等&#xff0c;从而协同…

MATLAB数据类型和运算符+矩阵创建

个人主页&#xff1a;学习前端的小z 个人专栏&#xff1a;HTML5和CSS3悦读 本专栏旨在分享记录每日学习的前端知识和学习笔记的归纳总结&#xff0c;欢迎大家在评论区交流讨论&#xff01; 文章目录 ✍一、MATLAB数据类型和运算符&#x1f48e;1 MATLAB的数据类型&#x1f339;…

QT常用控件

常用控件 控件概述QWidget 核⼼属性核⼼属性概览enabledgeometrywindowTitlewindowIconwindowOpacitycursorfonttoolTipfocusPolicystyleSheet 按钮类控件Push ButtonRadio ButtionCheck Box 显⽰类控件LabelLCD NumberProgressBarCalendar Widget 输⼊类控件Line EditText Edi…

[尚硅谷 flink] 状态管理 笔记

文章目录 10.1 状态概述10.2 状态分类1&#xff09;托管状态&#xff08;Managed State&#xff09;和原始状态&#xff08;Raw State&#xff09;2&#xff09;算子状态&#xff08;Operator State&#xff09;和按键分区状态&#xff08;Keyed State&#xff09; 10.3 按键分…