Spring Boot中的日志log原理與自定義日志格式
在企業(yè)級(jí)微服務(wù)架構(gòu)中,日志不僅僅是代碼的運(yùn)行記錄,更是排查問題的“第一現(xiàn)場(chǎng)”。尤其在復(fù)雜的分布式鏈路中,如果沒有一個(gè)唯一標(biāo)識(shí)串聯(lián)起所有的調(diào)用,面對(duì)海量日志,排查報(bào)錯(cuò)簡(jiǎn)直如同大海撈針。
本文將介紹一種日志追蹤的方式,來全鏈路追蹤請(qǐng)求的處理。并設(shè)置相應(yīng)的格式,輸出更加直觀、易處理的日志。
1. Spring boot的日志原理
為什么我們?cè)趩?dòng)Spring boot應(yīng)用的時(shí)候,控制臺(tái)能夠輸出日志。這里的日志是如何輸出的呢?
Spring Boot 巧妙地利用了事件發(fā)布/監(jiān)聽機(jī)制(Event/Listener) 來盡早拉起日志系統(tǒng)。
Spring Application在Run的過程中,會(huì)有不同的狀態(tài),在到達(dá)不同的狀態(tài)后,SpringApplicationRunListener會(huì)監(jiān)聽相應(yīng)的事件:
public interface SpringApplicationRunListener {
default void starting() {
}
default void environmentPrepared(ConfigurableEnvironment environment) {
}
default void contextPrepared(ConfigurableApplicationContext context) {
}
default void contextLoaded(ConfigurableApplicationContext context) {
}
default void started(ConfigurableApplicationContext context) {
}
default void running(ConfigurableApplicationContext context) {
}
default void failed(ConfigurableApplicationContext context, Throwable exception) {
}
}這里一共有7個(gè)方法,分別對(duì)應(yīng)SpringApplication在Run過程中對(duì)應(yīng)的7個(gè)狀態(tài)。如果我們想要在某個(gè)狀態(tài)后自定義一些操作,可以實(shí)現(xiàn)相應(yīng)的接口。
EventPublishingRunListener這個(gè)類就是實(shí)現(xiàn)了上述SpringApplicationRunListener的具體類,在不同的場(chǎng)景下,產(chǎn)生并發(fā)布不同的ApplicationRunEvent。而這些Event的LIstener,將會(huì)對(duì)相應(yīng)的Event做出相應(yīng)的處理。
這里,日志系統(tǒng)的主角就登場(chǎng)了org.springframework.boot.context.logging.LoggingApplicationListener,這個(gè)類實(shí)現(xiàn)了GenericApplicationListener,而GenericApplicationListener又繼承了ApplicationListener,而ApplicationLIstener的方法:
@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
/**
* Handle an application event.
* @param event the event to respond to
*/
void onApplicationEvent(E event);
}正是處理這個(gè)Event的地方。
翻看Spring boot的源碼,啟動(dòng)主要分兩步:
- 創(chuàng)建一個(gè)SpringApplication對(duì)象
- run方法
在new SpringApplication()方法中,通過spi方式,設(shè)置了org.springframework.boot.context.logging.LoggingApplicationListener,而在這個(gè)Listener中的onApplicationEvent方法中,接受ApplicationEvent作為參數(shù),進(jìn)行處理:
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationStartingEvent) {
onApplicationStartingEvent((ApplicationStartingEvent) event);
}
else if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
}
else if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent((ApplicationPreparedEvent) event);
}
else if (event instanceof ContextClosedEvent
&& ((ContextClosedEvent) event).getApplicationContext().getParent() == null) {
onContextClosedEvent();
}
else if (event instanceof ApplicationFailedEvent) {
onApplicationFailedEvent();
}
}從源碼可以看到,在眾多ApplicationEvent中,LoggingApplicationListener主要關(guān)注這5個(gè)Event:
- ApplicationStartingEvent
- ApplicationEnvironmentPreparedEvent
- ApplicationPreparedEvent
- ContextClosedEvent
- ApplicationFailedEvent
1.1 ApplicationStartingEvent
從名字就可以知道,ApplicationStartingEvent是Spring上下文正在啟動(dòng)的事件,那么這個(gè)事件是在什么時(shí)候產(chǎn)生的呢?
翻看源碼
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}在代碼中,首先獲取到所有的ApplicationRunListener,然后調(diào)用其starting()方法。
那么這個(gè)ApplicationStartingEvent事件產(chǎn)生時(shí),LoggingApplicationListener的onApplicationEvent做了哪些事呢?
private void onApplicationStartingEvent(ApplicationStartingEvent event) {
this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
this.loggingSystem.beforeInitialize();
}
第一行代碼就是根據(jù)classLoader獲取日志系統(tǒng)。Spring中支持多個(gè)不同的日志系統(tǒng),支持logback,java log等。然后再根據(jù)不同的日志系統(tǒng),做相應(yīng)的初始化操作。
那么,是如何獲得哪些不同的日志系統(tǒng)的呢?
/**
* Detect and return the logging system in use. Supports Logback and Java Logging.
* @param classLoader the classloader
* @return the logging system
*/
public static LoggingSystem get(ClassLoader classLoader) {
String loggingSystem = System.getProperty(SYSTEM_PROPERTY);
if (StringUtils.hasLength(loggingSystem)) {
if (NONE.equals(loggingSystem)) {
return new NoOpLoggingSystem();
}
return get(classLoader, loggingSystem);
}
return SYSTEMS.entrySet().stream().filter((entry) -> ClassUtils.isPresent(entry.getKey(), classLoader))
.map((entry) -> get(classLoader, entry.getValue())).findFirst()
.orElseThrow(() -> new IllegalStateException("No suitable logging system located"));
}
private static LoggingSystem get(ClassLoader classLoader, String loggingSystemClass) {
try {
Class<?> systemClass = ClassUtils.forName(loggingSystemClass, classLoader);
Constructor<?> constructor = systemClass.getDeclaredConstructor(ClassLoader.class);
constructor.setAccessible(true);
return (LoggingSystem) constructor.newInstance(classLoader);
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
從DEBUG的結(jié)果可以看到,當(dāng)前是默認(rèn)的SYSTEMS中的日志系統(tǒng),有3個(gè)。那么這3個(gè)日志系統(tǒng)是如何加載進(jìn)來的呢?
private static final Map<String, String> SYSTEMS;
static {
Map<String, String> systems = new LinkedHashMap<>();
systems.put("ch.qos.logback.core.Appender", "org.springframework.boot.logging.logback.LogbackLoggingSystem");
systems.put("org.apache.logging.log4j.core.impl.Log4jContextFactory",
"org.springframework.boot.logging.log4j2.Log4J2LoggingSystem");
systems.put("java.util.logging.LogManager", "org.springframework.boot.logging.java.JavaLoggingSystem");
SYSTEMS = Collections.unmodifiableMap(systems);
}
靜態(tài)變量與靜態(tài)代碼塊!可以看到,這個(gè)SYSTEMS是一個(gè)不可變的Map,其中設(shè)置3個(gè)不同的值,正是我們調(diào)試時(shí)看到的3個(gè)日志系統(tǒng)。
Spring boot在過濾后調(diào)用findFirst方法,第一個(gè)是logbackLoggingSystem,這個(gè)正是Springboot中的默認(rèn)日志系統(tǒng)。
那日志系統(tǒng)的初始化之前都做哪些事呢?
首先就是要獲取日志上下文LoggerContext,這里是使用Logback的日志系統(tǒng)獲取的,如果獲取到的Context不上Logback的,會(huì)有相應(yīng)提醒:
private LoggerContext getLoggerContext() {
ILoggerFactory factory = StaticLoggerBinder.getSingleton().getLoggerFactory();
Assert.isInstanceOf(LoggerContext.class, factory,
String.format(
"LoggerFactory is not a Logback LoggerContext but Logback is on "
+ "the classpath. Either remove Logback or the competing "
+ "implementation (%s loaded from %s). If you are using "
+ "WebLogic you will need to add 'org.slf4j' to "
+ "prefer-application-packages in WEB-INF/weblogic.xml",
factory.getClass(), getLocation(factory)));
return (LoggerContext) factory;
}
由于Spring boot的日志是Slf4J+Logback的日志系統(tǒng),因此這里一些通用的操作,就交給到Slf4JLoggingSystem去做,比如configureJdkLoggingBridgeHandler等。
1.2 ApplicationEnvironmentPreparedEvent
顧名思義,ApplicationEnvironmentPreparedEvent是在EnvironmentPrepared的時(shí)候觸發(fā)產(chǎn)生的事件:


LIstener中又是如何處理的呢?
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
if (this.loggingSystem == null) {
this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
}
initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());
}
/**
* Initialize the logging system according to preferences expressed through the
* {@link Environment} and the classpath.
* @param environment the environment
* @param classLoader the classloader
*/
protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
new LoggingSystemProperties(environment).apply();
this.logFile = LogFile.get(environment);
if (this.logFile != null) {
this.logFile.applyToSystemProperties();
}
this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);
initializeEarlyLoggingLevel(environment);
initializeSystem(environment, this.loggingSystem, this.logFile);
initializeFinalLoggingLevels(environment, this.loggingSystem);
registerShutdownHookIfNecessary(environment, this.loggingSystem);
}由于這個(gè)時(shí)候,已經(jīng)拿到environment,那么一些環(huán)境變量相關(guān)的值都可以設(shè)置到日志系統(tǒng)中了。比如日志級(jí)別。它從準(zhǔn)備好的 Environment 中提取與日志相關(guān)的配置(如 logging.level.*, logging.config, logging.file.name 等)。
初始化日志系統(tǒng)(LoggingSystem.initialize())。
真正去尋找并解析具體的配置文件(比如你自定義的 logback-spring.xml)。
這是日志系統(tǒng)真正開始“工作”的分水嶺。在這個(gè)事件之后,應(yīng)用打印的日志就會(huì)完全按照你 logback-spring.xml 中配置的格式(比如帶上 %X{traceId})輸出了。
1.3 ApplicationPreparedEvent
上面是Application的Environment已經(jīng)準(zhǔn)備好,這個(gè)就是SpringApplication已經(jīng)準(zhǔn)備好的事件了,仍舊是在run這段代碼中:

