SpringBoot 日志管理之自定義Appender


前言
前面兩章節我們介紹了一些日志框架的常見配置及使用實踐。一般上,在開發過程中,像log4j2、logback日志框架都提供了很多Appender,基本上可以滿足大部分的業務需求了。但在一些特殊需求或者需要將日志進行集中管理(集群部署時,日志是分拆到不同服務器上的,不可能去每一台服務器上去下載文件的,也不便於日志檢索)時,就需要自定義Appender,將日志集中輸出或者其他一些特殊需求。所以本章節就來簡單介紹下關於log4j2和logback的自定義Appender知識。

一點知識
log4j2自帶Appender
logback自帶Appender
自定義Appender
log4j2自定義Appender
logback自定義Appender
關於ShutdownHook
一點知識
參考資料
總結
最后
老生常談
一點知識
編寫自定義Appender時,我們先來看看log4j2和logback自帶了哪些Appender,了解下是否可以滿足我們的個性化需求,避免重復制造輪子。

log4j2自帶Appender
先看一張官網提供的Appender說明:

 

 

 

名稱 描述
AsyncAppender 使用一個單獨線程記錄日志,實現異步處理日志事件。
CassandraAppender 將日志信息輸出到一個Apache的Cassandra數據庫
ConsoleAppender 將日志信息輸出到控制台
FailoverAppender 包含其他appenders,按順序嘗試,直至成功或結尾
FileAppender 一個OutputStreamAppender,將日志輸出到文件
FlumeAppender 將日志輸出到Apache Flume系統
JDBCAppender 將日志通過JDBC輸出到關系型數據庫
JMS Appender 將日志輸出到JMS(Java Message Service)
JPAAppender 將日志輸出到JPA框架
HttpAppender 通過HTTP輸出日志
KafkaAppender 將日志輸出到Apache Kafka
MemoryMappedFileAppender 將日志輸出到一塊文件關聯的內存
OutputStreamAppender 將日志輸出到一個OutputStream
RandomAccessFileAppender 性能比FileAppender高20%~200%的文件輸出Appender
RewriteAppender 允許對日志信息進行加工
RollingFileAppender 按log文件最大長度限度生成新文件
RollingRandomAccessFA 添加了緩存的RollingFileAppender
RoutingAppender 將日志事件分類,按條件分配給子appender
SMTPAppender 將日志輸出到郵件
SocketAppender 將日志輸出到一個Socket
SyslogAppender 是一個SocketAppender,將日志輸出到遠程系統日志
ZeroMQ/JeroMQ Appender 使用JeroMQ庫將日志輸出到ZeroMQ終端
基本上已經覆蓋了百分之九十的業務場景了。相關的詳細說明或者配置大家自行搜索或者查看官網說明。
官網地址:http://logging.apache.org/log4j/2.x/manual/appenders.html

logback自帶Appender
和log4j2一樣,自帶的都差不多了。

名稱 描述
ConsoleAppender 將日志輸出到控制台
FileAppender 將日志輸出到文件
RollingFileAppender 滾動文件生成,按條件生成不同文件,配合TriggeringPolicy使用
SocketAppender 輸出日志到遠程實例中,明文傳輸
SSLSocketAppender 輸出日志到遠程實例中,密文傳輸
SMTPAppender 將日志輸出到郵件
DBAppender 日志事件插入數據庫中,需要提前創建表
SyslogAppender 是一個SocketAppender,將日志輸出到遠程系統日志
SiftingAppender 可基於任何給定的實時屬性分開(或者篩選)日志,如基於用戶會話分開日志事件
AmqpAppender 將日志輸出到MQ服務中
具體可查看:https://blog.csdn.net/tianyaleixiaowu/article/details/73327752 很詳細!

或者查看官網:https://logback.qos.ch/manual/appenders.html

自定義Appender
自定義Appender時,可以按實現的功能,適當的繼承(log4j2的appender類基本上被設置成了final無法繼承)或者參考一些已有的功能,當然了也可以直接繼承其基類接口的。以下就簡單的示例下,沒有實現特定的功能,⊙﹏⊙‖∣

log4j2自定義Appender
按官網的擴展說明,我們來簡單實現一個appender。

 

 

 

官網地址:http://logging.apache.org/log4j/2.x/manual/extending.html#Appenders

0.編寫自定義appender類,繼承AbstractAppender抽象實現類:

MyLog4j2Appender.java

