需求
當線上服務或者接口出現異常之後,第一時間需要做的就是追蹤日誌,找出問題到底出現在哪裏,但是在現有的分佈式及微服務的背景下,一個請求的調用鏈往往比較的長,所以一般情況下會選擇使用一個請求的唯一ID輸出爲日誌,然後便於日常運維過程的問題追蹤,如何優雅自如的自定義一個log輸出呢?下面使用AOP加上logback來給一個簡單優雅的方式;解放雙手,告別體力活。
Aop
這裏不做AOP的介紹。除了使用AOP也可以使用Filter去做,不管是AOP還是Filter,目的就是在請求來的時候將其攔住,然後往MDC中塞入自定義的一一些屬性,即可實現自定義的變量輸出
何爲MDC?
這裏的MDC就是一個工具類,其本質就是使用ThreadLocal
將自定義的變量存儲起來,這麼一說相信各位就知道這個自定義參數的套路了;請求之前將請求攔截,將自定義的屬性值存進去;業務過程中,如果打印日誌,就將本地ThreadLocal中自定義的屬性一起輸出。其實原理就這麼簡單,具體要如何輸出,要輸出什麼,就得看你自己的騷操作了!!!
配置
- 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"/>
<!--日誌文件夾的名稱 這裏即爲項目的name-->
<springProperty scope="context" name="APP_NAME" source="spring.application.name"/>
<!-- 文件輸出格式 可以使用 [%X{Key}] 進行輸出的自定義 然後使用MDC.set(Key,"value") 設置對應的值-->
<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">
<!--不能有這項配置!!!!!-->
<!--<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>
<!--日誌文件保留天數-->
<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>
- 要關注的配置
// 將此日誌拷貝到resources目錄下
// 此文只需要關注下面這一行配置,其他的可以忽略不用看
<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}] 自定義的請求唯一ID
// [%X{RequestURI}] 自定義的請求地址輸出
// [%X{ThreadId}] 自定義的線程Id的輸出
// 這裏可以根據自己的需要,做任何自己想要的自定義參數配置
配置切面
- 引入aop的jar
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 攔截所有的controller
@Aspect
@Component
@Order(0) // 切面的順序,越小越優先,對於多個切面Spring是使用責任鏈的模式 爲了一開始將日誌相關的參數初始化好,這裏設置爲最優先執行
public class LogInfoInitAspect
{
// 請求唯一ID
private final String RequestId = "RequestId";
// 請求的地址
private final String RequestURI = "RequestURI";
// 請求的線程ID
private final String ThreadId = "ThreadId";
// 請求的IP
private final String IP = "IP";
// 這裏最好使用環繞通知,在執行完之後 將MDC中設置的值清空
// 如果不使用環繞通知的話,可以使用Before設置值;使用After來清除值
// 意思是將com.你的包路徑.controller目錄下以Controller結尾類的方法調用全部織入下面的代碼塊
@Around("within(com.你的包路徑.controller..*Controller)")
public Object initLogInfoController(ProceedingJoinPoint joinPoint) throws Throwable
{
// 請求對象
HttpServletRequest request = ((ServletRequestAttributes) getRequestAttributes()).getRequest();
// 響應對象
HttpServletResponse response = ((ServletRequestAttributes) getRequestAttributes()).getResponse();
// 獲取客戶端的IP
String clientIP = getClientIP(request);
if (StringUtils.isNotBlank(clientIP))
{
MDC.put(IP, clientIP);
}
// 獲取執行當前創作的 線程
Thread thread = Thread.currentThread();
// 設置線程ID
MDC.put(ThreadId, String.valueOf(thread.getId()));
// 獲取請求地址
String requestURI = request.getRequestURI();
// 設置請求地址
MDC.put(RequestURI, requestURI);
// 生成當前請求的一個唯一UUID
String requestId = UUID.randomUUID().toString();
// 設置請求的唯一ID
MDC.put(RequestId, requestId);
// 將次唯一ID設置爲響應頭
response.setHeader(RequestId, requestId);
Object object = null;
try
{
// 調用目標方法
object = joinPoint.proceed();
return object;
}
catch (Throwable throwable)
{
throwable.printStackTrace();
throw throwable;
}
finally
{
// 注意,這裏一定要清理掉
// 否則可能會出現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;
}
}
- 測試
@RestController
@RequestMapping("/test")
@Slf4j
public class TestController
{
@GetMapping("/lt")
public String logTest()
{
log.info("我是測試日誌");
return "1";
}
}
END!!!