背景

微服务模式下,一次请求可能会经过多个服务。如果没有日志链将单次请求的日志串起来,定位问题时很容易陷入海量的日志中,无法快速定位问题。
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中去设置这些值的。

sleuth原理分析相关推荐

  1. sleuth原理详解

    sleuth原理 1.关键术语 Span:Span基本工作单元,发送一个远程调度任务就会产生一个Span, Span 是用 64ID唯一标识的,Trace是用另一个64ID唯一标识的.Span还包含了 ...

  2. java signature 性能_Java常见bean mapper的性能及原理分析

    背景 在分层的代码架构中,层与层之间的对象避免不了要做很多转换.赋值等操作,这些操作重复且繁琐,于是乎催生出很多工具来优雅,高效地完成这个操作,有BeanUtils.BeanCopier.Dozer. ...

  3. Select函数实现原理分析

    转载自 http://blog.chinaunix.net/uid-20643761-id-1594860.html select需要驱动程序的支持,驱动程序实现fops内的poll函数.select ...

  4. spring ioc原理分析

    spring ioc原理分析 spring ioc 的概念 简单工厂方法 spirng ioc实现原理 spring ioc的概念 ioc: 控制反转 将对象的创建由spring管理.比如,我们以前用 ...

  5. 一次 SQL 查询优化原理分析(900W+ 数据,从 17s 到 300ms)

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 来源:Muscleape jianshu.com/p/0768eb ...

  6. 原理分析_变色近视眼镜原理分析

    随着眼镜的发展,眼镜的外型变得越来越好看,并且眼镜的颜色也变得多姿多彩,让佩戴眼镜的你变得越来越时尚.变色近视眼镜就是由此产生的新型眼镜.变色镜可以随着阳光的强弱变换不同的色彩. 变色眼镜的原理分析 ...

  7. jieba分词_从语言模型原理分析如何jieba更细粒度的分词

    jieba分词是作中文分词常用的一种工具,之前也记录过源码及原理学习.但有的时候发现分词的结果并不是自己最想要的.比如分词"重庆邮电大学",使用精确模式+HMM分词结果是[&quo ...

  8. EJB调用原理分析 (飞茂EJB)

    EJB调用原理分析 EJB调用原理分析 作者:robbin (MSN:robbin_fan AT hotmail DOT com) 版权声明:本文严禁转载,如有转载请求,请和作者联系 一个远程对象至少 ...

  9. 深入掌握Java技术 EJB调用原理分析

      深入掌握Java技术 EJB调用原理分析     一个远程对象至少要包括4个class文件:远程对象:远程对象的接口:实现远程接口的对象的stub:对象的skeleton这4个class文件. 在 ...

最新文章

  1. “神人”or“闲人”?你的未来由AI与大数据决定
  2. R语言points作图
  3. php中一个经典的!==的用法
  4. 求一批整数中出现最多的个位数字_C语言经典100例007-求低n-1位的数
  5. Sonic 开源移动端云真机测试平台 - windows系统下的sonic快速部署演示
  6. 【Docker】网络模式
  7. 【嵌入式】Modbus TCP功能码
  8. 防止UI界面被输入法遮挡(画面随输入法自适应)
  9. [力扣]1018_可被5整除的二进制前缀
  10. BugkuCTF-MISC题隐写3
  11. python循环输入若干学生信息网_python最简学生信息系统,练习while
  12. java读取、生成图片
  13. 比较好的浏览器_一款安卓黑科技手机浏览器 体积很小,功能很6!
  14. pd虚拟机 17.1.2 Intel核心Mac专用版
  15. perl查看文件,提取指定信息输出到文件
  16. 枚举、自动装箱与注解(元数据)
  17. 最新超详细的VMware虚拟机的下载与安装
  18. Oracle PO ER Model
  19. 热烈祝贺方正璞华两款产品入选2021年度江苏省工业软件优秀产品和应用解决方案拟推广名单
  20. 人群密度估计最新数据集NWPU-Crowd 最新研究进展

热门文章

  1. 宁静优美景色动态特效404网页源码
  2. Win10回收站里被删除了的图片如何恢复
  3. 小白知识之:聊聊显卡(功能,分类,原理,接口,性能评价)
  4. 【橙点同学】互联网农旅电商(初级)答案
  5. flink1.13.5编译,各种填坑
  6. html css动画自动旋转,HTML5 - 用CSS3动画制作场景切换效果(移动,旋转,淡入淡出等)...
  7. kafka干货(一):Confluent
  8. 遇到问题:微信发错消息,需要撤回,但超过了撤回时间,怎么办?
  9. Docsify Markdown表格列宽自定义
  10. vs如何把c语言编译成静态库,VS中Debug和Realease、及静态库和动态库的区别整理(转)...