0104 代碼方式動態刷新logback日誌配置

背景

日誌是一個系統或者說一個產品技術架構中重要組成部分。
常見的日誌框架以下:
日誌框架 說明 跟slf4j集成所需依賴
slf4j 日誌門面,具體實現由程序決定
jcl commons-logging
jcl-over-slf4j
jul jdk-logging slf4j-api
jul-to-slf4j
slf4j-jdk14
log4j log4j slf4j-api
log4j-over-slf4j
slf4j-log4j12
log4j2 log4j-api,log4j-core slf4j-api
log4j-slf4j-impl
logback logback-core,logback-classic slf4j-api

slf4j-logback的啓動過程

通常使用slf4j來操做日誌:
private static final Logger LOGGER =
        LoggerFactory.getLogger(LogbackAppenderExample.class);
 public static void main(String[] args) {
        LOGGER.trace("trace log");
        LOGGER.debug("debug log");
        LOGGER.info("info log");
        LOGGER.warn("warn log");
        LOGGER.error("error log");
        LOGGER.error("error log  xxx");
        LOGGER.error("error log   yyy");
        LOGGER.error("error log zzz");
        LOGGER.error("error log  aaa");
    }
經過這個來跟蹤Logger的初始過程;

1 LoggerFactory.getLogger

代碼以下:
public static Logger getLogger(Class<?> clazz) {
        Logger logger = getLogger(clazz.getName());
        if (DETECT_LOGGER_NAME_MISMATCH) {
            Class<?> autoComputedCallingClass = Util.getCallingClass();
            if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
                Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
                                autoComputedCallingClass.getName()));
                Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
            }
        }
        return logger;
    }
過程:
步驟 說明
1 獲取獲得Logger對象
2 若是有設置系統屬性 slf4j.detectLoggerNameMismatch=true
則找到調用getLogger方法的類名
若是跟傳入的類名不一致,則給出警告,給的類和調用方法的類不一致,並給出文檔地址
3 返回Logger對象

2 getLogger(clazz.getName())

經過類名獲得Logger
代碼以下:
public static Logger getLogger(String name) {
        ILoggerFactory iLoggerFactory = getILoggerFactory();
        return iLoggerFactory.getLogger(name);
 }
核心步驟
序號 步驟
1 獲得ILggerFactory對象
2 經過工廠,傳入名字,獲得Logger對象

3 getILoggerFactory()

獲得日誌工廠
代碼以下:
public static ILoggerFactory getILoggerFactory() {
        if (INITIALIZATION_STATE == UNINITIALIZED) {
            synchronized (LoggerFactory.class) {
                if (INITIALIZATION_STATE == UNINITIALIZED) {
                    INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                    performInitialization();
                }
            }
        }

        switch (INITIALIZATION_STATE) {
        case SUCCESSFUL_INITIALIZATION:
            return StaticLoggerBinder.getSingleton().getLoggerFactory();
        case NOP_FALLBACK_INITIALIZATION:
            return NOP_FALLBACK_FACTORY;
        case FAILED_INITIALIZATION:
            throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
        case ONGOING_INITIALIZATION:
            // support re-entrant behavior.
            // See also http://jira.qos.ch/browse/SLF4J-97
            return SUBST_FACTORY;
        }
        throw new IllegalStateException("Unreachable code");
    }
核心步驟:
序號 步驟
1 若是初始化狀態值爲 未初始化
同步加鎖 synchronized(LoggerFactory.class)
再次判斷 初始化狀態值爲 未初始化,若是是:
設置初始化狀態值爲 正在初始化
而後 執行初始化 _performInitialization_()
2 而後根據初始化狀態的條件作不一樣的處理
若是 初始化失敗,拋出異常,並提示哪裏失敗了
若是 正在初始化, 返回替代工廠SubstituteLoggerFactory,日誌通常也是委託給NOPLogger
若是 空回退初始化 返回空的工廠 NOPLoggerFactory,不輸出日誌的空實現
若是 成功初始化,調用StaticLoggerBinder.getLoggerFactory返回工廠
若是不在以上的狀態,直接拋出異常,沒法抵達的code;

4 _performInitialization_()

執行初始化
代碼:
private final static void performInitialization() {
        bind();
        if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
            versionSanityCheck();
        }
 }
核心步驟
序號 步驟說明
1 綁定
2 若是初始化成功,則進行版本明智檢查

5 bind()

