先放個官方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就根據配置檔案生成了在項目中,檔案夾,檔案名,日期一樣不落
還有剩下半句配置,我一貼日志裡面的檔案内容,日期/等級啥的一一對應,本來{}裡面的翻譯也挺準确的
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
接下來寫資料庫部分了看到
<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有什麼講究嗎,去翻翻官方文檔(文章最上面那個連結)
仔細一看類名ConsoleAppender和FileAppender,就知道這兩個類是幹嘛的了,
在往上一級OutputStreamAppender,這個明顯就是IO流才用到的,pass
那我的自定義Appender咋辦,再往上啊 UnsynchronizedAppenderBase
這個在官方API文檔也有說明,但是英語渣渣的我,還是看看其他資料吧
在讀logback源碼系列文章(五)——Appender 中看到了不少東西,摘抄一下
實作Appender接口有2個base類,一個是AppenderBase,另一個是UnsynchronizedAppenderBase,這2個類非常接近,80%以上的代碼都是相同的。
如果我們自己要自定義Appender的話,隻要寫一個類繼承自這2個base類就好
下面看一個簡單的Appender,就是我自己寫的MyAppender
- public class MyAppender extends AppenderBase<LoggingEvent> {
- @Override
- protected void append(LoggingEvent eventObject) {
- System.out.println(eventObject.getMessage());
- }
- }
看完以後,哦,原來要重寫一下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();
}
}
}