Spring Boot 中使用 LogBack 配置

LogBack是一個日誌框架,它與Log4j能夠說是同出一源,都出自Ceki Gülcü之手。(log4j的原型是早前由Ceki Gülcü貢獻給Apache基金會的)下載地址 logback.qos.ch/download.ht…html

LogBack、Slf4j和Log4j之間的關係

Slf4jThe Simple Logging Facade for Java的簡稱,是一個簡單日誌門面抽象框架,它自己只提供了日誌Facade API和一個簡單的日誌類實現,通常常配合Log4j,LogBack,java.util.logging使用。Slf4j做爲應用層的Log接入時,程序能夠根據實際應用場景動態調整底層的日誌實現框架(Log4j/LogBack/JdkLog…)java

LogBackLog4j都是開源日記工具庫,LogBackLog4j的改良版本,比Log4j擁有更多的特性,同時也帶來很大性能提高。詳細數據可參照下面地址:Reasons to prefer logback over log4jgit

LogBack官方建議配合Slf4j使用,這樣能夠靈活地替換底層日誌框架。github

TIPS:爲了優化log4j,以及更大性能的提高,Apache基金會已經着手開發了log4j 2.0, 其中也借鑑和吸取了logback的一些先進特性,目前log4j2還處於beta階段spring

logback取代log4j的理由

一、更快的實現:Logback的內核重寫了,在一些關鍵執行路徑上性能提高10倍以上。並且logback不只性能提高了,初始化內存加載也更小了。
二、很是充分的測試:Logback通過了幾年,數不清小時的測試。Logback的測試徹底不一樣級別的。
三、Logback-classic很是天然實現了SLF4j:Logback-classic實現了SLF4j。在使用SLF4j中,你都感受不到logback-classic。並且由於logback-classic很是天然地實現了slf4j , 所 以切換到log4j或者其餘,很是容易,只須要提供成另外一個jar包就OK,根本不須要去動那些經過SLF4JAPI實現的代碼。
四、很是充分的文檔 官方網站有兩百多頁的文檔。
五、自動從新加載配置文件,當配置文件修改了,Logback-classic能自動從新加載配置文件。掃描過程快且安全,它並不須要另外建立一個掃描線程。這個技術充分保證了應用程序能跑得很歡在JEE環境裏面。
六、Lilithlog事件的觀察者,和log4jchainsaw相似。而lilith還能處理大數量的log數據 。
七、謹慎的模式和很是友好的恢復,在謹慎模式下,多個FileAppender實例跑在多個JVM下,能 夠安全地寫道同一個日誌文件。RollingFileAppender會有些限制。LogbackFileAppender和它的子類包括 RollingFileAppender可以很是友好地從I/O異常中恢復。
八、配置文件能夠處理不一樣的狀況,開發人員常常須要判斷不一樣的Logback配置文件在不一樣的環境下(開發,測試,生產)。而這些配置文件僅僅只有一些很小的不一樣,能夠經過,和來實現,這樣一個配置文件就能夠適應多個環境。
九、Filters(過濾器)有些時候,須要診斷一個問題,須要打出日誌。在log4j,只有下降日誌級別,不過這樣會打出大量的日誌,會影響應用性能。在Logback,你能夠繼續 保持那個日誌級別而除掉某種特殊狀況,如alice這個用戶登陸,她的日誌將打在DEBUG級別而其餘用戶能夠繼續打在WARN級別。要實現這個功能只需加4行XML配置。能夠參考MDCFIlter
十、SiftingAppender(一個很是多功能的Appender):它能夠用來分割日誌文件根據任何一個給定的運行參數。如,SiftingAppender可以區別日誌事件跟進用戶的Session,而後每一個用戶會有一個日誌文件。
十一、自動壓縮已經打出來的log:RollingFileAppender在產生新文件的時候,會自動壓縮已經打出來的日誌文件。壓縮是個異步過程,因此甚至對於大的日誌文件,在壓縮過程當中應用不會受任何影響。
十二、堆棧樹帶有包版本:Logback在打出堆棧樹日誌時,會帶上包的數據。
1三、自動去除舊的日誌文件:經過設置TimeBasedRollingPolicy或者SizeAndTimeBasedFNATP的maxHistory屬性,你能夠控制已經產生日誌文件的最大數量。若是設置maxHistory 12,那那些log文件超過12個月的都會被自動移除。apache