綁定
代碼:
private final static void bind() {
        try {
            Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
            reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
            // the next line does the binding
            StaticLoggerBinder.getSingleton();
            INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
            reportActualBinding(staticLoggerBinderPathSet);
            fixSubstitutedLoggers();
            playRecordedEvents();
            SUBST_FACTORY.clear();
        } catch (NoClassDefFoundError ncde) {
            String msg = ncde.getMessage();
            if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
                INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
                Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
                Util.report("Defaulting to no-operation (NOP) logger implementation");
                Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
            } else {
                failedBinding(ncde);
                throw ncde;
            }
        } catch (java.lang.NoSuchMethodError nsme) {
            String msg = nsme.getMessage();
            if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
                INITIALIZATION_STATE = FAILED_INITIALIZATION;
                Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
                Util.report("Your binding is version 1.5.5 or earlier.");
                Util.report("Upgrade your binding to version 1.6.x.");
            }
            throw nsme;
        } catch (Exception e) {
            failedBinding(e);
            throw new IllegalStateException("Unexpected initialization failure", e);
        }
    }
關鍵步驟
序號 步驟
1 找到可能的靜態日誌綁定器的路徑集合findPossibleStaticLoggerBinderPathSet()
2 若是日誌有多個綁定器,打印到控制檯
若是是android平臺,忽略
依次打印出多個日誌綁定器,並給出文檔提示
3 得到惟一的靜態日誌綁定器StaticLoggerBinder.getSingleton()
綁定器內部持有LoggerContext和ContextSelectorStaticBinder
4 設置初始化狀態爲成功
5 打印出實際的日誌綁定器 ContextSelectorStaticBinder
6 設置SubstitutedLogger的委託爲實際的Logger; _fixSubstitutedLoggers_()
7 播放記錄的事件 playRecordedEvents()
8 清空委託工廠 SubstituteLoggerFactory

6 findPossibleStaticLoggerBinderPathSet()

找到可能的靜態日誌綁定器的路徑

代碼:
**java

static Set<URL> findPossibleStaticLoggerBinderPathSet() {
        // use Set instead of list in order to deal with bug #138
        // LinkedHashSet appropriate here because it preserves insertion order during iteration
        Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
        try {
            ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
            Enumeration<URL> paths;
            if (loggerFactoryClassLoader == null) {
                paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
            } else {
                paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
            }
            while (paths.hasMoreElements()) {
                URL path = paths.nextElement();
                staticLoggerBinderPathSet.add(path);
            }
        } catch (IOException ioe) {
            Util.report("Error getting resources from path", ioe);
        }
        return staticLoggerBinderPathSet;
    }

關鍵步驟:android

序號 步驟
1 若是LoggerFactory的類加載器爲空,系統類加載器獲得
org/slf4j/impl/StaticLoggerBinder.class 這個文件
分佈在不一樣的jar中,可能有多個;
2 若是不爲空,則經過LoggerFactoryLoader找到
org/slf4j/impl/StaticLoggerBinder.class 這個文件
3 把這些class對應的url彙總到結合中返回

image.png

image.png

7 playRecordedEvents()

放映記錄的事件

代碼:web

private static void playRecordedEvents() {
        List<SubstituteLoggingEvent> events = SUBST_FACTORY.getEventList();

        if (events.isEmpty()) {
            return;
        }

        for (int i = 0; i < events.size(); i++) {
            SubstituteLoggingEvent event = events.get(i);
            SubstituteLogger substLogger = event.getLogger();
            if( substLogger.isDelegateNOP()) {
                break;
            } else if (substLogger.isDelegateEventAware()) {
                if (i == 0)
                    emitReplayWarning(events.size());
                substLogger.log(event);
            } else {
                if(i == 0)
                    emitSubstitutionWarning(); 
                Util.report(substLogger.getName());
            }
        }
    }
關鍵步驟:
序號 步驟
1 獲得委託日誌工廠的事件,若是爲空,則結束
2 若是事件不爲空,取出來,
若是委託的日誌有空日誌,中斷
若是委託的日誌是委託事件, 打印日誌,並打印出播放的警告
不然,警告委託的日誌不可用,並打印出日誌的名稱

8 versionSanityCheck()

