一、为什么要引入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 包含了哪些配置项。
官网也给出了,如果需要再多地关于日志的配置,则引入了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 次数。
网友评论