log4j 2.x 架構(原始碼)
目錄
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