獲得StaticLoggerBinder的版本,並進行判斷是否合適。
LoggerFactory放了容許使用的StaticLoggerBinder的版本,若是不合適,會答應出警告。
源碼:
private final static void versionSanityCheck() {
        try {
            String requested = StaticLoggerBinder.REQUESTED_API_VERSION;

            boolean match = false;
            for (String aAPI_COMPATIBILITY_LIST : API_COMPATIBILITY_LIST) {
                if (requested.startsWith(aAPI_COMPATIBILITY_LIST)) {
                    match = true;
                }
            }
            if (!match) {
                Util.report("The requested version " + requested + " by your slf4j binding is not compatible with "
                                + Arrays.asList(API_COMPATIBILITY_LIST).toString());
                Util.report("See " + VERSION_MISMATCH + " for further details.");
            }
        } catch (java.lang.NoSuchFieldError nsfe) {
            // given our large user base and SLF4J's commitment to backward
            // compatibility, we cannot cry here. Only for implementations
            // which willingly declare a REQUESTED_API_VERSION field do we
            // emit compatibility warnings.
        } catch (Throwable e) {
            // we should never reach here
            Util.report("Unexpected problem occured during version sanity check", e);
        }
    }

9 StaticLoggerBinder.init()

靜態日誌綁定器的初始化

代碼:spring

void init() {
        try {
            try {
                new ContextInitializer(defaultLoggerContext).autoConfig();
            } catch (JoranException je) {
                Util.report("Failed to auto configure default logger context", je);
            }
            // logback-292
            if (!StatusUtil.contextHasStatusListener(defaultLoggerContext)) {
                StatusPrinter.printInCaseOfErrorsOrWarnings(defaultLoggerContext);
            }
            contextSelectorBinder.init(defaultLoggerContext, KEY);
            initialized = true;
        } catch (Exception t) { // see LOGBACK-1159
            Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", t);
        }
    }

核心過程apache

序號 步驟
1 新建上下文初始化器,而後自動配置;
new ContextInitializer(defaultLoggerContext).autoConfig();
2 若是沒有配置狀態監聽器,則打印出警告
3 上下文選擇綁定器初始化

10 ContextInitializer.autoConfig();

自動配置上下文

代碼:segmentfault

public void autoConfig() throws JoranException {
        StatusListenerConfigHelper.installIfAsked(loggerContext);
        URL url = findURLOfDefaultConfigurationFile(true);
        if (url != null) {
            configureByResource(url);
        } else {
            Configurator c = EnvUtil.loadFromServiceLoader(Configurator.class);
            if (c != null) {
                try {
                    c.setContext(loggerContext);
                    c.configure(loggerContext);
                } catch (Exception e) {
                    throw new LogbackException(String.format("Failed to initialize Configurator: %s using ServiceLoader", c != null ? c.getClass()
                                    .getCanonicalName() : "null"), e);
                }
            } else {
                BasicConfigurator basicConfigurator = new BasicConfigurator();
                basicConfigurator.setContext(loggerContext);
                basicConfigurator.configure(loggerContext);
            }
        }
    }

核心步驟api

序號 說明
1 若是沒有,安裝狀態監聽器
2 找到默認的配置文件或者URL,一次按照系統屬性
logback.configurationFile查找
按照logback-test.xml
按照logback.groovy
按照logback.xml
獲得配置文件
3 若是找到了,configureByResource(url);
4 不然,按照spi的方式找到Configurator的實現類,設置上下文,進行配置
若是spi方式拿不到,則使用缺省的BasicConfigurator(裏面只配置了一個控制檯)
設置上下文,進行配置

11 StaticLoggerBinder.getLoggerFactory

經過靜態日誌綁定器獲得日誌工廠,實現類是 LoggerContext;

源碼:網絡

public ILoggerFactory getLoggerFactory() {
        if (!initialized) {
            return defaultLoggerContext;
        }

        if (contextSelectorBinder.getContextSelector() == null) {
            throw new IllegalStateException("contextSelector cannot be null. See also " + NULL_CS_URL);
        }
        return contextSelectorBinder.getContextSelector().getLoggerContext();
    }

核心流程:架構

序號 步驟
1 若是沒有初始化,返回默認的LoggerContext
2 若是ContextSelectBinder不爲空,獲得ContextSeleter
3 經過ContextSelector獲得LoggerContext;

12 iLoggerFactory.getLogger(name)

這是一個接口,直接獲得一個Logger實例;
從上面的代碼以後,這裏的實例應該是一個LoggerContext對象
這個對象是核心,全部的日誌動做都在裏面;

logback-aliyun-appender

直接把日誌接入到阿里雲
對於初創企業來講,直接使用阿里雲的日誌服務很是方便,減小了本身搭建ELK的運維成本,直接按量付費,很是方便,我貼一下個人接入過程;

