天天看點

實戰分析:Spring boot java使用logback處理異常日志通過發送http請求,儲存到資料庫檔案中

先放個官方API文檔吧 https://logback.qos.ch/manual/appenders.html

言歸正傳,項目中有個功能:

線上項目一旦報錯,将報錯資訊通過http請求發送到自己部署開發環境伺服器項目中

将處理一下http請求資訊存起來,就是一個小型的異常資料庫,

當然也實作了開發環境查庫然後界面化,這個就是一個背景系統展現就不說了

主要想學習一下,如何将錯誤日志收集并發送,并加上自己的感悟和注釋記錄一下

ps:為了一些敏感的資訊吧 我就統一将所有敏感的替換的改為test  或者****号了  

在application.properties中

配置logging.level.*來具體輸出哪些包的日志級别
logging.level.root=info
logging.level.com.****.web=debug
#當然可以設定日志檔案輸出路徑以及檔案
logging.file=logs/web.log
           

2、Logback預設配置的步驟

     (1). 嘗試在 classpath 下查找檔案 logback-test.xml;

     (2). 如果檔案不存在,則查找檔案 logback.xml;

     (3). 如果兩個檔案都不存在,logback 用 Bas icConfigurator 自動對自己進行配置,這會導緻記錄輸出到控制台。

是以自己先寫一個logback.xml

<?xml version="1.0" encoding="UTF-8"?>
 <configuration debug="false">
    <!--讀取application.properties中的屬性  一會代碼用的到
        例如 test.logUrl=http://localhost:8098/-->
    <springProperty scope="context" name="logUrl" source="test.logUrl"/>
        
    <!-- 彩色日志 -->
    <!-- 彩色日志依賴的渲染類 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
    <!-- 彩色日志格式 -->
    <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" />

  

    <!-- Console 輸出設定 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>
    <!--logs檔案-->
	<appender name="FILEOUT" class="ch.qos.logback.core.rolling.RollingFileAppender">   
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">   
	      	<fileNamePattern>logs/web.%d{yyyy-MM-dd}.log</fileNamePattern>   
	      	<maxHistory>30</maxHistory>    
	    </rollingPolicy>
	    <encoder>
	       <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
	    </encoder>
    </appender>    
   

    <!-- 将日志寫入資料庫 -->
    <appender name="ASYNCOUT" class="ch.qos.logback.classic.AsyncAppender">
        <!-- 不丢失日志.預設的,如果隊列的80%已滿,則會丢棄TRACT、DEBUG、INFO級别的日志 -->
        <discardingThreshold>0</discardingThreshold>
        <!-- 更改預設的隊列的深度,該值會影響性能.預設值為256 -->
        <queueSize>8196</queueSize>
        <!-- 添加附加的appender,最多隻能添加一個 -->
        <appender-ref ref="DBOUT" />
    </appender>
    <appender name="DBOUT" class="com.*****.web.DbLoggerAppender">
        <!--這裡設定日志級别為error-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>error</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

  <!-- 日志輸出級别 -->
    <root level="INFO">
        <!--這裡就會依次根據以下三個,分别處理這個info日志-->
        <appender-ref ref="STDOUT" /><!--給控制台輸出-->
        <appender-ref ref="FILEOUT" /><!--給logs産生檔案輸出-->
        <appender-ref ref="ASYNCOUT" /><!--給專門的類  一會寫到資料庫-->
    </root>
</configuration>           

控制台輸出就是照舊輸出,會有特别整齊的格式和彩色配置,這個格式網上很多,就略過了

檔案logs就根據配置檔案生成了在項目中,檔案夾,檔案名,日期一樣不落

實戰分析:Spring boot java使用logback處理異常日志通過發送http請求,儲存到資料庫檔案中

還有剩下半句配置,我一貼日志裡面的檔案内容,日期/等級啥的一一對應,本來{}裡面的翻譯也挺準确的

<encoder>
	 <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>           
實戰分析:Spring boot java使用logback處理異常日志通過發送http請求,儲存到資料庫檔案中

接下來寫資料庫部分了看到

<appender name="ASYNCOUT" class="ch.qos.logback.classic.AsyncAppender">           

AsyncAppender這個類, logback進階特性使用(三)  裡面說的很詳細,文章說是以AsynAppender僅僅充當事件轉發器,必須引用另一個appender來做事。就是" <appender-ref ref="DBOUT" />"這句話,再接着DBOUT往裡面看看配置

對照看spring-boot-started-logging logback常用配置之<filter>标簽詳解,再詳細标注一下,這麼一看就明白了:

原來所有的error日志都讓filter過濾了給了appender指定的class類了(代碼中的DBloggerAppender)

<appender name="DBOUT" class="com.*****.web.DbLoggerAppender">
        <!--這裡設定日志級别為error-->
        <!--将過濾器的日志級别配置為INFO,所有INFO級别的日志交給appender中的class處理,非INFO級别的日志,被過濾掉。-->
        <!--LevelFilter: 級别過濾器,根據日志級别進行過濾。
        如果日志級别等于配置級别,過濾器會根據onMath 和 onMismatch接收或拒絕日志。有以下子節點:-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>error</level><!--<level>:設定過濾級别-->
            <onMatch>ACCEPT</onMatch><!--<onMatch>:用于配置符合過濾條件的操作-->
            <onMismatch>DENY</onMismatch><!--<onMismatch>:用于配置不符合過濾條件的操作-->
        </filter>
