dubbo traceId透傳實現日志鏈路追蹤(基於Filter和RpcContext實現)


一、要解決什么問題:

使用elk的過程中發現如下問題:

1.無法准確定位一個請求經過了哪些服務
2.多個請求線程的日志交替打印,不利於查看按時間順序查看一個請求的日志。

二、期望效果

能夠查看一個請求完整的鏈路日志,不受其它請求日志的干擾。

三、動手實現

消費端需要做什么:

1.新建一個攔截器,攔截所有請求,在調用接口前生成一個鏈路id(traceId)並放入log4j的MDC和dubbo的RpcContext的attachment,此處攔截器是基於jfinal實現,spring mvc可用其它攔截器或aop方案代替,只要實現生成traceId放入內存即可。

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.MDC;

import com.alibaba.dubbo.rpc.RpcContext;
import com.jfinal.aop.Interceptor;
import com.jfinal.aop.Invocation;
import com.jfinal.handler.Handler;
import com.jfinal.kit.StrKit;
import com.ttxn.frameworks.utils.CommonUtil;

public class TraceHandler extends Handler {

	@Override
	public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) {
		String token = request.getParameter("token");
		String userId = StrKit.notBlank(token) ? CommonUtil.getUserId(token) : "VISITOR";
		String uuid = CommonUtil.getUuid();
		String traceId = userId + "-" + uuid;
		RpcContext.getContext().setAttachment("traceId", traceId);
		MDC.put("traceId", traceId);
		next.handle(target, request, response, isHandled);
	}
	
}

2.修改log4j配置文件,在打印日志的開頭加上鏈路id變量(%X{traceId}),用於log4j打印日志時自動打印MDC中的鏈路id,此時消費端所有log4j日志都會輸出鏈路id。

# log4j.rootLogger=WARN, stdout, file
log4j.rootLogger=INFO, stdout , file
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%X{traceId}%n%-d{yyyy-MM-dd HH:mm:ss}%n[%p]-[Thread: %t]-[%C.%M()]: %m%n

# Output to the File
log4j.appender.file=org.apache.log4j.DailyRollingFileAppender
log4j.appender.file.DatePattern='_'yyyy-MM-dd'.log'
log4j.appender.file.File=${catalina.base}/logs/ttxn_log.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%X{traceId}%n%-d{yyyy-MM-dd HH:mm:ss}%n[%p]-[Thread: %t]-[%C.%M()]: %m%n

配置后的消費端log4j輸出效果:

Para: [APP公告]
VISITOR-1e2f6d11ca594ea7af3118567d00f004
2019-07-31 17:37:23
[INFO]-[Thread: DubboServerHandler-192.168.5.15:20884-thread-17]-[com.ttxn.frameworks.annotation.redis.RedisCacheInterceptor.intercept()]: cache not hit key=DATA_CACHE_common.getCfg ,filed=5bm/5pKt55S15Y+w

3.自定義一個dubbo filter,為什么消費端要定義這個filter呢?原因是一個接口可能調用多個服務或調用一個服務多次,這樣會涉及到多次rpc調用,而RpcContext 的 attachment只在一次rpc會話有效,所以我們需要在每次rpc調用之前放入一次traceId,才能保證多次rpc調用的服務端都能獲取到這個traceId,在業務代碼中每rpc一次就setAttachment一次太麻煩,所以我選擇放入過濾器,對應用開發人員透明。src/main/resources目錄下新建META-INF/dubbo/com.alibaba.dubbo.rpc.Filter文件,文件內容如下。

dubboRpcFilter=com.ttxn.api.filter.TraceFilter

修改consumer.xml文件,應用過濾器

<dubbo:consumer timeout="30000" filter="dubboRpcFilter"/> 
import com.alibaba.dubbo.rpc.Filter;
import com.alibaba.dubbo.rpc.Invocation;
import com.alibaba.dubbo.rpc.Invoker;
import com.alibaba.dubbo.rpc.Result;
import com.alibaba.dubbo.rpc.RpcContext;
import com.alibaba.dubbo.rpc.RpcException;
import com.jfinal.kit.StrKit;
import com.ttxn.frameworks.utils.trace.TraceIdUtil;

public class TraceFilter implements Filter {

	@Override
	public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        String traceId = RpcContext.getContext().getAttachment("traceId");
        if ( !StrKit.isBlank(traceId) ) {
            // *) 從RpcContext里獲取traceId並保存
        	TraceIdUtil.setTraceId(traceId);
        } else {
            // *) 交互前重新設置traceId, 避免信息丟失
        	traceId = TraceIdUtil.getTraceId();
            RpcContext.getContext().setAttachment("traceId", traceId);
        }
		Result result = invoker.invoke(invocation);
		return result;
	}

}

