SpringBoot基于Aop自定義Slf4j日志輸出格式

需求

當(dāng)線上服務(wù)或者接口出現(xiàn)異常之后抢呆,第一時(shí)間需要做的就是追蹤日志睦尽,找出問(wèn)題到底出現(xiàn)在哪里铁材,但是在現(xiàn)有的分布式及微服務(wù)的背景下,一個(gè)請(qǐng)求的調(diào)用鏈往往比較的長(zhǎng)惫叛,所以一般情況下會(huì)選擇使用一個(gè)請(qǐng)求的唯一ID輸出為日志倡勇,然后便于日常運(yùn)維過(guò)程的問(wèn)題追蹤,如何優(yōu)雅自如的自定義一個(gè)log輸出呢嘉涌?下面使用AOP加上logback來(lái)給一個(gè)簡(jiǎn)單優(yōu)雅的方式妻熊;解放雙手,告別體力活仑最。

Aop

這里不做AOP的介紹扔役。除了使用AOP也可以使用Filter去做,不管是AOP還是Filter警医,目的就是在請(qǐng)求來(lái)的時(shí)候?qū)⑵鋽r住亿胸,然后往MDC中塞入自定義的一一些屬性,即可實(shí)現(xiàn)自定義的變量輸出

何為MDC预皇?

這里的MDC就是一個(gè)工具類(lèi)侈玄,其本質(zhì)就是使用ThreadLocal將自定義的變量存儲(chǔ)起來(lái),這么一說(shuō)相信各位就知道這個(gè)自定義參數(shù)的套路了吟温;請(qǐng)求之前將請(qǐng)求攔截序仙,將自定義的屬性值存進(jìn)去;業(yè)務(wù)過(guò)程中鲁豪,如果打印日志潘悼,就將本地ThreadLocal中自定義的屬性一起輸出。其實(shí)原理就這么簡(jiǎn)單爬橡,具體要如何輸出挥等,要輸出什么,就得看你自己的騷操作了5涛病!迁客!

配置

  • logback-spring.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <configuration debug="true" scan="true" scanPeriod="30 seconds">
    
        <!--<springProperty scope="context" name="logLevel" source="log.level"/>-->
    
        <!--日志存放的路徑-->
        <springProperty scope="context" name="OPEN_FILE_PATH" source="log.path"/>
        <!--日志文件夾的名稱(chēng) 這里即為項(xiàng)目的name-->
        <springProperty scope="context" name="APP_NAME" source="spring.application.name"/>
    
        <!-- 文件輸出格式  可以使用 [%X{Key}] 進(jìn)行輸出的自定義 然后使用MDC.set(Key,"value") 設(shè)置對(duì)應(yīng)的值-->
        <property name="PATTERN"
                  value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{IP}] [%X{RequestId}] [%X{RequestURI}] [%thread] [%X{ThreadId}] %-5level %logger{36} - %msg%n"/>
        <!-- 輸出文件路徑 -->
        <!--<property name="OPEN_FILE_PATH" value="/logs"/>-->encoder
    
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>${PATTERN}</pattern>
                <charset>UTF-8</charset>
            </encoder>
        </appender>
    
        <!-- ch.qos.logback.core.rolling.RollingFileAppender 文件日志輸出 -->
        <appender name="OPEN-FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <!--不能有這項(xiàng)配置9Α4腔薄!U呈摇榄檬!-->
            <!--<Encoding>UTF-8</Encoding>-->
            <!--<File>${OPEN_FILE_PATH}/${APP_NAME}.log</File>-->
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <!--日志文件輸出的文件名-->
                <FileNamePattern>${OPEN_FILE_PATH}/${APP_NAME}/all/${APP_NAME}.%d{yyyy-MM-dd}-%i.log.zip</FileNamePattern>
                <!--日志文件保留天數(shù)-->
                <MaxHistory>30</MaxHistory>
                <totalSizeCap>10GB</totalSizeCap>
                <TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <!--日志文件最大的大小-->
                    <MaxFileSize>100MB</MaxFileSize>
                </TimeBasedFileNamingAndTriggeringPolicy>
            </rollingPolicy>
    
            <layout class="ch.qos.logback.classic.PatternLayout">
                <pattern>${PATTERN}</pattern>
            </layout>
        </appender>
    
        <!--輸出到debug-->
        <appender name="debug" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <FileNamePattern>${OPEN_FILE_PATH}/${APP_NAME}/debug/${APP_NAME}.%d{yyyy-MM-dd}-%i.log.zip</FileNamePattern>
                <MaxHistory>30</MaxHistory>
                <totalSizeCap>10GB</totalSizeCap>
                <TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <MaxFileSize>100MB</MaxFileSize>
                </TimeBasedFileNamingAndTriggeringPolicy>
            </rollingPolicy>
            <append>true</append>
            <encoder>
                <pattern>${PATTERN}</pattern>
                <charset>utf-8</charset>
            </encoder>
            <filter class="ch.qos.logback.classic.filter.LevelFilter"><!-- 只打印DEBUG日志 -->
                <level>DEBUG</level>
                <onMatch>ACCEPT</onMatch>
                <onMismatch>DENY</onMismatch>
            </filter>
        </appender>
    
        <!--輸出到info-->
        <appender name="info" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <FileNamePattern>${OPEN_FILE_PATH}/${APP_NAME}/info/${APP_NAME}.%d{yyyy-MM-dd}-%i.log.zip</FileNamePattern>
                <MaxHistory>30</MaxHistory>
                <totalSizeCap>10GB</totalSizeCap>
                <TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <MaxFileSize>100MB</MaxFileSize>
                </TimeBasedFileNamingAndTriggeringPolicy>
            </rollingPolicy>
            <append>true</append>
            <encoder>
                <pattern>${PATTERN}</pattern>
                <charset>utf-8</charset>
            </encoder>
            <filter class="ch.qos.logback.classic.filter.LevelFilter"><!-- 只打印INFO日志 -->
                <level>INFO</level>
                <onMatch>ACCEPT</onMatch>
                <onMismatch>DENY</onMismatch>
            </filter>
        </appender>
    
        <!--輸出到error-->
        <appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <FileNamePattern>${OPEN_FILE_PATH}/${APP_NAME}/error/${APP_NAME}.%d{yyyy-MM-dd}-%i.log.zip</FileNamePattern>
                <MaxHistory>30</MaxHistory>
                <totalSizeCap>10GB</totalSizeCap>
                <TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <MaxFileSize>100MB</MaxFileSize>
                </TimeBasedFileNamingAndTriggeringPolicy>
            </rollingPolicy>
            <append>true</append>
            <encoder>
                <pattern>${PATTERN}</pattern>
                <charset>utf-8</charset>
            </encoder>
            <filter class="ch.qos.logback.classic.filter.LevelFilter"><!-- 只打印ERROR日志 -->
                <level>ERROR</level>
                <onMatch>ACCEPT</onMatch>
                <onMismatch>DENY</onMismatch>
            </filter>
        </appender>
    
        <!--輸出到warn-->
        <appender name="warn" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <FileNamePattern>${OPEN_FILE_PATH}/${APP_NAME}/warn/${APP_NAME}.%d{yyyy-MM-dd}-%i.log.zip</FileNamePattern>
                <MaxHistory>30</MaxHistory>
                <totalSizeCap>10GB</totalSizeCap>
                <TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <MaxFileSize>100MB</MaxFileSize>
                </TimeBasedFileNamingAndTriggeringPolicy>
            </rollingPolicy>
            <append>true</append>
            <encoder>
                <pattern>${PATTERN}</pattern>
                <charset>utf-8</charset>
            </encoder>
            <filter class="ch.qos.logback.classic.filter.LevelFilter"><!-- 只打印WARN日志 -->
                <level>WARN</level>
                <onMatch>ACCEPT</onMatch>
                <onMismatch>DENY</onMismatch>
            </filter>
        </appender>
    
        <root level="info">
            <appender-ref ref="STDOUT"/>
            <appender-ref ref="OPEN-FILE"/>
            <appender-ref ref="debug"/>
            <appender-ref ref="info"/>
            <appender-ref ref="error"/>
            <appender-ref ref="warn"/>
        </root>
    </configuration>
    
  • 要關(guān)注的配置
    // 將此日志拷貝到resources目錄下
    // 此文只需要關(guān)注下面這一行配置,其他的可以忽略不用看
    <property name="PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{IP}] [%X{RequestId}] [%X{RequestURI}] [%thread] [%X{ThreadId}] %-5level %logger{36} - %msg%n"/>
    // 
    //
    // [%X{IP}]  自定義的IP輸出
    // [%X{RequestId}] 自定義的請(qǐng)求唯一ID
    // [%X{RequestURI}] 自定義的請(qǐng)求地址輸出
    // [%X{ThreadId}] 自定義的線程Id的輸出
    // 這里可以根據(jù)自己的需要衔统,做任何自己想要的自定義參數(shù)配置
    

