1. 程式人生 > >log4j 2.x 架構(原始碼)

log4j 2.x 架構(原始碼)

目錄

1.概述

1.1.元件概覽

1.2.靈活的配置

1.2.1.外掛發現機制

1.2.2.外掛裝配機制

1.2.3.配置檔案基本元素與物件的對映關係

事件級別

2.屬性佔位符

2.1.概述

2.2.Interpolator插值器

2.3.預設屬性配置

3.Logger

3.1.配置示例

3.1.1寫日誌邏輯

3.1.2 Additive

3.2.配置詳解

3.3.Logger繼承機制

3.4構建LoggerConfig樹

4.Appender

4.1.概述

4.2.框架支援的Appender實現

4.3.常用Appender詳解

4.3.1.ConsoleAppender

4.3.2.RollingFileAppender

5.Layout

5.1.概述

5.2.PatternLayout

5.2.1.模式字串

6.Manager

7.Filter

 AbstractFilterBuilder

BurstFilter

DynamicThresholdFilter

MapFilter

LevelRangeFilter

MarkerFilter

RegexFilter

 ThresholdFilter

TimeFilter

ScriptFilter

CompositeFilter

StructuredDataFilter

ThreadContextMapFilter



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介面的元件都可以引用一個過濾器進行事件過濾,包含LoggerConfigAppenderControl等.

框架實現的過濾器如下:

序號 工廠方法(類名.方法名) 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