/**
* 自定義log4j2輸出源,簡單的輸出到控制台
* @author oKong
*
*/
//這里的 MyLog4j2 對應就是 xml中,
/**
*
* <appenders>
* <MyLog4j2 name="customAppender" printString="一枚趔趄的猿">
* </MyLog4j2>
* </appenders>
*
*/
@Plugin(name = "MyLog4j2", category = "Core", elementType = "appender", printObject = true)
public class MyLog4j2Appender extends AbstractAppender {

String printString;
/**
*構造函數 可自定義參數 這里直接傳入一個常量並輸出
*
*/
protected MyLog4j2Appender(String name, Filter filter, Layout<? extends Serializable> layout,String printString) {
super(name, filter, layout);
this.printString = printString;
}

@Override
public void append(LogEvent event) {
if (event != null && event.getMessage() != null) {
// 此處自定義實現輸出
// 獲取輸出值:event.getMessage().toString()
// System.out.print(event.getMessage().toString());
// 格式化輸出
System.out.print(printString + ":" + getLayout().toSerializable(event));
}

}

/** 接收配置文件中的參數
*
* @PluginAttribute 字面意思都知道,是xml節點的attribute值,如<oKong name="oKong"></oKong> 這里的name 就是 attribute
* @PluginElement:表示xml子節點的元素,
* 如
* <oKong name="oKong">
* <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
* </oKong>
* 其中,PatternLayout就是 的 Layout,其實就是{@link Layout}的實現類。
*/
@PluginFactory
public static MyLog4j2Appender createAppender(
@PluginAttribute("name") String name,
@PluginElement("Filter") final Filter filter,
@PluginElement("Layout") Layout<? extends Serializable> layout,
@PluginAttribute("printString") String printString) {

if (name == null) {
LOGGER.error("no name defined in conf.");
return null;
}
//默認使用 PatternLayout
if (layout == null) {
layout = PatternLayout.createDefaultLayout();
}

return new MyLog4j2Appender(name, filter, layout, printString);
}

@Override
public void start() {
System.out.println("log4j2-start方法被調用");
super.start();
}

@Override
public void stop() {
System.out.println("log4j2-stop方法被調用");
super.stop();
}
}

簡單說明下,相關注意點:
* @Plugin注解:這個注解,是為了在之后配置log4j2-spring.xml時,指定的Appender Tag。
* 構造函數:除了使用父類的以外,也可以增加一些自己的配置。
* 重寫append()方法:這里面需要實現具體的邏輯,日志的去向。
* createAppender()方法:主要是接收log4j2-spring.xml中的配置項。

1.使用自定義的appender。

log4j2-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration status="WARN" monitorInterval="30" packages="cn.lqdev.learning">
<!--定義appenders-->
<appenders>
<MyLog4j2 name="oKong" printString="一枚趔趄的猿(log4j2)">
<!--輸出日志的格式-->
<PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
</MyLog4j2>
</appenders>
<!--然后定義logger,只有定義了logger並引入的appender,appender才會生效-->
<loggers>
<!--過濾掉spring和mybatis的一些無用的DEBUG信息-->
<logger name="org.springframework" level="INFO"></logger>
<logger name="org.mybatis" level="INFO"></logger>
<!-- 自定義包下設置為INFO,則可以看見輸出的日志不包含debug輸出了 -->
<logger name="cn.lqdev.learning" level="INFO"/>
<root level="all">
<appender-ref ref="oKong"/>
</root>
</loggers>
</configuration>

這里需要注意,需要在configuration中,加入屬性packages為自定類所在包名cn.lqdev.learning才會被掃描生效,不知道是否還有其他方法。

2.啟動后,就可以看見相關輸出了。

...部分省略...
一枚趔趄的猿(log4j2):[14:47:43:751] [INFO] - org.apache.juli.logging.DirectJDKLog.log(DirectJDKLog.java:180) - Using a shared selector for servlet write/read
一枚趔趄的猿(log4j2):[14:47:43:761] [INFO] - org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer.start(TomcatEmbeddedServletContainer.java:216) - Tomcat started on port(s): 8080 (http)
一枚趔趄的猿(log4j2):[14:47:43:764] [INFO] - org.springframework.boot.StartupInfoLogger.logStarted(StartupInfoLogger.java:57) - Started Chapter25Application in 2.03 seconds (JVM running for 3.164)
一枚趔趄的猿(log4j2):[14:47:43:764] [INFO] - cn.lqdev.learning.springboot.chapter25.Chapter25Application.main(Chapter25Application.java:14) - Chapter25啟動!

不知道如何整合log4j2的,可以查看:《第二十三章:日志管理之整合篇》

logback自定義Appender
logback的自定義,也是類似的,都是基於一個基類appender來實現。本身logback提供了AppenderBase和UnsynchronizedAppenderBase兩個抽象類(同步和非同步),所以我們自定義時,只需要看實際業務繼承其中的一個即可。先看下其類繼承結構:

 

 

 

0.編寫自定義appender類。

MyLogbackAppender.java

