填坑篇—SpringBoot整合log4j2

3,697 阅读2分钟

  之前对一个项目进行调优,将原先项目中的日志配置logBack,修改为log4j2,网上查找各种资料,官方文档,遇到很多坑,最终实现了修改,至此,特记录一下。

1.导入依赖,pom文件配置

  SpringBoot官方文档:

docs.spring.io/spring-boot…

  一个干净的项目如果要使用log4j2,可以采用下面的方式引入依赖:

        <!-- 排除 Spring-boot-starter 默认的日志配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- 引入log4j2依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>

  因为在老项目中,日志依赖混杂,在项目中引入依赖情况:

		<!-- slf4j的实现:log4j2,用来取代logback -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-log4j2</artifactId>
			<version>1.5.9.RELEASE</version>
		</dependency>
    
                <!-- log4j转 slf4j 输出-->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>log4j-over-slf4j</artifactId>
			<version>1.7.25</version>
		</dependency>

		<!--异步日志依赖-->
		<dependency>
			<groupId>com.lmax</groupId>
			<artifactId>disruptor</artifactId>
			<version>3.3.4</version>
		</dependency>

  总结:排干净项目中的Logback依赖,否则会导致日志在控制台打印却不能输出到文件中。

2.log4j2配置文件

参考(官方)文档:

www.docs4dev.com/docs/zh/log…

docs.spring.io/spring-boot…

  在项目resource目录下,新建log4j2.xml 或者 log4j2-spring.xm l。建议使用log4j2-spring.xml,否则还需在application.properties或者.yml中配置好logging.config=你的配置文件名。

  网上找来的xml模版:

<?xml version="1.0" encoding="UTF-8"?>
<!--      Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,
     当设置成trace时,可以看到log4j2内部各种详细输出
-->
<!-- monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数 -->
<configuration monitorInterval="5">
    <!-- 日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->

    <!-- 变量配置 -->
    <Properties>
        <!--
            格式化输出:
            %d表示日期,
            %thread表示线程名,
            %-5level:级别从左显示5个字符宽度
            %msg:日志消息,%n是换行符
            %logger{36} 表示 Logger 名字最长36个字符
        -->
        <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight{%-5level}[%thread] %style{%logger{36}}{cyan} : %msg%n" />

        <!-- 定义日志存储的路径,不要配置相对路径 -->
        <property name="FILE_PATH" value="此处更换为你的日志文件存储路径" />
        <property name="FILE_NAME" value="此处更换为你的项目名称" />
    </Properties>

    <appenders>
        <console name="Console" target="SYSTEM_OUT">
            <!--输出日志的格式-->
            <PatternLayout pattern="${LOG_PATTERN}" disableAnsi="false" noConsoleNoAnsi="false"/>
            <!--控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
        </console>

        <!--
          这个会打印出所有的info及以下级别的信息,每次大小超过size,
          则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档
        -->
        <RollingFile name="RollingFileInfo" fileName="${FILE_PATH}/info.log" filePattern="${FILE_PATH}/${FILE_NAME}-INFO-%d{yyyy-MM-dd}_%i.log.gz">
            <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <Policies>
                <!--interval属性用来指定多久滚动一次,默认是1 hour-->
                <TimeBasedTriggeringPolicy interval="1"/>
                <SizeBasedTriggeringPolicy size="20MB"/>
            </Policies>
            <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖 -->
            <DefaultRolloverStrategy max="15"/>
        </RollingFile>

        <!-- 这个会打印出所有的warn及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
        <RollingFile name="RollingFileWarn" fileName="${FILE_PATH}/warn.log" filePattern="${FILE_PATH}/${FILE_NAME}-WARN-%d{yyyy-MM-dd}_%i.log.gz">
            <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <Policies>
                <!--interval属性用来指定多久滚动一次,默认是1 hour-->
                <TimeBasedTriggeringPolicy interval="1"/>
                <SizeBasedTriggeringPolicy size="20MB"/>
            </Policies>
            <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
            <DefaultRolloverStrategy max="15"/>
        </RollingFile>

        <!-- 这个会打印出所有的error及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
        <RollingFile name="RollingFileError" fileName="${FILE_PATH}/error.log" filePattern="${FILE_PATH}/${FILE_NAME}-ERROR-%d{yyyy-MM-dd}_%i.log.gz">
            <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <Policies>
                <!--interval属性用来指定多久滚动一次,默认是1 hour-->
                <TimeBasedTriggeringPolicy interval="1"/>
                <SizeBasedTriggeringPolicy size="20MB"/>
            </Policies>
            <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
            <DefaultRolloverStrategy max="15"/>
        </RollingFile>
    </appenders>

    <!--Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。-->
    <!--然后定义loggers,只有定义了logger并引入的appender,appender才会生效-->
    <loggers>
        <!--过滤掉spring和mybatis的一些无用的DEBUG信息-->
        <logger name="org.mybatis" level="info" additivity="false">
            <AppenderRef ref="Console"/>
        </logger>
        <!--监控系统信息-->
        <!--若是additivity设为false,则 子Logger 只会在自己的appender里输出,而不会在 父Logger 的appender里输出。-->
        <Logger name="org.springframework" level="info" additivity="false">
            <AppenderRef ref="Console"/>
        </Logger>

        <root level="info">
            <appender-ref ref="Console"/>
            <appender-ref ref="RollingFileInfo"/>
            <appender-ref ref="RollingFileWarn"/>
            <appender-ref ref="RollingFileError"/>
        </root>
    </loggers>
