前言
前面兩章節我們介紹了一些日志框架的常見配置及使用實踐。一般上,在開發過程中,像
log4j2
、logback
日志框架都提供了很多Appender
,基本上可以滿足大部分的業務需求了。但在一些特殊需求或者需要將日志進行集中管理(集群部署時,日志是分拆到不同服務器上的,不可能去每一台服務器上去下載文件的,也不便於日志檢索)時,就需要自定義Appender
,將日志集中輸出或者其他一些特殊需求。所以本章節就來簡單介紹下關於log4j2
和logback
的自定義Appender
知識。
一點知識
編寫自定義
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"/>
也是可以的。再或者在啟動類手動注冊這個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
系列教程,如有雷同,請多多包涵了。原創不易,碼字不易,還希望大家多多支持。若文中有所錯誤之處,還望提出,謝謝。
老生常談
- 個人QQ:
499452441
- 公眾號:
lqdevOps
個人博客:http://blog.lqdev.cn
完整示例:https://github.com/xie19900123/spring-boot-learning/tree/master/chapter-25
原文地址:http://blog.lqdev.cn/2018/08/25/springboot/chapter-twenty-five/