@Getter
@Setter
public class MyLogbackAppender extends UnsynchronizedAppenderBase<ILoggingEvent>{

Layout<ILoggingEvent> layout;

//自定義配置
String printString;

 

@Override
public void start(){
//這里可以做些初始化判斷 比如layout不能為null ,
if(layout == null) {
addWarn("Layout was not defined");
}
//或者寫入數據庫 或者redis時 初始化連接等等
super.start();
}


@Override
public void stop()
{
//釋放相關資源,如數據庫連接,redis線程池等等
System.out.println("logback-stop方法被調用");
if(!isStarted()) {
return;
}
super.stop();
}

@Override
public void append(ILoggingEvent event) {
if (event == null || !isStarted()){
return;
}
// 此處自定義實現輸出
// 獲取輸出值:event.getFormattedMessage()
// System.out.print(event.getFormattedMessage());
// 格式化輸出
System.out.print(printString + ":" + layout.doLayout(event));

}
}

也簡單說明下,相關注意點:
* start方法:初始時調用。故在編寫如數據庫入庫,連接緩存或者mq時,可以在這個方法里面進行初始化操作。
* stop:當停止時,調用。可做些資源釋放操作。

1.使用自定義appender:

logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!--定義日志文件的存儲地址 勿在 LogBack 的配置中使用相對路徑 -->
<property name="LOG_HOME" value="/home" />
<!-- 控制台輸出 -->
<appender name="MyLogback"
class="cn.lqdev.learning.springboot.chapter25.config.MyLogbackAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!-- 日志收集最低日志級別 -->
<level>INFO</level>
</filter>
<layout
class="ch.qos.logback.classic.PatternLayout">
<!--格式化輸出:%d表示日期,%thread表示線程名,%-5level:級別從左顯示5個字符寬度%msg:日志消息,%n是換行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</layout>
<!-- 自定義參數 -->
<printString>一枚趔趄的猿(logback)</printString>
</appender>

<!-- 自定義包下設置為INFO,則可以看見輸出的日志不包含debug輸出了 -->
<logger name="cn.lqdev.learning" level="INFO" />

<!-- 日志輸出級別 -->
<root level="INFO">
<appender-ref ref="MyLogback" />
</root>

</configuration>

2.應用啟動,查看控制台輸出,效果是一樣的:

...部分省略...
一枚趔趄的猿(logback):2018-08-25 15:01:57.486 [main] INFO org.apache.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8080"]
一枚趔趄的猿(logback):2018-08-25 15:01:57.497 [main] INFO org.apache.tomcat.util.net.NioSelectorPool - Using a shared selector for servlet write/read
一枚趔趄的猿(logback):2018-08-25 15:01:57.520 [main] INFO o.s.b.c.e.tomcat.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
一枚趔趄的猿(logback):2018-08-25 15:01:57.523 [main] INFO c.l.l.springboot.chapter25.Chapter25Application - Started Chapter25Application in 54.349 seconds (JVM running for 55.377)
一枚趔趄的猿(logback):2018-08-25 15:01:57.524 [main] INFO c.l.l.springboot.chapter25.Chapter25Application - Chapter25啟動!

關於ShutdownHook
當你運行了以上的自定義appender后,停止應用時,你會發現定義的stop方法並沒有被執行。還需要配置一個ShutdownHook系統鈎子,使得在jvm在退出時之前會調用。

一點知識
我們知道,在java中,注冊一個關閉鈎子是很簡單的,使用Runtime類即可,具體用法如下:

Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {

@Override
public void run() {
// 執行資源釋放操作

}
}));

而在SpringBoot中,只需要配置logging.register-shutdown-hook為true即可。

logging.register-shutdown-hook=true

對於logback而言,也可以在logback-spring.xml中配置:

<shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/>
1
也是可以的。再或者在啟動類手動注冊這個DelayingShutdownHook也是可以的

這里有個坑,log4j2而言,配置失效了。谷歌了一圈也沒有發現解決方法,網上的方案試了一遍都是不行。。很尷尬。要是使用log4j2的話,可以取巧下,在start()方法里面,注冊鈎子之后調用stop方法。希望有知道的大神分享下如何解決!

參考資料
https://blog.csdn.net/zhoucheng05_13/article/details/78494458
http://logging.apache.org/log4j/2.x/manual/appenders.html
http://logging.apache.org/log4j/2.x/manual/extending.html#Appenders
https://logback.qos.ch/manual/appenders.html
https://blog.csdn.net/hupoling/article/details/75353854
總結
本文主要是簡單介紹了log4j2和logback自定義appender相關知識。實現起來是相對簡單的,需要注意當涉及需要關閉釋放相關資源時,需要確認下關閉前是否有被調用,不然可能造成連接未關閉等行為,避免不必要的問題。關於最后使用log4j2關閉鈎子未生效問題,由於現在都默認使用logback了,這個問題就不深究了,還望有知道的同學分享下解決方案!謝謝!同時由於沒有對兩個框架有過多的深入了解,只能點到為止了,若文中有誤,還望指出!

最后
目前互聯網上很多大佬都有SpringBoot系列教程,如有雷同,請多多包涵了。原創不易,碼字不易,還希望大家多多支持。若文中有所錯誤之處,還望提出,謝謝。
————————————————
版權聲明:本文為CSDN博主「趔趄的猿」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/xie19900123/article/details/82056732


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM