sleuth原理分析

article/2025/9/20 22:09:30

背景

微服务模式下,一次请求可能会经过多个服务。如果没有日志链将单次请求的日志串起来,定位问题时很容易陷入海量的日志中,无法快速定位问题。
sleuth是spring cloud中日志链(调用链解决方案),引入该依赖后,日志中会自动添加(traceid,spanid)。当获取到traceid后,可以在kibana或者其他日志收集系统中,精确定位到本次的所有日志。
sleuth基本原理很简单,就是在入口生成(traceid,spanid),并在调用中将traceid传递下去。
在这里插入图片描述
备注:上图来自spring官方文档。

但是在基本原理之上,sleuth如何做到无侵入性,个人总结有如下3点:

  1. 在入口生成(traceid,spanid),对系统又没有侵入性,应该是自动配置了一个filter,这个filter去检查请求头中是否有(traceid,spanid),如果没有的话,则认为此处是请求的入口,需要生成(traceid,spanid);如果有的话,则认为本处是调用链的一环,需要复用传入的traceid,并将其传入到下一环节中;
  2. 未做任何配置,日志模板是怎么如何添加(traceid,spanid)的占位符。
  3. 日志打印时如何获取(traceid,spanid)

traceid,spanid的自动生成

sleuth对系统没有侵入性,而且要保障业务处理逻辑的日志中,写入traceid,spanid。sleuth应该自动配置了一个filter去生成(traceid,spanid),而且该filter优先级最高。
根据该推测,在业务代码中随便打一个断点,然后查看堆栈中的第一个filter。
在这里插入图片描述
brave.servlet.TracingFilter#doFilter

    HttpServletRequest req = (HttpServletRequest) request;HttpServletResponse res = servlet.httpServletResponse(response);// Prevent duplicate spans for the same requestTraceContext context = (TraceContext) request.getAttribute(TraceContext.class.getName());if (context != null) {// A forwarded request might end up on another thread, so make sure it is scopedScope scope = currentTraceContext.maybeScope(context);try {chain.doFilter(request, response);} finally {scope.close();}return;}# 调用链入口还没有生成traceid,因此走这个分支去生成Span span = handler.handleReceive(new HttpServletRequestWrapper(req));// Add attributes for explicit access to customization or span contextrequest.setAttribute(SpanCustomizer.class.getName(), span.customizer());request.setAttribute(TraceContext.class.getName(), span.context());SendHandled sendHandled = new SendHandled();request.setAttribute(SendHandled.class.getName(), sendHandled);Throwable error = null;Scope scope = currentTraceContext.newScope(span.context());......

未做任何配置,日志模板是怎么如何添加(traceid,spanid)的占位符

日志模板中包含(traceid,spanid)的占位符,肯定是初始化日志配置时,修改了默认的日志模板,因此需要追踪spring日志配置的初始化代码。经搜索知道spring的日志配置是在LoggingApplicationListener中配置,因此追踪到如下代码。
org.springframework.boot.context.logging.LoggingApplicationListener#onApplicationEvent

	public void onApplicationEvent(ApplicationEvent event) {if (event instanceof ApplicationStartingEvent) {onApplicationStartingEvent((ApplicationStartingEvent) event);}# 日志配置初始化在这个阶段else if (event instanceof ApplicationEnvironmentPreparedEvent) {onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);}......}private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {if (this.loggingSystem == null) {# loggingSystem 为spring抽象的日志系统,其包含了日志的配置,初始化等功能this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());}initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());}

org.springframework.boot.logging.LoggingSystem#get(java.lang.ClassLoader)
判断系统中存在的日志实现,取第一个,Logback优先。

	static {Map<String, String> systems = new LinkedHashMap<>();systems.put("ch.qos.logback.classic.LoggerContext","org.springframework.boot.logging.logback.LogbackLoggingSystem");systems.put("org.apache.logging.log4j.core.impl.Log4jContextFactory","org.springframework.boot.logging.log4j2.Log4J2LoggingSystem");systems.put("java.util.logging.LogManager", "org.springframework.boot.logging.java.JavaLoggingSystem");SYSTEMS = Collections.unmodifiableMap(systems);}public static LoggingSystem get(ClassLoader classLoader) {........return SYSTEMS.entrySet().stream().filter((entry) -> ClassUtils.isPresent(entry.getKey(), classLoader)).map((entry) -> get(classLoader, entry.getValue())).findFirst().orElseThrow(() -> new IllegalStateException("No suitable logging system located"));}

org.springframework.boot.context.logging.LoggingApplicationListener#initialize

	protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {new LoggingSystemProperties(environment).apply();this.logFile = LogFile.get(environment);if (this.logFile != null) {this.logFile.applyToSystemProperties();}this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);initializeEarlyLoggingLevel(environment);# 初始化日志系统initializeSystem(environment, this.loggingSystem, this.logFile);initializeFinalLoggingLevels(environment, this.loggingSystem);registerShutdownHookIfNecessary(environment, this.loggingSystem);}

org.springframework.boot.logging.logback.LogbackLoggingSystem#initialize

	public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {LoggerContext loggerContext = getLoggerContext();super.initialize(initializationContext, configLocation, logFile);......}private LoggerContext getLoggerContext() {# LoggerFactory本身就是单例,日志配置包含在了其中,因此对其进行配置即可ILoggerFactory factory = StaticLoggerBinder.getSingleton().getLoggerFactory();return (LoggerContext) factory;}

org.springframework.boot.logging.logback.LogbackLoggingSystem#loadDefaults

	protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) {LoggerContext context = getLoggerContext();stopAndReset(context);boolean debug = Boolean.getBoolean("logback.debug");if (debug) {StatusListenerConfigHelper.addOnConsoleListenerInstance(context, new OnConsoleStatusListener());}LogbackConfigurator configurator = debug ? new DebugLogbackConfigurator(context): new LogbackConfigurator(context);Environment environment = initializationContext.getEnvironment();# 关键点-从spring配置中获取值,并配置LogContext自己的LOG_LEVEL_PATTERN变量的值context.putProperty(LoggingSystemProperties.LOG_LEVEL_PATTERN,environment.resolvePlaceholders("${logging.pattern.level:${LOG_LEVEL_PATTERN:%5p}}"));context.putProperty(LoggingSystemProperties.LOG_DATEFORMAT_PATTERN, environment.resolvePlaceholders("${logging.pattern.dateformat:${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}"));context.putProperty(LoggingSystemProperties.ROLLING_FILE_NAME_PATTERN, environment.resolvePlaceholders("${logging.pattern.rolling-file-name:${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}"));new DefaultLogbackConfiguration(initializationContext, logFile).apply(configurator);context.setPackagingDataEnabled(true);}

org.springframework.boot.logging.logback.DefaultLogbackConfiguration#apply
上一步loadDefaults中会给LoggerContext配置${LOG_LEVEL_PATTERN:-%5p}占位符的值。

# 默认的日志模板private static final String FILE_LOG_PATTERN = "%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} "+ "${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}";void apply(LogbackConfigurator config) {synchronized (config.getConfigurationLock()) {base(config);Appender<ILoggingEvent> consoleAppender = consoleAppender(config);......}}# 完成日志模板的配置private Appender<ILoggingEvent> consoleAppender(LogbackConfigurator config) {ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<>();PatternLayoutEncoder encoder = new PatternLayoutEncoder();String logPattern = this.patterns.getProperty("logging.pattern.console", CONSOLE_LOG_PATTERN);encoder.setPattern(OptionHelper.substVars(logPattern, config.getContext()));config.start(encoder);appender.setEncoder(encoder);config.appender("CONSOLE", appender);return appender;}

org.springframework.cloud.sleuth.autoconfig.TraceEnvironmentPostProcessor#postProcessEnvironment
最终在sleuth中找到其通过TraceEnvironmentPostProcessor提前配置好了logging.pattern.level变量,用以将(traceid、spanid)的占位符嵌入到日志模板中

	public void postProcessEnvironment(ConfigurableEnvironment environment,SpringApplication application) {Map<String, Object> map = new HashMap<String, Object>();// This doesn't work with all logging systems but it's a useful default so you see// traces in logs without having to configure it.if (sleuthEnabled(environment)&& sleuthDefaultLoggingPatternEnabled(environment)) {map.put("logging.pattern.level", "%5p [${spring.zipkin.service.name:"+ "${spring.application.name:}},%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-Span-Export:-}]");}addOrReplace(environment.getPropertySources(), map);}

日志打印时如何获取filter中生成的(traceid、spanid)

跟踪日志打印的代码可以发现sleuth借助日志系统的MDC特性,将traceid、spanid塞到相关容器中,供logback替换占位符时查询真实的值。
日志系统有一些内置的变量,比如线程号,用于替换日志模板中的占位符;为了支持自定义的占位符,需要一个容器去存储这个占位符所对应的值。该容器即为MDC。可以百度MDC获得跟多相关知识。
通过截图可以看到logback从MDC中获取的traceid、spanid等值,用于后续替换占位符。
在这里插入图片描述
获取MDCPropertyMap的方法为ch.qos.logback.classic.util.LogbackMDCAdapter#getPropertyMap。
可以在设值的地方打断点,分析sleuth是何时设置进来的。
在这里插入图片描述
从下面截图中可以看到是在sleuth的filter中去设置这些值的。
在这里插入图片描述


http://chatgpt.dhexx.cn/article/KV6jT6yn.shtml

相关文章

快速学习-Sleuth--链路追踪

Sleuth–链路追踪 6.1 链路追踪介绍 在大型系统的微服务化构建中&#xff0c;一个系统被拆分成了许多模块。这些模块负责不同的功能&#xff0c;组合成 系统&#xff0c;最终可以提供丰富的功能。在这种架构中&#xff0c;一次请求往往需要涉及到多个服务。互联网应用构建 在…

SpringCloud学习笔记(十二)Sleuth 分布式请求链路跟踪

目录 一、Sleuth介绍 1、为什么会出现这个技术&#xff1f;需要解决哪些问题&#xff1f; 2、是什么 二、搭建链路监控步骤 1、zipkin搭建安装 1&#xff09;下载 2&#xff09;运行jar 3&#xff09;运行控制台 4&#xff09;术语 2、服务提供者 cloud-provider-pa…

MySQL5种索引类型

一、简介 MySQL目前主要有以下几种索引类型&#xff1a; 1.普通索引 2.唯一索引 3.主键索引 4.组合索引 5.全文索引 二、语句 CREATE TABLE table_name[col_name data type] [unique|fulltext][index|key][index_name](col_name[length])[asc|desc] 1.unique|fulltext为可选…

mysql索引类型和索引方式

1.什么是索引 在MySQL中&#xff0c;索引&#xff08;index&#xff09;也叫做“键&#xff08;key&#xff09;”&#xff0c;它是存储引擎用于快速找到记录的一种数据结构。 2.索引的分类 在MySQL中&#xff0c;通常我们所指的索引类型&#xff0c;有以下几种&#xff1a;…

Mysql索引的类型(单列索引、组合索引 btree索引 聚簇索引等)

一、索引的类型 Mysql目前主要有以下几种索引类型&#xff1a;FULLTEXT&#xff0c;HASH&#xff0c;BTREE&#xff0c;RTREE。 FULLTEXT 即为全文索引&#xff0c;目前只有MyISAM引擎支持。其可以在CREATE TABLE &#xff0c;ALTER TABLE &#xff0c;CREATE INDEX 使用&…

mysql索引类型有哪些?

在Mysql数据库当中&#xff0c;我们经常会谈到Sql语句&#xff0c;当然也会谈到索引优化&#xff0c;那么在数据库当中有哪些索引类型呢&#xff0c;博主在这里进行分享&#xff0c;希望对大家能有所帮助。 目录 1、B-Tree索引&#xff1a; 2、Hash索引&#xff1a; 3、Full…

什么是索引?Mysql目前主要的几种索引类型

一、索引 MySQL索引的建立对于MySQL的高效运行是很重要的&#xff0c;索引可以大大提高MySQL的检索速度。 打个比方&#xff0c;如果合理的设计且使用索引的MySQL是一辆兰博基尼的话&#xff0c;那么没有设计和使用索引的MySQL就是一个人力三轮车。 索引分单列索引和组合索引。…

Mysql索引类型与索引方法

写在前面&#xff1a; 乍一看这两个概念可能有点混&#xff0c;先上一张发图。 索引类型就是我们平常说的唯一索引&#xff0c;主键索引&#xff0c;组合索引等索引类型。 我们都知道索引是一种数据结构&#xff0c;到底我们建的索引应该以什么样的结构存储呢&#xff1f;存储…

MYSQL 索引类型

一、索引类型 在数据库表中&#xff0c;对字段建立索引可以大大提高查询速度。假如我们创建了一个 mytable表 代码如下: CREATE TABLE mytable( ID INT NOT NULL, username VARCHAR(16) NOT NULL ); 我们随机向里面插入了10000条记录&#xff0c;其中有一条&#xff1a;555…

常见索引类型

日常开发工作中&#xff0c;涉及到的数据存储&#xff0c;要做查询优化或想深入了解存储引擎&#xff0c;需要对索引知识有个起码的了解&#xff0c;下面介绍下最常见的四种索引结构。 位图索引哈希索引BTREE索引倒排索引 1、位图索引&#xff08;BitMap&#xff09; 位图索引…

数据库的五种索引类型

本文从如何建立mysql索引以及介绍mysql的索引类型,再讲mysql索引的利与弊,以及建立索引时需要注意的地方 首先:先假设有一张表,表的数据有10W条数据,其中有一条数据是nicknamecss,如果要拿这条数据的话需要些的sql是 SELECT * FROM award WHERE nickname css 一般情况下,在没…

mysql索引有哪些类型?

MySQL目前主要有的索引类型为&#xff1a;普通索引、唯一索引、主键索引、组合索引、全文索引。 通过给字段添加索引可以提高数据的读取速度&#xff0c;提高项目的并发能力和抗压能力。索引优化时mysql中的一种优化方式。索引的作用相当于图书的目录&#xff0c;可以根据目录…

你知道多少种索引?

前言 嗨&#xff0c;大家好&#xff0c;我是fancy呀。 在工作中我们常常用到索引&#xff0c;无论是普通索引还是唯一索引&#xff0c;都是一些常用的索引方式&#xff0c;目的就是为了提高查询效率&#xff0c;避免业务请求超时等问题。那么&#xff0c;当你在使用索引的时候…

动态规划算法 | 最长递增子序列

通过查阅相关资料发现动态规划问题一般就是求解最值问题。这种方法在解决一些问题时应用比较多&#xff0c;比如求最长递增子序列等。 有部分人认为动态规划的核心就是&#xff1a;穷举。因为要求最值&#xff0c;肯定要把所有可行的答案穷举出来&#xff0c;然后在其中找最值…

求最长递增子序列个数——C++

声明&#xff1a;本文原题主要来自力扣&#xff0c;记录此博客主要是为自己学习总结&#xff0c;不做任何商业等活动&#xff01; 一、下面是原题描述 给定一个未排序的整数数组&#xff0c;找到最长递增子序列的个数。 示例 1: 输入: [1,3,5,4,7] 输出: 2 解释: 有两个最长递…

最长递增子序列(LIS)

最长递增子序列&#xff08;LIS&#xff09; 问题描述&#xff1a; 求一个序列的最长递增子序列&#xff0c;这样的子序列是允许中间越过一些字符的&#xff0c;即留“空”。 例如&#xff1a;4 2 3 1 5 的最长递增子序列为 2 3 5&#xff0c;长度为 3 。 解法&#xff1a;…

【Leetcode】最长递增子序列问题及应用

文章目录 最长递增子序列问题及应用300. 最长递增子序列面试题 17.08. 马戏团人塔354. 俄罗斯套娃信封问题面试题 08.13. 堆箱子1691. 堆叠长方体的最大高度406. 根据身高重建队列 最长递增子序列问题及应用 300. 最长递增子序列 请参考 【Leetcode】计算最长系列&#xff08…

输出最长递增子序列

目录 题目&#xff1a; 输入描述: 输出描述: 示例1 输入 输出 示例2 输入 输出 说明 备注: 思路分析&#xff1a; 改进&#xff1a; 得到最长子序列&#xff1a; 易错点&#xff1a; 代码展示&#xff1a; 题目&#xff1a; 给定数组arr&#xff0c;设长度为n&…

NC91 最长递增子序列

NC91 最长递增子序列 这道题n的范围是1e5&#xff0c;因此不能使用常规的dp[i]&#xff0c;表示以i结尾的最大的子序列&#xff0c;因为这个时间复杂度是n方级别。因此要换一种算法。 贪心二分&#xff0c;时间复杂度为O(nlogn) 下面说说贪心二分的解法&#xff0c;举例说明基…

Vue3 最长递增子序列详解

Vue3 最长递增子序列研究 本文初衷 彻底讲清楚 Vue3 源码中实现最长递增子序列的算法。 概念名词 **最长递增子序列&#xff1a;**在一个给定的数值序列中&#xff0c;找到一个子序列&#xff0c;使得这个子序列元素的数值依次递增&#xff0c;并且这个子序列的长度尽可能地…