logbackを使ってExcelで読めるCSVファイルをログ出力する 

# はじめに
ユーザーから、Webアプリケーションの利用状況について分析・監査したいとの要望をいただきまして、
もともとログ出力にlogbackを使っていたのでlogbackからCSVファイルを出力させることで対応することにしました。

# 作ったもの
- BOM付UTF-8のCSVファイルを出力します。
    - Excelでは、BOMの付いていないUTF-8のCSVファイルを開くと文字化けします。
- Excelから読んだときに分かりやすくするため、ファイルの初めに固定文言のヘッダを付けます。
- ログファイルは月単位でローテーションさせます。

# 実現方法

[Add an expression at the start of every log file using Logback - Stack Overflow](https://stackoverflow.com/a/47530502/11687534) を参考に作りました。

## 独自Appenderの作成

ファイルの先頭にBOMと固定文言を入れるAppenderを用意します。

```AuthFileAppender.java
public class AuthFileAppender extends RollingFileAppender {
    @Override
    public void openFile(String fileName) throws IOException {
        super.openFile(fileName);
        File activeFile = new File(getFile());
        if (activeFile.exists() && activeFile.isFile() && activeFile.length() == 0) {
            // 出力対象が空ファイルである場合のみBOMとヘッダを出力する
            lock.lock();
            try {
                OutputStream outputStream = super.getOutputStream();
                // BOMを出力
                outputStream.write(0xef);
                outputStream.write(0xbb);
                outputStream.write(0xbf);
                // 固定文言（CSVヘッダ）を出力
                outputStream.write("操作日付,操作時刻,ユーザID,ユーザ名,操作内容,操作対象者ID・・・\n".getBytes());
                if (super.isImmediateFlush()) {
                    outputStream.flush();
                }
            } finally {
                lock.unlock();
            }
        }
    }
}
```

- 月単位のログローテーションにはLogbackにあるRollingFileAppenderの仕組みをそのまま使いたかったので、RollingFileAppenderを継承したクラスとします。
- 出力対象（activeFile）について、対象ファイルが空である場合にのみBOMと固定文言（CSVヘッダ）を出力します。
    - 上記のStack Overflowのページでは固定文言を外部設定するようにしていましたが、この実装ではハードコードしています。
- 操作内容,操作対象者ID・・・の箇所には、実際のコードではアプリケーション固有の項目名を設定しています。

## ログ出力用クラスの作成

ログインユーザ情報の取得処理など共通化したかったのと、Loggerも共通化したかったので、クラスを用意することにしました。

```AuthLogger.java
@Component
public class AuthLogger {

    /** 監査ログを出力するLogger */
    public static final Logger AUTH_LOGGER = LoggerFactory.getLogger(AuthLogger.class);

    /** 個人を扱うService */
    @Autowired
    protected PersonService personService;

    /**
     * 対象の個人を指定せず、監査ログを出力します。
     *
     * @param operation 操作内容
     */
    public void log(String operation) {
        log(operation, null);
    }

    /**
     * 対象の個人を指定して、監査ログを出力します。
     *
     * @param operation 操作内容
     * @param personId 扱った対象者のID
     */
    public void log(String operation, String personId) {
        SsoLoginData ssoLoginData = SsoLoginDataUtil.getSsoLoginData(); // ログインユーザ情報を取得
        AUTH_LOGGER.trace(String.join(
            ",", // カンマ区切りで出力する
            // 操作日付、操作時刻はlogback.xmlにてレイアウトで設定するためここでは不要
            ssoLoginData.getPersonId(), // ユーザID
            ssoLoginData.getUserName(), // ユーザ名
            operation, // 操作内容
            personId // ユーザ名
            // 実際はここでpersonServiceを使った出力内容を設定する
        ));
    }
}
```

- SpringBootで構築しているアプリだったので、このクラスもBean化して扱うことにしました。そのために`Component`を付けています。
    - また、PersonServiceは`@Autowired`でDIしています。
- Loggerの生成時には、AuthLogger.classをそのまま渡すことにしました。
    - よって、logback.xmlでは、nameとしてAuthLoggerの完全修飾クラス名を指定することになります。
- 操作内容のみ指定するメソッドと、扱った対象者のIDを指定するメソッドの両方を用意しました。
    - 対象者が特定されない処理では、前者によりログを出力します。
- CSVを出すのに、`String.join`を使っています。
    - ダブルクォーテーションで囲いたい場合や、出力する値に改行が含まれる場合などは、opencsvなどCSVに特化したライブラリを利用することをお勧めします。

## logback.xmlの設定追加

logback.xmlに、以下の設定を追加します。

```logback.xml
  <appender name="AUTH_APPENDER" class="your.project.web.common.util.AuthFileAppender">
    <file>/your/file/path.csv</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>/your/file/path.%d{yyyy-MM}.csv</fileNamePattern>
    </rollingPolicy>
    <encoder>
      <charset>UTF-8</charset>
      <pattern>%d{yyyy-MM-dd},%d{HH:mm:ss},%msg%n</pattern>
    </encoder>
  </appender>

  <logger name="your.project.web.common.util.AuthLogger" level="TRACE" additivity="false">
    <appender-ref ref="AUTH_APPENDER" />
  </logger>
```

- appender の設定について
    - 前述のAuthFileAppenderを利用します。
    - `/your/file/path`の箇所は、実際は`${log.auth.output}`としており、アプリケーションのビルド時に稼働する環境に応じた値に置換しています。
    - 月単位でローテーションさせるため、rollingPolicyのfileNamePatternは`%d{yyyy-MM}`とします。
    - CSVの最初の2項目である操作日付と操作時刻はLogbackの機能により出すことにしました。このため、patternの最初に`%d{yyyy-MM-dd},%d{HH:mm:ss},`を設定しています。
- loggerの設定について
    - nameは、前述のとおりAuthLoggerの完全修飾クラス名とします。
        - AuthLoggerにてLogger生成時に独自の名前を指定したならば、その名前をここでも指定すればよいです。
    - `additivity="false"`を指定することで。AUTH_APPENDERのみが利用されるようにしています。

## 個別実装へのログ出力の追加

今回は、SpringMVCのControllerに前述のAuthLoggerをDIして、直接logメソッドを実行することにしました。（ここでは実装は省略します。）
プロジェクトの規模や要件によってはAOPにより出力させる選択肢もあると思います。

# まとめと感想

Logbackにはファイルの先頭に固定文言（BOMも含めて）を入れるような機能はなかったため独自のAppenderを用意することになりましたが、比較的に簡単に実現できました。
今回は出力したログファイルをユーザが直接Excelで確認する前提だったため、上記のような手段をとりました。
要件によってはログファイルはBOMなし・ヘッダなしで出力しておき、あとからBOM・ヘッダとスクリプトなどで結合するという手段も取れると思います。