五年Java經驗,面試仍是說不出日誌該怎麼寫更好?——日誌規範與最佳實踐篇

本文是一個系列,歡迎關注html

查看上一篇文章能夠掃描文章下方的二維碼,點擊往期回顧-日誌系列便可查看全部相關文章java

概覽

上一篇咱們討論了爲何要使用日誌框架,此次咱們深刻問題的根源,爲何咱們須要日誌?git

大多數開發人員會糾結日誌該怎麼輸出,何時輸出,輸出了會不會有人看等問題,讓咱們跳出開發人員的侷限來考慮這個問題:誰須要日誌?日誌有幾種?日誌都須要輸出什麼?如何輸出日誌?github

誰須要日誌?

  • 開發者 開發人員在開發過程當中須要輸出一些變量方便調試,正確的作法是使用日誌來輸出(使用 System.out來輸出,一不當心發佈到線上,會被項目經理痛批);其次線上問題很難重放,用戶的表述通常都會失真,何況不少用戶發現 bug 就刪 app 關網頁走人了
  • 運維人員 整個系統大部分時間都是運維人員來維護,日誌能夠幫助運維人員來了解系統狀態(不少運維繫統接入的也是日誌),運維人員發現日誌有異常信息也能夠及時通知開發來排查
  • 運營人員 沒錯,就是運營人員,好比電商的轉化率、視頻網站的完播率、普通PV數據等均可以經過日誌進行統計,隨着大數據技術的普及,這部分日誌佔比也愈來愈高
  • 安全人員 雖然大多數企業不重視安全,可是安全也能夠經過日誌來進行預警,好比某個用戶忽然大額轉帳、再好比數據庫忽然出現大量無條件分頁查庫(拖庫)等等

日誌有幾種?

  • 調試日誌 用於開發人員開發或者線上回溯問題。
  • 診斷日誌 通常用於運維人員監控系統與安全人員分析預警。
  • 埋點日誌 通常用於運營決策分析,也有用做微服務調用鏈路追蹤的(運維、調試)。
  • 審計日誌 與診斷日誌相似,診斷日誌偏向運維,審計日誌偏向安全。

日誌都須要輸出什麼?

注:日誌級別會在下面講解sql

  • 調試日誌
    • DEBUG 或者 TRACE 級別,好比方法調用參數,網絡鏈接具體信息,通常是開發者調試程序使用,線上非特殊狀況關閉這些日誌
    • INFO 級別,通常是比較重要卻沒有風險的信息,如初始化環境、參數,清理環境,定時任務執行,遠程調用第一次鏈接成功
    • WARN 級別,有可能有風險又不影響系統繼續執行的錯誤,好比系統參數配置不正確,用戶請求的參數不正確(要輸出具體參數方便排查),或者某些耗性能的場景,好比一次請求執行過久、一條sql執行超過兩秒,某些第三方調用失敗,不太可能被運行的if分支等
    • ERROR 級別,用於程序出錯打印堆棧信息,不該該用於輸出程序問題以外的其餘信息,須要注意打印了日誌異常(Exception)就不該該拋(throw)了
  • 診斷日誌 通常輸出 INFO 級別,請求響應時間,內存佔用等等,線上接入監控系統時打開,建議輸出到獨立的文件,能夠考慮 JSON 格式方便外部工具分析
  • 埋點日誌 業務按需定製,好比上文提到的轉化率能夠在用戶付款時輸出日誌,完播率能夠在用戶播放完成後請求一次後臺輸出日誌,通常可輸出 INFO 級別,建議輸出到獨立的文件,能夠考慮JSON格式方便外部工具分析
  • 審計日誌 大多 WARN 級別或者 INFO 級別,通常是敏感操做便可輸出,登錄、轉帳付款、受權消權、刪除等等,建議輸出到獨立的文件,能夠考慮JSON格式方便外部工具分析

通常調試日誌由開發者自定義輸出,其餘三種應該根據實際業務需求來定製。數據庫

日誌的其餘注意點

  1. 線上日誌應儘可能謹慎,要思考:這個位置輸出日誌能幫助排除問題嗎?輸出的信息與排查問題相關嗎?輸出的信息足夠排除問題嗎?作到很多輸出必要信息,很少輸出無用信息(拖慢系統,淹沒有用信息)
  2. 超級 SessionId 與 RequestId,不管是單體應用仍是微服務架構,應該爲每一個用戶每次登錄生成一個超級 SessionId,方便跟蹤區分一個用戶;RequestId,每次請求生成一個 RequestId,用於跟蹤一次請求,微服務也能夠用於鏈路追蹤
  3. 日誌要儘可能單行輸出,一條日誌輸出一行,不然不方便閱讀以及其餘第三方系統或者工具分析
  4. 公司內部應該制定一套通用的日誌規範,包括日誌的格式,變量名(駝峯、下劃線),分隔符(「=」或「:」等),什麼時候輸出(好比規定調用第三方先後輸出INFO日誌),公司的日誌規範應該不斷優化、調整,找到適合公司業務的最佳規範

