Spring Boot中的日志log原理与自定义日志格式

在企业级微服务架构中,日志不仅仅是代码的运行记录,更是排查问题的"第一现场"。尤其在复杂的分布式链路中,如果没有一个唯一标识串联起所有的调用,面对海量日志,排查报错简直如同大海捞针。

本文将介绍一种日志追踪的方式,来全链路追踪请求的处理。并设置相应的格式,输出更加直观、易处理的日志。

1. Spring boot的日志原理

为什么我们在启动Spring boot应用的时候,控制台能够输出日志。这里的日志是如何输出的呢?

Spring Boot 巧妙地利用了事件发布/监听机制(Event/Listener) 来尽早拉起日志系统。

Spring Application在Run的过程中,会有不同的状态,在到达不同的状态后,SpringApplicationRunListener会监听相应的事件:

java 复制代码
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个方法,分别对应SpringApplication在Run过程中对应的7个状态。如果我们想要在某个状态后自定义一些操作,可以实现相应的接口。

EventPublishingRunListener这个类就是实现了上述SpringApplicationRunListener的具体类,在不同的场景下,产生并发布不同的ApplicationRunEvent。而这些Event的LIstener,将会对相应的Event做出相应的处理。

这里,日志系统的主角就登场了org.springframework.boot.context.logging.LoggingApplicationListener,这个类实现了GenericApplicationListener,而GenericApplicationListener又继承了ApplicationListener,而ApplicationLIstener的方法:

java 复制代码
@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {

	/**
	 * Handle an application event.
	 * @param event the event to respond to
	 */
	void onApplicationEvent(E event);

}

正是处理这个Event的地方。

翻看Spring boot的源码,启动主要分两步:

  • 创建一个SpringApplication对象
  • run方法
    在new SpringApplication()方法中,通过spi方式,设置了org.springframework.boot.context.logging.LoggingApplicationListener,而在这个Listener中的onApplicationEvent方法中,接受ApplicationEvent作为参数,进行处理:
java 复制代码
@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主要关注这5个Event:

  • ApplicationStartingEvent
  • ApplicationEnvironmentPreparedEvent
  • ApplicationPreparedEvent
  • ContextClosedEvent
  • ApplicationFailedEvent

1.1 ApplicationStartingEvent

从名字就可以知道,ApplicationStartingEvent是Spring上下文正在启动的事件,那么这个事件是在什么时候产生的呢?

翻看源码

java 复制代码
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,然后调用其starting()方法。

那么这个ApplicationStartingEvent事件产生时,LoggingApplicationListener的onApplicationEvent做了哪些事呢?

java 复制代码
private void onApplicationStartingEvent(ApplicationStartingEvent event) {
		this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
		this.loggingSystem.beforeInitialize();
	}

第一行代码就是根据classLoader获取日志系统。Spring中支持多个不同的日志系统,支持logback,java log等。然后再根据不同的日志系统,做相应的初始化操作。

那么,是如何获得哪些不同的日志系统的呢?

