logrotate

logrotate は、放っておけば際限なく肥大してしまう各種ログファイルに対して、世代ローテーションをして何代目かになったら破棄するとか、それらをメールでどこかに送信するなど、様々な処理が行える。世代管理やサイズ制限などの機構を自前で持たないプログラムからのログを管理するのになくてはならない道具だ。

Table of Contents

Logrotate実行の流れ

logrotate 自体はデーモンではないので cron (※1) と組み合わせて利用する。実行の流れは、RedHat系ディストリビューションでは以下のようになっている:

※1 RHEL/CentOS 6 ではシステムメンテナンスジョブの主駆動機構が cron から anacron に変わっており、cron.daily, cron.weekly 配下のスクリプトの実行も anacron の方に任されている。
※2 正確には、run-parts はシェルスクリプトで、RHEL/CentOS 5系までは cron 関連のパッケージ、6系からは anacron 関連のパッケージに含まれている。

つまり、logrotate で管理したい新たなログがあれば /etc/logrotate.d/ ディレクトリにある既存のファイルを編集するか、新たに作ってやればいいわけだ。相手が syslogd を経由してログを吐き出しているプログラムで、特にねじくれた処理を必要としないのなら、既存の /etc/logrotate.d/syslog にログファイル名を書き足すだけでも充分だ。仮に、対象のログが /var/log/some/some.log だとしたら、下記の赤字の箇所を足す:

#/etc/logrotate.d/syslog
/var/log/messages /var/log/secure /var/log/maillog /var/log/spooler /var/log/boot.log /var/log/cron /var/log/some/some.log {
  sharedscripts
  postrotate
    /bin/kill -HUP `cat /var/run/syslogd.pid 2> /dev/null` 2> /dev/null || true
  endscript
}

参考のため、主設定ファイル /etc/logrotate.conf の内容も見ておこう:

#/etc/logrotate.conf
weekly
rotate 4
create
include /etc/logrotate.d
/var/log/wtmp {
  monthly
  create 0644 root utmp
  rotate 1
}

設定の書き方

logrotateの主設定ファイルは /etc/logrotate.conf。通常は、そこにはデフォルトとなるグローバルな設定だけを書き、ログファイルグループ毎の個別の設定は /etc/logrotate.d/ 配下の設定ファイルに分割し、下記に述べる include ディレクティブで読み込ませる。logrotate の設定書式の基本は;

