Help us understand the problem. What is going on with this article?

LogbackのDBAppenderを継承して自作したDBテーブルにログを出力する

LogbackのDBAppenderを使ってDBにログを保存しようとすると

logging_event
logging_event_property
logging_event_exception

という、予め決まったテーブルの利用がデフォルトとなります。
(下記、参考のURL先で『DBAppender』でページ検索すると各テーブルの定義が確認できます)

でも、自分で好きなログを出力したいですよね。
というわけでDBAppenderを継承して、オリジナルのテーブルにログを出力するサンプルを作ってみました。

データベース

DBはH2を利用しています。
また、H2にログを保存するテーブルは起動時に「schema.sql」を実行して生成しています。
このあたりは、下記、参考文献の「Spring Boot で Spring JDBC を使う」がとても参考になります。

テーブル構成

logging_event テーブルは、テーブル名を「hoge_logging」に変更し、カラム構成も変更します。
logging_event_property テーブルは、テーブル名のみ「hoge_property」に変更します。
logging_event_exception はそのまま利用します。
動作確認用のサンプルなので、テーブル名やカラム構成に特に意味はありません。

サンプル

全部で4ファイルあります。

1. schema.sql

hoge_logging がログを書き込むテーブル、hoge_property、logging_event_exception がログの付随情報テーブルとなります。
全テーブルにある event_id は、テーブル間の参照キーです。

schema.sql
DROP TABLE logging_event_exception IF EXISTS;
DROP TABLE hoge_property IF EXISTS;
DROP TABLE hoge_logging IF EXISTS;

CREATE TABLE hoge_logging
(
    id VARCHAR(256),
    name VARCHAR(256),
    event_id IDENTITY(1,1) NOT NULL
);

CREATE TABLE hoge_property
(
    event_id BIGINT NOT NULL,
    mapped_key VARCHAR(254) NOT NULL,
    mapped_value LONGVARCHAR,
    PRIMARY KEY(event_id, mapped_key),
    FOREIGN KEY(event_id) REFERENCES hoge_logging(event_id)
);

CREATE TABLE logging_event_exception
(
    event_id BIGINT NOT NULL,
    i SMALLINT NOT NULL,
    trace_line VARCHAR(256) NOT NULL,
    PRIMARY KEY(event_id, i),
    FOREIGN KEY(event_id) REFERENCES hoge_logging(event_id)
);

2. SampleDBAppender.java

DBAppenderを継承し、カスタマイズしたクラスです。
基底のDBAppenderクラスでは start() メソッド内で 各テーブルへのINSERT文を生成しています。
そこで、start()をオーバーライドして super.start() 実行後に各INSERT文を置き換えます。

INSERT実処理は subAppend()、secondarySubAppend()で実行されます。
subAppend()はloggingテーブルへの、secondarySubAppend()は その他2テーブルへのログ追加です。
今回はその他2テーブルのカラム構成を変更していないので、secondarySubAppend()はそのまま利用します。

subAppend()では start()で作成したSQL文に合うようデータを追加します。
今回の場合、1番目に id、2番目に name です。
event_id は、DBAppenderクラスの更に基底クラス DBAppenderBase<ILoggingEvent> 内で付与されるので追加不要です。

SampleDBAppender.java
package com.example.demo.filter;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.util.ArrayList;
import java.util.List;

import ch.qos.logback.classic.db.DBAppender;
import ch.qos.logback.classic.spi.ILoggingEvent;

public class SampleDBAppender extends DBAppender {

    public enum ColumnNames {
        ID,
        NAME,
    }

    private String loggingTableName = "hoge_logging";
    private String propertyTableName = "hoge_property";

    public List<String> getColumnNames() {

        List<String> result = new ArrayList<String>();

        for (ColumnNames data : ColumnNames.values()) {
            result.add(data.toString());
        }

        return result;
    }

    public void start() {
        super.start();
        super.insertSQL = this.buildInsertSQL();
        super.insertPropertiesSQL = this.buildInsertPropertiesSQL();
    }

    protected String buildInsertSQL() {
        StringBuilder builder = new StringBuilder("INSERT INTO ");
        String tableName = this.loggingTableName;
        builder.append(tableName);

        List<String> nameList = this.getColumnNames();
        List<String> joinList = new ArrayList<String>();

        for (int i = 0, max = nameList.size(); i < max; i++) {
            joinList.add("?");
        }

        builder.append(" (");
        builder.append(String.join(",", nameList));
        builder.append(") ");

        builder.append("VALUES (");
        builder.append(String.join(",", joinList));
        builder.append(")");

        return builder.toString();
    }

