SpringBoot基於Aop自定義Slf4j日誌輸出格式

需求

當線上服務或者接口出現異常之後,第一時間需要做的就是追蹤日誌,找出問題到底出現在哪裏,但是在現有的分佈式及微服務的背景下,一個請求的調用鏈往往比較的長,所以一般情況下會選擇使用一個請求的唯一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";
      }
  }

file


END!!!

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