序言

害~ 年底不幸没躲过公司裁员,断了一个月粮后来到一家新公司,公司的技术基建相对上家几乎可以说还是0,项目也大多数单体为主,架构比较混乱。
为了方便后续微服务化的展开,我花了2天时间写了一个简单的traceId实现,以供业务观测实现

思路

由于公司是使用的日志框架是slf4j+logback,那么就直接从logback本身提供的扩展上做改造,很快,我就想到了一个2个方案
1. 基于MDC做存储
2. 基于TTL做存储

MDC本质上是日志框架提供的一个ThreadLocal访问接口,只要变量存在于MDC中,那么通过在logback配置文件中加上参数占位符,就能直接取到需要的数据。但是MDC本身有一个致命的缺点:它不是跨线程的。其仅仅是使用了ThreadLocal而不是InheritableThreadLocal或者TransmittableThreadLocal这样具有跨线程性质的threadLocal对象。
当然这个问题也不是不可以解决,只要自己在代码里同样写一个ch.qos.logback.classic.util.LogbackMDCAdapter,并使用TransmittableThreadLocal对其进行改造。这样利用了jvm的类加载机制(同样的类只有第一次加载的才会生效),使我们自己实现的LogbackMDCAdapter替换掉logback原有的。但是这种方法会带来不稳定因素,我们没法保证我们自己写的类一定会先被jvm加载。

基于上述种种原因,我决定还是脱离MDC,选择方案2.

具体实现

基于方案2的思路,我们主要解决的是3个问题

  1. 日志解析时怎么通过logback配置文件直接取到traceId
  2. 若是使用了AsyncAppender引用做异步日志,那AsyncAppender的work线程怎么才能获取traceId。
  3. 线程池中的traceId怎么获取

以及一个扩展性的问题

  1. 能否不单单是存traceId, 维度是否能更宽广些,方便列式数据库的索引

日志解析时如何取到traceId

当使用PatternLayout时,logback会通过ch.qos.logback.core.pattern.Converter的实现类去解析pattern
在这里插入图片描述
logback会对pattern进行分词,对每一个词,都会有一个converter进行解析转换成需要的字符串,比如

${appName} %date [%traceId] [%thread] %-5level [%logger{50}] %file:%line - %msg%n

它会被分成

  • ${appName}
  • %date
  • [%traceId]
  • [%thread]
  • %-5level
  • [%logger{50}]
  • %file:%line
    • %msg%n

每一个词都由一个Converter进行解析。因此我们要做的就是两点

  1. 让logback知道当我们在logback配置中的pattern写了 [%traceId]时,它要用哪个Converter获取参数

  2. converter如何获取参数

判断使用哪个Converter

converter和关键词的映射关系存在PatternLayout的static代码块中,具体数据结构是
Map<关键词字符串, converter的类名>
在这里插入图片描述
因此,我们只需要继承PatternLayout并在其static块中加入映射即可,最后在logback中指定该TraceIdPatternLayout
在这里插入图片描述
在这里插入图片描述
当TraceIdPatternLayout被jvm加载时,static代码块就会被执行,映射关系建立

converter如何获取traceId

在使用的同步日志的情况下可以直接从TTL取出即可,若是用AsyAppender进行异步化了呢?那就需要做点手脚了。
在这里插入图片描述

异步日志如何获取traceId

使用异步日志一般是用AsyncAppender去引用真实输出日志的appender,生产日志的线程和输出日志的线程并不是同一个线程
在这里插入图片描述
上图中,绿色的块是产生日志的线程,红色的块是logback的work线程,显然work线程没法拿到属于绿色线程的traceId

我的解决思路是将event放入blockQueue之前,将traceId放入event。为此我们需要对event做一层代理,并且能够在某个地方将生成的event对象变成我们自定义的带traceId的event代理对象。

1. 定义代理对象
实现ILoggingEvent接口,组合ILoggingEvent对象,增加traceId字段

在这里插入图片描述
2. 实现代理对象的转换
自己写一个自定义的AsyncAppender,在将event放入阻塞队列之前,变成event代理对象。我在这里直接复制了AsyncAppender的代码,重写了append方法,并增加了代理转换部分

在这里插入图片描述
这样在取traceId时就能直接通过get方法拿到,不需要到ttl里去拿(本身也没有)
在这里插入图片描述
最后记得在日志配置中使用自定义的TraceAsyncAppender去ref实际输出日志的appender

线程池中的日志,怎么取到traceId

因为使用了阿里的TransmittableThreadLocal,所以方法有2种

  1. 使用阿里提供的工具类TtlExecutors对线程池包装一下,获得代理线程池。这种方式有一定的代码侵入性

  2. 使用阿里官方提供的java-agent,即通过插桩代理完成,该方式在main方法执行之前修改了类的字节码,改变了类的行为,需要在启动时添加-javaagent 参数

两种方式建议看官方文档,个人建议是agent方式,毕竟对代码没有侵入性~

总结

这种实现方式还是比较灵活的,除了traceId,还可以向代理event对象塞入更多的信息,比如一般观测平台的日志列存储需要索引,我们可以依据业务写日志解析器,将解析后的索引也放入ttl中,并通过event代理对象给到日志输出线程。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