本文是一個系列,歡迎關注更新html
本文全篇高能,請作好心理準備後再食用,讀完請記得點贊。java
上一篇咱們討論了日誌的性能以及日誌的優缺點,有朋友說我沒有乾貨,痛定思痛,決定來一篇乾貨,讓小夥伴們見識一下員外真正的實力💪,討論一下生產環境如何動態按一次請求、一個用戶來調整日誌級別,甚至輸出獨立文件。本文代碼較多,建議讀者運行一下。git
設想一個需求:客服妹妹反饋有用戶線上發現了BUG,你拿來日誌分析,一個 200M 的日誌文件看的頭大,最後仍是沒找到有用的信息,你終於決定開啓 DEBUG 日誌,重啓了項目,讓用戶再次操做一下,結果開了兩分鐘線上磁盤就告警了,因而你被運維人員痛批一頓,由於停生產環境又被 BOSS 大批一頓。github
試着給本身當一下產品經理,提些需求:數據庫
整理了以上需求,咱們來嘗試逐一解決。vim
這個需求很簡單,想必你們也都會,Logback 和 Log4j2 都原生實現了日誌監控日誌文件熱加載,使用起來也特別簡單,只須要在配置文件中修改,固然,框架做者爲了混(e)淆(xin)使用者,使用了不同的配置方式:緩存
<!-- logback 配置 -->
<configuration scan="true" scanPeriod="30 seconds" >
...
</configuration>
<!-- log4j2 配置 -->
<Configuration monitorInterval="30">
...
</Configuration>
如此簡單的實現,不能很好知足咱們的需求,咱們決定再接再礪,畢竟咱們是有追求的人。服務器
咱們來分解一下問題:微信
咱們來逐一解決問題:session
區分用戶最佳實踐,給管理員開放功能,列出在線用戶,點擊用戶便可選擇用戶輸出的日誌級別。
動態調整日誌級別,這個是咱們最棘手的地方,分析Logback API 會發現 ch.qos.logback.classic.Logger
已經提供了#setLevel
方法,這個方法看似可以在運行過程當中改變日誌的輸出級別,可是讀過我以前文章的小夥伴確定知道,Logger 實例建議static final
,因此Logger
實例是多線程共享的,若是咱們修改了Logger
的級別,確定會污染其餘請求乃至其餘用戶,看來這樣是行不通了,如何解決?
沒辦法時只有兩條路線,閱讀源碼以及翻閱文檔,咱們先從源碼入手,咱們先來分析一下Logback是如何決定是否輸出一條日誌的:
咱們隨便輸出一條日誌log.debug("A debug Log");
,斷點跟進去,發現真正的判斷邏輯在filterAndLog_0_Or3Plus
,源碼以下:
private void filterAndLog_0_Or3Plus(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params, final Throwable t) { // 神奇的方法 final FilterReply decision = loggerContext .getTurboFilterChainDecision_0_3OrMore(marker, this, level, msg, params, t); // 若是上面神奇的方法返回 NEUTRAL 才判斷日誌級別 if (decision == FilterReply.NEUTRAL) { if (effectiveLevelInt > level.levelInt) { return; } // 返回 DENY 根本就不判斷日誌級別了 } else if (decision == FilterReply.DENY) { return; } // 若是可以執行到這裏,則輸出日誌 buildLoggingEventAndAppend(localFQCN, marker, level, msg, params, t); }
分析源碼咱們得出結論,神奇的方法返回值優先級要高於日誌級別判斷,這樣就有意思了,咱們能不能定製那個神奇方法呢,繼續跟進去發現,TurboFilterList
繼承了CopyOnWriteArrayList<TurboFilter>
,其自己就是一個List,其中的TurboFilterList#getTurboFilterChainDecision
裏面邏輯就是循環本身獲取FilterReply
而後返回,一塊兒看一下代碼:
public FilterReply getTurboFilterChainDecision(final Marker marker, final Logger logger, final Level level, final String format, final Object[] params, final Throwable t) { final int size = size(); // 若是隻有一個,直接返回結果,期間若是異常直接返回 NEUTRAL // size不多是0,由於調用者已經判斷了,這裏再也不展現 if (size == 1) { try { TurboFilter tf = get(0); return tf.decide(marker, logger, level, format, params, t); } catch (IndexOutOfBoundsException iobe) { return FilterReply.NEUTRAL; } } // 若是有多個,循環獲取第一個不是 NEUTRAL 的結果返回,其餘再也不判斷 Object[] tfa = toArray(); final int len = tfa.length; for (int i = 0; i < len; i++) { // for (TurboFilter tf : this) { final TurboFilter tf = (TurboFilter) tfa[i]; final FilterReply r = tf .decide(marker, logger, level, format, params, t); if (r == FilterReply.DENY || r == FilterReply.ACCEPT) { return r; } } return FilterReply.NEUTRAL; }
爲何做者要把一個
TurboFilter
時的邏輯與多個TurboFilter
時的邏輯分開寫?關注公衆號回覆TurboFilter
獲取答案(額外有一段解析,做爲微信粉絲福利)。
梳理一下思路,每一條日誌,不管級別,最終都會進入filterAndLog_0_Or3Plus
方法進行判斷是否輸出,而其判斷又會優先判斷TurboFilterList#getTurboFilterChainDecision
的返回值,getTurboFilterChainDecision
中使用了TurboFilter
,跟進源碼咱們發現TurboFilterList
是空的,咱們幾乎能夠斷言,做者不會無緣無故搞一個空的List
,必定是用來給咱們擴展的,咱們去翻翻文檔,確定找獲得擴展方法,翻看文檔中關於TurboFilters
的部分,咱們找到以下描述:
TurboFilters
TurboFilter
objects all extend theTurboFilter
abstract class. Like the regular filters, they use ternary logic to return their evaluation of the logging event.Overall, they work much like the previously mentioned filters. However, there are two main differences between
Filter
andTurboFilter
objects.
TurboFilter
objects are tied to the logging context. Hence, they are called not only when a given appender is used, but each and every time a logging request is issued. Their scope is wider than appender-attached filters.More importantly, they are called before the
LoggingEvent
object creation.TurboFilter
objects do not require the instantiation of a logging event to filter a logging request. As such, turbo filters are intended for high performance filtering of logging events, even before the events are created.
大概意思和咱們分析的也差很少,TurboFilter
也能夠控制日誌是否可以輸出,並且優先級要高於普通的Filter
,這不奇怪,畢竟Turbo
嘛。
文檔中給出了實例,接下來咱們來定義一個本身TurboFilter
吧。
/** * DynamicLoggingFilter * * @author jiyanwai * @date 2020-01-15 16:09:16 */ @Slf4j public class DynamicLoggingFilter extends TurboFilter { public static final String DEBUG_HEADER_NAME = "X-Debug"; public static final String DEBUG_SESSION_KEY = DEBUG_HEADER_NAME; /** * 返回值 FilterReply.DENY,FilterReply.NEUTRAL或FilterReply.ACCEPT * 若是是 DENY,則不會再輸出 * 若是是 ACCEPT,則直接輸出 * 若是是 NEUTRAL,走來日誌輸出判斷邏輯 * * @return FilterReply.DENY,FilterReply.NEUTRAL或FilterReply.ACCEPT */ @Override public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) { // 這裏能夠過濾咱們本身的logger boolean isMyLogger = logger.getName().startsWith("com.jiyuanwai"); if (!isMyLogger) { return FilterReply.NEUTRAL; } RequestAttributes requestAttributes = RequestContextHolder .getRequestAttributes(); // 項目啓動或者運行定時器時,可能沒有 RequestAttributes if (requestAttributes == null) { return FilterReply.NEUTRAL; } // 先判斷 RequestHeader,用於區分線程 ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes; // 按照請求參數判斷,實際生產環境能夠開發功能給管理人員功能,將用戶惟一標示放入緩存或者session HttpServletRequest request = servletRequestAttributes.getRequest(); String debug = request.getHeader(DEBUG_HEADER_NAME); boolean debugBoolean = Boolean.parseBoolean(debug); if (debugBoolean) { return FilterReply.ACCEPT; } // 再判斷 Session HttpSession session = request.getSession(false); if (session == null) { return FilterReply.NEUTRAL; } Object attribute = session.getAttribute(DEBUG_SESSION_KEY); debugBoolean = Boolean.parseBoolean((String) attribute); if (debugBoolean) { return FilterReply.ACCEPT; } return FilterReply.NEUTRAL; } }
日誌配置文件調整以下:
<configuration scan="false" debug="false"> <turboFilter class="com.jiyuanwai.logging.extend.dynamic.DynamicLoggingFilter" /> ... <root level="error"> <appender-ref ref="STDOUT"/> </root> </configuration>
寫一個方法測試一下
@GetMapping public void debugLevel() { log.debug("A debug Log"); }
這裏推薦你們使用Idea內置的 HTTP Request:
HTTP Request
# 測試 RequestHeader 使用 GET http://localhost:8082/log/level Accept: */* # 經過Header,避免污染請求參數(不然文件上傳會有問題) X-Debug: true Cache-Control: no-cache ###
執行以後,能夠在控制檯看到:
22:50:19.816 DEBUG [082-exec-5] c.j.l.e.c.LogController ( 35) - A debug Log
咱們完成了按照請求來動態調整日誌。
繼續測試一下Session
,這裏我就簡單處理,僅僅在 Controller 增長一下做爲測試,線上能夠配合在線用戶管理功能實現,小夥伴們能夠按需擴展,若是有困難能夠留言,我收集你們的困難統一解決。
@PutMapping
public void startDebugBySession(HttpSession session) {
// 僅供測試,線上須要開發功能,獲取對對應用戶 session,而後放入屬性
session.setAttribute(DynamicLoggingFilter.DEBUG_SESSION_KEY, "true");
}
開啓 Session 級別 debug 請求以下
# 以 Session 開啓當前用戶 DEBUG 模式 PUT http://localhost:8082/log/level Accept: */* Cache-Control: no-cache ###
# 去掉自定義Header,再次測試 GET http://localhost:8082/log/level Accept: */* Cache-Control: no-cache ###
結果以下
09:28:16.662 DEBUG [082-exec-1] c.j.l.e.c.LogController ( 40) - A debug Log
Tocken 與 Session 幾乎沒有差異,這裏就再也不展現了。
至此,咱們已經很優雅的實現了按照請求、按照用戶來動態輸出日誌,能夠自豪一下了。
讓咱們來實現最後一個需求,動態輸出日誌文件。
強者的世界
咱們先分析一下需求,想要按照用戶臨時輸出文件,區分用戶可使用以前的方法,咱們須要解決的問題是,咱們如何將特定的用戶輸出到特定的文件?
讀過員外之前文章的朋友都知道,真正負責輸出日誌的是 Appender,咱們優先去翻翻文檔看看官方有沒有解決方案,通過一番硬啃文檔,咱們發現了 SiftingAppender
配合discriminator
再配合MDC
彷佛能解決咱們的需求,SiftingAppender
能夠根據用戶會話分離日誌事件,這樣不一樣用戶生成的日誌就能夠進入不一樣的日誌文件,甚至能夠每一個用戶一個日誌文件。discriminator
很好理解按照字面意思就是鑑別器能夠用來鑑別,但MDC
是什麼?
"Mapped Diagnostic Context" is essentially a map maintained by the logging framework where the application code provides key-value pairs which can then be inserted by the logging framework in log messages. MDC data can also be highly helpful in filtering messages or triggering certain actions.
簡單翻譯一下,MDC
就是日誌框架維護的一個Map
,能夠用來過濾和觸發操做,員外總結了兩個最佳實踐:
MDC
,見下文。discriminator
配合MDC
能夠動態輸出文件了,咱們來試一下。實現思路,使用Filter
而且第一個執行,直接來看代碼
/** * LogFilter 用於處理動態日誌 * * @author jiyanwai * @date 2020-01-16 06:05:19 */ @Slf4j @Component @Order(Integer.MIN_VALUE) @WebFilter("/*") public class LogFilter implements Filter { public static final String REQUEST_ID_MDC_KEY = "requestId"; public static final String SESSION_ID_MDC_KEY = "sessionId"; public static final String STAND_ALONE_HEADER_KEY = "X-StandAlone-File"; public static final String STAND_ALONE_SESSION_KEY = STAND_ALONE_HEADER_KEY; @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { try { try { HttpServletRequest request = (HttpServletRequest) servletRequest; // requestId 與超級 SessionId,能夠根據需求自行定製 String requestId = CommonUtils.generateRequestId(); MDC.put(REQUEST_ID_MDC_KEY, requestId); String sessionId = request.getRequestedSessionId(); MDC.put(SESSION_ID_MDC_KEY, sessionId); // 只有開啓了獨立文件輸出,才放入MDC String standAlone = request.getHeader(STAND_ALONE_HEADER_KEY); if (standAlone == null) { standAlone = (String) request.getSession().getAttribute(STAND_ALONE_SESSION_KEY); } if (standAlone != null) { // 此處能夠任意定製不一樣路徑,sessionId會被拼接到文件名,參考下文修改後的xml MDC.put(STAND_ALONE_SESSION_KEY, sessionId); } } catch (Exception e) { // 此到處理異常,不影響業務 log.error("Error handler dynamic log", e); } // 繼續執行,不處理其餘Filter異常 filterChain.doFilter(servletRequest, servletResponse); } finally { // 切記要清理環境 MDC.clear(); } } }
配置文件增長SiftAppender
<appender name="SIFT" class="ch.qos.logback.classic.sift.SiftingAppender"> <discriminator> <key>X-StandAlone-File</key> <!-- MDC取不到,默認「all」 --> <defaultValue>all</defaultValue> </discriminator> <sift> <!-- Appender Name須要獨立 --> <appender name="FILE-${X-StandAlone-File}" class="ch.qos.logback.core.FileAppender"> <!-- 正在記錄的日誌文件的路徑及文件名 --> <!-- 輸出位置,${X-StandAlone-File}取MDC --> <file>${LOG_PATH}/${X-StandAlone-File}.log</file> <!-- 日誌記錄器的滾動策略,按日期,按大小記錄 --> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <!-- 按日滾動 --> <fileNamePattern>${LOG_PATH}/%d{yyyy-MM}/log-%d{yyyy-MM-dd-HH}-%i.${X-StandAlone-File}.gz</fileNamePattern> <!-- 單個文件最大50M --> <maxFileSize>50MB</maxFileSize> <!-- 最多佔用5G磁盤空間,500個文件(總共不能超過該5G) --> <maxHistory>500</maxHistory> <totalSizeCap>5GB</totalSizeCap> </rollingPolicy> <!-- 追加方式記錄日誌 --> <append>true</append> <!-- 日誌文件的格式 --> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${FILE_LOG_PATTERN}</pattern> <charset>utf-8</charset> </encoder> </appender> </sift> <root level="error"> <!-- 爲了方便測試,同時輸出到控制檯 --> <appender-ref ref="STDOUT"/> <appender-ref ref="SIFT"/> </root> </appender>
調整Controller增長方法
/** * 爲當前用戶開啓debug,測試使用,生產環境請配合在線用戶管理相關功能來實現 * * @param session session */ @PutMapping public void startDebugBySession(HttpSession session, @RequestParam(required = false) boolean standAlone) { // 僅供測試,線上須要開發功能,獲取對對應用戶 session,而後放入屬性 session.setAttribute(DynamicLoggingFilter.DEBUG_SESSION_KEY, "true"); if (standAlone) { session.setAttribute(LogFilter.STAND_ALONE_SESSION_KEY, ""); } }
先使用Header模式來測試沒有開啓獨立輸出會文件結構如何
# 測試 RequestHeader 使用
GET http://localhost:8082/log/level
Accept: */*
# 經過Header,避免污染請求參數(不然文件上傳會有問題)
X-Debug: true
Cache-Control: no-cache
###
非獨立輸出文件
內容以下,能夠看到咱們的RequestId與超級SessionId都成功的寫到日誌裏面了
2020-01-16 09:54:05.827 DEBUG [http-nio-8082-exec-1] c.j.l.extend.controller.LogController --- requestId=ee77a879576147bcafdcb745ba5767d3, sessionId=21F4DE2AADBA675F2135601D760BF18E : A debug Log
使用Http Request進行測試開啓 debug,而且開啓獨立文件
# 以 Session 開啓當前用戶 DEBUG 模式 PUT http://localhost:8082/log/level Accept: */* Cache-Control: no-cache Content-Type: application/x-www-form-urlencoded standAlone=true ###
測試日誌輸出
# 測試 Session 模式使用 GET http://localhost:8082/log/level Accept: */* Cache-Control: no-cache ###
獨立輸出文件
咱們以前採用了日誌框架原生解決動態級別,問題在於修改起來不方便,須要鏈接線上服務器採用vim來修改,上文提到了 Logback 提供了API能夠直接熱修改,咱們來看一下:
/** * 修改單個日誌級別 * * @param loggerName 日誌實例名稱 * @param level 級別 */ @PostMapping public void changeLoggingLevel(String loggerName, @RequestParam(required = false, defaultValue = "DEBUG") String level) { ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(loggerName); logger.setLevel(Level.toLevel(level)); }
測試一下:
# 測試 調整全局 日誌級別 POST http://localhost:8082/log/level Accept: */* Cache-Control: no-cache Content-Type: application/x-www-form-urlencoded loggerName=ROOT ###
若是咱們想要還原:
/** * 將日誌級別重置爲配置文件默認 */ @DeleteMapping public void restoreLoggingLevel() { LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); try { JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(context); context.reset(); configurator.doConfigure(new ClassPathResource("logback.xml").getInputStream()); } catch (JoranException | IOException ignore) { // StatusPrinter will handle this } StatusPrinter.printInCaseOfErrorsOrWarnings(context); }
繼續測試
# 測試還原日誌級別爲配置文件級別 DELETE http://localhost:8082/log/level Accept: */* Cache-Control: no-cache ###
這一篇咱們實現了按請求、用戶級別動態輸出級別與文件,如今已經應該能知足 99% 的場景了,員外只用過 Logback,若是讀者想使用 Log4j2,能夠按照思路嘗試本身實現,若是點贊超過 1000,我也能夠實現出來給你們用,下一篇我會作一些前文勘誤、讀者問題反饋,還有少許的最佳實踐,歡迎關注更新。
最後給你們貼一張分析過程的思惟導圖,但願可以幫助你們學會分析、解決問題。
以上是我的觀點,若是有問題或錯誤,歡迎留言討論指正,碼字不易,若是以爲寫的不錯,求關注、求點贊、求轉發。
掃碼關注公衆號,第一時間得到更新
代碼:https://github.com/jiyuanwai/logging-extend
原文出處:https://www.cnblogs.com/xuningfans/p/12202726.html