日誌是一個系統或者說一個產品技術架構中重要組成部分。
常見的日誌框架以下:
日誌框架 | 說明 | 跟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來操做日誌:
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的初始過程;
代碼以下:
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對象 |
經過類名獲得Logger
代碼以下:
public static Logger getLogger(String name) { ILoggerFactory iLoggerFactory = getILoggerFactory(); return iLoggerFactory.getLogger(name); }
核心步驟
序號 | 步驟 |
---|---|
1 | 獲得ILggerFactory對象 |
2 | 經過工廠,傳入名字,獲得Logger對象 |
獲得日誌工廠
代碼以下:
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; |
執行初始化
代碼:
private final static void performInitialization() { bind(); if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) { versionSanityCheck(); } }
核心步驟
序號 | 步驟說明 |
---|---|
1 | 綁定 |
2 | 若是初始化成功,則進行版本明智檢查 |
綁定
代碼:
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 |
找到可能的靜態日誌綁定器的路徑
代碼:
**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彙總到結合中返回 |
放映記錄的事件
代碼: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 | 若是事件不爲空,取出來, 若是委託的日誌有空日誌,中斷 若是委託的日誌是委託事件, 打印日誌,並打印出播放的警告 不然,警告委託的日誌不可用,並打印出日誌的名稱 |
獲得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); } }
靜態日誌綁定器的初始化
代碼: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 | 上下文選擇綁定器初始化 |
自動配置上下文
代碼: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(裏面只配置了一個控制檯) 設置上下文,進行配置 |
經過靜態日誌綁定器獲得日誌工廠,實現類是 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; |
這是一個接口,直接獲得一個Logger實例;
從上面的代碼以後,這裏的實例應該是一個LoggerContext對象
這個對象是核心,全部的日誌動做都在裏面;
直接把日誌接入到阿里雲
對於初創企業來講,直接使用阿里雲的日誌服務很是方便,減小了本身搭建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>
主要是模仿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()); } }); } } }
今天學會了:
- slf4j的日誌裝配過程,分析了源碼;
- 學會了使用代碼的方式動態刷新logback的日誌配置;
- 一種接入阿里雲日誌的實現方式。
- 常見的slf4j的日誌組合方式的使用;
原創不易,轉載請註明出處,歡迎溝通交流。