[logfile [logfile ...] {]
 directive
 [directive ...]
[}]
ディレクティブ一覧 ("no"のついたものは同種ディレクティブの否定を意味する)
# # で始まる行はコメントとして無視される
create [mode] [owner] [group] カレントログファイルをローテーション (下記の postrotate も) した後、代わりに空の新規ログファイルを作って置いてくる。その属性も指定できる。mode は 0755 のようなオクテット書式。指定しない属性については元のファイルの属性が引き継がれる
nocreate 上記をグローバルな設定にした場合に、個別定義内で create を無効にしたい際に使用
copy/nocopy カレントログファイルをリネームする代わりに、そのファイルのコピーを作る。create とは事実上の排他関係にあり、create は同時指定しても無視される
copytruncate/nocopytruncate copy の動作を行った後、元のログファイルの内容 を消去する。見かけ上は create と同じ結果となる。これはログファイルをリロードする術のないプログラムへの対処法のひとつ。例えば Oracle 10g R1/R2 の alert ログに対しては、これをやらないと以前のファイル (例えば alert_xx.log.1) にログが吐き続けられる (Oracleのバージョンによって異なる気配あり)
rotate num 世代ローテーションのステップ数。例えば元のログファイルが a.log だとして、 num を 2 にしておくと a.log => a.log.1 => a.log.2 => 廃棄 となる。0 だと a.log => 廃棄
start num 初代ローテーションファイルの末尾に付加するナンバー。デフォルトは 1。 num を例えば 5 にすると、a.log => a.log.5 => a.log.6 => ... と推移
extension ext ローテーションした旧ログファイルに拡張子 ext を付ける。ドットも必要。例えば some.log を "extension .bak" の設定でローテーションすると、初代ローテーションログは some.log.1.bak となる。圧縮も行う場合、圧縮による拡張子はさらにその後ろに付く
compress/nocompress ローテーションした後の旧ファイルに圧縮を掛ける。デフォルトは nocompress
compresscmd cmd ログファイルの圧縮に使用するプログラムを指定。デフォルトは gzip
uncompresscmd cmd ログファイルの解凍に使用するプログラムを指定。デフォルトは gunzip
compressoptions opt 圧縮プログラムへ渡すオプション。デフォルトは gzip に渡す "-9" (圧縮率最大)。"-9 -s" のようなスペース入りだと受け付けなかったので、複数のオプションが必要な場合は "-9s" のようにショートオプション連続形を使用のこと
compressext ext 圧縮後のファイルに付ける拡張子 (ドットも必要)。デフォルトでは、使用する圧縮コマンドに応じたものが付くとされているが、bzip2 を使っても gz になってしまうなど、あまり当てにならない
delaycompress/nodelaycompress 圧縮処理を、その次のローテーションの時まで遅らせる。compress も指定しないと無意味
olddir dir ローテーションした旧ログを dir に移動する。移動先は元と同じデバイス上でなければならない。元のログ位置に対する相対指定も有効
noolddir 上記の否定
mail address 旧ログファイルを address に送信する。どの段階のものを送るかは maillast などのオプションで決まる
nomail 上記の否定
maillast mail に関するオプション。最終世代を経て破棄されるログをメールする (デフォルト)
mailfirst mail に関するオプション。初代ローテーションログをメールする
daily/weekly/monthly ログローテーションを日毎/週毎/月毎に行う。暗黙の既定値は daily。例えば weekly なら、毎日実行を掛けたとしても、その週で最初に必要条件を満たした際にだけローテーションが行われる
size num[K/M] ログのサイズが num バイトを超えていればローテーションを行う。この条件は daily, weekly などの条件より優先される。キロ/メガバイトでの指定も可能
ifempty 元のログファイルが空でもローテーションを行う (デフォルト)
notifempty 元のログファイルが空ならばローテーションしない
missingok 指定のログファイルが実在しなかったとしてもエラーを出さずに処理続行
nomissingok 指定のログファイルが実在しなければエラーを出力 (デフォルト)
firstaction
script
endscript
実際にローテーションの条件に合致するログファイルがひとつでもあった場合に、ローテーションを行う前 (prerotate のアクションよりも前) に一度だけ実行するスクリプト。個別定義内でのみ指定可能。prerotate との違いは、sharedscripts の有効無効にかかわらず、ローテーション定義ブロック{}全体に対して1度だけしか実行されない点だ。スクリプトが終了コード0以外を返すと、このローテーション定義ブロックで定義されたファイルローテーションはどれひとつとして実行されない。後述の 捨てられるログを外部保管させる を読んでもらうと動作仕様がもっとよく分かるはずだ
prerotate
script
endscript
実際にローテーションの条件に合致するログファイルがひとつでもあった場合に、ローテーションの前に (firstaction よりは後) に実行するスクリプト。個別定義内でのみ指定可能。logrotate-3.7.4 以降では、この時すでに、カレント以外の旧ファイルはローテーションされている。捨てられるログを外部保管させる も参照のこと
postrotate
script
endscript
実際にローテーションが行われた後 (lastaction よりは前) に実行するスクリプト。個別定義内でのみ指定可能
lastaction
script
endscript
実際にローテーションが行われた後 (postrotate よりも後) に実行するスクリプト。個別定義内でのみ指定可能。firstaction の類縁で、sharedscripts の有効無効に関わりなく、その個別定義全体に対して一度だけ実行される。スクリプトが異常終了した場合はエラーを上げるが、ローテーション行為がロールバックされたりはしない
nosharedscripts ローテーションの条件に合致するログが複数あった場合に、prerotate, postrotate のスクリプトを各ログファイル毎に実行する (デフォルト)
sharedscripts ローテーションの条件に合致するログが複数あった場合に、prerotate, postrotate のスクリプトを一度だけ実行する
include file_or_dir 設定ファイル内でこの記述のある位置に別の設定ファイルを読み込む。ディレクトリを指定した場合、その dir 内から、ディレクトリおよび名前付きパイプ以外のレギュラーファイルがアルファベット順に読み込まれる
tabooext [+] ext[,ext,...] include でディレクトリを指定した場合に読み込みから除外するファイルの拡張子を指定。デフォルトで .rpmorig, .rpmsave, .rpmnew, .v, .swp, ~ が設定されている。+ を挟むと追加指定、挟まないと根こそぎ置き換えとなる

上記のディレクティブのうちいくつかは logrotate のバージョンによって挙動が少々異なる (postrotate の実行条件など)。

logrotate プログラム自体にもオプションがある。例えば -d オプションを付けるとデバグモードで走り、行われるはず の動作が詳細に出力されるだけで、実際には何も行われない。これは動作を理解するのにも役に立つ。 '-d 個別設定ファイル名 ' とすると作ったばかりの設定ファイルの試験ができる。動作実験のためのちょっとしたスクリプトを作ったので tar.gz して置いておく

捨てられるログを外部保管させる

ローカルディスク上でのローテーションは数世代に抑えたいが、古いログファイルは廃棄せずに外部媒体や別のディスクに移して長期保存(アーカイブ)したい、という、2段階管理の必要なケースが出てきた。これは logrotate 自体の機能だけでは無理だが、ちょっとしたシェルスクリプトを用意し、firstaction ブロックまたは prerotate ブロックからそれを呼んで処理してやれば実現できる。ローテーション管理下にある最古世代のログファイルを、廃棄する直前にシェルスクリプトで他所へコピーし、あとは何喰わぬ顔で通常のローテーションをさせるという手筈だ。また、logrotate には、旧ログを別ディレクトリへ移動する設定(olddir設定句)の場合でも、別のデバイス上へは移動できないという制限があるが、この手を使えばその縛りからも逃れられる。