</configuration>

3.配置文件解读

3.1 日志级别

  在log4j2中, 共有8个级别,按照从低到高为:ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF。

  • All:最低等级的,用于打开所有日志记录;
  • Trace:是追踪,就是程序推进一下;
  • Debug:指出细粒度信息事件对调试应用程序是非常有帮助的;
  • Info:消息在粗粒度级别上突出强调应用程序的运行过程;
  • Warn:输出警告及warn以下级别的日志;
  • Error:输出错误信息日志;
  • Fatal:输出每个严重的错误事件将会导致应用程序的退出的日志;
  • OFF:最高等级的,用于关闭所有日志记录。
Event Level LoggerConfig Level
TRACE DEBUG INFO WARN ERROR FATAL OFF
ALL YES YES YES YES YES YES NO
TRACE YES NO NO NO NO NO NO
DEBUG YES YES NO NO NO NO NO
INFO YES YES YES NO NO NO NO
WARN YES YES YES YES NO NO NO
ERROR YES YES YES YES YES NO NO
FATAL YES YES YES YES YES YES NO
OFF NO NO NO NO NO NO NO

3.2 xml配置解读

根节点Configuration
	有两个属性:
		status:指定log4j2本身的打印日志的级别.
		monitorinterval:指定log4j自动重新配置的监测间隔时间,单位是s,最小是5s.
	有两个子节点:	
		Appenders
		Loggers

Appenders节点
	常见的有三种子节点:Console、RollingFile、File
		Console节点用来定义输出到控制台的Appender.
			name:指定Appender的名字.
			target:SYSTEM_OUT 或 SYSTEM_ERR,一般只设置默认:SYSTEM_OUT.
			PatternLayout:输出格式,不设置默认为:%m%n.
		File节点用来定义输出到指定位置的文件的Appender.
			name:指定Appender的名字.
			fileName:指定输出日志的目的文件带全路径的文件名.
			PatternLayout:输出格式,不设置默认为:%m%n.
		RollingFile节点用来定义超过指定条件自动删除旧的创建新的Appender.
			name:指定Appender的名字.
			fileName:指定输出日志的目的文件带全路径的文件名.
			PatternLayout:输出格式,不设置默认为:%m%n.
			filePattern : 指定当发生Rolling时,文件的转移和重命名规则.
			Policies:指定滚动日志的策略,就是什么时候进行新建日志文件输出日志.
			TimeBasedTriggeringPolicy:Policies子节点,基于时间的滚动策略,interval属性用来指定多久滚动一次,默认是1 hour。modulate=true用来调整时间:比如现在是早上3am,interval是4,那么第一次滚动是在4am,接着是8am,12am...而不是7am.
			SizeBasedTriggeringPolicy:Policies子节点,基于指定文件大小的滚动策略,size属性用来定义每个日志文件的大小.
			DefaultRolloverStrategy:用来指定同一个文件夹下最多有几个日志文件时开始删除最旧的,创建新的(通过max属性)。
Loggers节点
	Root节点用来指定项目的根日志,如果没有单独指定Logger,那么就会默认使用该Root日志输出
		level:日志输出级别
    AppenderRef:Root的子节点,用来指定该日志输出到哪个Appender
	Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。
		level:日志输出级别
		name:用来指定该Logger所适用的类或者类所在的包全路径,继承自Root节点.
		AppenderRef:Logger的子节点,用来指定该日志输出到哪个Appender,如果没有指定,就会默认继承自Root.如果指定了,那么会在指定的这个Appender和Root的Appender中都会输出,此时我们可以设置Logger的additivity="false"只在自定义的Appender中进行输出。