java 复制代码
	/**
	 * 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的结果可以看到,当前是默认的SYSTEMS中的日志系统,有3个。那么这3个日志系统是如何加载进来的呢?

java 复制代码
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);
	}

静态变量与静态代码块!可以看到,这个SYSTEMS是一个不可变的Map,其中设置3个不同的值,正是我们调试时看到的3个日志系统。

Spring boot在过滤后调用findFirst方法,第一个是logbackLoggingSystem,这个正是Springboot中的默认日志系统。

那日志系统的初始化之前都做哪些事呢?

首先就是要获取日志上下文LoggerContext,这里是使用Logback的日志系统获取的,如果获取到的Context不上Logback的,会有相应提醒:

java 复制代码
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的日志系统,因此这里一些通用的操作,就交给到Slf4JLoggingSystem去做,比如configureJdkLoggingBridgeHandler等。

1.2 ApplicationEnvironmentPreparedEvent

顾名思义,ApplicationEnvironmentPreparedEvent是在EnvironmentPrepared的时候触发产生的事件:

LIstener中又是如何处理的呢?

java 复制代码
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);
	}

由于这个时候,已经拿到environment,那么一些环境变量相关的值都可以设置到日志系统中了。比如日志级别。它从准备好的 Environment 中提取与日志相关的配置(如 logging.level.*, logging.config, logging.file.name 等)。

初始化日志系统(LoggingSystem.initialize())。

真正去寻找并解析具体的配置文件(比如你自定义的 logback-spring.xml)。

这是日志系统真正开始"工作"的分水岭。在这个事件之后,应用打印的日志就会完全按照你 logback-spring.xml 中配置的格式(比如带上 %X{traceId})输出了。

1.3 ApplicationPreparedEvent

上面是Application的Environment已经准备好,这个就是SpringApplication已经准备好的事件了,仍旧是在run这段代码中:

日志系统的处理:

java 复制代码
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);
		}
	}

这一步主要是将日志系统与 Spring 容器打通。

它会将当前正在使用的 LoggingSystem 实例、LogFile 实例等,注册为 Spring 容器中的单例 Bean(Singleton)。

为什么要这样做?是为了让后续 Spring 容器里的其他 Bean(比如 Spring Boot Actuator)能够直接 @Autowired 注入并操作底层的日志系统。

1.4 ContextClosedEvent 和ApplicationFailedEvent

这里已经是日志清理相应的资源了

应用正常关闭(ContextClosedEvent)或者启动过程中抛出异常导致失败(ApplicationFailedEvent)。

LoggingApplicationListener 的处理动作:调用 LoggingSystem.cleanUp()。

2.Spring boot自带日志的局限性

  • 缺乏上下文:无法区分当前日志属于哪个请求。
  • 多线程断流:一旦进入子线程或线程池,主线程的上下文信息(如用户信息、请求 ID)就会丢失。
  • 格式不统一:不同模块日志风格迥异,难以通过 ELK 等工具进行结构化分析。

3.自定义日志格式

由于默认格式的缺点,因此我们可以做一些适当的配置,来优化日志的输出,便于采集和分析。

使用spring-logback.xml文件来进行配置,这里做一个最简单的适配:

xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>

    <!-- 定义LOG_HOME为当前目录 -->
    <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:级别从左显示5个字符宽度%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>
                        {
                        <!--打印时间-->
                        "time": "%date{yyyy-MM-dd HH:mm:ss.SSS}",
                        <!--日志级别-->
                        "level": "%level",
                        <!--类中的第几行-->
                        "caller": "%X{traceId}",
                        <!--日志打印的信息-->
                        "msg": "%message",
                        <!--堆栈异常信息-->
                        "stack_trace":"%xEx",
                        <!--类名 -->
                        "class": "%c",
                        <!--类中第几行 -->
                        "method":"%M"
                        }
                    </pattern>
                </pattern>
            </providers>
        </encoder>
    </appender>


    <!--根据日志级别分离日志,分别输出到不同的文件-->
    <appender name="fileInfoLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <pattern>
                    <pattern>
                        {
                        <!--打印时间-->
                        "time": "%date{yyyy-MM-dd HH:mm:ss.SSS}",
                        <!--日志级别-->
                        "level": "%level",
                        <!--类中的第几行-->
                        "caller": "%X{traceId}",
                        <!--日志打印的信息-->
                        "msg": "%message",
                        <!--堆栈异常信息-->
                        "stack_trace":"%xEx",
                        <!--类名 -->
                        "class": "%c",
                        <!--类中第几行 -->
                        "method":"%M"
                        }
                    </pattern>
                </pattern>
            </providers>
        </encoder>
        <!--滚动策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--按时间保存日志 修改格式可以按小时、按天、月来保存-->
            <fileNamePattern>${LOG_HOME}/log.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!--保存时长-->
            <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>

这里主要设置了日志存放的位置以及大小,在文件中和在控制台输出的格式。当我们把这个文件添加到resource目录下时,启动日志就会如:

4. 请求追踪-多线程

在线程池或者进入子线程,我们设置的唯一ID就会丢失,这样不利于排查问题。因此需要把每个请求所鞋带的唯一ID,透传到子线程或者线程池。

4.1 为什么会丢失?

ID会丢失的根本原因是MDC(Mapped Diagnostic Context)底层依赖的是 ThreadLocal,而 ThreadLocal 的数据是与具体的"线程实例"强绑定的。

什么是MDC?

MDC 的全称是 Mapped Diagnostic Context(映射诊断上下文)。 你可以把它生动地理解为 "主线程的随身背包"或者"行李牌"

它最早由 Log4j 提出,现在 SLF4J(Spring Boot 默认的日志门面)和 Logback/Log4j2(日志实现)都对它提供了完美支持。

  • 数据结构:本质上是一个 Map<String, String>。

  • 物理存储:它的底层完全依赖于 ThreadLocal。

这意味着 MDC 中的数据是线程隔离的。A 线程看不见 B 线程的 MDC 数据。

这也意味着 MDC 的生命周期通常绑定在当前线程的单次任务执行周期内。

核心 API:

  • MDC.put("key", "value"):往当前线程的 map 里存数据。

  • MDC.get("key"):取数据。

  • MDC.remove("key"):删掉某条数据。

  • MDC.clear():清空当前线程的所有数据(极其重要,防止内存泄漏和污染)。

由于物理存储依赖ThreadLocal,因此A 线程看不见 B 线程的 MDC 数据,这就造成了丢失的现象。

4.2 手动进行传递

既然线程间MDC的数据是隔离的,那么我们就可以手动进行传递。

创建一个TraceIdUtil.java类

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

}

再添加一个ThreadMdcUtil.java类:

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);
            }
            //设置traceId
            setTraceIdIfAbsent();
            try {
                runnable.run();
            } finally {
                TraceIdUtil.clear();
            }
        };
    }
}

在线程池中使用的话,可以使用如下的工具类ThreadPoolExecutorMdcExecutor.java

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

这样就可以主动把ID传入子线程或者线程池,就不会丢失了。

如何展示呢?我这里封装了一个starter,直接使用自定义starter就可以使用了。

4.3 创建自己的starter

这里我创建了一个自定义的starter,同时在filter中增加id追踪。

LogMdcFilter.java

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生效

java 复制代码
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());
        // 拦截规则
        registration.addUrlPatterns("/*");
        // 过滤器名称
        registration.setName("logMDCFilter");
        // 过滤器顺序
        registration.setOrder(0);
        return registration;
    }
}

当然了,不要忘了使用上面的spring-logback.xml文件来格式化日志。

这部分代码托管于这个仓库。

5.总结

本文从源码层面探讨了spring的日志系统运作原理,以及在使用中的局限性,并使用了一个可行的方案解决了追踪ID丢失的问题,并且最后自定义了一个spring-boot-starter,用来处理日志全链路追踪。

当然,这个手动传递ID的方法不够优雅,阿里开源的TransmittableThreadLocal(TTL)可以实现零代码侵入的全量链路打通。这里挖个坑,有空再来填。

相关推荐
野生技术架构师1 小时前
Spring Boot + JPackage:构建独立安装包!
java·spring boot·后端
弹简特1 小时前
【JavaEE11-后端部分】SpringMVC06-综合案例2-从用户登录看前后端交互:接口文档与HTTP通信详解
java·spring boot·spring·http·java-ee·tomcat
怕浪猫1 小时前
第17章:反射与泛型编程——运行时能力与代码复用
后端·go·编程语言
xing-xing1 小时前
Spring Data Elasticsearch
后端·spring·elasticsearch
若光6721 小时前
springboot防抖 限流 幂等实现 AOP注解实现
java·spring boot·后端
今天你TLE了吗2 小时前
JVM学习笔记:第五章——堆内存
java·jvm·笔记·后端·学习
摸鱼的春哥2 小时前
春哥的Agent通关秘籍10:本地RAG实战(上)
前端·javascript·后端
彭于晏Yan2 小时前
LangChain4j实战二:集成到Springboot
java·spring boot·后端·langchain
Charlie_lll2 小时前
力扣解题-无重复字符的最长子串
后端·算法·leetcode