日志框架之TLog讲解分析

1 TLog

1.1 引言

随着微服务盛行,很多公司都把系统按照业务边界拆成了很多微服务,在排错查日志的时候,因为业务链路贯穿着很多微服务节点,导致定位某个请求的日志以及上下游业务的日志会变得有些困难。

这时候可能有的小伙伴就会想到使用SkyWalkingPinpoint等分布式追踪系统来解决,并且这些系统通常都是无侵入性的,同时也会提供相对友好的管理界面来进行链路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("测试");
    }
}

这里是通过slf4jLoggerFactory获取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进行数据拼接生成日志标签,所以默认只打出spanIdtraceId
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);
    }

TLogContextTLog是一个核心的组件,这个组件内部是使用了TransmittableThreadLocal来传递traceIdpreApp等信息。

当有一个请求过来的时候,会从解析出traceIdpreApp等信息,然后设置到TransmittableThreadLocal中,之后就可以在整个调用链路中从TLogContext中获取到traceId等信息。

1.5.3 TLogRPCHandler

在这里插入图片描述

这个组件是用来处理调用方传递的traceIdpreApp等信息,设置到TLogContextMDC中,同时根据日志标签的格式生成日志标签。

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也支持常见的DubboDubboxOpenFeign三大RPC框架,在SpringBoot项目中不需要任何配置,只需要引入依赖就可以实现traceId在服务之间的传递

1.6.2.1 对Dubbo和Dubbox的支持

对于DubboDubbox的支持是基于DubboFilter扩展点来的

在这里插入图片描述

TLog通过SPI机制扩展Filter,在消费者发送请求前从TLogContext获取到traceId,然后将traceId和其它调用者数据设置请求数据中,服务提供者在处理请求的时候,也会经过Filter,从请求中获取到traceId等信息,然后设置到TLogContext中,从而实现了traceIddubbo的消费者和提供者之间的传递。

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; }
}

原理也是一样的,就是适配了GatewayGlobalFilter,从请求头中获取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分钟即可接入
  • 支持常见的log4jlog4j2logback三大日志框架,并提供自动检测,完成适配
  • 支持dubbo,dubbox,feign三大RPC框架
  • 支持Spring Cloud GatewaySoul网关
  • 支持HttpClientOkhttphttp调用框架标签传递
  • 支持多种任务框架,JDKTimerTaskQuartz,XXL-JOB,spring-scheduled
  • 支持日志标签的自定义模板的配置,提供多个系统级埋点标签的选择
  • 支持异步线程的追踪,包括线程池,多级异步线程等场景
  • 几乎无性能损耗,快速稳定,经过压测,损耗在0.01%

参考链接:https://mp.weixin.qq.com/s/yvzC7KI-nziRunivWee9sA

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容