OK,理論就聊到這裏,接下來讓咱們回到技術層面。apache

使用概念

若是要想要學會使用日誌框架,先要理解幾個簡單概念,LoggerAppendersLayout日誌級別級別繼承(Level Inheritance)json

Logger(日誌實例)

用於輸出日誌,調用一次org.slf4j.LoggerFactory#getLogger(java.lang.Class<?>)org.slf4j.LoggerFactory#getLogger(java.lang.String)就會產生一個日誌實例,相同參數會共用同一個實例。安全

Appenders

日誌輸出器,logback 預約義了輸出到控制檯、文件、Socket 服務器、MySQL、PostgreSQL、Oracle 和其餘數據庫、JMS 和 UNIX Syslog 系統調用等實現,經過配置文件配置便可使用,固然咱們經常使用的只有控制檯和文件兩種。服務器

Layout

用於控制日誌輸出格式,前文所說的」自動輸出日誌相關信息,如:日期、線程、方法名稱等等「就能夠用 Layout 來控制,實際使用很簡單,寫一個 Layout 格式定義表達式(pattern)便可,使用方法相似於Java 的SimpleDateFomat

日誌級別

RFC 5424 (page 11)規定了 8 種日誌級別,可是SLF4j 只定義了 5 種日誌級別,分別是 ERROR、WARN、INFO、DEBUG、TRACE 這五個級別從高到低,配置級別越高日誌輸出就越少,以下圖

日誌級別

咱們看到滑動條上五個點正好對應五個級別,滑動指示器能夠左右移動,指示器做爲分界點,指示器左側均可以輸出,右側都不能輸出,左右調整指示器就能夠調整日誌的輸出,滑倒右側就能夠所有輸出,滑倒左側就能夠減小輸出,那麼是否可以完全關閉輸出呢?答案是能夠的,配置文件中還能夠配置爲 ALL 與 OFF,分別對應全部(等價於TRACE)與關閉。

級別繼承

理解了日誌級別,讓咱們來考慮兩個場景:

  • 某些重要業務輸出 INFO 級別,其餘業務輸出WARN級別的日誌,同時關閉全部庫、框架的日誌

有需求就會有解決方案,其實很簡單,logback 與 log4j 都支持按照日誌實例來配置,如今問題解決了,可是新的問題又來了,若是線上全部日誌都輸出 INFO 級別,難道要一個一個配置嗎?這時候就就要請出咱們上面所提到的級別繼承,若是 Java 同樣,logback 與 log4j 中也都是單根繼承模型,Java 中是 Object,日誌中是 ROOT,以下圖:

日誌繼承關係示例

有了繼承機制,咱們只須要將 ROOT 調整到 INFO 級別,再按照需求細化調整咱們業務對應的 logger 實例級別便可知足絕大多數場景。

Codding 實戰

問:把大象裝冰箱分幾步?分三步:一、引入依賴,二、編碼輸出日誌,三、調整配置文件。前文已經講過步驟一,若是沒有看過的讀者請移步公衆號查看往期回顧,這裏直接進入步驟二。

步驟二

若是項目中使用了Lombok,那麼能夠直接在類上面加@Slf4j註解既可得到日誌實例,不然可使用static final org.slf4j.Logger logger = LoggerFactory.getLogger(TestLog.class);來獲取日誌實例

具體日誌輸出方法以下:

logger.trace("A TRACE Message");
logger.debug("A DEBUG Message");
logger.info("An INFO Message");
logger.warn("A WARN Message");
logger.error("An ERROR Message");

這裏有個注意點,儘可能使用參數佔位而不要手動拼接字符串,以下

String level = "Trace";
// 反例
logger.trace("A " + level + " Message");
// 正確的作法
logger.trace("A {} Message", level);