</appender>           

指定的這個自己寫的DBloggerAppender類,就是自定義Appender,

那這個Appender有什麼講究嗎,去翻翻官方文檔(文章最上面那個連結)

實戰分析:Spring boot java使用logback處理異常日志通過發送http請求,儲存到資料庫檔案中

仔細一看類名ConsoleAppender和FileAppender,就知道這兩個類是幹嘛的了,

在往上一級OutputStreamAppender,這個明顯就是IO流才用到的,pass

那我的自定義Appender咋辦,再往上啊 UnsynchronizedAppenderBase

這個在官方API文檔也有說明,但是英語渣渣的我,還是看看其他資料吧

在讀logback源碼系列文章(五)——Appender 中看到了不少東西,摘抄一下

實作Appender接口有2個base類,一個是AppenderBase,另一個是UnsynchronizedAppenderBase,這2個類非常接近,80%以上的代碼都是相同的。

如果我們自己要自定義Appender的話,隻要寫一個類繼承自這2個base類就好 

下面看一個簡單的Appender,就是我自己寫的MyAppender 
  1. public class MyAppender extends AppenderBase<LoggingEvent> {  
  2.     @Override  
  3.     protected void append(LoggingEvent eventObject) {  
  4.         System.out.println(eventObject.getMessage());  
  5.     }  
  6. }  

看完以後,哦,原來要重寫一下Append方法,再回過頭看項目DbLoggerAppender代碼,果不其然

public class DbLoggerAppender extends UnsynchronizedAppenderBase<LoggingEvent> {
    @Override
	protected void append(LoggingEvent le) {
           //業務處理
    }
}           

到了這一步,基本上就差不多明了日志如何實作自定義的,接下來業務方法具體實作,大部分都貼注釋吧.

看官到這裡就可以撤了,下面是具體的業務

我自己make學習一下,剖析一下代碼是怎麼寫的,

package com.****.web;

import java.net.InetAddress;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.databind.ObjectMapper;

import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.classic.spi.StackTraceElementProxy;
import ch.qos.logback.classic.spi.ThrowableProxy;
import ch.qos.logback.classic.spi.ThrowableProxyUtil;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.UnsynchronizedAppenderBase;

public class DbLoggerAppender extends UnsynchronizedAppenderBase<LoggingEvent> {

	@Override
	protected void append(LoggingEvent le) {
		
		try {
			/*
			這個和配置是一一對應的,貼上logback.xml的代碼

			<!--讀取application.properties中的屬性  一會代碼用的到
             test.logUrl=http://localhost:8098/-->
    		<springProperty scope="context" name="logUrl" source="test.logUrl"/>*/
			String logUrl = context.getProperty("logUrl") + "saveprogramlog";

            String content = le.getFormattedMessage();
			//異常内容,就是log.error("出錯", e);的内容
			//如果沒有就是以下内容了,例如(貼多點,友善大家了解)
			//{dataSource-11} init error
			//uriSessionMapFullCount is full
			//session ip change too many
			//Application run failed
			/*
			***************************
			APPLICATION FAILED TO START
			***************************

			Description:

			Web server failed to start. Port 8088 was already in use.

			Action:

			Identify and stop the process that's listening on port 8088 or configure this application to listen on another port.
			*/
			
			Map<String, Object> log = new HashMap<String, Object>();
			log.put("timestamp", le.getTimeStamp() );
			log.put("level", le.getLevel().levelStr);
			log.put("content", content);
			log.put("serviceName", "WEB");//給這個服務起個别名 Spring boot可是多服務的...
			log.put("serviceIP", InetAddress.getLocalHost().getHostAddress());//主機IP

			StringBuilder builder = new StringBuilder();
			//這個builder 得到的内容就是密密麻麻的報錯資訊  有縮進和換行的那種
			//具體實作我也不懂..希望有大拿評論貼連結或者剖析..
			IThrowableProxy thProxy = le.getThrowableProxy();
			while (thProxy != null) {
				builder.append(thProxy.getClassName() + ": " + thProxy.getMessage() );
				builder.append(CoreConstants.LINE_SEPARATOR);
				
		        for (StackTraceElementProxy step : le.getThrowableProxy().getStackTraceElementProxyArray()) {
		            String string = step.toString();
		            builder.append(CoreConstants.TAB).append(string);
		            ThrowableProxyUtil.subjoinPackagingData(builder, step);
		            builder.append(CoreConstants.LINE_SEPARATOR);
		        }
		        
		        thProxy = thProxy.getCause();
			}

	                log.put("cause", builder.toString());
	                //json序列化
			ObjectMapper om = new ObjectMapper();
			String logJson = om.writeValueAsString(log);
			
			HttpHeaders headers = new HttpHeaders();
			// 送出方式,大部分的情況下,送出方式都是表單送出
			headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

			MultiValueMap<String, String> map= new LinkedMultiValueMap<String, String>();
			map.add("log", logJson);

			HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(map, headers);

			RestTemplate restTemplate = new RestTemplate();
			restTemplate.postForEntity( logUrl, request , String.class );
			
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}


}
           

繼續閱讀