日志系統(tǒng)的處理:
private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
ConfigurableListableBeanFactory beanFactory = event.getApplicationContext().getBeanFactory();
if (!beanFactory.containsBean(LOGGING_SYSTEM_BEAN_NAME)) {
beanFactory.registerSingleton(LOGGING_SYSTEM_BEAN_NAME, this.loggingSystem);
}
if (this.logFile != null && !beanFactory.containsBean(LOG_FILE_BEAN_NAME)) {
beanFactory.registerSingleton(LOG_FILE_BEAN_NAME, this.logFile);
}
if (this.loggerGroups != null && !beanFactory.containsBean(LOGGER_GROUPS_BEAN_NAME)) {
beanFactory.registerSingleton(LOGGER_GROUPS_BEAN_NAME, this.loggerGroups);
}
}
這一步主要是將日志系統(tǒng)與 Spring 容器打通。
它會(huì)將當(dāng)前正在使用的 LoggingSystem 實(shí)例、LogFile 實(shí)例等,注冊(cè)為 Spring 容器中的單例 Bean(Singleton)。
為什么要這樣做?是為了讓后續(xù) Spring 容器里的其他 Bean(比如 Spring Boot Actuator)能夠直接 @Autowired 注入并操作底層的日志系統(tǒng)。
1.4 ContextClosedEvent 和ApplicationFailedEvent
這里已經(jīng)是日志清理相應(yīng)的資源了
應(yīng)用正常關(guān)閉(ContextClosedEvent)或者啟動(dòng)過程中拋出異常導(dǎo)致失?。ˋpplicationFailedEvent)。
LoggingApplicationListener 的處理動(dòng)作:調(diào)用 LoggingSystem.cleanUp()。

