持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第10天,点击查看活动详情
前言
Logback是java的日志开源组件,是log4j创始人写的,性能比log4j要好。Logback分成三个模块,logback-core,logback-classic 和 logback-access。在Logback里,最重要的三个类分别是 Logger,Appender,Layout。Logger:日志记录器,把它关联到应用对应的 context 上后,主要用于存放日志对象,定义日志类型,级别等;Appender:指定日志输出的目的地,目的地可以是控制台、文件、远程套接字服务器、MySQL、PostreSQL、Oracle 和其他数据库、JMS 和远程 UNIX Syslog 守护进程等;Layout:负责把事件转换成字符串,格式化日志信息的输出。
我们的项目中只需要引入logback的依赖就可以实现日志的数据,并且有默认的日志输出格式。Spring Boot项目默认采用的就是logback。当我们启动项目后,也有固定的日志输出格式,但是我们什么都没有配置。是因为Spring Boot默认添加了依赖也通过编码的方式给出了默认的配置。
本篇文章首先结合代码讲解一下Spring Boot中logback的日志输出原理,然后再结合源码讲解微服务中用于链路追踪的打印日志时的sleuth框架数据traceid的原理。
原理概述
logback配置文件加载顺序
读取配置文件的顺序如下:
1、在classpath查找logback-test.xml
2、如果该文件不存在,logback尝试寻找logback.groovy
3、如果该文件不存在,logback尝试寻找logback.xml
4、如果该文件不存在,logback会在META-INF下查找com.qos.logback.classic.spi.Configurator接口的实现
5、如果依然找不到,则会使用默认的ch.qos.logback.classic.BasicConfigurator,导致日志直接打印到控制台,日志等级为DEBUG,日志的格式为 %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
以下是logback的基本日志输出配置
public class BasicConfigurator extends ContextAwareBase implements Configurator {
public BasicConfigurator() {
}
public void configure(LoggerContext lc) {
this.addInfo("Setting up default configuration.");
ConsoleAppender<ILoggingEvent> ca = new ConsoleAppender();
ca.setContext(lc);
ca.setName("console");
LayoutWrappingEncoder<ILoggingEvent> encoder = new LayoutWrappingEncoder();
encoder.setContext(lc);
TTLLLayout layout = new TTLLLayout();
layout.setContext(lc);
layout.start();
encoder.setLayout(layout);
ca.setEncoder(encoder);
ca.start();
Logger rootLogger = lc.getLogger("ROOT");
rootLogger.addAppender(ca);
}
}
项目启动的时候org.slf4j.impl.StaticLoggerBinder类调用ContextInitializer的autoConfig()方法,在这个方法里面实现了logback加载配置文件的逻辑。
ch.qos.logback.classic.util.ContextInitializer
public static final String GROOVY_AUTOCONFIG_FILE = "logback.groovy";
public static final String AUTOCONFIG_FILE = "logback.xml";
public static final String TEST_AUTOCONFIG_FILE = "logback-test.xml";
public static final String CONFIG_FILE_PROPERTY = "logback.configurationFile";
public void autoConfig() throws JoranException {
StatusListenerConfigHelper.installIfAsked(this.loggerContext);
// 这个方法实现按照配置文件顺序读取logback配置
URL url = this.findURLOfDefaultConfigurationFile(true);
if (url != null) {
this.configureByResource(url);
} else {
// 通过SPI机制到META-INF下加载配置文件
Configurator c = (Configurator)EnvUtil.loadFromServiceLoader(Configurator.class);
if (c != null) {
try {
c.setContext(this.loggerContext);
c.configure(this.loggerContext);
} catch (Exception var4) {
throw new LogbackException(String.format("Failed to initialize Configurator: %s using ServiceLoader", c != null ? c.getClass().getCanonicalName() : "null"), var4);
}
} else {
// 通过基本配置类ch.qos.logback.classic.BasicConfigurator配置logback
BasicConfigurator basicConfigurator = new BasicConfigurator();
basicConfigurator.setContext(this.loggerContext);
basicConfigurator.configure(this.loggerContext);
}
}
}
// 这个方法实现按照配置文件顺序读取logback配置
public URL findURLOfDefaultConfigurationFile(boolean updateStatus) {
ClassLoader myClassLoader = Loader.getClassLoaderOfObject(this);
URL url = this.findConfigFileURLFromSystemProperties(myClassLoader, updateStatus);
if (url != null) {
return url;
} else {
url = this.getResource("logback-test.xml", myClassLoader, updateStatus);
if (url != null) {
return url;
} else {
url = this.getResource("logback.groovy", myClassLoader, updateStatus);
return url != null ? url : this.getResource("logback.xml", myClassLoader, updateStatus);
}
}
}
springboot的logback默认配置
springboot项目启动后通过编码的方式移除了logback的默认配置,添加了自己的logback配置,但是如果有logback的配置文件springboot的默认logback的配置将不会执行,所以不提供logback.xml配置文件也可以输出日志,日志输出的格式是按springboot的默认配置输出:
"%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} "
+ "%clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} "
+ "%clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} "
+ "%clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"
org.springframework.boot.logging.logback.LogbackLoggingSystem
// 也尝试加载了logback默认顺序的配置文件,这个方法在父抽象类中被调用,用来判断是否有文件在决定是否调用loadDefaults方法加载springboot自己的logback默认配置
@Override
protected String[] getStandardConfigLocations() {
return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy",
"logback.xml" };
}
// 给环境变量中添加了两个属性LOG_LEVEL_PATTERN、LOG_DATEFORMAT_PATTERN
@Override
protected void loadDefaults(LoggingInitializationContext initializationContext,
LogFile logFile) {
LoggerContext context = getLoggerContext();
stopAndReset(context);
LogbackConfigurator configurator = new LogbackConfigurator(context);
Environment environment = initializationContext.getEnvironment();
context.putProperty(LoggingSystemProperties.LOG_LEVEL_PATTERN,
environment.resolvePlaceholders(
"${logging.pattern.level:${LOG_LEVEL_PATTERN:%5p}}"));
context.putProperty(LoggingSystemProperties.LOG_DATEFORMAT_PATTERN,
environment.resolvePlaceholders(
"${logging.pattern.dateformat:${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}"));
// 加载springboot对于logback的默认配置
new DefaultLogbackConfiguration(initializationContext, logFile)
.apply(configurator);
context.setPackagingDataEnabled(true);
}
org.springframework.boot.logging.AbstractLoggingSystem父抽象类中调用了loadDEfaults方法
private void initializeWithConventions(
LoggingInitializationContext initializationContext, LogFile logFile) {
String config = getSelfInitializationConfig();
if (config != null && logFile == null) {
// self initialization has occurred, reinitialize in case of property changes
reinitialize(initializationContext);
return;
}
if (config == null) {
config = getSpringInitializationConfig();
}
if (config != null) {
loadConfiguration(initializationContext, config, logFile);
return;
}
loadDefaults(initializationContext, logFile);
}
org.springframework.boot.logging.logback.DefaultLogbackConfiguration
public void apply(LogbackConfigurator config) {
synchronized (config.getConfigurationLock()) {
base(config);
Appender<ILoggingEvent> consoleAppender = consoleAppender(config);
if (this.logFile != null) {
Appender<ILoggingEvent> fileAppender = fileAppender(config,
this.logFile.toString());
config.root(Level.INFO, consoleAppender, fileAppender);
}
else {
config.root(Level.INFO, consoleAppender);
}
}
}
private Appender<ILoggingEvent> consoleAppender(LogbackConfigurator config) {
ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<>();
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
// 如果没有配置logging.pattern.console那么就使用默认的pattern,这就是为什么配置文件中添加了logging.patter.console配置后就可以安装自己的方式输出
// 默认的pattern为 private static final String CONSOLE_LOG_PATTERN = "%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} "
+ "%clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} "
+ "%clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} "
+ "%clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}";
String logPattern = this.patterns.getProperty("logging.pattern.console",
CONSOLE_LOG_PATTERN);
encoder.setPattern(OptionHelper.substVars(logPattern, config.getContext()));
config.start(encoder);
appender.setEncoder(encoder);
config.appender("CONSOLE", appender);
return appender;
}
sleuth改变console日志格式
org.springframework.cloud.sleuth.autoconfig.TraceEnvironmentPostProcessor这个继承了EnvironmentPostProcessor,因此可以修改spring的环境变量,这个类在环境变量中添加了logging.pattern.level,这个环境变量包含了appName,traceId,spanId。在2.1.0之前sleuth通过org.springframework.cloud.sleuth.log.Slf4jCurrentTraceContext给MDC中添加traceId等信息,2.1.0及之后通过org.springframework.cloud.sleuth.log.Slf4jScopeDecorator给MDC中添加traceId等信息。
public class TraceEnvironmentPostProcessor implements EnvironmentPostProcessor {
private static final String PROPERTY_SOURCE_NAME = "defaultProperties";
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
Map<String, Object> map = new HashMap<String, Object>();
// This doesn't work with all logging systems but it's a useful default so you see
// traces in logs without having to configure it.
if (Boolean
.parseBoolean(environment.getProperty("spring.sleuth.enabled", "true"))) {
map.put("logging.pattern.level",
"%5p [${spring.zipkin.service.name:${spring.application.name:-}},%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-Span-Export:-}]");
}
addOrReplace(environment.getPropertySources(), map);
}
private void addOrReplace(MutablePropertySources propertySources,
Map<String, Object> map) {
MapPropertySource target = null;
if (propertySources.contains(PROPERTY_SOURCE_NAME)) {
PropertySource<?> source = propertySources.get(PROPERTY_SOURCE_NAME);
if (source instanceof MapPropertySource) {
target = (MapPropertySource) source;
for (String key : map.keySet()) {
if (!target.containsProperty(key)) {
target.getSource().put(key, map.get(key));
}
}
}
}
if (target == null) {
target = new MapPropertySource(PROPERTY_SOURCE_NAME, map);
}
if (!propertySources.contains(PROPERTY_SOURCE_NAME)) {
propertySources.addLast(target);
}
}
}
如果要取消sleuth的默认配置可通过如下方式配置,这时候可以在logback配置文件或者java编码中自定义配置输出格式,官方文档
logging:
pattern:
level: '%5p'