目录
1.概述
1.1.组件概览
在log4j2中,LogManager就是日志的门面,相当于slf4j-api中的LoggerFactory.
框架为每个类加载分配了一个单独的LoggerContext,用于管理所有创建出来的Logger实例.
ContextSelector则负责管理类加载器到对应的LoggerContext实例之间的映射关系.
log4j2中,有5个关键概念:
- LoggerConfig:日志配置,用于整合多个Appender,进行日志打印.
- Appender:追加器,用于操作Layout和Manager,往单一目的地进行日志打印.
- Layout:布局,用于把LogEvent日志事件序列化成字节序列,不同Layout实现具有不同的序列化方式.
- Manager:管理器,用于管理输出目的地,如:RollingFileManager用于管理文件滚动以及将字节序列写入到指定文件中.
- Filter:过滤器,用于对LogEvent日志事件加以过滤,LoggerConfig和Appender都可以配置过滤器,也就是说日志事件会经过一总一分两层过滤.
组件架构如下:
组件架构
1.2.灵活的配置
1.2.1.插件发现机制
在log4j2中,一切皆插件,框架通过PluginRegistry
扫描并发现插件配置.
PluginRegistry
支持两种扫描方式
- 一种是使用指定的ClassLoader读取classpath下所有的
META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat
文件,产生PluginType
; - 另一种是扫描classpath下指定的packageName,内省带有
@Plugin
注解的类文件,产生PluginType
.
插件配置以PluginType
的形式保存在插件注册表中,PluginType
的作用类似于spring中BeanDefinition
,定义了如何创建插件实例.
插件类通过@PluginFactory
注解或者@PluginBuilderFactory
注解配置插件实例的实例化和属性注入方式.
1.2.2.插件装配机制
log4j2知道如何实例化插件后,我们就可以通过编写配置文件(如:log4j2.xml),进行插件的实例化和属性注入了.Configuration
全局配置对象负责保存所有解析到的配置.
通过ConfigurationFactory.getConfiguration()
可以使用不同的工厂生产不同的配置对象,不同的Configuration
实现可以解析不同格式的配置,如:xml,yaml,json等.
以xml文件为例,文件中每个元素都会最终对应一个插件实例,元素名称实际就是PluginType中的name,实例的属性可以从子元素对应的实例获取,也可以从自身元素的属性配置获取.
因此,xml中dom树的元素嵌套关系,也就是log4j组件实例的引用嵌套关系.
xml,yaml,json格式文件都可以描述这种嵌套关系,因此log4j2中定义了与文件格式无关的数据结构,Node来抽象配置.
AbstractConfiguration.setup()
负责提取配置,形成Node树.AbstractConfiguration.doConfigure()
负责根据Node树,进行插件实例化和属性注入.
1.2.3.配置文件基本元素与对象的映射关系
序号 | xml元素 | 工厂方法(类名.方法名) | 对象类型 |
---|---|---|---|
1 | <Properties> | PropertiesPlugin.configureSubstitutor() |
StrLookup |
2 | <Property> | Property.createProperty() |
Property |
3 | <Loggers> | LoggersPlugin.createLoggers() |
Loggers |
4 | <Logger> | LoggerConfig.createLogger() |
LoggerConfig |
5 | <Root> | RootLogger.createLogger() |
LoggerConfig |
6 | <AppenderRef> | AppenderRef.createAppenderRef() |
AppenderRef |
7 | <Filters> | CompositeFilter.createFilters() |
CompositeFilter |
8 | <Appenders> | AppendersPlugin.createAppenders() |
ConcurrentMap<String, Appender> |
事件级别
public enum StandardLevel {
OFF(0),
FATAL(100),
ERROR(200),
WARN(300),
INFO(400),
DEBUG(500),
TRACE(600),
ALL(Integer.MAX_VALUE);
}
public final class Level
{
public boolean isMoreSpecificThan(final Level level) {
return this.intLevel <= level.intLevel;
}
public boolean isInRange(final Level minLevel, final Level maxLevel) {
return this.intLevel >= minLevel.intLevel && this.intLevel <= maxLevel.intLevel;
}
}
2.属性占位符
2.1.概述
在log4j2中,环境变量信息(键值对)被封装为StrLookup对象,该对象作用类似于spring框架中的PropertySource.
在配置文件中,基本上所有的值的配置都可以通过参数占位符引用环境变量信息,格式为:${prefix:key}.
2.2.Interpolator插值器
Interpolator内部以Map<String,StrLookup>的方式,封装了很多StrLookuo对象,key则对应参数占位符${prefix:key}中的prefix.
同时,Interpolator内部还保存着一个没有prefix的StrLookup实例,被称作默认查找器,它的键值对数据来自于log4j2.xml配置文件中的<Properties>元素的配置.
当参数占位符${prefix:key}带有prefix前缀时,Interpolator会从指定prefix对应的StrLookup实例中进行key查询,
当参数占位符${key}没有prefix时,Interpolator则会从默认查找器中进行查询.
Interpolator中默认支持的StrLookup查找方式如下(StrLookup查找器实现类均在org.apache.logging.log4j.core.lookup包下):
序号 | prefix | 插件类型 | 描述 |
---|---|---|---|
1(*) | sys | SystemPropertiesLookup | 从jvm属性中查找 |
2(*) | env | EnvironmentLookup | 从操作系统环境变量中获取value |
3 | marker | MarkerLookup | 判断以指定key为名称的Marker标签是否存在,存在则返回key,否则返回null |
4 | jvmrunargs | JmxRuntimeInputArgumentsLookup | 获取jmx的运行时输入参数 |
5 | bundle | ResourceBundleLookup | 通过ResourceBundle查找value,格式:${prefix:bundleName:bundleKey} |
6 | java | JavaLookup | 获取jvm进程信息,只指定固定的key值,包括:(1)version:jdk版本(2)runtime:运行环境信息(3)vm:虚拟机名称(4)os:操作系统信息(5)hw:硬件信息(6)locale:地区信息 |
7 | main | MainMapLookup | 暂未启用 |
8 | log4j | Log4jLookup | 只支持两个key:(1)configLocation:获取log4j2配置文件的绝对路径(2)configParentLocation:获取配置文件所在目录的绝对路径 |
9 | date | DateLookup | 以指定格式,获取当前系统时间或LogEvent的时间戳,通过key来指定日期格式字符串 |
10 | sd | StructuredDataLookup | 从LogEvent中引用的StructuredDataMessage中获取value |
11 | ctx | ContextMapLookup | 从ThreadContext中获取value |
12 | map | MapLookup | 暂未启用 |
13 | jndi | JndiLookup | 使用jndi(javax.naming)获取value |
2.3.默认属性配置
注意:Properties元素一定要配置在最前面,否则不生效.
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Properties>
<Property name="customKey_1">customValue_1</Property>
<Property name="customKey_2">customValue_2</Property>
</Properties>
</Configuration>
3.Logger
3.1.配置示例
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" dest="err" verbose="false">
<Appenders>
<Console name="console">
<PatternLayout pattern="%date{yyyy-MM-dd HH:mm:ss.SSS} %C.%M %message%n"/>
</Console>
</Appenders>
<Loggers>
<Root additivity="true" level="error" includeLocation="true" >
<AppenderRef ref="console" level="info">
<ThresholdFilter level="warn" onMatch="NEUTRAL" onMismatch="DENY"/>
</AppenderRef>
<Property name="customeKey">customeValue</Property>
<ThresholdFilter level="warn" onMatch="NEUTRAL" onMismatch="DENY"/>
</Root>
<Logger name="com.lixin" additivity="true" level="info" includeLocation="true">
<AppenderRef ref="console" level="info">
<ThresholdFilter level="warn" onMatch="NEUTRAL" onMismatch="DENY"/>
</AppenderRef>
<Property name="customeKey">customeValue</Property>
<ThresholdFilter level="warn" onMatch="NEUTRAL" onMismatch="DENY"/>
</Logger>
</Loggers>
</Configuration>
3.1.1写日志逻辑
//AbstractLogger
public void debug(final Marker marker, final MessageSupplier msgSupplier, final Throwable t) {
logIfEnabled(FQCN, Level.DEBUG, marker, msgSupplier, t);
}
//AbstractLogger
public void logIfEnabled(final String fqcn, final Level level, final Marker marker,
final MessageSupplier msgSupplier, final Throwable t) {
if (isEnabled(level, marker, msgSupplier, t)) {
logMessage(fqcn, level, marker, msgSupplier, t);
}
}
//具体 Logger
public boolean isEnabled(final Level level, final Marker marker, final String message, final Object... params) {
return privateConfig.filter(level, marker, message, params);
}
//privateConfig
boolean filter(final Level level, final Marker marker, final String msg, final Object... p1) {
final Filter filter = config.getFilter();
if (filter != null) {
final Filter.Result r = filter.filter(logger, level, marker, msg, p1);
if (r != Filter.Result.NEUTRAL) {
return r == Filter.Result.ACCEPT;
}
}
return level != null && intLevel >= level.intLevel();
}
//AbstractLogger
protected void logMessage(final String fqcn, final Level level, final Marker marker, final String message,
final Throwable t) {
logMessage(fqcn, level, marker, messageFactory.newMessage(message), t);
}
//Logger
public void logMessage(final String fqcn, final Level level, final Marker marker, final Message message,
final Throwable t) {
final Message msg = message == null ? new SimpleMessage(Strings.EMPTY) : message;
final ReliabilityStrategy strategy = privateConfig.loggerConfig.getReliabilityStrategy();
strategy.log(this, getName(), fqcn, marker, level, msg, t);
}
//具体 DefaultReliabilityStrategy
public void log(final Supplier<LoggerConfig> reconfigured, final String loggerName, final String fqcn, final Marker marker, final Level level,
final Message data, final Throwable t) {
loggerConfig.log(loggerName, fqcn, marker, level, data, t);
}
//LoggerConfig
private void processLogEvent(final LogEvent event, LoggerConfigPredicate predicate) {
event.setIncludeLocation(isIncludeLocation());
if (predicate.allow(this)) {
callAppenders(event);
}
logParent(event, predicate);
}
//LoggerConfig
protected void callAppenders(final LogEvent event) {
final AppenderControl[] controls = appenders.get();
//noinspection ForLoopReplaceableByForEach
for (int i = 0; i < controls.length; i++) {
controls[i].callAppender(event);
}
}
//AppenderControl
public void callAppender(final LogEvent event) {
if (shouldSkip(event)) {
return;
}
callAppenderPreventRecursion(event);
}
//AppenderControl
private void callAppenderPreventRecursion(final LogEvent event) {
try {
recursive.set(this);
callAppender0(event);
} finally {
recursive.set(null);
}
}
//AppenderControl
private void callAppender0(final LogEvent event) {
ensureAppenderStarted();
if (!isFilteredByAppender(event)) {
tryCallAppender(event);
}
}
//AppenderControl
private void tryCallAppender(final LogEvent event) {
try {
appender.append(event);
} catch (final RuntimeException ex) {
handleAppenderError(ex);
} catch (final Exception ex) {
handleAppenderError(new AppenderLoggingException(ex));
}
}
//Appender
void append(LogEvent event);
//AbstractOutputStreamAppender
@Override
public void append(final LogEvent event) {
try {
tryAppend(event);
} catch (final AppenderLoggingException ex) {
error("Unable to write to stream " + manager.getName() + " for appender " + getName() + ": " + ex);
throw ex;
}
}
private void tryAppend(final LogEvent event) {
if (Constants.ENABLE_DIRECT_ENCODERS) {
directEncodeEvent(event);
} else {
writeByteArrayToManager(event);
}
}
protected void directEncodeEvent(final LogEvent event) {
// getLayout().encode(event, manager);
getLayout().encode(event, manager);
if (this.immediateFlush || event.isEndOfBatch()) {
manager.flush();
}
}
//PatternLayout
public void encode(final LogEvent event, final ByteBufferDestination destination) {
if (!(eventSerializer instanceof Serializer2)) {
super.encode(event, destination);
return;
}
final StringBuilder text = toText((Serializer2) eventSerializer, event, getStringBuilder());
final Encoder<StringBuilder> encoder = getStringBuilderEncoder();
encoder.encode(text, destination);
trimToMaxSize(text);
}
//PatternLayout
private StringBuilder toText(final Serializer2 serializer, final LogEvent event,
final StringBuilder destination) {
return serializer.toSerializable(event, destination);
}
protected void writeByteArrayToManager(final LogEvent event) {
//getLayout().toByteArray(event),layout格式化数据
final byte[] bytes = getLayout().toByteArray(event);
if (bytes != null && bytes.length > 0) {
manager.write(bytes, this.immediateFlush || event.isEndOfBatch());
}
}
//OutputStreamManager
protected void write(final byte[] bytes, final boolean immediateFlush) {
write(bytes, 0, bytes.length, immediateFlush);
}
protected synchronized void write(final byte[] bytes, final int offset, final int length, final boolean immediateFlush) {
if (immediateFlush && byteBuffer.position() == 0) {
writeToDestination(bytes, offset, length);
flushDestination();
return;
}
if (length >= byteBuffer.capacity()) {
// if request length exceeds buffer capacity, flush the buffer and write the data directly
flush();
writeToDestination(bytes, offset, length);
} else {
if (length > byteBuffer.remaining()) {
flush();
}
byteBuffer.put(bytes, offset, length);
}
if (immediateFlush) {
flush();
}
}
protected synchronized void writeToDestination(final byte[] bytes, final int offset, final int length) {
try {
getOutputStream().write(bytes, offset, length);
} catch (final IOException ex) {
throw new AppenderLoggingException("Error writing to stream " + getName(), ex);
}
}
3.1.2 Additive
private void processLogEvent(final LogEvent event, LoggerConfigPredicate predicate) {
event.setIncludeLocation(isIncludeLocation());
if (predicate.allow(this)) {
callAppenders(event);
}
logParent(event, predicate);
}
private void logParent(final LogEvent event, final LoggerConfigPredicate predicate) {
if (additive && parent != null) {
parent.log(event, predicate);
}
}
3.2.配置详解
- additivity:日志可加性,如果配置为true,则在日志打印时,会通过Logger继承关系递归调用父Logger引用的Appender进行日志打印.
注意:该属性默认为true.在递归打印日志时,会忽略父Logger的level配置 - level:用于控制允许打印的日志级别上线,在配置示例中,只有级别<=info的LogEvent才会被放行,级别优先级顺序为OFF<FATAL<ERROR<WARN<INFO<DEBUG<TRACE<ALL
注意:level属性的配置时可选的,在获取level时会通过Logger继承关系递归获取,RootLogger的级别默认为error,其他默认为null.也就是说,如果全都不配置level的话,则所有Logger级别都默认为error. - includeLocation:如果配置为true,则打印日志时可以附带日志点源码位置信息输出.同步日志上下文默认为true,异步默认为false.
- LoggerConfig元素下可以单独配置Property元素,添加属性键值对,这些属性会在每次打印日志时,被追加到LogEvent的contextData中
- LoggerConfig支持配置过滤器,在判断是否打印日志时,先过滤器判断过滤,然后再级别判断过滤.
- AppenderRef:顾名思义,就是配置当前Logger引用的Appender.同时,AppenderRef也支持配置level和Filter,进行更细粒度的日志过滤
- LoggerConfig等于总开关,AppenderRef则为各个子开关,两个开关都通过才能打印日志
3.3.Logger继承机制
log4j2框架会根据LoggerConfig的name建立对象之间的继承关系.这种继承机制与java的package很像,name以点进行名称空间分割,子名称空间继承父名称空间.
名称空间可以是全限定类名,也可以是报名.整个配置树的根节点就是RootLogger.
举例:假如我们的配置的Logger如下:
<Root/>
<Logger name="com"/>
<Logger name="com.lixin.DemoClass"/>
<Logger name="org"/>
<Logger name="org.springframework"/>
配置树
当通过LogManager.getLogger(name)获取Logger实例时,会根据name逐级递归直到找到匹配的LoggerConfig,或者递归到Root根节点为止.
3.4构建LoggerConfig树
//AbstractConfiguration,在初始化配置时设置层次。
//通过key查找,如果未找到自己的,往上找祖先。
public LoggerConfig getLoggerConfig(final String loggerName) {
LoggerConfig loggerConfig = loggerConfigs.get(loggerName);
if (loggerConfig != null) {
return loggerConfig;
}
String substr = loggerName;
while ((substr = NameUtil.getSubName(substr)) != null) {
loggerConfig = loggerConfigs.get(substr);
if (loggerConfig != null) {
return loggerConfig;
}
}
return root;
}
public synchronized void addLoggerFilter(final org.apache.logging.log4j.core.Logger logger, final Filter filter) {
final String loggerName = logger.getName();
final LoggerConfig lc = getLoggerConfig(loggerName);
if (lc.getName().equals(loggerName)) {
//如果是自己的配置
lc.addFilter(filter);
} else {
//不是自己的配置,则新创建一个配置,设置parent
final LoggerConfig nlc = new LoggerConfig(loggerName, lc.getLevel(), lc.isAdditive());
nlc.addFilter(filter);
nlc.setParent(lc);
loggerConfigs.putIfAbsent(loggerName, nlc);
setParents();
logger.getContext().updateLoggers();
}
}
public synchronized void addLoggerAppender(final org.apache.logging.log4j.core.Logger logger,
final Appender appender) {
final String loggerName = logger.getName();
appenders.putIfAbsent(appender.getName(), appender);
final LoggerConfig lc = getLoggerConfig(loggerName);
if (lc.getName().equals(loggerName)) {
lc.addAppender(appender, null, null);
} else {
final LoggerConfig nlc = new LoggerConfig(loggerName, lc.getLevel(), lc.isAdditive());
nlc.addAppender(appender, null, null);
nlc.setParent(lc);
loggerConfigs.putIfAbsent(loggerName, nlc);
setParents();
logger.getContext().updateLoggers();
}
}
public synchronized void setLoggerAdditive(final org.apache.logging.log4j.core.Logger logger, final boolean additive) {
final String loggerName = logger.getName();
final LoggerConfig lc = getLoggerConfig(loggerName);
if (lc.getName().equals(loggerName)) {
lc.setAdditive(additive);
} else {
final LoggerConfig nlc = new LoggerConfig(loggerName, lc.getLevel(), additive);
nlc.setParent(lc);
loggerConfigs.putIfAbsent(loggerName, nlc);
setParents();
logger.getContext().updateLoggers();
}
}
private void setParents() {
for (final Map.Entry<String, LoggerConfig> entry : loggerConfigs.entrySet()) {
final LoggerConfig logger = entry.getValue();
String key = entry.getKey();
if (!key.isEmpty()) {
//根据点号(.)按层次分别设置parent
final int i = key.lastIndexOf('.');
if (i > 0) {
key = key.substring(0, i);
LoggerConfig parent = getLoggerConfig(key);
if (parent == null) {
parent = root;
}
logger.setParent(parent);
} else {
logger.setParent(root);
}
}
}
}
4.Appender
4.1.概述
追加器,负责控制Layout进行LogEvent的序列化,以及控制Manager对序列化后的字节序列进行输出.
在log4j2.xml配置文件中,配置方式如下:
<Appenders>
<具体的Appender插件名称>
</具体的Appender插件名称>
</Appenders>
4.2.框架支持的Appender实现
序号 | 工厂方法 | xml元素 | 具体类 | 作用 |
---|---|---|---|---|
1 | NullAppender.createAppender | <Null> | NullAppender | |
2 | ConsoleAppender.newBuilder | <Console> | ConsoleAppender | 控制台输出 |
3 | FileAppender.newBuilder | <File> | FileAppender | 往一个固定文件,流式追加日志 |
5 | RollingFileAppender.newBuilder | <RollingFile> | RollingFileAppender | 日志文件可滚动,滚动策略可配置,可按时间,文件大小等方式.流式追加日志 |
6 | AsyncAppender.newBuilder | <Async> | AsyncAppender | 内部引用一组appender,通过异步线程+队列方式调用这些appender |
7 | RollingRandomAccessFileAppender.newBuilder | <RollingRandomAccessFile> | RollingRandomAccessFileAppender | 日志文件可滚动,使用RandomAccessFile追加日志 |
8 | RandomAccessFileAppender.newBuilder | <RandomAccessFile> | RandomAccessFileAppender | 往一个固定文件,使用RandomAccessFile追加日志 |
8 | OutputStreamAppender.newBuilder | <OutputStream> | OutputStreamAppender | |
9 | MemoryMappedFileAppender.newBuilder | <MemoryMappedFile> | MemoryMappedFileAppender | 比RandomAccessFile性能高 |
10 | JdbcAppender.newBuilder | <JDBC> | JdbcAppender | |
11 | JpaAppender.createAppender | <JPA> | JpaAppender | |
12 | JeroMqAppender.createAppender | <JeroMQ> | JeroMqAppender | |
13 | KafkaAppender.newBuilder | <Kafka> | KafkaAppender | |
14 | JmsAppender.newBuilder | <JMS> <JMSQueue> <JMSTopic> |
JmsAppender | |
15 | Rewrite.createAppender | <Rewrite> | RewriteAppender | |
16 | RoutingAppender.newBuilder | <Routing> | RoutingAppender | 路由追加器 可根据pattern模式字符串,路由到内部管理的其他Appender实例, 支持LogEvent事件重写和Appender定期清理 |
17 | CountingNoOpAppender.createAppender | <CountingNoOp> | CountingNoOpAppender | |
18 | FailoverAppender.createAppender | <Failover> | FailoverAppender | |
19 | ScriptAppenderSelector | <ScriptAppenderSelector> | ||
20 | SmtpAppender.createAppender | <SMTP> | SmtpAppender | |
21 | SocketAppender.newBuilder | <Socket> | SocketAppender | |
22 | SyslogAppender.newBuilder | <Syslog> | SyslogAppender | |
23 | WriterAppender.newBuilder | <Writer> | WriterAppender |
4.3.常用Appender详解
4.3.1.ConsoleAppender
控制台追加器,用于把日志输出到控制台,一般本地调试时使用.
配置示例如下:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" dest="err" verbose="false">
<Appenders>
<!-- follow和direct不能同时为true,如果follow为true则会跟随底层输出流的变化,direct为true则固定指向输出流 -->
<Console name="console" target="SYSTEM_OUT" follow="false" direct="true">
<PatternLayout pattern="%date{yyyy-MM-dd HH:mm:ss.SSS} %C.%M %message%n"/>
</Console>
</Appenders>
<Loggers>
<Root additivity="true" level="error" includeLocation="true" >
<AppenderRef ref="console" level="info"/>
</Root>
</Loggers>
</Configuration>
4.3.2.RollingFileAppender
文件滚动追加器,用于向本地磁盘文件中追加日志,同时可以通过触发策略(TriggeringPolicy)和滚动策略(RolloverStrategy)控制日志文件的分片,避免日志文件过大.
线上环境常用.
常用的触发策略包含两种:
- TimeBasedTriggeringPolicy:基于时间周期性触发滚动,一般按天滚动
- SizeBasedTriggeringPolicy:基于文件大小触发滚动,可以控制单个日志文件的大小上限
滚动策略的实现包含两种:
-
DefaultRolloverStrategy:默认滚动策略
该策略内部维护一个最小索引和最大索引,每次滚动时,会删除历史文件,之后剩余文件全部进行一轮重命名,最后创建新的不带有索引后缀的文件进行日志追加默认策略
-
DirectWriteRolloverStrategy:直接写滚动策略
该策略内部会维护一个一直自增的文件索引,每次滚动时直接创建新的带有索引后缀的文件进行日志追加,同步清理历史的文件.直接写策略
配置示例如下:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" dest="err" verbose="false">
<Properties>
<Property name="logDir">/Users/lixin46/workspace/demo/logdemo/logs</Property>
<Property name="pattern">%date{yyyy-MM-dd HH:mm:ss.SSS} %C.%M %message%n</Property>
</Properties>
<Appenders>
<Console name="console" >
<PatternLayout pattern="${pattern}"/>
</Console>
<!-- 使用DirectWriteRolloverStrategy策略时,不需要配置fileName -->
<RollingFile name="fileAppender" fileName="${logDir}/request.log" filePattern="${logDir}/request.log-%d-%i">
<PatternLayout pattern="${pattern}"/>
<!-- 所有策略中,只要任意策略满足就会触发滚动 -->
<Policies>
<!-- 滚动时间周期,只有数量,单位取决于filePattern中%d的配置 -->
<TimeBasedTriggeringPolicy interval="1"/>
<SizeBasedTriggeringPolicy size="10b"/>
</Policies>
<!-- 限制最多保留5个文件,索引自增 -->
<!--<DirectWriteRolloverStrategy maxFiles="5"/>-->
<!-- 限制最多保留5个文件,索引从2到6 -->
<DefaultRolloverStrategy fileIndex="max" min="2" max="6"/>
</RollingFile>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="console" level="info"/>
<AppenderRef ref="fileAppender" level="info" />
</Root>
</Loggers>
</Configuration>
5.Layout
5.1.概述
布局对象,职责是把指定的LogEvent转换成可序列化对象(如:String),或者直接序列化成字节数组.
log4j2支持很多的序列化格式,如:普通模式字符串,JSON字符串,yaml字符串,XML格式字符串,HTML字符串等等.
类体系如下:
layout类体系
5.2.PatternLayout
模式布局是我们最常使用的,它通过PatternProcessor模式解析器,对模式字符串进行解析,得到一个List<PatternConverter>转换器列表和List<FormattingInfo>格式信息列表.
在PatternLayout序列化时,会遍历每个PatternConverter,从LogEvent中取不同的值进行序列化输出.
5.2.1.模式字符串
模式字符串由3部分组成,格式为:%(格式信息)(转换器名称){选项1}{选项2}...
- 格式信息
数据结构如下:
public final class FormattingInfo {
// 默认配置,右对齐,长度不限,左侧截断
private static final FormattingInfo DEFAULT = new FormattingInfo(false, 0, Integer.MAX_VALUE, true);
// 字段最小长度
private final int minLength;
// 字段最大长度
private final int maxLength;
// 是否左对齐,默认为false,长度过短时,左侧填充空白
private final boolean leftAlign;
// 是否左侧截断,默认为true,长度过长时,删除左侧内容
private final boolean leftTruncate;
}
模式字符串的格式为:
%-(minLength).-(maxLength)(转换器名称){选项字符串}
minLength代表字段的最小长度限制,当字段内容长度小于最小限制时,会进行空格填充.
minLength前面的-负责控制对齐方式,默认为右对齐(左边空格填充),如果加上-,则会切换为左对齐方式(右边空格填充)
maxLength代表字段的最大长度限制,当字段内容长度大于最大限制时,会进行内容阶段
maxLength前面的-负责控制阶段方向,默认为左侧阶段,如果加上-,则会切换为右侧阶段
minLength和maxLength之间用点分隔.
格式信息中所有属性都是可选的,不配置,则使用默认值
- 转换器名称
log4j2会通过PluginManager
收集所有类别为Converter的插件,同时分析插件类上的@ConverterKeys
注解,获取转换器名称,并建立名称到插件实例的映射关系.
PatternParser识别到转换器名称的时候,会查找映射.
框架支持的所有转换器如下:
序号 | 名称 | 类型 | 描述 |
---|---|---|---|
1 | d date |
DatePatternConverter | 日志的时间戳 |
2 | p level |
LevelPatternConverter | 日志级别 |
3 | m msg message |
MessagePatternConverter | 日志中的消息内容 |
4 | C class |
ClassNamePatternConverter | 日志打印点所在类的类名 注意:需要给LoggerincludeLocation="true"属性开启位置 |
5 | M method |
MethodLocationPatternConverter | 日志打印点所在方法的方法名 注意:需要给LoggerincludeLocation="true"属性开启位置 |
6 | c logger |
LoggerPatternConverter | Logger实例的名称 |
7 | n | LineSeparatorPatternConverter | 专门追加换行符 |
8 | properties | Log4j1MdcPatternConverter | |
9 | ndc | Log4j1NdcPatternConverter | |
10 | enc encode |
EncodingPatternConverter | |
11 | equalsIgnoreCase | EqualsIgnoreCaseReplacementConverter | |
12 | equals | EqualsReplacementConverter | |
13 | xEx xThroable xException |
ExtendedThrowablePatternConverter | |
14 | F file |
FileLocationPatternConverter | 注意:需要给LoggerincludeLocation="true"属性开启位置 |
15 | l location |
FullLocationPatternConverter | 相当于%C.%M(%F:%L) 注意:需要给LoggerincludeLocation="true"属性开启位置 |
16 | highlight | HighlightConverter | |
17 | L line |
LineLocationPatternConverter | 日志打印点的代码行数 注意:需要给LoggerincludeLocation="true"属性开启位置 |
18 | K map MAP |
MapPatternConverter | |
19 | marker | MarkerPatternConverter | 打印完整标记,格式如:标记名[父标记名[祖父标记名]],一个标记可以有多个父标记 |
20 | markerSimpleName | MarkerSimpleNamePatternConverter | 只打印标记的名称 |
21 | maxLength maxLen |
MaxLengthConverter | |
22 | X mdc MDC |
MdcPatternConverter | LogEvent.getContextData() 映射诊断上下文 |
23 | N nano |
NanoTimePatternConverter | |
24 | x NDC |
NdcPatternConverter | LogEvent.getContextStack() 嵌套诊断上下文 |
25 | replace | RegexReplacementConverter | |
26 | r relative |
RelativeTimePatternConverter | |
27 | rEx rThrowable rException |
RootThrowablePatternConverter | |
28 | style | StyleConverter | |
29 | T tid threadId |
ThreadIdPatternConverter | 线程id |
30 | t tn thread threadName |
ThreadNamePatternConverter | 线程名称 |
31 | tp threadPriority |
ThreadPriorityPatternConverter | 线程优先级 |
32 | ex throwable Exception |
ThrowablePatternConverter | 异常 |
33 | u uuid |
UuidPatternConverter | 生成一个uuid,随日志一起打印,用于唯一标识一条日志 |
34 | notEmpty varsNotEmpty variablesNotEmpty |
VariablesNotEmptyReplacementConverter |
- 选项字符串
有时我们需要对特定的转换器进行特殊的配置,如:给DatePatternConverter配置时间格式,这个时候需要通过选项字符串配置.
PatternParser会提取模式字符串中的所有选项,保存在一个List<String>中,每个{}包裹的内容作为一个选项.
当创建转换器时,框架会自动扫描转换器类中声明的静态工厂方法newInstance,同时支持两种可选的形参,一种是Configuration,另一种String[]则会注入选项列表.
选项列表的识别由不同的转换器各自定义.
最后,以一个实际的例子解释配置:
日志会输出时间,类名,方法名,消息以及一个换行符.
同时,我们给DatePatternConverter指定了了时间格式,并且限制全限定类名最小长度为5,右截断,最大为10,左对齐.
<PatternLayout pattern="%date{yyyy-MM-dd HH:mm:ss.SSS} %-5.-10C.%M %message%n"/>
6.Manager
管理器的职责主要是控制目标输出流,以及把保存在ByteBuffer字节缓冲区中的日志序列化结果,输出到目标流中.
如:RollingFileManager需要在每次追加日志之前,进行滚动检查,如果触发滚动还会创建新的文件输出流.
manager继承体系如下:
manager继承体系
7.Filter
过滤器的核心职责就是对LogEvent
日志事件进行匹配,匹配结果分为匹配和不匹配,结果值有3种:接受,拒绝,中立.可由用户自定义匹配和不匹配的行为结果.
所有实现了Filterable
接口的组件都可以引用一个过滤器进行事件过滤,包含LoggerConfig
和AppenderControl
等.
框架实现的过滤器如下:
序号 | 工厂方法(类名.方法名) | xml元素 | 作用 |
---|---|---|---|
1 | BurstFilter.newBuilder | <BurstFilter> | |
2 | DynamicThresholdFilter.createFilter | <DynamicThresholdFilter> | |
3 | LevelRangeFilter.createFilter | <LevelRangeFilter> | |
4 | MapFilter.createFilter | <MapFilter> | |
5 | MarkerFilter.createFilter | <MarkerFilter> | |
6 | RegexFilter.createFilter | <RegexFilter> | |
7 | ScriptFilter.createFilter | <ScriptFilter> | |
8 | StructuredDataFilter.createFilter | <StructuredDataFilter> | |
9 | ThreadContextMapFilter.createFilter | <ThreadContextMapFilter><ContextMapFilter> | |
10 | ThresholdFilter.createFilter | <ThresholdFilter> | 根据LogEvent的级别进行过滤,如果LogEvent.level<=ThresholdFilter.level,则返回匹配的结果,否则返回不匹配的结果.如:过滤器为info,日志为error,则error<=info返回匹配结果 |
11 | TimeFilter.createFilter | <TimeFilter> | 判断日志时间是否在指定的时间区间内 |
enum Result {
/**
* The event will be processed without further filtering based on the log Level.
*/
ACCEPT,
/**
* No decision could be made, further filtering should occur.
*/
NEUTRAL,
/**
* The event should not be processed.
*/
DENY;
public static Result toResult(final String name) {
return toResult(name, null);
}
public static Result toResult(final String name, final Result defaultResult) {
return EnglishEnums.valueOf(Result.class, name, defaultResult);
}
}
AbstractFilterBuilder
用于根据配置文件中设置创建Filter
public static abstract class AbstractFilterBuilder<B extends AbstractFilterBuilder<B>> {
public static final String ATTR_ON_MISMATCH = "onMismatch";
public static final String ATTR_ON_MATCH = "onMatch";
@PluginBuilderAttribute(ATTR_ON_MATCH)
private Result onMatch = Result.NEUTRAL;
@PluginBuilderAttribute(ATTR_ON_MISMATCH)
private Result onMismatch = Result.DENY;
public Result getOnMatch() {
return onMatch;
}
public Result getOnMismatch() {
return onMismatch;
}
/**
* Sets the Result to return when the filter matches. Defaults to Result.NEUTRAL.
* @param onMatch the Result to return when the filter matches.
* @return this
*/
public B setOnMatch(final Result onMatch) {
this.onMatch = onMatch;
return asBuilder();
}
/**
* Sets the Result to return when the filter does not match. The default is Result.DENY.
* @param onMismatch the Result to return when the filter does not match.
* @return this
*/
public B setOnMismatch(final Result onMismatch) {
this.onMismatch = onMismatch;
return asBuilder();
}
@SuppressWarnings("unchecked")
public B asBuilder() {
return (B) this;
}
}
BurstFilter
频率控制过滤器
<BurstFilter level="INFO" rate="16" maxBurst="100"/>
level :BurstFilter过滤的事件级别
rate :每秒允许的 log 事件的平均值
maxBurst:当BurstFilter过滤的事件超过 rate 值,排队的 log 事件上限。超过此上限的 log ,将被丢弃。默认情况下 maxBurst = 100*rate
按以上配置,假定每个 log 事件的执行时间较长,输出 117 个 log 事件( INFO级别)到RollingFileAppenders,BurstFilter会过滤得到INFO级别的 log 事件,之后会发生: 16 个 log 事件在执行, 100 个等待执行, 1 个被丢弃。
private static final long NANOS_IN_SECONDS = 1000000000;
//默认
private static final int DEFAULT_RATE = 10;
private static final int DEFAULT_RATE_MULTIPLE = 100;
private static final int HASH_SHIFT = 32;
private BurstFilter(final Level level, final float rate, final long maxBurst, final Result onMatch,
final Result onMismatch) {
super(onMatch, onMismatch);
this.level = level;
this.burstInterval = (long) (NANOS_IN_SECONDS * (maxBurst / rate));
//构造maxBurst个令牌
for (int i = 0; i < maxBurst; ++i) {
available.add(createLogDelay(0));
}
}
private Result filter(final Level level) {
if (this.level.isMoreSpecificThan(level)) {
//DelayQueue<LogDelay> history = new DelayQueue<>();
LogDelay delay = history.poll();
while (delay != null) {
available.add(delay);
delay = history.poll();
}
delay = available.poll();
if (delay != null) {
//获取到令牌,则表示match
delay.setDelay(burstInterval);
history.add(delay);
return onMatch;
}
return onMismatch;
}
return onMatch;
}
public static class Builder extends AbstractFilterBuilder<Builder>
{
public BurstFilter build() {
if (this.rate <= 0) {
this.rate = DEFAULT_RATE;
}
if (this.maxBurst <= 0) {
//默认为100 * rate
this.maxBurst = (long) (this.rate * DEFAULT_RATE_MULTIPLE);
}
return new BurstFilter(this.level, this.rate, this.maxBurst, this.getOnMatch(), this.getOnMismatch());
}
}
DynamicThresholdFilter
可以过滤具有特定的属性某一级别的日志
<DynamicThresholdFilter key="loginId" defaultThreshold="ERROR" onMatch="ACCEPT" onMismatch="NEUTRAL">
<KeyValuePair key="User1" value="DEBUG"/>
</DynamicThresholdFilter>
如果用户的登录ID被捕获在ThreadContext的Map中则可以启用debug级的日志
private Result filter(final Level level, final ReadOnlyStringMap contextMap) {
//<DynamicThresholdFilter key="loginId"
final String value = contextMap.getValue(key);
if (value != null) {
Level ctxLevel = levelMap.get(value);
if (ctxLevel == null) {
ctxLevel = defaultThreshold;
}
return level.isMoreSpecificThan(ctxLevel) ? onMatch : onMismatch;
}
return Result.NEUTRAL;
}
MapFilter
MapFilter可以对Map中的信息进行过滤,进而记录特定事件,比如登入、退出
<MapFilter onMatch="ACCEPT" onMismatch="NEUTRAL" operator="or">
<KeyValuePair key="eventId" value="Login"/>
<KeyValuePair key="eventId" value="Logout"/>
</MapFilter>
protected boolean filter(final Map<String, String> data) {
boolean match = false;
for (int i = 0; i < map.size(); i++) {
//map.getKeyAt(i) key="eventId"
final String toMatch = data.get(map.getKeyAt(i));
//map.getValueAt(i) value="Login"
match = toMatch != null && ((List<String>) map.getValueAt(i)).contains(toMatch);
if ((!isAnd && match) || (isAnd && !match)) {
break;
}
}
return match;
}
LevelRangeFilter
private Result filter(final Level level) {
return level.isInRange(this.minLevel, this.maxLevel) ? onMatch : onMismatch;
}
MarkerFilter
对LogEvent中的Marker 进行过滤
<MarkerFilter marker="FLOW" onMatch="ACCEPT" onMismatch="DENY"/>
private Result filter(final Marker marker) {
return marker != null && marker.isInstanceOf(name) ? onMatch : onMismatch;
}
LogManager.getLogger((Class<?>)caller[0]).error(MarkerManager.getMarker("FATALMARKER"), caller[1] + ": " + value);
RegexFilter
对格式化消息和非格式化消息进行正则匹配过滤
<RegexFilter regex=".* test .*" onMatch="ACCEPT" onMismatch="DENY"/>
private Result filter(final String msg) {
if (msg == null) {
return onMismatch;
}
final Matcher m = pattern.matcher(msg);
return m.matches() ? onMatch : onMismatch;
}
ThresholdFilter
对level进行过滤
<ThresholdFilter level="TRACE" onMatch="ACCEPT" onMismatch="DENY"/>
private Result filter(final Level testLevel) {
return testLevel.isMoreSpecificThan(this.level) ? onMatch : onMismatch;
}
TimeFilter
基于时间段的日志过滤
<TimeFilter start="05:00:00" end="05:30:00" onMatch="ACCEPT" onMismatch="DENY"/>
Result filter(final long currentTimeMillis) {
if (currentTimeMillis >= midnightTomorrow || currentTimeMillis < midnightToday) {
initMidnight(currentTimeMillis);
}
return currentTimeMillis >= midnightToday + start && currentTimeMillis <= midnightToday + end //
? onMatch // within window
: onMismatch;
}
ScriptFilter
<Scripts>
<ScriptFile name="filter.js" language="JavaScript" path="src/test/resources/scripts/filter.js" charset="UTF-8" />
<ScriptFile name="filter.groovy" language="groovy" path="src/test/resources/scripts/filter.groovy" charset="UTF-8" />
</Scripts>
<ScriptFilter onMatch="ACCEPT" onMisMatch="DENY">
<ScriptRef ref="filter.groovy" />
</ScriptFilter>
public Result filter(final Logger logger, final Level level, final Marker marker, final Message msg,
final Throwable t) {
final SimpleBindings bindings = new SimpleBindings();
bindings.put("logger", logger);
bindings.put("level", level);
bindings.put("marker", marker);
bindings.put("message", msg);
bindings.put("parameters", null);
bindings.put("throwable", t);
bindings.putAll(configuration.getProperties());
bindings.put("substitutor", configuration.getStrSubstitutor());
final Object object = configuration.getScriptManager().execute(script.getName(), bindings);
return object == null || !Boolean.TRUE.equals(object) ? onMismatch : onMatch;
}
CompositeFilter
组合过滤器
<Filters>
<MarkerFilter marker="EVENT" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
<DynamicThresholdFilter key="loginId" defaultThreshold="ERROR"
onMatch="ACCEPT" onMismatch="NEUTRAL">
<KeyValuePair key="User1" value="DEBUG"/>
</DynamicThresholdFilter>
</Filters>
StructuredDataFilter
ThreadContextMapFilter
参考链接:https://www.jianshu.com/p/0c882ced0bf5