序言
害~ 年底不幸没躲过公司裁员,断了一个月粮后来到一家新公司,公司的技术基建相对上家几乎可以说还是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个问题
- 日志解析时怎么通过logback配置文件直接取到traceId
- 若是使用了AsyncAppender引用做异步日志,那AsyncAppender的work线程怎么才能获取traceId。
- 线程池中的traceId怎么获取
以及一个扩展性的问题
- 能否不单单是存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进行解析。因此我们要做的就是两点
-
让logback知道当我们在logback配置中的pattern写了 [%traceId]时,它要用哪个Converter获取参数
-
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种
-
使用阿里提供的工具类TtlExecutors对线程池包装一下,获得代理线程池。这种方式有一定的代码侵入性
-
使用阿里官方提供的java-agent,即通过插桩代理完成,该方式在main方法执行之前修改了类的字节码,改变了类的行为,需要在启动时添加-javaagent 参数
两种方式建议看官方文档,个人建议是agent方式,毕竟对代码没有侵入性~
总结
这种实现方式还是比较灵活的,除了traceId,还可以向代理event对象塞入更多的信息,比如一般观测平台的日志列存储需要索引,我们可以依据业务写日志解析器,将解析后的索引也放入ttl中,并通过event代理对象给到日志输出线程。