這樣作能夠提升效率,若是不輸出日誌,第一種狀況也會拼接字符串形成性能損耗,第二種就不會有此問題(阿里巴巴Java開發手冊(華山版)這裏表述有問題,佔位符效率更高是由於儘可能延遲進行字符串處理,若是不須要輸出的日誌就不處理了,下一篇原理分析會展開),另外咱們也不須要if (logger.isTraceEnabled())來進行判斷了(性能損耗不高,可是代碼好看多了)。

步驟三

配置文件須要區分 logback 與 log4j2,兩種框架在配置文件上有差異但很類似,來看具體配置文件。

logback 配置文件位置

lobback 支持 xml 與 groovy 腳本兩種配置方式,logback 查找配置文件位置規則以下(後續文章會講如何修改位置)

  1. logback 嘗試在類路徑中找到一個名爲 logback-test.xml 的文件。
  2. 若是找不到此類文件,則 logback 會嘗試在類路徑中找到名爲 logback.groovy 的文件。
  3. 若是找不到這樣的文件,它將在類路徑中檢查文件 logback.xml。
  4. 若是找不到此類文件,則經過查找文件 META-INF\services\ch.qos.logback.classic.spi.Configurator,若是有這個文件且內容是 com.qos.logback.classic.spi.Configurator 實現類的全類名,直接加載這個實現類。
  5. 若是以上方法均不能成功執行,則 logback 會使用 BasicConfigurator 自動進行自我配置,會將日誌輸出到控制檯。

這段長長的文字其實不用看,咱們就把logback.xml放入Classpath根目錄就能夠了。。

logback 配置文件編寫規則

logback 配置文件主要分爲三類,一個或多個 Appender,用於定義輸出位置(不一樣文件位置,或者網絡又或者數據庫);一個或多個 Logger,用於細化配置不一樣 logger 的輸出級別以及位置;一個 ROOT,是一個特殊的logger,用於配置根 Logger。

日誌配配置文件格式

咱們一塊兒來看下面的配置文件實例

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="false" debug="false">

    <!-- 定義日誌文件的存儲地 -->
    <property name="LOG_PATH" value="/var/log"/>

    <property name="CONSOLE_LOG_PATTERN"
              value="%d{HH:mm:ss.SSS} %-5level [%10.10thread] %-30.30logger{29}\(%4L\\) - %msg%n"/>
    <!-- 控制檯輸出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>utf-8</charset>
        </encoder>
    </appender>

    <!-- 文件日誌格式(打印日誌,不打印行號) -->
    <property name="FILE_LOG_PATTERN"
              value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%10.10thread] %-30.30logger{29} - %msg%n"/>

    <appender name="FILE_ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在記錄的日誌文件的路徑及文件名 -->
        <file>${LOG_PATH}/log.log</file>
        <!-- 日誌記錄器的滾動策略,按日期,按大小記錄 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- yyyy-MM-dd 按日滾動 -->
            <fileNamePattern>${LOG_PATH}/log-%d{yyyy-MM-dd}.%i.log</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>

    <!-- 日誌輸出級別 STDOUT:控制檯;FILE_ALL:文件 -->
    <root level="warn">
        <appender-ref ref="STDOUT"/>
    </root>
    <logger name="druid.sql" level="warn" additivity="true"/>
    <logger name="druid.sql.ResultSet" level="warn" additivity="true"/>
    <logger name="com.alibaba.druid.pool.DruidDataSource" level="debug" additivity="true">
        <appender-ref ref="FILE_ALL"/>
    </logger>
</configuration>

上面配置文件定義了兩個 Appender,一個輸出控制檯,另外一個輸出到文件而且自動滾動。需注意的是property標籤至關於定義一個變量,可使用${xxx}進行引用,CONSOLE_LOG_PATTERN 與 FILE_LOG_PATTERN 定義了控制檯與文件打印格式,具體編寫方式相似於 Java 的SimpleDateFomat就不在此展開了,具體能夠參考

log4j2 配置文件位置

