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 は、テーブル間の参照キーです。
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> 内で付与されるので追加不要です。
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 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にキャストして利用しています。
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が有しているログ出力機能の再利用もできました。
参考
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