到這里,消費端的工作就做完了,如果你和我一樣是jfinal的架構,還需要做一件事,自定義jfinal的請求日志打印器,如下:

import java.io.IOException;
import java.io.Writer;

import org.slf4j.MDC;

import com.alibaba.dubbo.rpc.RpcContext;
/**
 * jfinal請求日志打印工具,用來獲得jfinal action report的日志內容,進行自定義后輸出
 * @author Administrator
 *
 */
public class JFinalActionReportWriter extends Writer {
	
	private static final String LOG_PREFIX = "[START]";
	
	public void write(String str) throws IOException {
		String traceId = MDC.get("traceId");
		System.out.print(LOG_PREFIX + traceId + str);
	}
	public void write(char[] cbuf, int off, int len) throws IOException {}
	public void flush() throws IOException {}
	public void close() throws IOException {}
}

GlobalConfig中增加如下配置

    public void configConstant(Constants me)
    {
        ActionReporter.setWriter(new JFinalActionReportWriter());
    }

就可以自定義jfinal請求輸出日志內容,如下,這部分日志是不受log4j配置影響的,所以我們需要做這一步。

[START]VISITOR-dafe84f6ee2f4d2b907a4c7ef8f8d20c
JFinal action report -------- 2019-07-31 17:37:14 ------------------------------
Url         : GET /app/subject/searchSubjectV3_4
Controller  : com.ttxn.api.controller.app.SubjectRest.(SubjectRest.java:1)
Method      : searchSubjectV3_4
Interceptor : com.ttxn.api.interceptor.APIExceptionInterceptor.(APIExceptionInterceptor.java:1)
              com.ttxn.frameworks.plugins.spring.IocInterceptor.(IocInterceptor.java:1)
              com.ttxn.api.decorator.CoverImgInterceptor.(CoverImgInterceptor.java:1)
Parameter   : query=  
生產端需要做什么:

1.和消費端一樣需要自定義dubbo攔截器,新增配置文件src\main\resources\META-INF\dubbo\com.alibaba.dubbo.rpc.Filter,文件內容:

dubboContextFilter=com.ttxn.webservice.interceptor.dubbo.ContextFilter

修改provider.xml,對生產者應用此攔截器:

	<!-- 提供方調用過程缺省攔截器,將攔截所有service -->
	<dubbo:provider filter="dubboContextFilter"/>

在自定義攔截器中獲取traceId,放入MDC:



package com.ttxn.webservice.interceptor.dubbo;

import java.lang.annotation.Annotation;
import java.sql.SQLException;

import org.apache.log4j.MDC;

import com.alibaba.dubbo.rpc.Filter;
import com.alibaba.dubbo.rpc.Invocation;
import com.alibaba.dubbo.rpc.Invoker;
import com.alibaba.dubbo.rpc.Result;
import com.alibaba.dubbo.rpc.RpcContext;
import com.alibaba.dubbo.rpc.RpcException;
import com.jfinal.kit.StrKit;
import com.jfinal.plugin.activerecord.Db;
import com.jfinal.plugin.activerecord.IAtom;
import com.ttxn.frameworks.annotation.transaction.Transaction;
import com.ttxn.frameworks.utils.trace.TraceIdUtil;

public class ContextFilter implements Filter
{
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation)
        throws RpcException
    {
        String traceId = RpcContext.getContext().getAttachment("traceId");
        if (StrKit.notBlank(traceId)) {
        	MDC.put("traceId", traceId);
        }
    	
    	Result result = invoker.invoke(invocation);
        return result;
    }


}

修改log4j文件,輸出格式中加上traceId變量(%X{traceId}):

# log4j.rootLogger=WARN, stdout, file
log4j.rootLogger=INFO, stdout , file
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%X{traceId}%n%-d{yyyy-MM-dd HH:mm:ss}%n[%p]-[Thread: %t]-[%C.%M()]: %m%n

# Output to the File
log4j.appender.file=org.apache.log4j.DailyRollingFileAppender
log4j.appender.file.DatePattern='_'yyyy-MM-dd'.log'
log4j.appender.file.File=${catalina.base}/logs/ttxn_log.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%X{traceId}%n%-d{yyyy-MM-dd HH:mm:ss}%n[%p]-[Thread: %t]-[%C.%M()]: %m%n

到此就實現了traceId從消費端到生產端的透傳,生產端log4j日志如下:

Para: [快報]
VISITOR-1e2f6d11ca594ea7af3118567d00f004
2019-07-31 17:37:23
[INFO]-[Thread: DubboServerHandler-192.168.5.15:20884-thread-15]-[com.ttxn.frameworks.annotation.redis.RedisCacheInterceptor.intercept()]: cache not hit key=DATA_CACHE_base.newsList ,filed=15

我們就可以從kibana中按照traceId來過濾單次請求的所有日志,達到方便排查bug的效果。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM