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 はそのまま利用します。
動作確認用のサンプルなので、テーブル名やカラム構成に特に意味はありません。

#サンプル
全部で４ファイルあります。

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

```sql: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()は その他２テーブルへのログ追加です。
今回はその他２テーブルのカラム構成を変更していないので、secondarySubAppend()はそのまま利用します。

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

```java: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"のアペンダーとして設定しています。

```xml: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にキャストして利用しています。


```java: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/657089/3fab4283-1013-9664-a1c3-d98b48a12d49.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