4.log4j2-spring.xml读取.properties中的属性值

  Spring Environment property: logging.config.并不能为log4j2-spring提供属性,我们需要通过MDC为其获取.properties中的属性。

4.1 写一个监听类

import org.slf4j.MDC;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.context.event.ApplicationFailedEvent;
import org.springframework.boot.context.event.ApplicationPreparedEvent;
import org.springframework.boot.context.event.ApplicationStartingEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.GenericApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertySource;

public class ApplicationStartedEventListener implements GenericApplicationListener {


    public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 10;

    private static Class<?>[] EVENT_TYPES = { ApplicationStartingEvent.class,
            ApplicationEnvironmentPreparedEvent.class, ApplicationPreparedEvent.class,
            ContextClosedEvent.class, ApplicationFailedEvent.class };

    private static Class<?>[] SOURCE_TYPES = { SpringApplication.class,
            ApplicationContext.class };

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationEnvironmentPreparedEvent) {

            ConfigurableEnvironment envi = ((ApplicationEnvironmentPreparedEvent) event).getEnvironment();

            PropertySource<?> propertySource = envi.getPropertySources().get("读取到的配置文件,比如applicationConfig: [classpath:/application.properties]");
            //你自己的逻辑,切记MDC.put(key,value)
            ...
            MDC.put("LOG_PATH", loggingPath);
        }
    }
    /*
     * (non-Javadoc)
     *
     * @see org.springframework.core.Ordered#getOrder()
     */
    @Override
    public int getOrder() {
        // TODO Auto-generated method stub
        return DEFAULT_ORDER;
    }

    /*
     * (non-Javadoc)
     *
     * @see org.springframework.context.event.GenericApplicationListener#
     * supportsEventType(org.springframework.core.ResolvableType)
     */
    @Override
    public boolean supportsEventType(ResolvableType resolvableType) {
        return isAssignableFrom(resolvableType.getRawClass(), EVENT_TYPES);
    }

    @Override
    public boolean supportsSourceType(Class<?> sourceType) {
        return isAssignableFrom(sourceType, SOURCE_TYPES);
    }

    private boolean isAssignableFrom(Class<?> type, Class<?>... supportedTypes) {
        if (type != null) {
            for (Class<?> supportedType : supportedTypes) {
                if (supportedType.isAssignableFrom(type)) {
                    return true;
                }
            }
        }
        return false;
    }

}

4.2 在Application启动类中添加自定义的启动监听ApplicationStartedEventListener

 	SpringApplication app = new SpringApplication(AppMain.class);
        ApplicationStartedEventListener asel = new ApplicationStartedEventListener();
	app.addListeners(asel);
	app.run(args);

4.3 在log4j2.xml中使用MDC定义的属性

  使用方式 ${ctx:key}

5. 异步日志

  参考文档;logging.apache.org/log4j/2.x/m…

5.1 让所有的日志异步输出

  jvm启动参数增加:

-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector

  log4j2配置文件不需要变动

5.2 同步和异步混合使用

  不需要增加5.1中的jvm启动参数。

  官网的配置文件

<?xml version="1.0" encoding="UTF-8"?>
 
<!-- No need to set system property "log4j2.contextSelector" to any value
     when using <asyncLogger> or <asyncRoot>. -->
 
<Configuration status="WARN">
  <Appenders>
    <!-- Async Loggers will auto-flush in batches, so switch off immediateFlush. -->
    <RandomAccessFile name="RandomAccessFile" fileName="asyncWithLocation.log"
              immediateFlush="false" append="false">
      <PatternLayout>
        <Pattern>%d %p %class{1.} [%t] %location %m %ex%n</Pattern>
      </PatternLayout>
    </RandomAccessFile>
  </Appenders>
  <Loggers>
    <!-- pattern layout actually uses location, so we need to include it -->
    <AsyncLogger name="com.foo.Bar" level="trace" includeLocation="true">
      <AppenderRef ref="RandomAccessFile"/>
    </AsyncLogger>
    <Root level="info" includeLocation="true">
      <AppenderRef ref="RandomAccessFile"/>
    </Root>
  </Loggers>
</Configuration>

