はじめに
Apache Logging Services Projectが提供するLog4jは、ファイルサイズによってログファイルのローテーションを行うRollingFileAppender
や日付でローテーションを行うDailyRollingFileAppender
を提供しています。
しかし、ファイルサイズと日付の両方でローテートするAppenderは提供されていないため、両方の機能を同時に利用することはできません。また、DailyRollingFileAppender
を利用する場合は、バックアップログファイル数を設定できないため、Disk Fullへの対策を検討する必要があります。
本稿ではRollingFileAppender
とDailyRollingFileAppender
の機能を組み合わせたAppenderを作成する方法を紹介し、その利用方法について示します。
対象読者
Javaプログラミングの経験があり、Log4jを使ったことがある方を対象とします。Log4jの基本については、『Log4Jコネクションプーリング対応JDBCAppenderでパフォーマンスを向上する』や、筆者が管理するLog4j Q&Aを参照ください。
必要な環境
なお、筆者は以下の環境で動作確認をしています。
- JDK 5.0 Update 4
- Log4j 1.2.13
RollingFileAppenderとDailyRollingFileAppender
Log4jはさまざまなAppenderを用意していますが、操作ログやエラーログの記録のために、以下のAppenderが広く使われていると思います。
RollingFileAppender
DailyRollingFileAppender
RollingFileAppender
ファイルサイズでログファイルのローテーションを行うAppenderです。ログファイルのサイズが最大ファイルサイズを超えたときに、ログファイルのローテーションが発生します。バックアップログファイル数が最大バックアップログファイル数を超えたら最も古いログファイルが削除されます。
プロパティ
org.apache.log4j.RollingFileAppender
では以下のプロパティが用意されています。
プロパティ名 | 設定例 | 説明 |
MaxFileSize | 10MB | 最大ファイルサイズ。このファイルサイズを超えるとログファイルのローテーションが発生する |
MaxBackupIndex | 10 | 最大バックアップログファイル数。バックアップログファイル数がこの値を超えると、最も古いログファイルが削除される |
DailyRollingFileAppender
日付でログファイルのローテーションを行うAppenderです。DatePattern
で示される日付が変わったときに、ログファイルのローテーションが発生します。
プロパティ
org.apache.log4j.DailyRollingFileAppender
では、以下のプロパティが用意されています。
プロパティ名 | 設定例 | 説明 |
DatePattern | '.'yyyy-MM-dd | ログファイルのローリングのスケジュールを指定します。月、週、日、時、分などでローリングさせることが可能です。詳細な設定方法はLog4j API SpecificationのDailyRollingFileAppender の項を参照ください。 |
両Appenderの関係と新規Appenderの位置付け
クラス名からはRollingFileAppender
とDailyRollingFileAppender
に親子関係があるように思えますが、実際はそうではありません。そのため、DailyRollingFileAppender
でRollingFileAppender
のプロパティを使うことはできません。Log4jでは「サイズと日付でローテートするAppender」は提供されていないのです。
そこで、両者の機能を併せ持つCompositeRollingFileAppender
クラスを新規に作成します。既存のAppenderと同様にCompositeRollingFileAppender
もFileAppender
を継承します。以下にUML図を示します。
CompositeRollingFileAppenderの実装
パッケージ
org.apache.log4j
へのパッケージアクセスが必要になるため、本クラスのパッケージをorg.apache.log4j
とします。
ローテーション発生の検出
Log4jではログイベントが発生すると、org.apache.log4j.WriterAppender#subAppend(LoggingEvent)
メソッドが呼び出され、ログ出力が行われます。RollingFileAppender
やDailyRollingFileAppender
では、このメソッド内でローテーションの有無を検出しています。そこで、CompositeRollingFileAppender
のsubAppend
メソッドは、両者のローテーションの検出を組み合わせた形で実装します。
protected void subAppend(LoggingEvent event) { // 日付ベースのローテーションが発生するかどうかチェックし、 // 発生する場合はローテーションを行う。 long n = System.currentTimeMillis(); if (n >= nextCheck) { now.setTime(n); nextCheck = rc.getNextCheckMillis(now); try { rollOverTime(); } catch (IOException ioe) { LogLog.error("rollOver() failed.", ioe); } } // サイズベースのローテーションが発生するかどうかチェックし、 // 発生する場合はローテーションを行う if ((fileName != null) && ((CountingQuietWriter) qw).getCount() >= maxFileSize) { rollOverSize(); } super.subAppend(event); }
サイズベースのローテーション
サイズベースのローテーションでは、まずバックアップログファイル数がMaxBackupIndex
以上であるかチェックし、そうであれば最も古いログファイルを削除します。
RollingFileAppender
ではファイルサイズによるローテーションのみ行われるため、ログファイルのバックアップインデックスから最も古いログファイルを特定できますが、CompositeRollingFileAppender
ではローテーションの方法が2種類(サイズベースと日付ベース)あるため、別の判定方法が必要になります。本稿の実装ではログファイルのタイムスタンプから最も古いログファイルを判断しています。
次に、バックアップログファイルの名称を変更します。具体的にはファイル名に付けられたインデックスを増やすようにします。この時、既に日付でローテートされたログファイルはローテーションの対象にしません。
以下のソースコードは、サイズベースのローテーションを行う処理の抜粋です。詳細はサンプルファイルに含まれるソースコードを参照下さい。
public void rollOverSize() { if (maxBackupIndex > 0) { rotateLogFilesBy(BY_SIZE); } try { this.setFile(fileName, false, bufferedIO, bufferSize); } catch (IOException e) { LogLog.error("setFile(" + fileName + ", false) call failed.", e); } } private void rotateLogFilesBy(int mode) { // 日付が付けられていないログファイルを検索する List notDailyLogs = new ArrayList(); File[] allLogs = getAllLogFiles(); for (int i = 0; i < allLogs.length; i++) { if (!isDailyLotatedLog(allLogs[i])) { notDailyLogs.add(allLogs[i]); } } int notDailyLogNum = notDailyLogs.size(); // モードに従ってローテーションを行う if (mode == BY_SIZE) { File file = null; File target = null; // 最も古いログファイルを削除する if (getBackupLogFileNum() >= maxBackupIndex) { deleteOldestFile(); } // Map {(maxBackupIndex - 1), ..., 2, 1} to // {maxBackupIndex, ..., 3, 2} for (int i = notDailyLogNum - 1; i >= 1; i--) { file = new File(fileName + "." + i); if (file.exists()) { target = new File(fileName + '.' + (i + 1)); LogLog.debug("Renaming file " + file + " to " + target); file.renameTo(target); } } // Rename fileName to fileName.1 target = new File(fileName + "." + 1); this.closeFile(); // keep windows happy. file = new File(fileName); LogLog.debug("Renaming file " + file + " to " + target); file.renameTo(target); } else if (mode == BY_DATE) { ・・・ } }
日付ベースのローテーション
日付ベースのローテーションでも、まずバックアップログファイル数がMaxBackupIndex
以上であるかチェックし、そうであれば最も古いログファイルを削除します。
次に、バックアップログファイル名に日付を付与します。日付が付けられたバックアップログファイルは以後リネームされることはありません。以下のソースコードは、日付ベースのローテーションを行う処理の抜粋です。
public void rollOverTime() throws IOException { /* Compute filename, but only if datePattern is specified */ if (datePattern == null) { errorHandler.error("Missing DatePattern option in rollOver()."); return; } String datedFilename = fileName + sdf.format(now); // It is too early to roll over because we are still within the // bounds of the current interval. Rollover will occur once the // next interval is reached. if (scheduledFilename.equals(datedFilename)) { return; } rotateLogFilesBy(BY_DATE); try { // This will also close the file. This is OK since multiple // close operations are safe. this.setFile(fileName, false, this.bufferedIO, this.bufferSize); } catch (IOException e) { errorHandler.error("setFile(" + fileName + ", false) call failed."); } scheduledFilename = datedFilename; } private void rotateLogFilesBy(int mode) { // 日付が付けられていないログファイルを検索する List notDailyLogs = new ArrayList(); File[] allLogs = getAllLogFiles(); for (int i = 0; i < allLogs.length; i++) { if (!isDailyLotatedLog(allLogs[i])) { notDailyLogs.add(allLogs[i]); } } int notDailyLogNum = notDailyLogs.size(); // モードに従ってローテーションを行う if (mode == BY_SIZE) { ・・・ } else if (mode == BY_DATE) { // close current file, and rename it to datedFilename this.closeFile(); // 最も古いログファイルを削除する if (getBackupLogFileNum() >= maxBackupIndex) { deleteOldestFile(); } // ログファイル名に日付を付与する for (int i = 1; i <= notDailyLogNum - 1; i++) { String from = fileName + '.' + i; String to = scheduledFilename + '.' + i; renameFile(from, to); } renameFile(fileName, scheduledFilename); } }
RollingFileAppender
とDailyRollingFileAppender
を組み合わせるという性質上、それぞれのクラスのソースコードをベースにしています。上記のソースコードにある英語のコメントは元のソースコードに付けられていたものです。またLog4jのバージョンによって
RollingFileAppender
とDailyRollingFileAppender
のソースコードも異なるため、それらを組み合わせる上記実装もベースとなるLog4jのバージョンによって異なります。本稿で紹介するコードはLog4j 1.2.13をベースにしています。プロパティ
org.apache.log4j.CompositeRollingFileAppender
で用意したプロパティは次のとおりです。
プロパティ名 | 設定例 | デフォルト値 | 説明 |
MaxFileSize | 10MB | java.lang.Long#MAX_VALUE | 最大ファイルサイズ。日付でのみローテーションさせたい場合は、このオプションを省略してください |
MaxBackupIndex | 10 | 1 | 最大バックアップログファイル数 |
DatePattern | '.'yyyy-MM-dd-HH | '.'yyyy-MM-dd | ログファイルのローテーションのスケジュール |
それぞれのプロパティの詳細は、RollingFileAppender
やDailyRollingFileAppender
と同じです。
設定ファイル例
以下のサンプルは、最大ファイルサイズ10MB、最大バックアップログファイル数10で、日ごとにローテーションを行う「log4j.xml」の設定例です。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/"> <appender name="FILE" class="org.apache.log4j.CompositeRollingFileAppender"> <param name="File" value="log/sample.log" /> <param name="MaxFileSize" value="10MB" /> <param name="MaxBackupIndex" value="10" /> <param name="datePattern" value="'.'yyyy-MM-dd" /> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%d %t %-5p %c{2} - %m%n"/> </layout> </appender> <logger name="jp.co.okisoft.esc.log4j"> <level value="debug" /> <appender-ref ref="FILE" /> </logger> </log4j:configuration>
ファイルサイズは無視して、とにかく日付だけでローテーションさせたい場合は、上記の設定においてMaxFileSize
プロパティを省略します。DailyRollingFileAppender
では最大バックアップログファイル数を指定することができませんが、CompositeRollingFileAppender
を利用することで、バックアップ数制限付きの日付ベースローテーションを実現できます。
サンプルアプリケーション
今回作成したCompositeRollingFileAppender
の動作確認のために用いるサンプルアプリケーションを以下に示します。while
ループ内でINFO
レベルのログを出力し続けるだけのシンプルなコードです。
package jp.co.okisoft.esc.log4j; import org.apache.log4j.Logger; public class RollingSample { private static Logger logger = Logger.getLogger(RollingSample.class); public static void main(String[] args) throws Exception { while (true) { Thread.sleep(100); logger.info("test"); } } }
-Dlog4j.debug=true