springboot2+logback將日誌輸出到oracle數據庫的踩坑之旅

背景

根據本人寫博客的慣例,先交代下背景。在公司的系統中,咱們的配置文件是切分有好幾個的,不一樣的配置文件裏面配置內容有着不一樣,對於日誌的輸出,也須要對不一樣的環境作出不一樣的輸出,這是一個前提,本文即將講述到的將日誌輸出到oracle數據庫就是分環境輸出的,本地測試的日誌是很是多的,服務也時常重啓,調試等,所以本地環境的日誌不宜輸出到數據庫,而線上環境不一樣,線上環境的日誌輸出比本地要少不少,也不常常重啓服務。所以本文要講的內容有如下兩點:java

  • 如何在logback的配置文件中配置不一樣的profile?
  • 如何將日誌輸出到數據庫?這個過程遇到了什麼坑?怎麼解決的?

本文將在如下軟件版本中進行,不一樣版本是否存在差別還沒有測試mysql

springboot v2.1.0
logback    v1.2.3
oracle     11.2
java       1.8.0_171

Logback的不一樣環境配置

在中國,但凡是有什麼不懂的,第一個想到的就是百度,固然,本人第一反應也是先百度,果不其然,第一頁各類解決方案琳琅滿目,不過通過本人的實踐,得出如下比較有用的,也是本人最終採用的方案,各位看官若是看到這篇文章,能夠直接參考。git

網上的說法有不少,有些是定義多個logback的文件,例如logback-dev.xmllogback-prod.xml等等諸如此類的,而後在application.yml內進行根據不一樣的profiles.active來選擇不一樣的日誌配置文件。不過這種方式我並無測試過,重點講一下下面的方法。github

在同一個logback配置文件中,根據不一樣的<springProfile>來區分,具體能夠相似如下的寫法:spring

<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的日誌,發現以下的報錯sql

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,重啓應用,發現能夠經過。springboot

logback.xml在springboot中,是先於spring上下文加載的,所以,在加載這個配置文件時,還不知道到底採用哪一個profile,也就是說<springProfile>這個標籤並不會在這個階段起做用。logback-spring.xml是先初始化spring上下文,這個時候<springProfile>才生效oracle

Logback將日誌輸出到數據庫

我這裏採用的是oracle數據庫,所以針對oracle數據庫作一些簡單說明,而且指出所遇到的坑。app

可能有朋友會問,我想將日誌輸出到數據庫,那麼數據庫的表我是要本身建嗎?要建成怎麼樣的呢?答案是不須要的,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套件來實現這個功能。若是是一些不太大的系統,也可使用本文所講述的方式進行存儲。

相關文章
相關標籤/搜索