6.混合使用同异步日志,异步不输出至控制台和文件的原因

  之前配置log4j2.xml的时候,没有设置AsyncLogger、root的 level。这时,设置了AsyncLogger的类(name="类/包")不打印到控制台,也不输出到文件里,经过排查注释掉AsyncLogger,恢复了正常,于是暂时采用了完全异步模式输出日志。

  建立一个新module只针对日志进行测试,移植以前的配置,发现在注释掉AsyncLogger后,控制台竟只输出ERROR级别的日志(当时只设置了INFO、WARN、ERROR三种级别),随后发现配置文件中缺少了logging.level.root=info(此处再给自己挖个坑,如果log4j2-spring.xml无法直接读取*.properties文件中的配置,那么root的日志级别是怎么设置的,AsyncLogger的日志级别是否也可以通过此方式进行配置),增加root的level,配置为info,此时控制台能够全部输出,增加AsyncLogger并设置level="info",解决问题。

  总结:缺少设置AsyncLogger的level(目前测试默认是ERROR)。additivity="true"(此时控制台也可以输出)。

  附上填坑过程代码;

package com.xyz.study.log;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

/**
 * @author gem.xu
 */
@Component
public class LogTest {
    private Logger log = LoggerFactory.getLogger(LogTest.class);

    @PostConstruct
    public void init(){
        log.info("加载中 这是info信息");
        log.warn("加载中 这是warn信息");
        log.error("加载中 这是error信息");
        //以为是被最后一个覆盖了,结果还是输出了error
        log.info("加载中 这是info信息");
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="OFF">

    <Properties>
        <property name="CONSOLE_LOG_PATTERN"
                  value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %highlight{%-5level} %logger{36} - %msg%n"/>
        <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        <property name="FILE_PATH" value="${ctx:LOG_PATH}"/>
        <property name="FILE_NAME" value="logtest"/>
    </Properties>

    <appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout>
                <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            </PatternLayout>
        </Console>
        <!--  日志输出配置 file文件方式   immediateFlush="true"-->
        <RollingFile name="RollingFileInfo" fileName="${FILE_PATH}/${FILE_NAME}.log"
                     filePattern="${FILE_PATH}/%d{yyyy-MM-dd}/${FILE_NAME}-%d{yyyy-MM-dd}-%i.log.gz"
                     immediateFlush="false">
            <PatternLayout>
                <Pattern>${LOG_PATTERN}</Pattern>
            </PatternLayout>

            <!-- 记录日志级别-->
            <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>

            <Policies>
                <!--interval属性用来指定多久滚动一次,默认是1 hour-->
                <TimeBasedTriggeringPolicy interval="24"/>
                <!--    日志文件达到y阈值开始滚动    -->
                <SizeBasedTriggeringPolicy size="500MB"/>
            </Policies>
            <!-- 同一文件夹最多多少 开始覆盖 默认7 这这里设置30-->
            <DefaultRolloverStrategy max="30"/>
        </RollingFile>

        <!-- 日志输出配置 file文件方式-->
        <RollingFile name="ERROR" fileName="${FILE_PATH}/${FILE_NAME}_error.log"
                     filePattern="${FILE_PATH}/%d{yyyy-MM-dd}/${FILE_NAME}-%d{yyyy-MM-dd}-%i.log.gz"
                     >
            <PatternLayout>
                <Pattern>${LOG_PATTERN}</Pattern>
            </PatternLayout>

            <!-- 记录日志级别-->
            <ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>

            <Policies>
                <!--interval属性用来指定多久滚动一次,默认是24 hour-->
                <TimeBasedTriggeringPolicy interval="24"/>
                <!--    日志文件达到y阈值开始滚动    -->
                <SizeBasedTriggeringPolicy size="500MB"/>
            </Policies>
            <!-- 同一文件夹最多多少 开始覆盖 默认7 这这里设置30-->
            <DefaultRolloverStrategy max="30"/>
        </RollingFile>
    </appenders>

    <loggers>
        <!--  异步日志  -->
        <AsyncLogger level="info" name="com.xyz.study.log.LogTest" includeLocation="true" additivity="true" >
            <appender-ref ref="RollingFileInfo"/>
            <appender-ref ref="ERROR"/>
        </AsyncLogger>
        <root >
            <appender-ref level="info" ref="Console"/>
<!--            <appender-ref ref="RollingFileInfo"/>-->
<!--            <appender-ref ref="ERROR"/>-->
        </root>
    </loggers>
</configuration>