springboot項目中日誌使用

1、爲什麼加日誌

1.1 日誌是什麼?

日誌文件提供精確的系統記錄,根據日誌最終定位到錯誤詳情和根源。日誌的特點是,它描述一些離散的(不連續的)事件。例如:應用通過一個滾動的文件輸出 INFO 或 ERROR 信息,並通過日誌收集系統,存儲到一些存儲引擎(Elasticsearch)中方便查詢。

1.2 日誌作用

  • 打印調試:即可以用日誌來記錄變量或者某一段邏輯。記錄程序運行的流程,即程序運行了哪些代碼,方便排查邏輯問題。

  • 問題定位:程序出異常或者出故障時快速的定位問題,方便後期解決問題。因爲線上生產環境無法 debug,在測試環境去模擬一套生產環境,費時費力。所以依靠日誌記錄的信息定位問題,這點非常重要。還可以記錄流量,後期可以通過 ELK(包括 EFK 進行流量統計)。

  • 用戶行爲日誌:記錄用戶的操作行爲,用於大數據分析,比如監控、風控、推薦等等。這種日誌,一般是給其他團隊分析使用,而且可能是多個團隊,因此一般會有一定的格式要求,開發者應該按照這個格式來記錄,便於其他團隊的使用。當然,要記錄哪些行爲、操作,一般也是約定好的,因此,開發者主要是執行的角色。

  • 根因分析(甩鍋必備*):即在關鍵地方記錄日誌。方便在和各個終端定位問題時,別人說時你的程序問題,你可以理直氣壯的拿出你的日誌說,看,我這裏運行了,狀態也是對的。這樣,對方就會乖乖去定位他的代碼,而不是互相推脫。

1.3 什麼時候記錄日誌

  • 系統初始化:系統或者服務的啓動參數。核心模塊或者組件初始化過程中往往依賴一些關鍵配置,根據參數不同會提供不一樣的服務。務必在這裏記錄 INFO 日誌,打印出參數以及啓動完成態服務表述。

  • 編程語言提示異常:如今各類主流的編程語言都包括異常機制,業務相關的流行框架有完整的異常模塊。這類捕獲的異常是系統告知開發人員需要加以關注的,是質量非常高的報錯。應當適當記錄日誌,根據實際結合業務的情況使用 WARN 或者 ERROR 級別。

  • 業務流程預期不符:除開平臺以及編程語言異常之外,項目代碼中結果與期望不符時也是日誌場景之一,簡單來說所有流程分支都可以加入考慮。取決於開發人員判斷能否容忍情形發生。常見的合適場景包括外部參數不正確,數據處理問題導致返回碼不在合理範圍內等等。

  • 系統核心角色,組件關鍵動作:系統中核心角色觸發的業務動作是需要多加關注的,是衡量系統正常運行的重要指標,建議記錄 INFO 級別日誌,比如電商系統用戶從登錄到下單的整個流程;微服務各服務節點交互;核心數據表增刪改;核心組件運行等等,如果日誌頻度高或者打印量特別大,可以提煉關鍵點 INFO 記錄,其餘酌情考慮 DEBUG 級別。

  • 第三方服務遠程調用:微服務架構體系中有一個重要的點就是第三方永遠不可信,對於第三方服務遠程調用建議打印請求和響應的參數,方便在和各個終端定位問題,不會因爲第三方服務日誌的缺失變得手足無措。

2、日誌框架

SpringBoot工程自帶logback和slf4j的依賴
Slf4j 英文全稱爲 “ Simple Logging Facade for Java ”,爲 Java 提供的簡單日誌門面。Facade 門面,更底層一點說就是接口。它允許用戶以自己的喜好,在工程中通過 Slf4j 接入不同的日誌系統。
Logback 是 Slf4j 的原生實現框架,同樣也是出自 Log4j 一個人之手,但擁有比 Log4j 更多的優點、特性和更做強的性能,Logback 相對於 Log4j 擁有更快的執行速度。基於我們先前在 Log4j 上的工作,Logback 重寫了內部的實現,在某些特定的場景上面,甚至可以比之前的速度快上 10 倍。在保證 Logback 的組件更加快速的同時,同時所需的內存更加少。

