日誌鏈路追蹤的必要性
雷迪森俺的杰特們,日誌查詢不管在測試環境或是生產環境,都是作爲一個開發人員經常要去找、要去看的東西。某個業務出現BUG,那麼如何迅速的定位我們所要找的日誌及相關的其它日誌?事關測試及開發人員定位問題的效率,往往大家都是根據自己打印的日誌的關鍵字去查詢日誌,但是在併發量大、或業務流程長導致日誌不連貫的場景中往往你無法通過一次、兩次的日誌查詢看清整個業務線的日誌。甚至只能通過去看實時日誌 然後重現,才能看清問題。然而生產環境中很多時候哪會給你重現的機會,同時現在分佈式微服務大力推行,一旦調用其他服務出了BUG 是不是又要重新去找關鍵字查日誌?
問題來了,爲什麼這麼麻煩大家還不去優化?咱打印日誌弄的像京東購物一樣,弄個訂單號本次請求的所有日誌都帶上該訂單號,後臺每一次的響應都帶上日誌訂單號,或者只要拋異常就帶上日誌訂單號返回至前端。然後找日誌直接查訂單號不就可以了嗎? 測試人員拿着日誌ID 交給開發 哪還需要復現?效率提高10倍 有沒有?完美 有了方案 咱就去幹!
新建過濾器
第一步 當然是攔截所有的HTTP請求給每個請求所產生的日誌都加上一個唯一標識。
引入maven依賴
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.13</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>provided</scope>
<version>1.1.7</version>
</dependency>
新建過濾器MdcFilter 繼承 MDCInsertingServletFilter
Logback自帶的ch.qos.logback.classic.helpers.MDCInsertingServletFilter能夠將HTTP請求的hostname, request URI, user-agent等信息裝入MDC
package com.test.trace.web.filter;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.MDC;
import com.alibaba.dubbo.common.utils.StringUtils;
import com.test.trace.support.AbstractMyThreadContext;
import com.test.trace.support.AbstractUUIDShort;
import ch.qos.logback.classic.helpers.MDCInsertingServletFilter;
public class MdcFilter extends MDCInsertingServletFilter {
private static final String HEARDER_FOR_TRACE_ID = "X-TRACE-ID";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!(request instanceof HttpServletRequest)) {
return;
}
/**如前端傳遞了唯一標識ID 拿出來直接用 根據業務這段也可以刪除 然後去掉if判斷*/
String traceId =
((HttpServletRequest) request).getHeader(HEARDER_FOR_TRACE_ID);
if (StringUtils.isEmpty(traceId)) {
traceId = AbstractUUIDShort.generate();
}
AbstractMyThreadContext.setTraceId(traceId);
MDC.put(AbstractMyThreadContext.MDC_TRACE_ID,traceId);
try {
//從新調動父類的doFilter方法
super.doFilter(request, response, chain);
} finally {
//資源回收
MDC.remove(AbstractMyThreadContext.MDC_TRACE_ID);
AbstractMyThreadContext.removeTraceId();
}
}
}
package com.test.trace.web.filter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnClass(name = "org.springframework.web.servlet.DispatcherServlet")
public class MdcFilterAutoConfigure {
@Bean
@ConditionalOnMissingBean(MdcFilter.class)
public MdcFilter mdcFilter() {
return new MdcFilter();
}
}
spring 註解小課堂開課啦
- @Configuration 表示該類爲spring配置類
- @ConditionalOnClass 判斷某個類是否存在於 classpath 中兩個一起用就是 當classpath 中存在org.springframework.web.servlet.DispatcherServlet 就加載MdcFilterAutoConfigure
- @Bean 產生一個Bean,然後交給Spring容器管理。@Configuration與@Bean結合使用。@Configuration可理解爲用spring的時候xml裏面的<beans>標籤,@Bean可理解爲用spring的時候xml裏面的<bean>標籤
- @ConditionalOnMissingBean 當指定的Bena不存在時返回true,結合@Bean使用就是如果容器中MdcFilter不存在咱們就產生一個MdcFilter交給spring
日誌鏈路ID生成工具類
package com.test.trace.support;
import java.util.Base64;
import java.util.UUID;
public abstract class AbstractUUIDShort {
/**生成唯一ID*/
public static String generate() {
UUID uuid = UUID.randomUUID();
return compressedUUID(uuid);
}
protected static String compressedUUID(UUID uuid) {
byte[] byUuid = new byte[16];
long least = uuid.getLeastSignificantBits();
long most = uuid.getMostSignificantBits();
long2bytes(most, byUuid, 0);
long2bytes(least, byUuid, 8);
return Base64.getEncoder().encodeToString(byUuid);
}
protected static void long2bytes(long value, byte[] bytes, int offset) {
for (int i = 7; i > -1; i--) {
bytes[offset++] = (byte) ((value >> 8 * i) & 0xFF);
}
}
}
日誌鏈路ID存儲、與銷燬工具類
主要適用於調用dubbo接口時候取出TraceId且做到保證子線程也能拿到TraceId,其實如果不涉及跨系統的接口調用這個類是沒有存在的必要的。
package com.test.trace.support;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
public abstract class AbstractMyThreadContext {
private static final Logger log = LoggerFactory.getLogger(AbstractMyThreadContext.class);
/**
* logback模板對應的變量
*/
public final static String MDC_TRACE_ID = "traceId";
public static final String TRACE_ID = "com.test.trace.MyThreadContext_TRACE_ID_KEY";
//確保子線程能夠繼承父線程的數據
private static final ThreadLocal<Map<Object, Object>> RESOURCES =
new InheritableThreadLocalMap<Map<Object, Object>>();
protected AbstractMyThreadContext() {}
public static Map<Object, Object> getResources() {
return RESOURCES != null ? new HashMap<Object, Object>(RESOURCES.get()) : null;
}
public static void setResources(Map<Object, Object> newResources) {
if (newResources == null || newResources.isEmpty()) {
return;
}
RESOURCES.get().clear();
RESOURCES.get().putAll(newResources);
}
private static Object getValue(Object key) {
return RESOURCES.get().get(key);
}
public static Object get(Object key) {
if (log.isTraceEnabled()) {
log.trace("get() - in thread [{}]",Thread.currentThread().getName());
}
Object value = getValue(key);
if ((value != null) && log.isTraceEnabled()) {
log.trace("Retrieved value of type [{}] for key [{}] bound to thread [{}]",
value.getClass().getName(), key, Thread.currentThread().getName());
}
return value;
}
public static void put(Object key, Object value) {
if (key == null) {
throw new IllegalArgumentException("key cannot be null");
}
if (value == null) {
remove(key);
return;
}
RESOURCES.get().put(key, value);
if (log.isTraceEnabled()) {
log.trace("Bound value of type [{}] for key [{}] to thread [{}]",
value.getClass().getName(), key, Thread.currentThread().getName());
}
}
public static Object remove(Object key) {
Object value = RESOURCES.get().remove(key);
if ((value != null) && log.isTraceEnabled()) {
log.trace("Retrieved value of type [{}] for key [{}] from thread [{}]",
value.getClass().getName(), key, Thread.currentThread().getName());
}
return value;
}
public static void remove() {
RESOURCES.remove();
}
//從線程局部變量中獲取TraceId
public static String getTraceId() {
return (String) get(TRACE_ID);
}
//將TraceId攝入線程局部變量中
public static void setTraceId(String xid) {
put(TRACE_ID, xid);
}
//清除線程局部變量中的TraceId
public static void removeTraceId() {
remove(TRACE_ID);
}
private static final class InheritableThreadLocalMap<T extends Map<Object, Object>>
extends InheritableThreadLocal<Map<Object, Object>> {
@Override
protected Map<Object, Object> initialValue() {
return new HashMap<Object, Object>(1 << 4);
}
@SuppressWarnings({"unchecked"})
@Override
protected Map<Object, Object> childValue(Map<Object, Object> parentValue) {
if (parentValue != null) {
return (Map<Object, Object>) ((HashMap<Object, Object>) parentValue).clone();
} else {
return null;
}
}
}
}
logback.xml設置日誌輸出格式
<pattern> 屬性配置:(貼出我配置的格式 [%X{traceId}] 這個就是線程ID了)
[%-5level] [%contextName] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{req.remoteHost}] [%X{req.requestURI}] [%X{traceId}] %logger - %msg%n
到這裏整個日誌鏈路追蹤完成了一半,已經可以做到所有HTTP的請求都給加上traceId,但是如果我們調用dubbo接口,MdcFilter 是無法攔截的,所以導致logback中找不到traceId,所以我們需要再加一個com.alibaba.dubbo.rpc.Filter用於守好dubbo的消費者和服務提供者的門,消費者需要交出自己的traceId,服務提供者收到請求將traceId攝入。
詳見我的上一篇博客:Dubbo之Filter過濾器(攔截器)的使用
比較懶 這裏我就不做過多的描述了 抱歉。
當你完成這一步之後整個日誌鏈路系統基本就完成了90% 如果你足夠努力,同時把EFK弄起來 查詢日誌效率直接一步登天
- 名詞解析 EFK
通過filebeat實時收集 nginx訪問日誌 ▷ filebeat將收集的日誌傳輸至elasticsearch集羣 ▷ 通過kibana展示日誌。
但是會思考的同學就會問了,老夫的定時任務執行報錯了還不一樣日了狗,日誌裏面的traceId是null的啊。
說白了上面的方法還是隻搞定外部發起的請求 程序自己的定時任務咋整呢?
這時候面試常出現的問題AOP 浮出水面。
AOP有需要了解的同學可以進傳送門: 輕鬆實現aop功能的三種方式
新建攔截器
package com.test.trace.interceptor;
import com.test.trace.support.AbstractMyThreadContext;
import com.test.trace.support.AbstractUUIDShort;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
/**
* @description logback全局日誌交易id攔截器
*/
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MdcTraceIdMethodInteceptor implements MethodInterceptor {
protected final Logger log = LoggerFactory.getLogger(this.getClass());
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
if (MDC.get(AbstractMyThreadContext.MDC_TRACE_ID) != null) {
return invocation.proceed();
}
try {
String traceId = AbstractUUIDShort.generate();
AbstractMyThreadContext.setTraceId(traceId);
MDC.put(AbstractMyThreadContext.MDC_TRACE_ID,traceId);
return invocation.proceed();
} catch (Throwable e) {
log.warn("MdcTraceIdMethodInteceptor error", e.getMessage());
throw e;
} finally {
MDC.remove(AbstractMyThreadContext.MDC_TRACE_ID);
AbstractMyThreadContext.removeTraceId();
}
}
}
package com.test.trace.interceptor;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @descriptionlogback全局日誌交易id攔截器配置 <br/>
* 主要針對例如 定時任務 MQ等 非HTTP發起的請求沒有traceId 配置需要攔截過濾的地址使用 <br/>
* 配置demo :<br/>
* log.traceId.pointcutExpression=execution(* com.test.service.rabbitmq..*.*(..)) || execution(* com.test.job..*.*(..))
*/
@Configuration
@ConditionalOnProperty(name = "log.traceId.pointcutExpression")
public class MdcTraceIdConfiguration {
@Value("${log.traceId.pointcutExpression}")
private String POINTCUT_EXPRESSION;
@Bean("MdcTraceIdMethodInteceptor")
public MdcTraceIdMethodInteceptor mdcTraceIdMethodInteceptor() {
return new MdcTraceIdMethodInteceptor();
}
@Bean("MdcTraceIdAdvisor")
public Advisor MdcTraceIdAdvisor(MdcTraceIdMethodInteceptor mdcTraceIdMethodInteceptor) {
AspectJExpressionPointcut cut = new AspectJExpressionPointcut();
cut.setExpression(POINTCUT_EXPRESSION);
Advisor advisor = new DefaultPointcutAdvisor(cut, mdcTraceIdMethodInteceptor);
return advisor;
}
}
註解小課堂又開課啦:錯誤日誌找不到,多半是太懶,打一頓就好
- @ConditionalOnProperty 當可以讀取到指定name的配置時爲true,如果還配置了value屬性,那就要比對配置的值要和指定的value一樣纔是true。
到這裏就徹底完成了 將漏網之魚的路徑配置一下,打一個jar包各個項目只需依賴一下。如果需要各自配置 log.traceId.pointcutExpression 就搞定了。