springboot2+logback將日誌輸出到oracle資料庫的踩坑之旅
背景
根據本人寫部落格的慣例,先交代下背景。在公司的系統中,我們的配置檔案是切分有好幾個的,不同的配置檔案裡面配置內容有著不同,對於日誌的輸出,也需要對不同的環境做出不同的輸出,這是一個前提,本文即將講述到的將日誌輸出到oracle資料庫就是分環境輸出的,本地測試的日誌是非常多的,服務也時常重啟,除錯等,因此本地環境的日誌不宜輸出到資料庫,而線上環境不同,線上環境的日誌輸出比本地要少很多,也不經常重啟服務。因此本文要講的內容有以下兩點:
- 如何在logback的配置檔案中配置不同的profile?
- 如何將日誌輸出到資料庫?這個過程遇到了什麼坑?怎麼解決的?
本文將在以下軟體版本中進行,不同版本是否存在差異尚未測試
springboot v2.1.0
logback v1.2.3
oracle 11.2
java 1.8.0_171
Logback的不同環境配置
在中國,但凡有什麼不懂的,第一個想到的就是百度,當然,本人第一反應也是先百度,果不其然,第一頁各種解決方案琳琅滿目,不過經過本人的實踐,得出以下比較有用的,也是本人最終採用的方案,各位看官如果看到這篇文章,可以直接參考。
網上的說法有很多,有些是定義多個logback的檔案,例如logback-dev.xml
和logback-prod.xml
等等諸如此類的,然後在application.yml
內進行根據不同的profiles.active
在同一個logback配置檔案中,根據不同的<springProfile>
來區分,具體可以類似以下的寫法:
<springProfile name="prod"> <root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="infoAppender"/> <appender-ref ref="errorAppender"/> <appender-ref ref="DbAppender"/> </root> </springProfile> <springProfile name="dev"> <root level="INFO"> <appender-ref ref="CONSOLE"/> <!--<appender-ref ref="infoAppender"/>--> <!--<appender-ref ref="errorAppender"/>--> <appender-ref ref="DbAppender"/> </root> </springProfile>
通過不同的<springProfile>
標籤來區分不同的環境使用那些appender,但是當本人在原來的logback.xml
中這樣配置的時候,並不起作用。打開了logback的日誌,發現如下的報錯
21:35:42,913 |-ERROR in ch.qos.logback.core.joran.spi.Interpreter@64:32 - no applicable action for [springProfile], current ElementPath is [[configuration][springProfile]]
21:35:42,913 |-ERROR in ch.qos.logback.core.joran.spi.Interpreter@65:28 - no applicable action for [root], current ElementPath is [[configuration][springProfile][root]]
21:35:42,913 |-ERROR in ch.qos.logback.core.joran.spi.Interpreter@66:42 - no applicable action for [appender-ref], current ElementPath is [[configuration][springProfile][root][appender-ref]]
21:35:42,914 |-ERROR in ch.qos.logback.core.joran.spi.Interpreter@67:47 - no applicable action for [appender-ref], current ElementPath is [[configuration][springProfile][root][appender-ref]]
21:35:42,914 |-ERROR in ch.qos.logback.core.joran.spi.Interpreter@68:48 - no applicable action for [appender-ref], current ElementPath is [[configuration][springProfile][root][appender-ref]]
21:35:42,914 |-ERROR in ch.qos.logback.core.joran.spi.Interpreter@69:45 - no applicable action for [appender-ref], current ElementPath is [[configuration][springProfile][root][appender-ref]]
21:35:42,914 |-ERROR in ch.qos.logback.core.joran.spi.Interpreter@72:31 - no applicable action for [springProfile], current ElementPath is [[configuration][springProfile]]
21:35:42,914 |-ERROR in ch.qos.logback.core.joran.spi.Interpreter@73:28 - no applicable action for [root], current ElementPath is [[configuration][springProfile][root]]
21:35:42,914 |-ERROR in ch.qos.logback.core.joran.spi.Interpreter@74:42 - no applicable action for [appender-ref], current ElementPath is [[configuration][springProfile][root][appender-ref]]
21:35:42,914 |-ERROR in ch.qos.logback.core.joran.spi.Interpreter@77:45 - no applicable action for [appender-ref], current ElementPath is [[configuration][springProfile][root][appender-ref]]
日誌的輸出也是按照預設的輸出,這是為什麼呢?正在本人百思不得其解的時候,突然靈機一動,下面開始敲黑板了!既然是<springProfile>
那麼是不是和spring有關的?如果spring的context沒有被載入,那麼怎麼知道到底是哪個profiles起作用呢?
對於這個問題,本人嘗試將logback.xml
替換成了logback-spring.xml
,重啟應用,發現可以通過。
logback.xml
在springboot中,是先於spring上下文
載入的,因此,在載入這個配置檔案時,還不知道到底採用哪個profile,也就是說<springProfile>
這個標籤並不會在這個階段起作用。logback-spring.xml
是先初始化spring上下文
,這個時候<springProfile>
才生效
Logback將日誌輸出到資料庫
我這裡採用的是oracle資料庫,因此針對oracle資料庫做一些簡單說明,並且指出所遇到的坑。
可能有朋友會問,我想將日誌輸出到資料庫,那麼資料庫的表我是要自己建嗎?要建成怎麼樣的呢?答案是不需要的,logback官方有提供有對應的sql指令碼,直接找到對應的資料庫指令碼進行建立即可,要不然你自己建的表,logback也不認識啊對吧。 logback資料庫指令碼
以下是針對oracle的資料庫指令碼
CREATE SEQUENCE logging_event_id_seq MINVALUE 1 START WITH 1;
CREATE TABLE logging_event
(
timestmp NUMBER(20) NOT NULL,
formatted_message VARCHAR2(4000) NOT NULL,
logger_name VARCHAR(254) NOT NULL,
level_string VARCHAR(254) NOT NULL,
thread_name VARCHAR(254),
reference_flag SMALLINT,
arg0 VARCHAR(254),
arg1 VARCHAR(254),
arg2 VARCHAR(254),
arg3 VARCHAR(254),
caller_filename VARCHAR(254) NOT NULL,
caller_class VARCHAR(254) NOT NULL,
caller_method VARCHAR(254) NOT NULL,
caller_line CHAR(4) NOT NULL,
event_id NUMBER(10) PRIMARY KEY
);
-- the / suffix may or may not be needed depending on your SQL Client
-- Some SQL Clients, e.g. SQuirrel SQL has trouble with the following
-- trigger creation command, while SQLPlus (the basic SQL Client which
-- ships with Oracle) has no trouble at all.
CREATE TRIGGER logging_event_id_seq_trig
BEFORE INSERT ON logging_event
FOR EACH ROW
BEGIN
SELECT logging_event_id_seq.NEXTVAL
INTO :NEW.event_id
FROM DUAL;
END;
/
CREATE TABLE logging_event_property
(
event_id NUMBER(10) NOT NULL,
mapped_key VARCHAR2(254) NOT NULL,
mapped_value VARCHAR2(1024),
PRIMARY KEY(event_id, mapped_key),
FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
);
CREATE TABLE logging_event_exception
(
event_id NUMBER(10) NOT NULL,
i SMALLINT NOT NULL,
trace_line VARCHAR2(254) NOT NULL,--# 此處需要注意
PRIMARY KEY(event_id, i),
FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
);
上述指令碼大體上並不會有什麼大的問題,但是要注意logging_event_exception
這個表的trace_line
這個欄位,在這裡是定義了254個長度,這個欄位主要是用來記錄異常堆疊的,一條堆疊對應一條記錄,那麼有時候堆疊的資訊遠遠不止254個字元,因此,這個長度就會造成不夠長而報錯。針對這種情況,建議設定成1024個長度。
有些包名類名就比較長的堆疊,怎麼可能只有254個長度呢!
建立好資料表之後,接下來就是配置logback-spring.xml
了,為了提高效能,採用了資料庫連線池,由於專案中採用的是druid
,因此,沿用專案中的資料庫連線池。在經過一番搜尋之後,得到了不少類似下面的配置,呃呃呃呃呃
從上面的配置看,不難看出,配置了c3p0
的資料庫連線和資料庫方言,看著似乎沒有什麼問題,動手試試看吧,直接複製貼上到自己的配置檔案,執行^^^ 似乎不好使啊!再仔細認真看一遍,似乎沒有錯誤啊,只不過是把C3P0換成了Druid而已啊
<appender name="DbAppender" class="ch.qos.logback.classic.db.DBAppender">
<connectionSource class="ch.qos.logback.core.db.DataSourceConnectionSource">
<dataSource class="com.alibaba.druid.pool.DruidDataSource">
<driverClass>oracle.jdbc.OracleDriver</driverClass>
<user>username</user>
<password>pass</password>
<url>jdbc:oracle:thin:@ip:1521:orcl</url>
<sqlDialect class="ch.qos.logback.core.db.dialect.OracleDialect"/>
</dataSource>
</connectionSource>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
結果,報錯了……心碎
10:25:33,859 |-ERROR in ch.qos.logback.core.joran.spi.Interpreter@47:30 - no applicable action for [driverClass], current ElementPath is [[configuration][appender][connectionSource][dataSource][driverClass]]
10:25:33,864 |-ERROR in ch.qos.logback.core.joran.spi.Interpreter@48:23 - no applicable action for [user], current ElementPath is [[configuration][appender][connectionSource][dataSource][user]]
10:25:33,867 |-ERROR in ch.qos.logback.core.joran.spi.Interpreter@51:83 - no applicable action for [sqlDialect], current ElementPath is [[configuration][appender][connectionSource][dataSource][sqlDialect]]
最主要的報錯原因和最上面的springProfile類似,就是沒有適用的action……巴拉巴拉,奇了怪了!後面猜測是不是因為指定了資料來源,不同的資料來源裡面的配置不一樣?在初始化資料庫連線池的時候,通過反射構造連線池的時候,沒有找到對應名字的欄位?於是乎根據druid的配置,換成了如下的配置
<appender name="DbAppender" class="ch.qos.logback.classic.db.DBAppender">
<connectionSource class="ch.qos.logback.core.db.DataSourceConnectionSource">
<dataSource class="com.alibaba.druid.pool.DruidDataSource">
<driverClassName>oracle.jdbc.OracleDriver</driverClassName>
<username>username</username>
<password>pass</password>
<url>jdbc:oracle:thin:@ip:1521:orcl</url>
<sqlDialect class="ch.qos.logback.core.db.dialect.OracleDialect"/>
</dataSource>
</connectionSource>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
重新執行,這次雖然還是沒有成功,但是報錯明顯變少了,僅有sqlDialect報錯……啊哈哈,似乎發現了黎明之前的黑暗!
對於不同的資料庫連線池,logback是不知道內部跟jdbc相關的配置的名稱是怎麼樣的,因此,使用不同的資料庫連線池時,要根據其內部的名稱來配置
dataSource
標籤!根據這個規律,一些資料庫的其他配置,例如最大連線數,最大空閒連線數等應該也是可以修改的,本人並沒有測試。
在掙扎了一段時間之後,實在想不出為什麼<sqlDialect>
會報錯,是logback不支援嗎?還是?看來只能一探原始碼方知究竟了。在查看了logback的原始碼之後,發現在一個叫DBUtil
的類裡面終於找到了真相!
public class DBUtil extends ContextAwareBase {
private static final String POSTGRES_PART = "postgresql";
private static final String MYSQL_PART = "mysql";
private static final String ORACLE_PART = "oracle";
// private static final String MSSQL_PART = "mssqlserver4";
private static final String MSSQL_PART = "microsoft";
private static final String HSQL_PART = "hsql";
private static final String H2_PART = "h2";
private static final String SYBASE_SQLANY_PART = "sql anywhere";
private static final String SQLITE_PART = "sqlite";
public static SQLDialectCode discoverSQLDialect(DatabaseMetaData meta) {
SQLDialectCode dialectCode = SQLDialectCode.UNKNOWN_DIALECT;
try {
String dbName = meta.getDatabaseProductName().toLowerCase();
if (dbName.indexOf(POSTGRES_PART) != -1) {
return SQLDialectCode.POSTGRES_DIALECT;
} else if (dbName.indexOf(MYSQL_PART) != -1) {
return SQLDialectCode.MYSQL_DIALECT;
} else if (dbName.indexOf(ORACLE_PART) != -1) {
return SQLDialectCode.ORACLE_DIALECT;
} else if (dbName.indexOf(MSSQL_PART) != -1) {
return SQLDialectCode.MSSQL_DIALECT;
} else if (dbName.indexOf(HSQL_PART) != -1) {
return SQLDialectCode.HSQL_DIALECT;
} else if (dbName.indexOf(H2_PART) != -1) {
return SQLDialectCode.H2_DIALECT;
} else if (dbName.indexOf(SYBASE_SQLANY_PART) != -1) {
return SQLDialectCode.SYBASE_SQLANYWHERE_DIALECT;
} else if (dbName.indexOf(SQLITE_PART) != -1) {
return SQLDialectCode.SQLITE_DIALECT;
} else {
return SQLDialectCode.UNKNOWN_DIALECT;
}
} catch (SQLException sqle) {
// we can't do much here
}
return dialectCode;
}
// 以下程式碼省略……
}
原來logback是可以根據Connection
獲取到DatabaseMetaData
物件,然後根據meta來獲取到底是哪個資料庫產品,從而自動返回對應的方言。換句話說就是,根本不需要手動配置什麼sqlDialect,自動可以獲取,(其實根據jdbcurl都知道是什麼資料庫了)那麼我們在裡面設定方言其實有點畫蛇添足了。
可能以前的歷史版本是需要手動設定dialect的,在現在的版本應該是改進了,本人也沒有去比對過歷史原始碼,只是猜測而已!有興趣的看官可以自己去github比對下。
好了,到了這裡,踩坑就結束了,下面就貼上logback+druid連線池的appender配置,對於其他資料庫連線池,我想如果看了上面的內容,應該也都會怎麼配置避免掉進這個坑裡面了。
<appender name="DbAppender" class="ch.qos.logback.classic.db.DBAppender">
<connectionSource class="ch.qos.logback.core.db.DataSourceConnectionSource">
<dataSource class="com.alibaba.druid.pool.DruidDataSource">
<driverClassName>oracle.jdbc.OracleDriver</driverClassName>
<username>username</username>
<password>pass</password>
<url>jdbc:oracle:thin:@ip:1521:orcl</url>
</dataSource>
</connectionSource>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
後記
針對一些場景,報錯日誌比較多的情況下,異常堆疊也是比較多的,存放在資料庫視情況而定,從效能的角度來說,資料庫並不是最優的選擇。本文也只是拋磚引玉而已,如果對日誌的搜尋和儲存效能有比較大的需求,並不建議直接存放到資料庫。如果是一套比較大的系統,還是建議使用ELK
套件來實現這個功能。如果是一些不太大的系統,也可以使用本文所講述的