2.Spring boot自帶日志的局限性
- 缺乏上下文:無法區(qū)分當(dāng)前日志屬于哪個(gè)請(qǐng)求。
- 多線程斷流:一旦進(jìn)入子線程或線程池,主線程的上下文信息(如用戶信息、請(qǐng)求 ID)就會(huì)丟失。
- 格式不統(tǒng)一:不同模塊日志風(fēng)格迥異,難以通過 ELK 等工具進(jìn)行結(jié)構(gòu)化分析。
3.自定義日志格式
由于默認(rèn)格式的缺點(diǎn),因此我們可以做一些適當(dāng)?shù)呐渲?,來?yōu)化日志的輸出,便于采集和分析。
使用spring-logback.xml文件來進(jìn)行配置,這里做一個(gè)最簡(jiǎn)單的適配:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<!-- 定義LOG_HOME為當(dāng)前目錄 -->
<property name="LOG_HOME" value="./logs"/>
<appender name="consoleLog_local" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化輸出(配色):%d表示日期,%thread表示線程名,%-5level:級(jí)別從左顯示5個(gè)字符寬度%msg:日志消息,%n是換行符-->
<pattern>[traceId-%X{traceId}] %yellow(%d{yyyy-MM-dd HH:mm:ss}) %red([%thread]) %highlight(%-5level) %boldMagenta(%logger{50}) - %cyan(%msg%n)
</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<pattern>
<pattern>
{
<!--打印時(shí)間-->
"time": "%date{yyyy-MM-dd HH:mm:ss.SSS}",
<!--日志級(jí)別-->
"level": "%level",
<!--類中的第幾行-->
"caller": "%X{traceId}",
<!--日志打印的信息-->
"msg": "%message",
<!--堆棧異常信息-->
"stack_trace":"%xEx",
<!--類名 -->
"class": "%c",
<!--類中第幾行 -->
"method":"%M"
}
</pattern>
</pattern>
</providers>
</encoder>
</appender>
<!--根據(jù)日志級(jí)別分離日志,分別輸出到不同的文件-->
<appender name="fileInfoLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<pattern>
<pattern>
{
<!--打印時(shí)間-->
"time": "%date{yyyy-MM-dd HH:mm:ss.SSS}",
<!--日志級(jí)別-->
"level": "%level",
<!--類中的第幾行-->
"caller": "%X{traceId}",
<!--日志打印的信息-->
"msg": "%message",
<!--堆棧異常信息-->
"stack_trace":"%xEx",
<!--類名 -->
"class": "%c",
<!--類中第幾行 -->
"method":"%M"
}
</pattern>
</pattern>
</providers>
</encoder>
<!--滾動(dòng)策略-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--按時(shí)間保存日志 修改格式可以按小時(shí)、按天、月來保存-->
<fileNamePattern>${LOG_HOME}/log.%d{yyyy-MM-dd}.log</fileNamePattern>
<!--保存時(shí)長-->
<MaxHistory>90</MaxHistory>
<!--文件大小-->
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
</appender>
<root level="info">
<springProfile name="!local">
<appender-ref ref="fileInfoLog"/>
<appender-ref ref="consoleLog"/>
</springProfile>
<springProfile name="local">
<appender-ref ref="consoleLog_local"/>
<appender-ref ref="fileInfoLog"/>
</springProfile>
</root>
</configuration>這里主要設(shè)置了日志存放的位置以及大小,在文件中和在控制臺(tái)輸出的格式。當(dāng)我們把這個(gè)文件添加到resource目錄下時(shí),啟動(dòng)日志就會(huì)如:

