Spring Boot日志之logback输出原理

1,139 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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'