美文网首页
spring boot使用日志组件log4j2

spring boot使用日志组件log4j2

作者: 天草二十六_简村人 | 来源:发表于2019-09-26 12:36 被阅读0次

    一、为什么要引入log4j2

    有以下几个原因:

    1、日志格式的统一,减少日志采集阶段,在正则匹配时的误差。

    2、以前是将控制台的日志重定向至日志文件里,这个在做日志切割的时候,会有问题。

    3、把所有的日志都存放在一个文件里,区分不了埋点日志还是普通日志。所以趁着业务上有埋点的需求,有必须引入log4j2这样的日志组件了。为什么不使用默认的logback呢?原因主要是log4j2支持异步logger了。

    这里引用一段log4j2的优点吧,注意不是与logback的区别。

    • 1). 插件式结构。 Log4j 2 支持插件式结构。我们可以根据自己的需要自行扩展 Log4j 2. 我们可以实现自己的
      appender 、 logger 、 filter 。
    • 2). 配置文件优化。在配置文件中可以引用属性,还可以直接替代或传递到组
      件。而且支持 json 格式的配置文件。不像其他的日志框架,它在重新配置的时候不会丢失之前的日志文件。
    • 3). Java 5 的并发性。 Log4j 2 利用 Java 5 中的并发特性支持,尽可能地执行最低层次的加锁。解决了在 log4j 1.x 中存
      留的死锁的问题。
    • 4). 异步 logger 。 Log4j 2 是基于 LMAX Disruptor 库的。在多线程的场景下,和已有的日志框架相比,异步的 logger 拥有 10 倍左右的效率提升。

    二、 引入xml文件

    1、为什么需要单独引入xml文件?

    (1)约定优先于配置。spring boot最喜欢做的事情就是默认实现了,关于日志这块,它的默认实现是logback。核心类是LoggingSystem。

    又必要看下它的源码实现,发现有三个日志实现类:LogbackLoggingSystem、Log4J2LoggingSystem、JavaLoggingSystem。


    image.png
    package org.springframework.boot.logging;
    
    import java.util.Collections;
    import java.util.EnumSet;
    import java.util.LinkedHashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    import java.util.function.Function;
    import java.util.function.Predicate;
    import java.util.function.Supplier;
    import org.springframework.util.ClassUtils;
    import org.springframework.util.StringUtils;
    
    public abstract class LoggingSystem {
        public static final String SYSTEM_PROPERTY = LoggingSystem.class.getName();
        public static final String NONE = "none";
        public static final String ROOT_LOGGER_NAME = "ROOT";
        private static final Map<String, String> SYSTEMS;
    
        public LoggingSystem() {
        }
    
    //抽象方法,也作钩子方法
        public abstract void beforeInitialize();
    
        public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
        }
    
    //空实现,子类可以重写该方法
        public void cleanUp() {
        }
    
        public Runnable getShutdownHandler() {
            return null;
        }
    
        public Set<LogLevel> getSupportedLogLevels() {
            return EnumSet.allOf(LogLevel.class);
        }
    
        public void setLogLevel(String loggerName, LogLevel level) {
            throw new UnsupportedOperationException("Unable to set log level");
        }
    
        public List<LoggerConfiguration> getLoggerConfigurations() {
            throw new UnsupportedOperationException("Unable to get logger configurations");
        }
    
        public LoggerConfiguration getLoggerConfiguration(String loggerName) {
            throw new UnsupportedOperationException("Unable to get logger configuration");
        }
    
        public static LoggingSystem get(ClassLoader classLoader) {
            String loggingSystem = System.getProperty(SYSTEM_PROPERTY);
            if (StringUtils.hasLength(loggingSystem)) {
    // 这里可以将下面的"none".equals修改为NONE.equals
                return (LoggingSystem)("none".equals(loggingSystem) ? new LoggingSystem.NoOpLoggingSystem() : get(classLoader, loggingSystem));
            } else {
    // findFirst() 是意指取第一个实现者LogbackLoggingSystem
                return (LoggingSystem)SYSTEMS.entrySet().stream().filter((entry) -> {
    // 注意这里是isPresent,不是isParent哈,返回真/假。过滤掉已加载过的类。
                    return ClassUtils.isPresent((String)entry.getKey(), classLoader);
                }).map((entry) -> {
    //动态加载日志实现类
                    return get(classLoader, (String)entry.getValue());
                }).findFirst().orElseThrow(() -> {
                    return new IllegalStateException("No suitable logging system located");
                });
            }
        }
    
        private static LoggingSystem get(ClassLoader classLoader, String loggingSystemClass) {
            try {
    // ClassUtils.forName()这个方法通过反射机制实现动态加载。
                Class<?> systemClass = ClassUtils.forName(loggingSystemClass, classLoader);
                return (LoggingSystem)systemClass.getConstructor(ClassLoader.class).newInstance(classLoader);
            } catch (Exception var3) {
                throw new IllegalStateException(var3);
            }
        }
    // LoggingSystem 的三个实现
        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);
        }
    
     static class NoOpLoggingSystem extends LoggingSystem {
            NoOpLoggingSystem() {
            }
    
            public void beforeInitialize() {
            }
    
            public void setLogLevel(String loggerName, LogLevel level) {
            }
    
            public List<LoggerConfiguration> getLoggerConfigurations() {
                return Collections.emptyList();
            }
    
            public LoggerConfiguration getLoggerConfiguration(String loggerName) {
                return null;
            }
        }
    }
    
    

    (2)、上面我们知道了spring boot的默认实现是logback,并且实现了绝大多数的功能。但是对外提供了哪些配置项呢。

    这就需要看下配置类LoggingSystemProperties了。
    第一部分是说LogFile的相关配置,第二部分是说SystemProperty系统配置。

    package org.springframework.boot.logging;
    
    import org.springframework.boot.system.ApplicationPid;
    import org.springframework.core.env.ConfigurableEnvironment;
    import org.springframework.core.env.Environment;
    import org.springframework.core.env.PropertyResolver;
    import org.springframework.core.env.PropertySourcesPropertyResolver;
    import org.springframework.util.Assert;
    
    public class LoggingSystemProperties {
        public static final String PID_KEY = "PID";
        public static final String EXCEPTION_CONVERSION_WORD = "LOG_EXCEPTION_CONVERSION_WORD";
        public static final String LOG_FILE = "LOG_FILE";
        public static final String LOG_PATH = "LOG_PATH";
        public static final String CONSOLE_LOG_PATTERN = "CONSOLE_LOG_PATTERN";
        public static final String FILE_LOG_PATTERN = "FILE_LOG_PATTERN";
        public static final String FILE_MAX_HISTORY = "LOG_FILE_MAX_HISTORY";
        public static final String FILE_MAX_SIZE = "LOG_FILE_MAX_SIZE";
        public static final String LOG_LEVEL_PATTERN = "LOG_LEVEL_PATTERN";
        public static final String LOG_DATEFORMAT_PATTERN = "LOG_DATEFORMAT_PATTERN";
        private final Environment environment;
    
        public LoggingSystemProperties(Environment environment) {
            Assert.notNull(environment, "Environment must not be null");
            this.environment = environment;
        }
    
        public void apply() {
            this.apply((LogFile)null);
        }
    
        public void apply(LogFile logFile) {
            PropertyResolver resolver = this.getPropertyResolver();
            this.setSystemProperty(resolver, "LOG_EXCEPTION_CONVERSION_WORD", "exception-conversion-word");
            this.setSystemProperty("PID", (new ApplicationPid()).toString());
            this.setSystemProperty(resolver, "CONSOLE_LOG_PATTERN", "pattern.console");
            this.setSystemProperty(resolver, "FILE_LOG_PATTERN", "pattern.file");
            this.setSystemProperty(resolver, "LOG_FILE_MAX_HISTORY", "file.max-history");
            this.setSystemProperty(resolver, "LOG_FILE_MAX_SIZE", "file.max-size");
            this.setSystemProperty(resolver, "LOG_LEVEL_PATTERN", "pattern.level");
            this.setSystemProperty(resolver, "LOG_DATEFORMAT_PATTERN", "pattern.dateformat");
            if (logFile != null) {
                logFile.applyToSystemProperties();
            }
    
        }
    
        private PropertyResolver getPropertyResolver() {
            if (this.environment instanceof ConfigurableEnvironment) {
                PropertyResolver resolver = new PropertySourcesPropertyResolver(((ConfigurableEnvironment)this.environment).getPropertySources());
                ((PropertySourcesPropertyResolver)resolver).setIgnoreUnresolvableNestedPlaceholders(true);
                return resolver;
            } else {
                return this.environment;
            }
        }
    // 解析logging.开头的配置项
        private void setSystemProperty(PropertyResolver resolver, String systemPropertyName, String propertyName) {
            this.setSystemProperty(systemPropertyName, resolver.getProperty("logging." + propertyName));
        }
    
        private void setSystemProperty(String name, String value) {
            if (System.getProperty(name) == null && value != null) {
                System.setProperty(name, value);
            }
    
        }
    }
    
    

    详细见官网https://docs.spring.io/spring-boot/docs/2.1.2.RELEASE/reference/htmlsingle/#boot-features-logging-format 包含了哪些配置项。

    logging配置项.png

    官网也给出了,如果需要再多地关于日志的配置,则引入了log4j2和logback的xml文件。

    三、引入后,之前的代码写法,会受影响吗?

    不会。因为都是通过slf4j门面模式来做的。


    LogbackLoggingSystem继承Slf4JLoggingSystem.png
    Log4J2LoggingSystem继承Slf4JLoggingSystem.png

    在java中,你使用如下写法:

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    private Logger logger =  LoggerFactory.getLogger(this.getClass());
    logger.info("打印日志");
    
    // 或者使用lombok注解
    
    import lombok.extern.slf4j.Slf4j;
    @Slf4j
    public class A{
          log.info("打印日志");
    }
    

    四、log4j2.xml模板

    需求:业务埋点日志、异步打印日志、日志格式自定义。

    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration status="WARN" monitorInterval="5">
        <Properties>
            <Property name="AppName">user-service</Property>
    
            <Property name="customizePattern">
                time=%d{yyyy-MM-dd}T%d{HH:mm:ss.SSS+0800}`appName=${AppName}`tradeId=%X{X-B3-TraceId}`spanId=%X{X-B3-SpanId}`parentSpanId=%X{X-B3-ParentSpanId}`threadId=%thread`logLevel=%level`%m%ex%n
            </Property>
    
            <Property name="defaultPattern">
                time=%d{yyyy-MM-dd}T%d{HH:mm:ss.SSS+0800}`appName=${AppName}`tradeId=%X{X-B3-TraceId}`spanId=%X{X-B3-SpanId}`parentSpanId=%X{X-B3-ParentSpanId}`threadId=%thread`logLevel=%level`msg=%m%ex%n
            </Property>
    
            <Property name="baseLogDir">log</Property>
    
            <Property name="BurialPointLogFile">${baseLogDir}/burialPoint.log</Property>
            <Property name="SysLogFile">${baseLogDir}/sys.log</Property>
        </Properties>
        <Appenders>
            <RollingRandomAccessFile name="BurialPointLogFileAppender" fileName="${BurialPointLogFile}"
                                     filePattern="${BurialPointLogFile}-%d{yyyy-MM-dd}-%i.gz" immediateFlush="false">
                <PatternLayout>
                    <Pattern>${customizePattern}</Pattern>
                </PatternLayout>
                <Policies>
                    <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
                    <SizeBasedTriggeringPolicy size="500MB"/>
                </Policies>
                <!-- 业务埋点日志, 文件保存3天, 每个小时最多生成100个文件 -->
                <DefaultRolloverStrategy max="100">
                    <Delete basePath="${baseLogDir}" maxDepth="1">
                        <IfFileName glob="*.gz"/>
                        <IfLastModified age="3d"/>
                    </Delete>
                </DefaultRolloverStrategy>
            </RollingRandomAccessFile>
    
            <RollingRandomAccessFile name="SysLogFileAppender" fileName="${SysLogFile}"
                                     filePattern="${SysLogFile}-%d{yyyy-MM-dd}-%i.gz" immediateFlush="false">
                <PatternLayout>
                    <Pattern>${defaultPattern}</Pattern>
                </PatternLayout>
                <Policies>
                    <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
                    <SizeBasedTriggeringPolicy size="500MB"/>
                </Policies>
                <!-- 系统日志, 文件保存7天, 每个小时最多生成20个文件 -->
                <DefaultRolloverStrategy max="20">
                    <Delete basePath="${baseLogDir}" maxDepth="1">
                        <IfFileName glob="*.gz"/>
                        <IfLastModified age="7d"/>
                    </Delete>
                </DefaultRolloverStrategy>
            </RollingRandomAccessFile>
        </Appenders>
    
        <Loggers>
            <Logger name="org.springframework.boot" level="info" includeLocation="false" additivity="false">
                <appender-ref ref="SysLogFileAppender"/>
            </Logger>
            <Logger name="org.springframework" level="error" includeLocation="false" additivity="false">
                <appender-ref ref="SysLogFileAppender"/>
            </Logger>
            <Logger name="org.apache" level="warn" includeLocation="false" additivity="false">
                <appender-ref ref="SysLogFileAppender"/>
            </Logger>
            <Logger name="org.hibernate" level="error" includeLocation="false" additivity="false">
                <appender-ref ref="SysLogFileAppender"/>
            </Logger>
    
            <Logger name="com.xxl.job" level="warn" includeLocation="false" additivity="false">
                <appender-ref ref="SysLogFileAppender"/>
            </Logger>
    
            <Logger name="com.alibaba.fastjson" level="error" includeLocation="false" additivity="false">
                <appender-ref ref="SysLogFileAppender"/>
            </Logger>
    
            <!-- 埋点日志, 采用异步模式 -->
            <AsyncLogger name="BurialPoint" level="info" includeLocation="false" additivity="false">
                <appender-ref ref="BurialPointLogFileAppender"/>
            </AsyncLogger>
    
            <AsyncRoot level="info">
                <AppenderRef ref="SysLogFileAppender"/>
            </AsyncRoot>
        </Loggers>
    
    </Configuration>
    

    五、log4j2的配置说明

    (1).根节点Configuration有两个属性:status和monitorinterval,有两个子节点:Appenders和Loggers(表明可以定义多个Appender和Logger).

    status用来指定log4j本身的打印日志的级别.

    monitorinterval用于指定log4j自动重新配置的监测间隔时间,单位是s,最小是5s.

    (2).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:指定新建日志文件的名称格式.

    Policies:指定滚动日志的策略,就是什么时候进行新建日志文件输出日志.

    TimeBasedTriggeringPolicy:Policies子节点,基于时间的滚动策略,interval属性用来指定多久滚动一次,默认是1 hour。modulate=true用来调整时间:比如现在是早上3am,interval是4,那么第一次滚动是在4am,接着是8am,12am...而不是7am.

    SizeBasedTriggeringPolicy:Policies子节点,基于指定文件大小的滚动策略,size属性用来定义每个日志文件的大小.

    DefaultRolloverStrategy:用来指定同一个文件夹下最多有几个日志文件时开始删除最旧的,创建新的(通过max属性)。

    (3).Loggers节点,常见的有两种:Root和Logger.

    Root节点用来指定项目的根日志,如果没有单独指定Logger,那么就会默认使用该Root日志输出

    level:日志输出级别,共有8个级别,按照从低到高为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF.

    AppenderRef:Root的子节点,用来指定该日志输出到哪个Appender.

    Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。

    level:日志输出级别,共有8个级别,按照从低到高为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF.

    name:用来指定该Logger所适用的类或者类所在的包全路径,继承自Root节点.

    AppenderRef:Logger的子节点,用来指定该日志输出到哪个Appender,如果没有指定,就会默认继承自Root.如果指定了,那么会在指定的这个Appender和Root的Appender中都会输出,此时我们可以设置Logger的additivity="false"只在自定义的Appender中进行输出。

    (4).关于日志level.

    共有8个级别,按照从低到高为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF.

    All:最低等级的,用于打开所有日志记录.

    Trace:是追踪,就是程序推进以下,你就可以写个trace输出,所以trace应该会特别多,不过没关系,我们可以设置最低日志级别不让他输出.

    Debug:指出细粒度信息事件对调试应用程序是非常有帮助的.

    Info:消息在粗粒度级别上突出强调应用程序的运行过程.

    Warn:输出警告及warn以下级别的日志.

    Error:输出错误信息日志.

    Fatal:输出每个严重的错误事件将会导致应用程序的退出的日志.

    OFF:最高等级的,用于关闭所有日志记录.

    程序会打印高于或等于所设置级别的日志,设置的日志等级越高,打印出来的日志就越少。

    六、log4j2的通用配置

    在resources目录下,新建文件log4j2.component.properties,和log4j2.xml同一目录。

    • 环形队列的大小
      AsyncLogger.RingBufferSize=10000
      AsyncLoggerConfig.RingBufferSize=10000
    • 自动降级--丢弃日志
      log4j2.AsyncQueueFullPolicy=Discard
    • 队列满后丢弃INFO级别的日志
      log4j2.DiscardThreshold=INFO

    七、pom依赖的调整

    注意:starter-web 和 log4j2 的位置必须调整至最顶部。

    <dependencies>
           <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <exclusions>
                    <exclusion>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-logging</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-log4j2</artifactId>
            </dependency>
    
            <dependency>
                <groupId>com.lmax</groupId>
                <artifactId>disruptor</artifactId>
                <version>3.4.2</version>
            </dependency>
    ...
    ...
    </dependencies>
    

    八、磁盘空间估算

    根据日志文件的生成和删除策略,评估磁盘空间的最大需求。
    我们是基于大小和时间的双重文件滚动策略,并配合压缩。单位时间内控制最多保留日志个数,并控制总的日志留
    存时间。

    • 1 )单个日志文件的最大大小为 500MB ,每个小时内最多生成 100 个文件,日志文件最多保留 3 天。
      推导出日志文件留存的最大数为 7200 个。

    • 2 )考虑到历史日志文件我们采用了 gz 压缩算法,压缩比例为 1/50 的话,我们可以算出最多需要 72GB 的磁盘空间。
      日志文件留存的个数 = 3d * 24h * 100 个 /h = 7200 个
      日志文件的空间需求 = 500MB + 7200 个 * 500MB * (1/50) (压缩比) = 72.5GB

    • 3 )日志打印中的埋点日志,采用异步方式,减少 IO 次数。

    相关文章

      网友评论

          本文标题:spring boot使用日志组件log4j2

          本文链接:https://www.haomeiwen.com/subject/dttfuctx.html