配置

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <property resource="logback.properties"/>

	<!--appender通過使用該標籤指定日誌的收集策略-->
	<!--name屬性指定appender命名-->
	<!-- class屬性指定輸出策略,通常有兩種,控制檯輸出和文件輸出-->
    <appender name="CONSOLE-LOG" class="ch.qos.logback.core.ConsoleAppender">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>[%d{yyyy-MM-dd' 'HH:mm:ss.sss}] [%C] [%t] [%L] [%-5p] %m%n</pattern>
        </layout>
    </appender>
    <!--獲取比info級別高(包括info級別)但除error級別的日誌-->
    <appender name="INFO-LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
    	<!--filter標籤,通過使用該標籤指定過濾策略-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
        	<!--level標籤指定過濾的類型-->
            <level>ERROR</level>
            <onMatch>DENY</onMatch>
            <onMismatch>ACCEPT</onMismatch>
        </filter>
        <!--encoder標籤,使用該標籤下的標籤指定日誌輸出格式-->
        <encoder>
            <pattern>[%d{yyyy-MM-dd' 'HH:mm:ss.sss}] [%C] [%t] [%L] [%-5p] %m%n</pattern>
        </encoder>

        <!--滾動策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--路徑-->
            <fileNamePattern>${LOG_INFO_HOME}//%d.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
    </appender>
    <appender name="ERROR-LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
        <encoder>
            <pattern>[%d{yyyy-MM-dd' 'HH:mm:ss.sss}] [%C] [%t] [%L] [%-5p] %m%n</pattern>
        </encoder>
        <!--rollingPolicy標籤指定收集策略,比如基於時間進行收集-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--fileNamePattern標籤指定生成日誌保存路徑-->
            <fileNamePattern>${LOG_ERROR_HOME}//%d.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
    </appender>
    
	 <!--必填標籤,用來指定最基礎的日誌輸出級別-->
    <root level="info">
		<!--添加append-->
        <appender-ref ref="CONSOLE-LOG" />
        <appender-ref ref="INFO-LOG" />
        <appender-ref ref="ERROR-LOG" />
    </root>
</configuration>

異步日誌輸出

之前的日誌配置方式是基於同步的,每次日誌輸出到文件都會進行一次磁盤IO。採用異步寫日誌的方式而不讓此次寫日誌發生磁盤IO,阻塞線程從而造成不必要的性能損耗。異步輸出日誌的方式很簡單,添加一個基於異步寫日誌的appender,並指向原先配置的appender即可

<!-- 異步輸出 -->
    <appender name="ASYNC-INFO" class="ch.qos.logback.classic.AsyncAppender">
        <!-- 不丟失日誌.默認的,如果隊列的80%已滿,則會丟棄TRACT、DEBUG、INFO級別的日誌 -->
        <discardingThreshold>0</discardingThreshold>
        <!-- 更改默認的隊列的深度,該值會影響性能.默認值爲256 -->
        <queueSize>256</queueSize>
        <!-- 添加附加的appender,最多隻能添加一個 -->
        <appender-ref ref="INFO-LOG"/>
    </appender>

    <appender name="ASYNC-ERROR" class="ch.qos.logback.classic.AsyncAppender">
        <!-- 不丟失日誌.默認的,如果隊列的80%已滿,則會丟棄TRACT、DEBUG、INFO級別的日誌 -->
        <discardingThreshold>0</discardingThreshold>
        <!-- 更改默認的隊列的深度,該值會影響性能.默認值爲256 -->
        <queueSize>256</queueSize>
        <!-- 添加附加的appender,最多隻能添加一個 -->
        <appender-ref ref="ERROR-LOG"/>
    </appender>

多環境下的日誌配置

logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
    <!--
        簡要描述
        日誌格式 => %d{HH:mm:ss.SSS}(時間) [%-5level](日誌級別) %logger{36}(logger名字最長36個字符,否則按照句點分割) - %msg%n(具體日誌信息並且換行)

        開發環境 => ${basepackage}包下控制檯打印DEBUG級別及以上、其他包控制檯打印INFO級別及以上
        演示(測試)環境 => ${basepackage}包下控制檯打印INFO級別及以上、其他包控制檯以及文件打印WARN級別及以上
        生產環境 => 控制檯以及文件打印ERROR級別及以上

        日誌文件生成規則如下:
        文件生成目錄 => ${logdir}
        當日的log文件名稱 => ${appname}.log
        其他時候的log文件名稱 => ${appname}.%d{yyyy-MM-dd}.log
        日誌文件最大 => ${maxsize}
        最多保留 => ${maxdays}天
    -->
    <!--自定義參數 -->
    <!--用來指定日誌文件的上限大小,那麼到了這個值,就會刪除舊的日誌-->
    <property name="maxsize" value="30MB" />
    <!--只保留最近90天的日誌-->
    <property name="maxdays" value="90" />
    <!--application.yml 傳遞參數 -->
    <!--log文件生成目錄-->
    <springProperty scope="context" name="logdir" source="resources.logdir"/>
    <!--應用名稱-->
    <springProperty scope="context" name="appname" source="resources.appname"/>
    <!--項目基礎包-->
    <springProperty scope="context" name="basepackage" source="resources.basepackage"/>

    <!--輸出到控制檯 ConsoleAppender-->
    <appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
        <!--展示格式 layout-->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>
                <pattern>%d{HH:mm:ss.SSS} [%-5level] %logger{36} - %msg%n</pattern>
            </pattern>
        </layout>
    </appender>
    <!--輸出到文件 FileAppender-->
    <appender name="fileLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--
            日誌名稱,如果沒有File 屬性,那麼只會使用FileNamePattern的文件路徑規則
            如果同時有<File>和<FileNamePattern>,那麼當天日誌是<File>,明天會自動把今天
            的日誌改名爲今天的日期。即,<File> 的日誌都是當天的。
        -->
        <File>${logdir}/${appname}.log</File>
        <!--滾動策略,按照時間滾動 TimeBasedRollingPolicy-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--文件路徑,定義了日誌的切分方式——把每一天的日誌歸檔到一個文件中,以防止日誌填滿整個磁盤空間-->
            <FileNamePattern>${logdir}/${appname}.%d{yyyy-MM-dd}.log</FileNamePattern>
            <maxHistory>${maxdays}</maxHistory>
            <totalSizeCap>${maxsize}</totalSizeCap>
        </rollingPolicy>
        <!--日誌輸出編碼格式化-->
        <encoder>
            <charset>UTF-8</charset>
            <pattern>%d{HH:mm:ss.SSS} [%-5level] %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 開發環境-->
    <springProfile name="dev">
        <root level="INFO">
            <appender-ref ref="consoleLog"/>
        </root>
        <!--
            additivity是子Logger 是否繼承 父Logger 的 輸出源(appender) 的標誌位
            在這裏additivity配置爲false代表如果${basepackage}中有INFO級別日誌則子looger打印 root不打印
        -->
        <logger name="${basepackage}" level="DEBUG" additivity="false">
            <appender-ref ref="consoleLog"/>
        </logger>
    </springProfile>

    <!-- 演示(測試)環境-->
    <springProfile name="test">
        <root level="WARN">
            <appender-ref ref="consoleLog"/>
            <appender-ref ref="fileLog"/>
        </root>
        <logger name="${basepackage}" level="INFO" additivity="false">
            <appender-ref ref="consoleLog"/>
            <appender-ref ref="fileLog"/>
        </logger>
    </springProfile>

    <!-- 生產環境 -->
    <springProfile name="prod">
        <root level="ERROR">
            <appender-ref ref="consoleLog"/>
            <appender-ref ref="fileLog"/>
        </root>
    </springProfile>
</configuration>

application.yml

#應用配置
resources:
    # log文件寫入地址
    logdir: logs/
    # 應用名稱
    appname: spring-boot-example
    # 日誌打印的基礎掃描包
    basepackage: com.spring.demo.springbootexample

使用不同環境啓動測試logger配置是否生效,在開發環境下將打印DEBUG級別以上的四條logger記錄,在演示環境下降打印INFO級別以上的三條記錄並寫入文件,在生產環境下只打印ERROR級別以上的一條記錄並寫入文件

  @RequestMapping("/logger")
    @ResponseBody
    public WebResult logger() {
        logger.trace("日誌輸出 {}", "trace");
        logger.debug("日誌輸出 {}", "debug");
        logger.info("日誌輸出 {}", "info");
        logger.warn("日誌輸出 {}", "warn");
        logger.error("日誌輸出 {}", "error");
        return "00";
    }

切面日誌處理

  • 定義註解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface OutputLog {
    boolean value() default true;
}

  • 切面類
@Aspect
@Component
public class LoggerAspect {

    private static final Logger logger = LoggerFactory.getLogger(LoggerAspect.class);

    @Pointcut("@annotation(com.link.springmvc.logAop.OutputLog)")
    public void weblog(){
    }

    @Around("weblog()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        long beginTime = System.currentTimeMillis();
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        List<Object> logArgs = Arrays.stream(point.getArgs())
                .filter(arg -> (!(arg instanceof HttpServletRequest) && !(arg instanceof HttpServletResponse)))
                .collect(Collectors.toList());
        try {
            logger.info("請求url={}, 請求參數={}", request.getRequestURI(), JSON.toJSONString(logArgs));
        } catch (Exception e) {
            logger.error("請求參數獲取異常", e);
        }
        Object result = point.proceed();
        //執行時長(毫秒)
        long time = System.currentTimeMillis() - beginTime;
        try {
            logger.info("請求耗時={}ms, 返回結果={}", time, JSON.toJSONString(result));
        } catch (Exception e) {
            logger.error("返回參數獲取異常", e);
        }
        return result;
    }

}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章