引入依賴:mvc

<!--日誌-->
        <dependency>
            <groupId>com.aliyun.openservices</groupId>
            <artifactId>aliyun-log-logback-appender</artifactId>
        </dependency>
<!--spring日誌橋接,使用的commoon-logging-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
        </dependency>
<!--log4j日誌橋接,zk使用的log4j-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>log4j-over-slf4j</artifactId>
        </dependency>

而後按照 代碼刷新logback日誌配置的方法,把日誌配置放到apollo,啓動的時候就能夠接入到阿里雲日誌了。

貼一下配置:

<configuration>
    <!--爲了防止進程退出時,內存中的數據丟失,請加上此選項-->
    <shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/>
    <appender name="loghubAppender" class="com.aliyun.openservices.log.logback.LoghubAppender">
        <!--必選項-->
        <!-- 帳號及網絡配置 -->
        <endpoint>cn-xxx.log.aliyuncs.com</endpoint>
        <accessKeyId>xxxxx</accessKeyId>
        <accessKeySecret>xxxxx</accessKeySecret>

        <!-- sls 項目配置 -->
        <project>ts-app-xxx</project>
        <logStore>ts-app-xxx</logStore>
        <!--必選項 (end)-->

        <!-- 可選項 -->
        <topic>topic2</topic>
        <source>source2</source>

        <!-- 可選項 詳見 '參數說明'-->
        <totalSizeInBytes>104857600</totalSizeInBytes>
        <maxBlockMs>60</maxBlockMs>
        <ioThreadCount>2</ioThreadCount>
        <batchSizeThresholdInBytes>524288</batchSizeThresholdInBytes>
        <batchCountThreshold>4096</batchCountThreshold>
        <lingerMs>2000</lingerMs>
        <retries>3</retries>
        <baseRetryBackoffMs>100</baseRetryBackoffMs>
        <maxRetryBackoffMs>100</maxRetryBackoffMs>

        <!-- 可選項 經過配置 encoder 的 pattern 自定義 log 的格式 -->
        <encoder>
            <pattern>%d %-5level [%thread] %logger{0}: %msg</pattern>
        </encoder>

        <!-- 可選項 設置時間格式 -->
        <timeFormat>yyyy-MM-dd'T'HH:mmZ</timeFormat>
        <!-- 可選項 設置時區 -->
        <timeZone>Asia/Shanghai</timeZone>

        <filter class="ch.qos.logback.classic.filter.ThresholdFilter"><!-- 只打印INFO級別的日誌 -->
            <level>INFO</level>
<!--            <onMatch>ACCEPT</onMatch>-->
<!--            <onMismatch>DENY</onMismatch>-->
        </filter>

<!--        <mdcFields>THREAD_ID,MDC_KEY</mdcFields>-->
    </appender>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg %X{THREAD_ID} %n</pattern>
        </encoder>
    </appender>

    <!-- 可用來獲取StatusManager中的狀態 -->
    <statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener"/>
    <!-- 解決debug模式下循環發送的問題 -->
    <logger name="org.apache.http.impl.conn.Wire" level="WARN" />

    <root>
        <level value="DEBUG"/>
        <appender-ref ref="loghubAppender"/>
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

代碼刷新logback日誌配置

主要是模仿LogbackLister的實現細節來模仿:
簡單的貼一下個人實現代碼:
package com.lifesense.opensource.spring;

import ch.qos.logback.classic.BasicConfigurator;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.util.StatusPrinter;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;

import javax.servlet.ServletContext;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Method;

/**
 * @author carter
 */
public class LogbackLoader {

    private static final String DEFAULT_LOG_BACK_XML = "<configuration>" +
            "<shutdownHook class=\"ch.qos.logback.core.hook.DelayingShutdownHook\"/>" +
            "<appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">" +
            "<encoder><pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg %X{THREAD_ID} %n</pattern></encoder>" +
            "</appender>" +
            "<statusListener class=\"ch.qos.logback.core.status.OnConsoleStatusListener\"/>" +
            "<logger name=\"org.apache.http.impl.conn.Wire\" level=\"WARN\" />" +
            "<root><level value=\"DEBUG\"/><appender-ref ref=\"STDOUT\"/>" +
            "</root></configuration>";

    /**
     * 初始化日誌配置
     */
    public static void initLogbackWithoutConfigFile(ServletContext servletContext) {
        initLogbackConfigFromXmlString(servletContext, DEFAULT_LOG_BACK_XML);

    }