LogBack的結構

LogBack被分爲3個組件,logback-core, logback-classiclogback-accessjson

logback-core提供了LogBack的核心功能,是另外兩個組件的基礎。瀏覽器

logback-classic則實現了Slf4jAPI,因此當想配合Slf4j使用時,須要將logback-classic加入classpath安全

logback-access是爲了集成Servlet環境而準備的,可提供HTTP-access的日誌接口。springboot

配置詳解

Github 代碼

代碼我已放到 Github ,導入spring-boot-logback 項目

github spring-boot-logback

Maven依賴

假如maven依賴中添加了spring-boot-starter-logging

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-logging</artifactId>
</dependency>
複製代碼

那麼,咱們的Spring Boot應用將自動使用logback做爲應用日誌框架,Spring Boot啓動的時候,由org.springframework.boot.logging.Logging-Application-Listener根據狀況初始化並使用。

可是呢,實際開發中咱們不須要直接添加該依賴,你會發現spring-boot-starter其中包含了 spring-boot-starter-logging,該依賴內容就是 Spring Boot 默認的日誌框架 logback

依賴關係

配置文件

配置文件 logback-spring.xml

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

    <!-- 日誌根目錄-->
    <springProperty scope="context" name="LOG_HOME" source="logging.path" defaultValue="/data/logs/spring-boot-logback"/>

    <!-- 日誌級別 -->
    <springProperty scope="context" name="LOG_ROOT_LEVEL" source="logging.level.root" defaultValue="DEBUG"/>

    <!--  標識這個"STDOUT" 將會添加到這個logger -->
    <springProperty scope="context" name="STDOUT" source="log.stdout" defaultValue="STDOUT"/>

    <!-- 日誌文件名稱-->
    <property name="LOG_PREFIX" value="spring-boot-logback" />

    <!-- 日誌文件編碼-->
    <property name="LOG_CHARSET" value="UTF-8" />

    <!-- 日誌文件路徑+日期-->
    <property name="LOG_DIR" value="${LOG_HOME}/%d{yyyyMMdd}" />

    <!--對日誌進行格式化-->
    <property name="LOG_MSG" value="- | [%X{requestUUID}] | [%d{yyyyMMdd HH:mm:ss.SSS}] | [%level] | [${HOSTNAME}] | [%thread] | [%logger{36}] | --> %msg|%n "/>

    <!--文件大小,默認10MB-->
    <property name="MAX_FILE_SIZE" value="50MB" />

    <!-- 配置日誌的滾動時間 ,表示只保留最近 10 天的日誌-->
    <property name="MAX_HISTORY" value="10"/>

    <!--輸出到控制檯-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 輸出的日誌內容格式化-->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>${LOG_MSG}</pattern>
        </layout>
    </appender>

    <!--輸出到文件-->
    <appender name="0" class="ch.qos.logback.core.rolling.RollingFileAppender">
    </appender>

    <!-- 定義 ALL 日誌的輸出方式:-->
    <appender name="FILE_ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--日誌文件路徑,日誌文件名稱-->
        <File>${LOG_HOME}/all_${LOG_PREFIX}.log</File>

        <!-- 設置滾動策略,當天的日誌大小超過 ${MAX_FILE_SIZE} 文件大小時候,新的內容寫入新的文件, 默認10MB -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

            <!--日誌文件路徑,新的 ALL 日誌文件名稱,「 i 」 是個變量 -->
            <FileNamePattern>${LOG_DIR}/all_${LOG_PREFIX}%i.log</FileNamePattern>

            <!-- 配置日誌的滾動時間 ,表示只保留最近 10 天的日誌-->
            <MaxHistory>${MAX_HISTORY}</MaxHistory>

            <!--當天的日誌大小超過 ${MAX_FILE_SIZE} 文件大小時候,新的內容寫入新的文件, 默認10MB-->
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>

        </rollingPolicy>

        <!-- 輸出的日誌內容格式化-->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>${LOG_MSG}</pattern>
        </layout>
    </appender>

    <!-- 定義 ERROR 日誌的輸出方式:-->
    <appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 下面爲配置只輸出error級別的日誌 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <OnMismatch>DENY</OnMismatch>
            <OnMatch>ACCEPT</OnMatch>
        </filter>
        <!--日誌文件路徑,日誌文件名稱-->
        <File>${LOG_HOME}/err_${LOG_PREFIX}.log</File>

        <!-- 設置滾動策略,當天的日誌大小超過 ${MAX_FILE_SIZE} 文件大小時候,新的內容寫入新的文件, 默認10MB -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

            <!--日誌文件路徑,新的 ERR 日誌文件名稱,「 i 」 是個變量 -->
            <FileNamePattern>${LOG_DIR}/err_${LOG_PREFIX}%i.log</FileNamePattern>

            <!-- 配置日誌的滾動時間 ,表示只保留最近 10 天的日誌-->
            <MaxHistory>${MAX_HISTORY}</MaxHistory>

            <!--當天的日誌大小超過 ${MAX_FILE_SIZE} 文件大小時候,新的內容寫入新的文件, 默認10MB-->
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>

        <!-- 輸出的日誌內容格式化-->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>${LOG_MSG}</Pattern>
        </layout>
    </appender>

    <!-- additivity 設爲false,則logger內容不附加至root ,配置以配置包下的全部類的日誌的打印,級別是 ERROR-->

    <logger name="org.springframework"     level="ERROR" />
    <logger name="org.apache.commons"      level="ERROR" />
    <logger name="org.apache.zookeeper"    level="ERROR"  />
    <logger name="com.alibaba.dubbo.monitor" level="ERROR"/>
    <logger name="com.alibaba.dubbo.remoting" level="ERROR" />

    <!-- ${LOG_ROOT_LEVEL} 日誌級別 -->
    <root level="${LOG_ROOT_LEVEL}">

        <!-- 標識這個"${STDOUT}"將會添加到這個logger -->
        <appender-ref ref="${STDOUT}"/>

        <!-- FILE_ALL 日誌輸出添加到 logger -->
        <appender-ref ref="FILE_ALL"/>

        <!-- FILE_ERROR 日誌輸出添加到 logger -->
        <appender-ref ref="FILE_ERROR"/>
    </root>

</configuration>
複製代碼

配置文件 application.properties

#日誌級別從低到高分爲TRACE < DEBUG < INFO < WARN < ERROR < FATAL,若是設置爲WARN,則低於WARN的信息都不會輸出
logging.level.root=trace

logging.path=/data/logs/spring-boot-logback
複製代碼

節點介紹

這裏參考,嘟嘟獨立博客,和 Albin 的文章

Spring Boot乾貨系列:(七)默認日誌logback配置解析

logback節點配置詳解

日誌會天天新建一個文件夾,日文文件配置的每50兆,一個文本文件,超過新寫入一個

文件夾:20171031
文件夾內容:all_spring-boot-logback0.log 
文件夾內容:all_spring-boot-logback1.log
文件夾內容:all_spring-boot-logback2.log

文件夾內容:err_spring-boot-logback0.log
複製代碼

MDC requestUUID

一種多線程下日誌管理實踐方式

logback MDC(Mapped Diagnostic Context)與分佈式系統的跟蹤系統

Slf4j MDC 使用和 基於 Logback 的實現分析

MDC介紹 -- 一種多線程下日誌管理實踐方式

MDC(Mapped Diagnostic Context,映射調試上下文)是 log4j 和 logback 提供的一種方便在多線程條件下記錄日誌的功能。某些應用程序採用多線程的方式來處理多個用戶的請求。在一個用戶的使用過程當中,可能有多個不一樣的線程來進行處理。典型的例子是 Web 應用服務器。當用戶訪問某個頁面時,應用服務器可能會建立一個新的線程來處理該請求,也可能從線程池中複用已有的線程。在一個用戶的會話存續期間,可能有多個線程處理過該用戶的請求。這使得比較難以區分不一樣用戶所對應的日誌。當須要追蹤某個用戶在系統中的相關日誌記錄時,就會變得很麻煩。

一種解決的辦法是採用自定義的日誌格式,把用戶的信息採用某種方式編碼在日誌記錄中。這種方式的問題在於要求在每一個使用日誌記錄器的類中,均可以訪問到用戶相關的信息。這樣纔可能在記錄日誌時使用。這樣的條件一般是比較難以知足的。MDC 的做用是解決這個問題。

MDC 能夠當作是一個與當前線程綁定的哈希表,能夠往其中添加鍵值對。MDC 中包含的內容能夠被同一線程中執行的代碼所訪問。當前線程的子線程會繼承其父線程中的 MDC 的內容。當須要記錄日誌時,只須要從 MDC 中獲取所需的信息便可。MDC 的內容則由程序在適當的時候保存進去。對於一個 Web 應用來講,一般是在請求被處理的最開始保存這些數據。

自定義攔截器 logback requestUUID

/** * 描述: 自定義攔截器 logback requestUUID * * @author yanpenglei * @create 2017-10-30 16:15 **/

public class ControllerInterceptor extends HandlerInterceptorAdapter {

    private Logger LOGGER = LoggerFactory.getLogger(ControllerInterceptor.class);

    //在請求處理以前回調方法
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String requestUUID = MDC.get("requestUUID");
        if (requestUUID == null || "".equals(requestUUID)) {
            String uuid = UUID.randomUUID().toString();
            uuid = uuid.replaceAll("-", "").toUpperCase();
            MDC.put("requestUUID", uuid);
            LOGGER.info("ControllerInterceptor preHandle 在請求處理以前生成 logback requestUUID:{}", uuid);
        }

        return true;// 只有返回true纔會繼續向下執行,返回false取消當前請求
    }

    //請求處理以後回調方法
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        /* 線程結束後須要清除,不然當前線程會一直佔用這個requestId值 */
        MDC.remove("requestUUID");
        LOGGER.info("ControllerInterceptor postHandle 請求處理以後清除 logback MDC requestUUID");
    }

    //整個請求處理完畢回調方法
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        /*整個請求線程結束後須要清除,不然當前線程會一直佔用這個requestId值 */
        MDC.clear();
        LOGGER.info("ControllerInterceptor afterCompletion 整個請求處理完畢清除 logback MDC requestUUID");
    }
}
複製代碼

對日誌進行格式化,時候用到

<property name="LOG_MSG" value="- | [%X{requestUUID}] | [%d{yyyyMMdd HH:mm:ss.SSS}] | [%level] | [${HOSTNAME}] | [%thread] | [%logger{36}] | --> %msg|%n "/>
複製代碼
/** * 描述:攔截器配置 * * @author yanpenglei * @create 2017-10-30 16:54 **/
@Configuration
public class MyWebMvcConfigurer extends WebMvcConfigurerAdapter {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        /** * 多個攔截器組成一個攔截器鏈 * addPathPatterns 用於添加攔截規則 * excludePathPatterns 用於排除攔截 */
        registry.addInterceptor(new ControllerInterceptor()).addPathPatterns("/**");
        super.addInterceptors(registry);
    }
}
複製代碼

日誌切面

@Aspect
@Component
public class LogAspect {

    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
    private static final String dateFormat = "yyyy-MM-dd HH:mm:ss";
    private static final String STRING_START = "\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n";
    private static final String STRING_END   = "\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n";

    @Pointcut("execution(* io.ymq.logback.controller..*(..))")
    public void serviceLog() {
    }

    @Around("serviceLog()")
    public Object around(ProceedingJoinPoint joinPoint) {
        try {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            Class<?> targetClass = method.getDeclaringClass();

            StringBuffer classAndMethod = new StringBuffer();

            Log classAnnotation = targetClass.getAnnotation(Log.class);
            Log methodAnnotation = method.getAnnotation(Log.class);

            if (classAnnotation != null) {
                if (classAnnotation.ignore()) {
                    return joinPoint.proceed();
                }
                classAndMethod.append(classAnnotation.value()).append("-");
            }

            if (methodAnnotation != null) {
                if (methodAnnotation.ignore()) {
                    return joinPoint.proceed();
                }
                classAndMethod.append(methodAnnotation.value());
            }

            String target = targetClass.getName() + "#" + method.getName();
            String params = JSONObject.toJSONStringWithDateFormat(joinPoint.getArgs(), dateFormat, SerializerFeature.WriteMapNullValue);

            log.info(STRING_START + "{} 開始調用--> {} 參數:{}", classAndMethod.toString(), target, params);

            long start = System.currentTimeMillis();
            Object result = joinPoint.proceed();
            long timeConsuming = System.currentTimeMillis() - start;

            log.info("\n{} 調用結束<-- {} 返回值:{} 耗時:{}ms" + STRING_END, classAndMethod.toString(), target, JSONObject.toJSONStringWithDateFormat(result, dateFormat, SerializerFeature.WriteMapNullValue), timeConsuming);
            return result;
        } catch (Throwable throwable) {
            log.error(throwable.getMessage(), throwable);
        }
        return null;
    }

}
複製代碼

測試 logback

瀏覽器訪問:http://127.0.0.1:8080/index/?content="我是測試內容"

@RestController
@RequestMapping(value = "/index")
public class IndexController {

    private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    /** * http://127.0.0.1:8080/index/?content="我是測試內容" * * @param content * @return */
    @Log("首頁IndexController")
    @RequestMapping(value="", method= RequestMethod.GET)
    public String index(@RequestParam String content) {
        LocalDateTime localDateTime = LocalDateTime.now();

        LOGGER.trace("請求參數:content:{}", content);
        LOGGER.debug("請求參數:content:{}", content);
        LOGGER.info("請求參數:content:{}", content);
        LOGGER.warn("請求參數:content:{}", content);
        LOGGER.error("請求參數:content:{}", content);

        return localDateTime + ",content:" + content;
    }
}
複製代碼

前面的07E94BA525CF4C97851E4B9E4ABB4890 就是經過logback 的 MDC 作到的

首頁IndexController 開始調用--> io.ymq.logback.controller.IndexController#index 參數:["\"我是測試內容\""]|
- | [07E94BA525CF4C97851E4B9E4ABB4890] | [20171101 10:02:35.589] | [DEBUG] | [DESKTOP-VG43S0C] | [http-nio-8080-exec-1] | [i.y.l.controller.IndexController] | --> 請求參數:content:"我是測試內容"|
- | [07E94BA525CF4C97851E4B9E4ABB4890] | [20171101 10:02:35.589] | [INFO] | [DESKTOP-VG43S0C] | [http-nio-8080-exec-1] | [i.y.l.controller.IndexController] | --> 請求參數:content:"我是測試內容"|
- | [07E94BA525CF4C97851E4B9E4ABB4890] | [20171101 10:02:35.589] | [WARN] | [DESKTOP-VG43S0C] | [http-nio-8080-exec-1] | [i.y.l.controller.IndexController] | --> 請求參數:content:"我是測試內容"|
- | [07E94BA525CF4C97851E4B9E4ABB4890] | [20171101 10:02:35.590] | [ERROR] | [DESKTOP-VG43S0C] | [http-nio-8080-exec-1] | [i.y.l.controller.IndexController] | --> 請求參數:content:"我是測試內容"|
- | [07E94BA525CF4C97851E4B9E4ABB4890] | [20171101 10:02:35.606] | [INFO] | [DESKTOP-VG43S0C] | [http-nio-8080-exec-1] | [i.y.logback.config.commons.LogAspect] | --> 
首頁IndexController 調用結束<-- io.ymq.logback.controller.IndexController#index 返回值:"2017-11-01T10:02:35.589,content:\"我是測試內容\"" 耗時:23ms
複製代碼

從上圖能夠看到,日誌輸出內容元素具體以下:

requestUUID:一次請求是惟一的
時間日期:精確到毫秒
日誌級別:ERROR, WARN, INFO, DEBUG or TRACE
主機名:
進程ID:
類路徑:
分隔符: --> 標識實際日誌的開始
日誌內容:

日誌切面的響應:

首頁IndexController 開始調用--> io.ymq.logback.controller.IndexController#index 參數:["\"我是測試內容\""]|
首頁IndexController 調用結束<-- io.ymq.logback.controller.IndexController#index 返回值:"2017-11-01T10:02:35.589,content:\"我是測試內容\"" 耗時:23ms
複製代碼

代碼我已放到 Github ,導入spring-boot-logback 項目

github spring-boot-logback

slf4j-logback 日誌以json格式導入ELK

Contact

  • 做者:鵬磊
  • 出處:www.ymq.io
  • Email:admin@souyunku.com
  • 版權歸做者全部,轉載請註明出處
  • Wechat:關注公衆號,搜雲庫,專一於開發技術的研究與知識分享

關注公衆號-搜雲庫
搜雲庫
相關文章
相關標籤/搜索