日志框架之TLog讲解分析
1 TLog
1.1 引言
随着微服务盛行,很多公司都把系统按照业务边界拆成了很多微服务,在排错查日志的时候,因为业务链路贯穿着很多微服务节点,导致定位某个请求的日志以及上下游业务的日志会变得有些困难。
这时候可能有的小伙伴就会想到使用SkyWalking
,Pinpoint
等分布式追踪系统来解决,并且这些系统通常都是无侵入性的,同时也会提供相对友好的管理界面来进行链路Span
的查询,但是搭建分布式追踪系统还是需要一定的成本的,所以本文要说的并不是这些分布式追踪系统,而是一款简单、易用、几乎零侵入、适合中小型公司使用的日志追踪框架TLog
1.2 简介
TLog
提供了一种最简单的方式来解决日志追踪问题,TLog
会自动的对日志进行打标签,自动生成traceId
贯穿你微服务的一整条链路,在排查日志的时候,可以根据traceId
来快速定位请求处理的链路。
TLog
不收集日志,只在原来打印的日志上增强,将请求链路信息traceId
绑定到打印的日志上。当出现微服务中那么多节点的情况,官方推荐使用TLog+日志收集
方案来解决。当然分布式追踪系统其实是链路追踪一个最终的解决方案,如果项目中已经上了分布式追踪系统,那TLog
并不适用。
如下图,是ELK
配合TLog
,快速定位请求处理的链路的示例。
TLog官网:https://tlog.yomahub.com
github地址:https://github.com/dromara/TLog
1.3 TLog操作
1.3.1 pom.xml
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>tlog-all-spring-boot-starter</artifactId>
<version>1.5.0</version>
</dependency>
1.3.2 替换logback配置项
到这其实就已经完成了配置。
1.3.3 测试
@RestController
public class Controller {
private static final Logger logger
= LoggerFactory.getLogger(Controller.class);
@RequestMapping(@*"/test")
public void test() {
Logger.info("测试");
}
}
这里是通过slf4j
的LoggerFactory
获取Logger
对象,因为logback
适配了slf4j
,最终会通过logback
来输出日志。
从这可以看出,11794076298070144 就是本次日志输出的时候生成的一个请求的traceId
,在排查日志的时候就可以通过这个traceId去搜索出整个请求的链路日志。
1.4 TLog接入方式
TLog
总共提供了三种方式接入项目:
Javaagent
接入方式- 字节码注入方式
- 日志框架适配器方式
上面案例的接入方式其实是属于日志框架适配器方式,并且是对于Logback
框架的适配。TLog
除了适配了Logback
框架,还适配了Log4j
框架和Log4j2
框架,项目中可自行选择。
Javaagent
接入方式和字节码注入方式相比与日志框架适配器方式对代码的入侵性更小,但是这两种方式仅仅只支持SpringBoot
项目,并且相较于日志框架适配器的方式,MDC
和异步日志功能并不支持,所以要想完整体验TLog
的功能,还是建议选择日志框架适配器方式,日志框架适配器方式其实接入也很快,其实也就是修改一下配置文件的事。
项目环境兼容对比 | SpringBoot项目自启动 | 非SpringBoot项目自启动 | SpringBoot项目外置容器 | 非SpringBoot项目外置容器 |
---|---|---|---|---|
Javaagent接入方式 | 适合 | 不适合 | 不适合 | 不适合 |
字节码注入方式 | 适合 | 适合 | 不适合 | 不适合 |
日志框架适配器方式 | 适合 | 适合 | 适合 | 适合 |
特性支持对比 | 同步日志 | MDC | 异步日志 |
---|---|---|---|
Javaagent接入方式 | 支持 | 不支持 | 不支持 |
字节码注入方式 | 支持 | 不支持 | 不支持 |
日志框架适配器方式 | 支持 | 支持 | 支持 |
1.5 TLog的基本原理
1.5.1 日志标签
前面在介绍TLog
的时候,提到TLog
会自动的对你的日志进行打标签,这个标签就是日志标签,一个日志标签最多可以包含如下信息:
preApp
:接口调用方服务名preHost
:接口调用方Host
preIp
:接口调用方ip
currIp
:当前服务ip
traceId
:链路id
,调用方如果传递就是传递的值,不传递就会重新生成spanId
:链路spanId
,默认是按照如下labelPattern
进行数据拼接生成日志标签,所以默认只打出spanId
和traceId
。
public static String labelPattern = "<$spanId><$traceId>";
public static String generateTLogLabel(String preApp, String preHost, String preIp,String currIp,String traceId,String spanId)
return labelPattern
.replace( target: "$preApp",preApp)
.replace( target: "$preHost",preHost)
.replace("$preIp",preIp)
.replace( target:"$currIp", currIp)
.replace( target:"$traceId",traceId)
.replace( target:"$spanId", spanId);
}
这也就是上面为什么示例中会输出 <0><11794076298070144>
这种格式的原因,前面的0其实就是spanId
如果想改变日志标签输出其它信息或者输出的顺序,只需要在SpringBoot
配置文件中配置日志标签的生成样式就行。
tlog.pattern=[$preApp][$preIp][$spanId][$traceId]
1.5.2 TLogContext
public class TLogContext {
private static boolean enableInvokeTimePrint = false:
private static boolean hasTLogMDC;
private static boolean hasLogstash;
private static final TransmittableThreadLocal<String> traceIdTL = new TransmittableThreadLocalo();
private static final TransmittableThreadLocal<String> preIvkAppTL = new TransmittableThreadlocalo();
private static final TransmittableThreadlocal<String> preIvkHostTL = new TransmittableThreadlocalo();
private static final TransmittableThreadLocal<String> preIpTl = new TransmittableThreadLocalo();
private static final TransmittableThreadLocal<String> currIpTL = new TransmittableThreadLocalo();
public static void putTraceId(string traceId) {
traceIdTL.set(traceId);
}
TLogContext
是TLog
是一个核心的组件,这个组件内部是使用了TransmittableThreadLocal
来传递traceId
、preApp
等信息。
当有一个请求过来的时候,会从解析出traceId
、preApp
等信息,然后设置到TransmittableThreadLocal
中,之后就可以在整个调用链路中从TLogContext
中获取到traceId
等信息。
1.5.3 TLogRPCHandler
这个组件是用来处理调用方传递的traceId
、preApp
等信息,设置到TLogContext
和MDC
中,同时根据日志标签的格式生成日志标签。
1.6 第三方框架的适配
在实际项目中,一个请求处理过程可能会出现以下情况
- 异步线程处理
- 跨服务调用
MQ
调用
那么对于这些情况来说,traceId
应该需要在异步线程、跨服务、MQ
等中传递,以便更好地排查一个请求的处理链路。
而TLog
对于以上可能出现的情况都做了大量的适配,保证traceId
能够在异步线程、微服务间、MQ
等中能够正确传递
1.6.1 异步线程
1.6.1.1 一般异步线程
所谓的一般异步线程就是指直接通过new Thread
的方法来创建异步线程,然后来执行,这种方式TLog
是天然支持携带traceId
的
@RestController
public class Controller {
private static final Logger logger = LoggerFactory.getLogger(Controller.class);
@RequestMapping("/test")
public void test() {
Logger.info("tomcat线程执行");
new Thread(() -> Logger.info("一般异步线程执行")).start();
}
}
执行结果
从这可以看出这种异步方式的确成功传递了traceId
1.6.1.2 线程池
对于线程池来说,其实默认也是支持传递traceId
,但是由于线程池中的线程是可以复用了,为了保证线程间的数据互不干扰,需要使用TLogInheritableTask
将提交的任务进行包装。
ThreadPoolExecutor pool =
new ThreadPoolExecutor(1, 2, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
pool.execute(new TLogInheritableTask() {
@Override
public void runTask() {
logger.info("异步执行");
}
});
上述代码的写法会有点耦合,每次提交任务都需要创建一个TLogInheritableTask
,比较麻烦,可以按如下写法进行简化。
public class TLogThreadPoolExecutor extends ThreadPoolExecutor{
//省去构造方法的填充
@Override
public void execute(Runnable command{
super.execute(wrapIfNecessary(command));
}
private TLogInheritableTask wrapIfNecessary(Runnable command) {
if(command instanceof TLogInheritableTask) {
return (TLogInheritableTask) command;
}
return new TLogInheritableTask() {
@Override
public void runTask() {
command.run();
}
};
}
}
自己写个TLogThreadPoolExecutor
继承ThreadPoolExecutor
,重写execute
方法(submit
最终也会调用execute
方法执行),然后将提交的任务统一包装成TLogInheritableTask
,这样需要使用线程池的地方直接创建TLogThreadPoolExecutor
就可以了,就不需要在提交任务的时候创建TLogInheritableTask
了。
ThreadPoolExecutor pool =
new TLogThreadPoolExecutor(1, 2, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
pool.execute(() -> logger.info("异步执行"));
1.6.2 对RPC框架的支持
除了对异步线程的支持,TLog
也支持常见的Dubbo
,Dubbox
,OpenFeign
三大RPC
框架,在SpringBoot
项目中不需要任何配置,只需要引入依赖就可以实现traceId
在服务之间的传递
1.6.2.1 对Dubbo和Dubbox的支持
对于Dubbo
和Dubbox
的支持是基于Dubbo
的Filter
扩展点来的
TLog
通过SPI
机制扩展Filter
,在消费者发送请求前从TLogContext
获取到traceId
,然后将traceId
和其它调用者数据设置请求数据中,服务提供者在处理请求的时候,也会经过Filter
,从请求中获取到traceId
等信息,然后设置到TLogContext
中,从而实现了traceId
在dubbo
的消费者和提供者之间的传递。
1.6.2.2 对OpenFeign的支持
对于OpenFeign
的支持其实也是通过Feign
提供的扩展点RequestInterceptor
来实现的
public class TLogFeignFilter implements RequestInterceptor{
private static final Logger log = LoggerFactory.getLogger(TLogFeignFilter.class);
@Value("${spring.application.name}")
private String appName;
@Override
public void apply(RequestTemplate requestTemplate) {
String traceId = TLogContext.getTraceId();
if(StringUtils.isNotBlank(traceId)){
requestTemplate.header(TLogConstants.TLOG_TRACE_KEY,traceId);
requestTemplate.header(TLogConstants.TLOG_SPANID_KEY,SpanIdGenerator.generateNextSpanId);
requestTemplate.header(TLogConstants.PRE_IVK_APP_KEY,appName);
requestTemplate.header(TLogConstants.PRE_IVK_APP_HOST,LocalhostUtil.getHostName());
requestTemplate.header(TLogConstants.PRE_IP_KEY, LocalhostUtil.getHostIp());
}else{
log .debug("[TLOG]本地threadLocal变量没有正确传递traceId,本次调用不传递traceId");
}
}
}
发送请求之前,从TLogContext
获取到traceId
,将traceId
等信息添加到请求头中,然后就可以通过Http
请求将traceId
等信息传递。
当被调用方接收到请求之后,会经过TLogWebInterceptor
这个拦截器进行拦截,从请求头中获取到这些参数,设置到TLogContext
中。
1.6.3 对常用Http框架的支持
除了一些RPC
框架,TLog
也对一些Http
框架进行了适配,比如
HttpClient、Okhttp、hutool-http、RestTemplate、forest
使用这些Http
框架也可以实现traceId
的传递
其实这些框架的适配跟Feign
的适配都是大同小异,都是基于这些Http
框架各自提供的扩展点进行适配的,将traceId
等信息放到请求头中,这里都不举例了,具体的使用方法可以在官网查看。
1.6.4 对SpringCloud Gateway的支持
同样的,TLog
也适配了SpringCloud Gateway
public class TLogGatewayFilter implements GlobalFilter, Ordered {
@Value("${spring.application.name}")
private String appName;
private static final Logger log = LoggerFactory.getLogger(TLogGatewayFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange,GatewayFilterChain chain){
return chain.filter(TLogWebFluxCommon.loadInstance().preHandle(exchange, appName)).
doFinally(signalType -> TLogWebFluxCommon.loadInstance().cleanThreadLocal());
}
@Override
public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; }
}
原理也是一样的,就是适配了Gateway
的GlobalFilter
,从请求头中获取traceId
等信息。
除了适配了Gateway网关,TLog也适配了Soul网关。
1.6.5 对MQ的支持
对于MQ
的支持跟异步线程差不多,需要将发送的消息包装成TLogMqWrapBean
对象
public class TLogMgWrapBean<T> extends TLogLabelBean
implements Serializable {
private static final Logger log = LoggerFactory.getLogger(TLogMqWrapBean.class);
private static final long serialVersionUID = 1L;
private T t;
public TLogMgWrapBean() {}
public TLogMgWrapBean(T t) {
this.t = t;
String traceId = TLogContext.getTraceId();
if (StringUtils.isNotBlank(traceId)) {
String appName = TLogSpringAware.getProperty("spring.application.name");
this.setTraceId(traceId);
this.setPreIvkApp(appName);
this.setPreIvkHost(Localhostutil.getHostName());
this.setPreIp(LocalhostUtil.getHostIp());
this.setSpanId(SpanIdGenerator.generateNextSpanId());
} else {
log.warn("[TLOG]本地kafka客户端没有正确传递traceId,本次发送不传递traceId");
}
}
发送的时候直接发送TLogMqWrapBean
对象过去
TLogMqWrapBean<BizBean> tLogMqWrap = new TLogMqWrapBean(bizBean);
mqClient.send(tLogMqWrap);
TLogMqWrapBean
会将traceId
等信息携带,消费者接受到TLogMqWrapBean
,然后通过TLogMqConsumerProcessor
处理业务消息。
TLogMqConsumerProcessor.process(tLogMqWrapBean, new TLogMqRunner<BizBean>() {
@Override
public void mqConsume(BizBean o) {
//业务操作
}
});
如此就实现了traceId
通过MQ
传递。
在实际使用中,根据不同的MQ
的类型,可以将消息包装成TLogMqWrapBean
对象的过程和处理消息的过程做统一的封装处理,以减少发送消息和处理消息对于TLog
的耦合
1.6.6 总结
其实从上面的各种适配可以看出,其实本质都是一样的,就是根据具体框架的扩展点,在发送请求之前从TLogContext
获取到traceId
,将traceId
等调用者的信息在请求中携带,然后被调用方解析请求,取出traceId
和调用者信息,设置到被调用方服务中的TLogContext
中。
所以,如果一旦需要遇到官方还未适配的框架或者组件,可以参照上述适配过程进行适配即可。
总的来说,TLog
是一款非常优秀的日志追踪的框架,很适合中小公司使用。这里来总结一下TLog
的特性:
- 通过对日志打印标签完成轻量级微服务日志追踪
- 提供三种接入方式:
javaagent
完全无侵入接入,字节码一行代码接入,基于配置文件的接入 - 对业务代码无侵入式设计,使用简单,10分钟即可接入
- 支持常见的
log4j
,log4j2
,logback
三大日志框架,并提供自动检测,完成适配 - 支持
dubbo,dubbox,feign
三大RPC
框架 - 支持
Spring Cloud Gateway
和Soul
网关 - 支持
HttpClient
和Okhttp
等http
调用框架标签传递 - 支持多种任务框架,
JDK
的TimerTask
,Quartz
,XXL-JOB,spring-scheduled - 支持日志标签的自定义模板的配置,提供多个系统级埋点标签的选择
- 支持异步线程的追踪,包括线程池,多级异步线程等场景
- 几乎无性能损耗,快速稳定,经过压测,损耗在
0.01%