    public static void initLogbackConfigFromXmlString(ServletContext servletContext, String xmlStr) {

        System.out.println("Initializing Logback from [\n" + xmlStr + "\n]");

        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();

        Assert.notNull(loggerContext, "獲取不到LoggerContext");

        loggerContext.getStatusManager().clear();
        loggerContext.reset();

        //安裝默認的日誌配置
        if (StringUtils.isBlank(xmlStr)) {
            BasicConfigurator basicConfigurator = new BasicConfigurator();
            basicConfigurator.setContext(loggerContext);
            basicConfigurator.configure(loggerContext);
            return;
        }

        //按照傳入的配置文件來配置
        JoranConfigurator configurator = new JoranConfigurator();
        configurator.setContext(loggerContext);
        InputStream in = new ByteArrayInputStream(xmlStr.getBytes());
        try {
            configurator.doConfigure(in);
        } catch (JoranException e) {
            System.out.println("初始化配置logback發生錯誤");
            e.printStackTrace();
        }

        //If SLF4J's java.util.logging bridge is available in the classpath, install it. This will direct any messages
        //from the Java Logging framework into SLF4J. When logging is terminated, the bridge will need to be uninstalled
        try {
            Class<?> julBridge = ClassUtils.forName("org.slf4j.bridge.SLF4JBridgeHandler", ClassUtils.getDefaultClassLoader());

            Method removeHandlers = ReflectionUtils.findMethod(julBridge, "removeHandlersForRootLogger");
            if (removeHandlers != null) {
                servletContext.log("Removing all previous handlers for JUL to SLF4J bridge");
                ReflectionUtils.invokeMethod(removeHandlers, null);
            }

            Method install = ReflectionUtils.findMethod(julBridge, "install");
            if (install != null) {
                servletContext.log("Installing JUL to SLF4J bridge");
                ReflectionUtils.invokeMethod(install, null);
            }
        } catch (ClassNotFoundException ignored) {
            //Indicates the java.util.logging bridge is not in the classpath. This is not an indication of a problem.
            servletContext.log("JUL to SLF4J bridge is not available on the classpath");
        }

        StatusPrinter.print(loggerContext);
    }


}
在springmvc上下文啓動的時候,可使用代碼的方式加載默認的日誌配置;
啓動完成以後,加上apollo的配置監聽器,這樣就能夠在apollo中實時的修改日誌的配置文件,代碼實時生效。
package com.lifesense.opensource.spring;

import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.ConfigService;
import com.ctrip.framework.apollo.model.ConfigChange;
import com.google.common.base.Strings;
import com.lifesense.opensource.commons.utils.WebResourceUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import java.util.Objects;
import java.util.Set;


/**
 * @author carter
 */
@Slf4j
public class ContextLoaderListener extends org.springframework.web.context.ContextLoaderListener {


    private static final String APOLLO_LOG_BACK_CONFIG_KEY = "log4j2.xml";

    @Override
    public void contextInitialized(ServletContextEvent event) {
        final ServletContext servletContext = event.getServletContext();

        final Config configFile = ConfigService.getAppConfig();
        String xmlContent = configFile.getProperty(APOLLO_LOG_BACK_CONFIG_KEY, "");
        if (!Strings.isNullOrEmpty(xmlContent)) {
            LogbackLoader.initLogbackConfigFromXmlString(servletContext, xmlContent);
            configFile.addChangeListener(configFileChangeEvent -> {
                final Set<String> newValue = configFileChangeEvent.changedKeys();
                if (!CollectionUtils.isEmpty(newValue) && newValue.contains(APOLLO_LOG_BACK_CONFIG_KEY)) {
                    final ConfigChange change = configFileChangeEvent.getChange(APOLLO_LOG_BACK_CONFIG_KEY);
                    System.out.println(String.format("log4j2.ml changed:old:\n %s , new : \n %s ", change.getOldValue(), change.getNewValue()));
                    LogbackLoader.initLogbackConfigFromXmlString(servletContext, change.getNewValue());
                }
            });
        }

    }
}

小結

今天學會了:

  1. slf4j的日誌裝配過程,分析了源碼;
  2. 學會了使用代碼的方式動態刷新logback的日誌配置;
  3. 一種接入阿里雲日誌的實現方式。
  4. 常見的slf4j的日誌組合方式的使用;
原創不易,轉載請註明出處,歡迎溝通交流。
相關文章
相關標籤/搜索