4. 請(qǐng)求追蹤-多線程
在線程池或者進(jìn)入子線程,我們?cè)O(shè)置的唯一ID就會(huì)丟失,這樣不利于排查問題。因此需要把每個(gè)請(qǐng)求所鞋帶的唯一ID,透?jìng)鞯阶泳€程或者線程池。
4.1 為什么會(huì)丟失?
ID會(huì)丟失的根本原因是MDC(Mapped Diagnostic Context)底層依賴的是 ThreadLocal,而 ThreadLocal 的數(shù)據(jù)是與具體的“線程實(shí)例”強(qiáng)綁定的。
什么是MDC?
MDC 的全稱是 Mapped Diagnostic Context(映射診斷上下文)。 你可以把它生動(dòng)地理解為 “主線程的隨身背包”或者“行李牌”。
它最早由 Log4j 提出,現(xiàn)在 SLF4J(Spring Boot 默認(rèn)的日志門面)和 Logback/Log4j2(日志實(shí)現(xiàn))都對(duì)它提供了完美支持。
- 數(shù)據(jù)結(jié)構(gòu):本質(zhì)上是一個(gè) Map<String, String>。
- 物理存儲(chǔ):它的底層完全依賴于 ThreadLocal。
這意味著 MDC 中的數(shù)據(jù)是線程隔離的。A 線程看不見 B 線程的 MDC 數(shù)據(jù)。
這也意味著 MDC 的生命周期通常綁定在當(dāng)前線程的單次任務(wù)執(zhí)行周期內(nèi)。
核心 API:
- MDC.put(“key”, “value”):往當(dāng)前線程的 map 里存數(shù)據(jù)。
- MDC.get(“key”):取數(shù)據(jù)。
- MDC.remove(“key”):刪掉某條數(shù)據(jù)。
- MDC.clear():清空當(dāng)前線程的所有數(shù)據(jù)(極其重要,防止內(nèi)存泄漏和污染)。
由于物理存儲(chǔ)依賴ThreadLocal,因此A 線程看不見 B 線程的 MDC 數(shù)據(jù),這就造成了丟失的現(xiàn)象。
4.2 手動(dòng)進(jìn)行傳遞
既然線程間MDC的數(shù)據(jù)是隔離的,那么我們就可以手動(dòng)進(jìn)行傳遞。
創(chuàng)建一個(gè)TraceIdUtil.java類
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import java.util.Map;
import java.util.UUID;
@Slf4j
public class TraceIdUtil {
public static final String TRACE_ID = "traceId";
public static String getTraceId() {
String traceId = null;
try {
traceId = MDC.get(TRACE_ID);
} catch (Exception e) {
log.error("mdc get error", e);
}
return traceId == null ? "" : traceId;
}
public static void setTraceId(String traceId) {
try {
MDC.put(TRACE_ID, traceId);
} catch (Exception e) {
log.error("mdc put error", e);
}
}
public static void removeTraceId() {
try {
MDC.remove(TRACE_ID);
} catch (Exception e) {
log.error("mdc remove error", e);
}
}
public static void clear() {
try {
MDC.clear();
} catch (Exception e) {
log.error("mdc clear error", e);
}
}
public static String generateTraceId() {
return "random-" + UUID.randomUUID().toString().replace("-", "");
}
public static void setContextMap(Map<String, String> contextMap) {
try {
MDC.setContextMap(contextMap);
} catch (Exception e) {
log.error("mdc setContextMap error", e);
}
}
public static Map<String, String> getCopyOfContextMap() {
try {
return MDC.getCopyOfContextMap();
} catch (Exception e) {
log.error("mdc getCopyOfContextMap error", e);
}
return null;
}
}再添加一個(gè)ThreadMdcUtil.java類:
import org.apache.commons.lang3.StringUtils;
import java.util.Map;
import java.util.concurrent.Callable;
public class ThreadMdcUtil {
public static void setTraceIdIfAbsent() {
if (StringUtils.isBlank(TraceIdUtil.getTraceId())) {
TraceIdUtil.setTraceId(TraceIdUtil.generateTraceId());
}
}
public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
return () -> {
if (context == null) {
TraceIdUtil.clear();
} else {
TraceIdUtil.setContextMap(context);
}
setTraceIdIfAbsent();
try {
return callable.call();
} finally {
TraceIdUtil.clear();
}
};
}
public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
return () -> {
if (context == null) {
TraceIdUtil.clear();
} else {
TraceIdUtil.setContextMap(context);
}
//設(shè)置traceId
setTraceIdIfAbsent();
try {
runnable.run();
} finally {
TraceIdUtil.clear();
}
};
}
}在線程池中使用的話,可以使用如下的工具類ThreadPoolExecutorMdcExecutor.java
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExecutorMdcExecutor extends ThreadPoolExecutor {
public ThreadPoolExecutorMdcExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
public ThreadPoolExecutorMdcExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
}
public ThreadPoolExecutorMdcExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
}
public ThreadPoolExecutorMdcExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
@Override
public void execute(Runnable task) {
super.execute(ThreadMdcUtil.wrap(task, TraceIdUtil.getCopyOfContextMap()));
}
@Override
public <T> Future<T> submit(Callable<T> task) {
return super.submit(ThreadMdcUtil.wrap(task, TraceIdUtil.getCopyOfContextMap()));
}
@Override
public Future<?> submit(Runnable task) {
return super.submit(ThreadMdcUtil.wrap(task, TraceIdUtil.getCopyOfContextMap()));
}
}這樣就可以主動(dòng)把ID傳入子線程或者線程池,就不會(huì)丟失了。
如何展示呢?我這里封裝了一個(gè)starter,直接使用自定義starter就可以使用了。
4.3 創(chuàng)建自己的starter
這里我創(chuàng)建了一個(gè)自定義的starter,同時(shí)在filter中增加id追蹤。
LogMdcFilter.java
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
import static com.yaoyuan.bi.utils.Constant.REQUEST_HEADER_KEY_ID;
public class LogMdcFilter implements Filter {
public static final String UNIQUE_ID_NAME = "traceId";
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
String requestId = Optional.of(servletRequest)
.filter(HttpServletRequest.class::isInstance)
.map(HttpServletRequest.class::cast)
.map(req -> req.getHeader(REQUEST_HEADER_KEY_ID))
.filter(header -> !header.trim().isEmpty())
.orElseGet(() -> UUID.randomUUID().toString().replace("-", ""));
MDC.put(UNIQUE_ID_NAME, requestId);
try {
filterChain.doFilter(servletRequest, servletResponse);
} finally {
MDC.remove(UNIQUE_ID_NAME);
}
}
}配置使filter生效
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<LogMdcFilter> logFilterRegistration() {
FilterRegistrationBean<LogMdcFilter> registration = new FilterRegistrationBean<>();
// 注入過濾器
registration.setFilter(new LogMdcFilter());
// 攔截規(guī)則
registration.addUrlPatterns("/*");
// 過濾器名稱
registration.setName("logMDCFilter");
// 過濾器順序
registration.setOrder(0);
return registration;
}
}當(dāng)然了,不要忘了使用上面的spring-logback.xml文件來格式化日志。
這部分代碼托管于這個(gè)倉庫。
5.總結(jié)
本文從源碼層面探討了spring的日志系統(tǒng)運(yùn)作原理,以及在使用中的局限性,并使用了一個(gè)可行的方案解決了追蹤ID丟失的問題,并且最后自定義了一個(gè)spring-boot-starter,用來處理日志全鏈路追蹤。
當(dāng)然,這個(gè)手動(dòng)傳遞ID的方法不夠優(yōu)雅,阿里開源的TransmittableThreadLocal(TTL)可以實(shí)現(xiàn)零代碼侵入的全量鏈路打通。這里挖個(gè)坑,有空再來填。
到此這篇關(guān)于Spring Boot中的日志log原理與自定義日志格式的文章就介紹到這了,更多相關(guān)springboot日志log內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java微服務(wù)架構(gòu)中的關(guān)鍵技術(shù)和設(shè)計(jì)原則解讀
Java是一種面向?qū)ο蟮母呒?jí)編程語言,具有跨平臺(tái)兼容性、自動(dòng)內(nèi)存管理等特點(diǎn),它支持多線程、異常處理,并擁有豐富的標(biāo)準(zhǔn)庫和強(qiáng)大的社區(qū)生態(tài),微服務(wù)架構(gòu)是將應(yīng)用分解為多個(gè)小型服務(wù)的設(shè)計(jì)風(fēng)格2024-11-11
springboot之redis cache TTL選項(xiàng)的使用
這篇文章主要介紹了springboot之redis cache TTL選項(xiàng)的使用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
SpringBoot+ MySQL多線程查詢與聯(lián)表查詢性能對(duì)比
本文主要介紹了SpringBoot+ MySQL多線程查詢與聯(lián)表查詢性能對(duì)比,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-05-05
SpringMvc+Mybatis+Pagehelper分頁詳解
這篇文章主要介紹了SpringMvc+Mybatis+Pagehelper分頁詳解,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下的相關(guān)資料2017-01-01
Java番外雜談之每天掃的二維碼你了解它內(nèi)含的信息嗎
二維碼已經(jīng)成為我們?nèi)粘I钪斜夭豢缮俚慕M成部分了,登錄需要掃一掃二維碼、買東西付錢需要掃一掃二維碼、開會(huì)簽到也需要掃一掃二維碼,那么如此使用的二維碼技術(shù),背后的原理是怎樣的呢?本文將結(jié)合二維碼的發(fā)展歷程以及典型應(yīng)用場(chǎng)景,分析二維碼背后的技術(shù)原理2022-02-02
SpringBoot防止大量請(qǐng)求攻擊的實(shí)現(xiàn)
在有些特定的時(shí)候需要加上IP訪問時(shí)間限制,防止一個(gè)IP多次訪問請(qǐng)求,本文主要介紹了SpringBoot防止大量請(qǐng)求攻擊的實(shí)現(xiàn),感興趣的可以了解一下2021-11-11