    protected String buildInsertPropertiesSQL() {
        StringBuilder builder = new StringBuilder("INSERT INTO ");
        String tableName = this.propertyTableName;
        builder.append(tableName).append(" (");
        builder.append("EVENT_ID").append(", ");
        builder.append("MAPPED_KEY").append(", ");
        builder.append("MAPPED_VALUE").append(") ");
        builder.append("VALUES (?, ?, ?)");
        return builder.toString();
    }


    @Override
    protected void subAppend(ILoggingEvent event, Connection connection, PreparedStatement insertStatement) 
            throws Throwable {

        Object[] array = event.getArgumentArray();
        List<String> columnNames = this.getColumnNames();       

        for (int i = 0, max = columnNames.size(); i < max; i++) {
            String data = (String)array[i];
            insertStatement.setString(i + 1, data);
        }

        int updateCount = insertStatement.executeUpdate();

        if (updateCount != 1) {
            addWarn("Failed to insert loggingEvent");
        }
    }
}

3. logback-spring.xml

Logback用の設定ファイルです。
上記「2.SampleDBAppender.java」をクラスとしたアペンダーを追加し、それを"DBLogger"のアペンダーとして設定しています。

logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE logback>

<configuration>

    <appender name="Stdout" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%m</pattern>
        </encoder>
    </appender>

    <appender name="DB" class="com.example.demo.filter.SampleDBAppender">
        <connectionSource class="ch.qos.logback.core.db.DataSourceConnectionSource">
            <dataSource class="org.apache.commons.dbcp2.BasicDataSource">
                <driverClassName>org.h2.Driver</driverClassName>
                <url>jdbc:h2:tcp://localhost/~/test</url>
                <username>sa</username>
                <password></password>
            </dataSource>
        </connectionSource>
    </appender>

    <logger name="DBLogger" level="debug" additivity="true">
        <appender-ref ref="DB" />
    </logger>

    <root level="DEBUG" >
        <appender-ref ref="Stdout" />
    </root>

</configuration>

4. HelloController.java

動作確認用のコントローラークラスです。
logging_event_exceptionへのログ追加を確認するために、org.apache.logging.log4j.Logger の

 public void log(Marker marker, String fqcn, int levelInt, String message, Object[] argArray, Throwable t);

を実行したいのですが、LoggerFactory.GetLogger()の戻り型は org.slf4j.Logger で、このインターフェースは上記関数が定義されていません。なので、LocationAwareLoggerにキャストして利用しています。

HelloController.java
package com.example.demo;

import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.slf4j.Marker;
import org.slf4j.spi.LocationAwareLogger;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    private final LocationAwareLogger log = (LocationAwareLogger)LoggerFactory.getLogger("DBLogger");

    @RequestMapping(path = "/hello", method = RequestMethod.GET)
    public String Hello() {
        String format = "";
        Object [] arguments = new Object[] {"100", "hoge太郎"}; 
        log.debug(format, arguments);
        return "hello";
    }

    @RequestMapping(path = "/goodbye", method = RequestMethod.GET)
    public String goodbye() {

        MDC.put("Key", "goodbye");

        try {
            throw new Exception("test exception");
        } catch (Exception e) {

            Marker marker = null;
            String fqcn = HelloController.class.getName();
            int level = LocationAwareLogger.ERROR_INT;
            String format = "id= {}, name= {}";
            Object [] arguments = new Object[] {"100", "hoge太郎"};
            Throwable t = e;            
            log.log(marker, fqcn, level, format, arguments, t);         
        }

        return "{ message: \"goodbye\" }";
    }
}

実行結果

任意のテーブル名およびカラム構成のテーブルにログが出力できました。
また、MDCや例外時のスタックトレースなどのDBAppenderが有しているログ出力機能の再利用もできました。
image.png

参考

Logback Home 第 4 章 アペンダー
http://logback.qos.ch/manual/appenders_ja.html

Spring Boot で Spring JDBC を使う
https://qiita.com/niwasawa/items/bd97d4eb179fe5a7ec3f

ログが出力されたかをDBを介してプログラムで確認する
https://qiita.com/neriudon/items/79f683b2ae7c4a0af293

hatarabo
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away