log4j2 支持 XML、JSON、YAML 或者 properties 格式的配置文件,具體查找方式以下:

  1. 檢查「 log4j.configurationFile」系統屬性,若是有,嘗試使用與文件擴展名匹配的ConfigurationFactory加載配置。
  2. 若是未設置系統屬性,則屬性 ConfigurationFactory 將在類路徑中查找 log4j2-test.properties。
  3. 若是找不到此類文件,則 YAML ConfigurationFactory將在類路徑中查找 log4j2-test.yaml或log4j2-test.yml。
  4. 若是找不到此類文件,則 JSON ConfigurationFactory 將在類路徑中查找 log4j2-test.json或log4j2-test.jsn。
  5. 若是找不到這樣的文件,XML ConfigurationFactory 將在類路徑中查找 log4j2-test.xml。
  6. 若是找不到測試文件,則屬性 ConfigurationFactory 將在類路徑上查找 log4j2.properties。
  7. 若是找不到屬性文件,則 YAML ConfigurationFactory 將在類路徑上查找 log4j2.yaml或log4j2.yml。
  8. 若是沒法找到 YAML 文件,則 JSON ConfigurationFactory 將在類路徑上查找 log4j2.json或log4j2.jsn。
  9. 若是沒法找到 JSON 文件,則 XML ConfigurationFactory 將嘗試在類路徑上找到 log4j2.xml。
  10. 若是找不到配置文件,使用 DefaultConfiguration 自動配置,將日誌輸出到控制檯。

這段更長的文字固然也不用看,咱們就把 log4j2.xml 放入 Classpath 根目錄就能夠了

log4j2 配置文件編寫

log4j 也是 Logger 與 Appender 配置項,也有一個ROOT的特殊 Logger,Appender 比logback支持更多輸出位置,如kafka、Cassandra、Flume等。

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="debug" strict="true">
    <!--  定義變量,能夠被${xxx}引用  -->
    <Properties>
        <Property name="baseDir">logs</Property>
    </Properties>

    <!--  定義 Appenders 用來指定輸出位置 -->
    <Appenders>
        <!-- 日誌滾動 $${date:yyyy-MM}:按月滾動文件夾 按小時、文件序號滾動,每次滾動都使用gz壓縮 -->
        <RollingFile name="RollingFile" fileName="${baseDir}/log.log"
                     filePattern="${baseDir}/$${date:yyyy-MM}/log-%d{yyyy-MM-dd-HH}-%i.log.gz">
            <!-- 日誌格式 -->
            <PatternLayout pattern="%d %p %c{1.} [%t] %m%n"/>
            <Policies>
                <!-- 時間滾動(按月滾動目錄,按小時滾動文件) -->
                <TimeBasedTriggeringPolicy/>
                <!-- 文件大小滾動(1小時內超過250M,強制滾動一次) -->
                <SizeBasedTriggeringPolicy size="250 MB"/>
            </Policies>
            <!-- 天天最多100個文件 -->
            <DefaultRolloverStrategy max="100">
                <!-- 刪除策略,超過三十天刪除,若是總文件小於100G,文件數量小於10個,則不會被刪除 -->
                <Delete basePath="${baseDir}" maxDepth="2">
                    <IfFileName glob="*/app-*.log.gz">
                        <IfLastModified age="30d">
                            <IfAny>
                                <IfAccumulatedFileSize exceeds="100 GB"/>
                                <IfAccumulatedFileCount exceeds="10"/>
                            </IfAny>
                        </IfLastModified>
                    </IfFileName>
                </Delete>
            </DefaultRolloverStrategy>
        </RollingFile>
    </Appenders>

    <Loggers>
        <!-- 多個logger -->
        <Logger name="org.apache.logging.log4j.test2" level="debug" additivity="false">
            <AppenderRef ref="RollingFile"/>
        </Logger>

        <!-- 一個ROOT -->
        <Root level="trace">
            <AppenderRef ref="STDOUT"/>
        </Root>
    </Loggers>
</Configuration>

能夠看得出 log4j2 與 logback 配置文件書寫大同小異,甚至一樣須要注意additivity="true"時致使的日誌重複輸出問題,畢竟 log4j1 與 logback 都是 Ceki大神都做品。

總結

得益於 Ceki 大佬的努力,日誌使用幾乎沒有有差別(Logback 與 Log4j2,Google 於 2018年4月開源了流式(fluent)日誌框架 Flogger,Slf4j 也將在2.0版本支持,而 Log4j2 再次落後,不過筆者認爲log4j2更強大,更多內容請關注下一篇文章)。關於日誌如何輸出本人也是經驗之談,免不了紕漏,歡迎補充指正,另外每一個公司都有不一樣的應用場景,具體應該遵照公司統一規範。

本篇更多傾向基礎使用,接下來的文章將展開對比、原理以及擴展日誌框架,敬請各位期待。

本文相關代碼

若是以爲寫的不錯,求關注、求點贊、求轉發,若是有問題或者文中有錯誤,歡迎留言討論。

掃碼關注公衆號,第一時間得到更新

掃碼關注

參考

https://logging.apache.org/log4j/2.x/manual/configuration.html

http://logback.qos.ch/manual/configuration.html

相關文章
相關標籤/搜索