1. 程式人生 > >springboot2+logback將日誌輸出到oracle資料庫的踩坑之旅

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.xmllogback-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套件來實現這個功能。如果是一些不太大的系統,也可以使用本文所講述的