まず、コピー処理をするシェルスクリプトを作る。これ (prerotacopy) だ。logrotate は、スクリプトブロック内に書かれたスクリプトに対して、firstaction の場合や、 sharedscripts の有効な prerotate の場合は、個別定義ブロックで謳われたローテート対象ファイルパターンリストをそのまま引数として渡してくる。nosharedscripts 状態の prerotate では、ファイルパターン毎に、実際にローテーション対象となるファイル名 (.2-YYYYmmdd未付加) を引数にしてスクリプトが呼ばれる。prerotacopy はそれをポジショナルパラメータで受け取り、ループで回して、最古世代のファイルを所定のディレクトリへコピーする。この時注意しなければならないのは、特に firstactionsharedscripts の場合、ログファイルのリストはワイルドカードを含むケースがあるわけで、シェルグロブの展開を踏まえて 2次元配列的に処理する必要があるという点。こんな感じだ (スクリプトから抜粋し少し単純化してある);

for V in $*; do
    for F in $V; do
        if [ -f $F.$GEN ]; then
            EXT=$(stat $F.$GEN |grep ^Modify |cut -d' ' -f2-3 |cut -d':' -f1 |tr -d '[[:blank:]-]')
            cp -pf $F.$GEN ${OUTDIR}/$(basename $F)-$EXT || :
        fi
    done
done

設定ファイルへの組み込みは下記のようにする。dateext (ローテート後のファイルが世代番号 ".x"付きでなく日付"-YYYYmmdd" になる) が有効になっていると、削除されるファイル名の判定のしようがないので、最上流の /etc/logrotate.confdateext が指定されている場合は当該の個別設定ファイルに nodateext の指定が必要だ。また、logrotate 自体でファイルを圧縮する設定(compress) にしているケースも今のところ想定していない。

/var/log/test/*.log {
    daily
    rotate 5
    create
    nodateext
    nocompress
    missingok
    firstaction
        /usr/local/bin/prerotacopy -n 5 -z -o /mnt/logsave/test ${1+"$@"}
    endscript
}

prerotacopy スクリプトには、-n N で最古世代のログファイルの世代番号、-o でコピー先ディレクトリを指定する。加えて -z オプションも付けると、単にコピーする代わりに gzip で圧縮する。コピー先へは、圧縮無しの場合は file-YYYYmmddHH、圧縮ありの場合は file-YYYYmmddHH.gz の名前で格納される。YYYYmmddHH はコピー元ファイルの最終更新日時から採るようにした。-a オプションは Auto-parent の意味合いで、例えば、

/var/log/ServerA/messages /var/log/ServerB/ntp.log { ...

という個別定義内で、

firstaction
    /usr/local/bin/prerotacopy -n 5 -a -o /mnt/logsave ${1+"$@"}
endscript

とやると、行き先は、ソースファイルの直上と同名のディレクトリが割り込んで /mnt/logsave/ServerA/messages-YYYYmmddHH, /mnt/logsave/ServerB/ntp.log-YYYYmmddHH という具合になる。この時の ServerAServerB ディレクトリは、存在しなければ自動的に作られる。ただし、それより上の /mnt/logsave/mnt がない時はエラー扱いとした。

ここでもうひとつ注意がある。prerotacopyprerotate ブロックで組み込む場合、logrotate のバージョンによって -n に指定すべき世代番号が異なるのだ。logrotate-3.7.1 では設定ファイルの rotate X と同じに。3.7.4 以降では、X +1 の数を指定しなければならないことが分かった。つまり、3.7.4 以降では、prerotate スクリプトが実行される時点で既に、旧世代ログのローテーションが行われている - ただしカレントログファイルのローテーションと最古ファイルの削除はまだの状態だ(つまりその時 xxx.log.1 は存在しない)。firstaction ならどちらのバージョンでも N=X でよかった。限られたバージョンしか試していないので、それ以外の場合は実地に試してみてから運用していただきたい。

prerotacopy を何度か使ったり改良したりしているうちに、意外な利用法もあることに気付いた。なにも、`rotate X' と `-n X' を必ずしも合わせる必要はないし、firstactionprerotate にこだわる必要もないのだ。例えば `rotate 5' のlogrotate定義内の postscript ブロックで '-n 1' を付けて呼んでやれば、さっきまで開かれていたローテートされたてのログファイルをアーカイブログ格納先へコピーすることができる。しかも、コピーしたことなど logrotate は知らないので、カレントログフォルダ内では通常通りの5世代の非圧縮ローテーションが実現する。アーカイブフォルダにどれだけ保存しておくかは、アーカイブフォルダに対して別途 find コマンドの(例えば100日間保存なら) `-mtime +100' フィルタによる時限削除を掛けて加減すればいい。ちなみに、この通りのことをやるなら、prerotacopy の呼び出しは、postrotate ブロック内の、(syslog管理のログなら) syslogdHUP シグナルを送った後にしたほうが、ファイルハンドルが完全に新規ログファイルに移るので確実に静止点をもってコピーできる。