最近在項目開發中遇到了一些問題,項目爲多機部署,大量日誌輸出導致很難篩出指定請求的全部相關日誌,以及下游服務調用對應的日誌。因此計劃對項目日誌打印進行一些小改造,使用一個traceId跟蹤請求的全部路徑,前提是不修改原有的打印方式。
簡單的解決思路
想要跟蹤請求,第一個想到的就是當請求來時生成一個traceId
放在ThreadLocal
裏,然後打印時去取就行了。但在不改動原有輸出語句的前提下自然需要日誌框架的支持了,搜索的一番發現主流日誌框架都提供了MDC
功能。
MDC 介紹
MDC(Mapped Diagnostic Context,映射調試上下文)是 log4j 和 logback 提供的一種方便在多線程條件下記錄日誌的功能。MDC 可以看成是一個與當前線程綁定的Map,可以往其中添加鍵值對。MDC 中包含的內容可以被同一線程中執行的代碼所訪問。當前線程的子線程會繼承其父線程中的 MDC 的內容。當需要記錄日誌時,只需要從 MDC 中獲取所需的信息即可。MDC 的內容則由程序在適當的時候保存進去。對於一個 Web 應用來說,通常是在請求被處理的最開始保存這些數據。
簡而言之,MDC就是日誌框架提供的一個InheritableThreadLocal
,項目代碼中可以將鍵值對放入其中,然後使用指定方式取出打印即可。
簡單總結一下主要思路:
- 當請求來的時候,用
Filter
攔截,主要初始化請求的上下文,包括MDC - 配置文件中打印MDC中的traceId和spanId
- 微服務之間調用時,服務調用方需要傳遞當前的上線文,這個時候解析上下文,spanId自增。
具體代碼
- 定義請求上下文,用ThradLocal存儲,代碼如下:
@Getter
@Setter
public class ProjectContext {
public static final String CONTEXT_KEY = "CONTEXT_KEY";
private static final String DEFAULT_SPAN = "1";
private static final Random RANDOM = new Random();
/**
* 每次請求唯一記錄值
*/
private String traceId;
/**
* 一次請求的多次處理唯一標記
*/
private String spanId;
/**
* 請求ip
*/
private String ip;
private static ThreadLocal<ProjectContext> LOCAL = new TransmittableThreadLocal<>();
public static ProjectContext getContext() {
ProjectContext context = LOCAL.get();
if (Objects.isNull(context)) {
context = new ProjectContext();
}
return context;
}
static void nextSpan() {
if (Objects.isNull(getContext())) {
initContext();
return;
}
if (Objects.isNull(getContext().getSpanId())) {
getContext().setSpanId(DEFAULT_SPAN);
return;
}
// 獲取當前的spanId
String span = getContext().getSpanId();
if (span.endsWith(".")) {
span = span.substring(0, span.length() - 1);
}
// 找到切割位置
int p = span.lastIndexOf(".");
String last = span.substring(p + 1);
// 最後需要自增的原數據
int lastId = Integer.parseInt(last);
// 完成自增並設置設置到spanId中
if (p < 0) {
getContext().setSpanId(String.valueOf(lastId + 1));
} else {
getContext().setSpanId(span.substring(0, p) + (lastId + 1));
}
}
/**
* 透傳上下文
*
* @param contextString 被序列化的上下文字符串
*/
public static void fromString(String contextString) {
ProjectContext context = GsonUtil.toBean(contextString, ProjectContext.class);
fromContext(context);
}
public static void fromContext(ProjectContext context) {
LOCAL.set(context);
nextSpan();
}
static void setContext(ProjectContext context) {
LOCAL.set(context);
}
public static void initContext(String ip) {
initContext();
ProjectContext context = getContext();
context.setIp(ip);
setContext(context);
}
public static void initContext() {
ProjectContext context = new ProjectContext();
context.setTraceId(String.valueOf(genLogId()));
context.setSpanId(DEFAULT_SPAN);
setContext(context);
}
public void release() {
}
public static long genLogId() {
return Math.round(((System.currentTimeMillis() % 86400000L) + RANDOM.nextDouble()) * 100000.0D);
}
@Override
public String toString() {
return GsonUtil.toJson(this);
}
}
- 定義過濾器,用於攔截所有的請求,初始化traceId和spanId,RequestFilter
@Slf4j
@WebFilter(filterName = "requestWrapperFilter", urlPatterns = "/*")
public class RequestFilter implements Filter {
public static final String TRACE_ID = "trace";
public static final String SPAN_ID = "span";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
/**
* 初始化請求鏈路信息:唯一key,日誌初始化,body包裝防止獲取日誌打印時後續不能繼續使用
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String contextString = ((HttpServletRequest) request).getHeader(ProjectContext.CONTEXT_KEY);
if (Objects.nonNull(contextString)) {
ProjectContext.fromString(contextString);
} else {
// 無內容時,也自動初始化
ProjectContext.initContext();
}
initLog();
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
public static void initLog() {
MDC.put(TRACE_ID, ProjectContext.getContext().getTraceId());
MDC.put(SPAN_ID, ProjectContext.getContext().getSpanId());
}
}
- 定義feign的請求攔截器,傳遞當前上下文信息,FeignClientInterceptor
public class FeignClientInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
try {
ProjectContext projectContext = ProjectContext.getContext();
if (Objects.nonNull(projectContext)) {
requestTemplate.header(CONTEXT_KEY, projectContext.toString());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
測試
-
啓動service-hi服務,代碼位置:https://github.com/fafeidou/fast-cloud-nacos/tree/master/fast-cloud-nacos-examples/cloud-rpc-examples/service-hi
-
啓動service-feign服務,代碼位置:https://github.com/fafeidou/fast-cloud-nacos/tree/master/fast-cloud-nacos-examples/cloud-rpc-examples/service-feign
訪問 http://localhost:9100/hi?name=456
可以看到spanId 的自增,同一個請求traceId是一樣的。