配置切面

  • 引入aop的jar
    <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    
  • 攔截所有的controller
    @Aspect
    @Component
    @Order(0) // 切面的順序鹿榜,越小越優(yōu)先,對(duì)于多個(gè)切面Spring是使用責(zé)任鏈的模式 為了一開(kāi)始將日志相關(guān)的參數(shù)初始化好锦爵,這里設(shè)置為最優(yōu)先執(zhí)行
    public class LogInfoInitAspect
    {
        // 請(qǐng)求唯一ID
        private final String RequestId = "RequestId";
        // 請(qǐng)求的地址
        private final String RequestURI = "RequestURI";
        // 請(qǐng)求的線程ID
        private final String ThreadId = "ThreadId";
        // 請(qǐng)求的IP
        private final String IP = "IP";
    
        // 這里最好使用環(huán)繞通知舱殿,在執(zhí)行完之后 將MDC中設(shè)置的值清空
        // 如果不使用環(huán)繞通知的話,可以使用Before設(shè)置值险掀;使用After來(lái)清除值
        // 意思是將com.你的包路徑.controller目錄下以Controller結(jié)尾類(lèi)的方法調(diào)用全部織入下面的代碼塊
        @Around("within(com.你的包路徑.controller..*Controller)")
        public Object initLogInfoController(ProceedingJoinPoint joinPoint) throws Throwable
        {
            // 請(qǐng)求對(duì)象
            HttpServletRequest request = ((ServletRequestAttributes) getRequestAttributes()).getRequest();
            // 響應(yīng)對(duì)象
            HttpServletResponse response = ((ServletRequestAttributes) getRequestAttributes()).getResponse();
    
            // 獲取客戶端的IP
            String clientIP = getClientIP(request);
            if (StringUtils.isNotBlank(clientIP))
            {
                MDC.put(IP, clientIP);
            }
    
            // 獲取執(zhí)行當(dāng)前創(chuàng)作的 線程
            Thread thread = Thread.currentThread();
            // 設(shè)置線程ID
            MDC.put(ThreadId, String.valueOf(thread.getId()));
    
            // 獲取請(qǐng)求地址
            String requestURI = request.getRequestURI();
            // 設(shè)置請(qǐng)求地址
            MDC.put(RequestURI, requestURI);
    
            // 生成當(dāng)前請(qǐng)求的一個(gè)唯一UUID
            String requestId = UUID.randomUUID().toString();
            // 設(shè)置請(qǐng)求的唯一ID
            MDC.put(RequestId, requestId);
            // 將次唯一ID設(shè)置為響應(yīng)頭
            response.setHeader(RequestId, requestId);
    
            Object object = null;
            try
            {
                // 調(diào)用目標(biāo)方法
                object = joinPoint.proceed();
                return object;
            }
            catch (Throwable throwable)
            {
                throwable.printStackTrace();
                throw throwable;
            }
            finally
            {
                // 注意沪袭,這里一定要清理掉
                // 否則可能會(huì)出現(xiàn)OOM的情況
                MDC.clear();
            }
        }
    
        /**
         * 在request中獲取到客戶端的IP
         *
         * @param request
         * @return
         */
        public String getClientIP(HttpServletRequest request)
        {
            String ip = request.getHeader("x-forwarded-for");
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
            {
                ip = request.getHeader("Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
            {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
            {
                ip = request.getRemoteAddr();
            }
            return ip;
        }
    }
    
  • 測(cè)試
    @RestController
    @RequestMapping("/test")
    @Slf4j
    public class TestController
    {
    
        @GetMapping("/lt")
        public String logTest()
        {
            log.info("我是測(cè)試日志");
            return "1";
        }
    }
    
    file

END!U燎狻冈绊!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市埠啃,隨后出現(xiàn)的幾起案子死宣,更是在濱河造成了極大的恐慌,老刑警劉巖碴开,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毅该,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡叹螟,警方通過(guò)查閱死者的電腦和手機(jī)鹃骂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)罢绽,“玉大人畏线,你說(shuō)我怎么就攤上這事×技郏” “怎么了寝殴?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)明垢。 經(jīng)常有香客問(wèn)我蚣常,道長(zhǎng),這世上最難降的妖魔是什么痊银? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任抵蚊,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘贞绳。我一直安慰自己谷醉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布冈闭。 她就那樣靜靜地躺著俱尼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪萎攒。 梳的紋絲不亂的頭發(fā)上遇八,一...
    開(kāi)封第一講書(shū)人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音耍休,去河邊找鬼刃永。 笑死,一個(gè)胖子當(dāng)著我的面吹牛羹应,可吹牛的內(nèi)容都是我干的揽碘。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼园匹,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼雳刺!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起裸违,我...
    開(kāi)封第一講書(shū)人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤掖桦,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后供汛,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體枪汪,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年怔昨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了雀久。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡趁舀,死狀恐怖赖捌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情矮烹,我是刑警寧澤越庇,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站奉狈,受9級(jí)特大地震影響卤唉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜仁期,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一桑驱、第九天 我趴在偏房一處隱蔽的房頂上張望竭恬。 院中可真熱鬧,春花似錦熬的、人聲如沸萍聊。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至此衅,卻和暖如春强戴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背挡鞍。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工骑歹, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人墨微。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓道媚,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親翘县。 傳聞我的和親對(duì)象是個(gè)殘疾皇子最域,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容