需求
當(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燎狻冈绊!