ablog

不器用で落着きのない技術者のメモ

OracleのアラートログをローテートするPerlスクリプト

例えばディレクトリ構成はこんな感じで、

$ find /home/oracle/scripts/ -type f
/home/oracle/scripts/log_rotater.pl
/home/oracle/scripts/log/alert.log
/home/oracle/scripts/conf/alert.conf

実行してみる。

$ perl log_rotater.pl conf/alert.conf 

実行ログを見てみる。

$ cat log/alert.log
[2009-05-05 02:50:03] [INFO] log_rotater.pl started.
[2009-05-05 02:50:03] [INFO] Copied file: /export/home/oracle/admin/orcl/bdump/alert_orcl.log --> /export/home/oracle/admin/orcl/bdump/alert_orcl.log_20090505-025003.
[2009-05-05 02:50:03] [INFO] Truncated file: /export/home/oracle/admin/orcl/bdump/alert_orcl.log.
[2009-05-05 02:50:03] [INFO] log_rotater.pl finished.

Oracle を停止してからローテートするなら全然問題ないんだけど、Oracle を停止せずにローテートする場合にちょっと気になることがある。アラートログを copy して、truncate してるんだけど、copy してから trucnate するまでの一瞬にログに書き込みがあったら、その一瞬に書込まれたメッセージが消えてしまう。lock、copy、truncate、unlock ってすれば良いのかな?lock している間にアラートログに書き込みがあったら、待機してくれるのかな?今度検証してみよう。

ソースコードは以下の通り。

  • alert.conf
##################################################
#
# ◆ ログ・メンテナンス・スクリプト設定ファイル
#
##################################################

#
# ◆ メンテナンス対象ログに関する設定
#

# 対象ファイル
TARGET_FILE => '/export/home/oracle/admin/orcl/bdump/alert_orcl.log',

# 対象ファイルの保持期限(日数)
TARGET_FILE_RETENTION_POLICY => 31,

#
# ◆ このスクリプトに関する設定
#

# 実行ログのパス
SCRIPT_LOG_PATH => '/home/oracle/scripts/log/alert.log',

# 実行ログを保持する世代数
SCRIPT_LOG_RETENTION_POLICY => 7,
  • log_rotater.pl
#!/usr/bin/perl
use strict;
use Time::localtime;
use File::Copy;
use File::Basename;

##############################
#
# ◆ 設定
#
##############################

# 設定ファイルの必須項目
my @conf_mandatory =('TARGET_FILE','TARGET_FILE_RETENTION_POLICY','SCRIPT_LOG_PATH','SCRIPT_LOG_RETENTION_POLICY');


##############################
#
# ◆ 初期化処理
#
##############################

# 引数をチェックする
if ( $#ARGV < 0) {
	&print_usage;
	exit(1);
}

# 設定ファイルを読み込む
my $conf = &get_conf($ARGV[0]);

# このスクリプトの実行ログをローテートする
&rotate($conf->{SCRIPT_LOG_PATH}, $conf->{SCRIPT_LOG_RETENTION_POLICY});

# このスクリプトの実行ログをオープンする
open (OUT, "> $conf->{SCRIPT_LOG_PATH}") 
	or print("Failed to create file: $conf->{SCRIPT_LOG_PATH}.") and exit(1);

# 処理開始をログに書き込む
&write_log('INFO', "$0 started.");


##############################
#
# ◆ メイン処理
#
##############################

# 対象ファイルをローテートする
&rotate_files;

# 保持期限を過ぎたファイルを削除する
&delete_expired_files;


##############################
#
# ◆ 終了処理
#
##############################

# 処理終了をログに書き込む
&write_log('INFO', "$0 finished.");

# このスクリプトの実行ログをクローズする
close (OUT);

# 正常終了(戻り値:0)する
exit(0);


##############################
#
# ◆ サブルーチン
#
##############################

# ◆ 使用方法を表示する
sub print_usage {
	print "$0: missing argument\n";
	print "Usage  : perl $0 configuration_file\n";
	print "Example: perl $0 sample.conf\n";
}

#
# ◆ 設定情報を取得する
# 
#	$_[0]: 設定ファイルのパス
#	  OUT: 設定情報
sub get_conf {
	my $conf;
	my $filename = $_[0];
	my $str = &read_file($filename);
	eval ('$conf = { dummy=>1,' . $str . '};1;') 
		or print("Error in conf file. $@.") and exit(1);
	foreach my $item (@conf_mandatory) {
		if (!exists($conf->{$item})) {
			print("$item does not exist in $filename.") and exit(1);
		} elsif ($conf->{$item} eq '') {
			print("$item is mandatory in $filename.") and exit(1);
		}
	}
	return $conf;
}

#
# ◆ 実行ログをローテートする
# 
#	$_[0]: 実行ログのパス
#	$_[1]: 実行ログを保持する世代数
sub rotate {
	my($filename,$max) = @_;
	my $cur;
	return if ($max < 0);
	if (-f "$filename.$max") {
		unlink ("$filename.$max")
			or &error_exit("Failed to delete file: $filename.$max.");
	}
	for ($cur = $max; $cur > 1; $cur--) {
		my $prev = $cur - 1;
		if (-f "$filename.$prev") {
			rename("$filename.$prev","$filename.$cur") 
				or &error_exit("Failed to rotate files: $filename.$prev --> $filename.$cur.");
		}
	}
	if (-f "$filename") {
		rename("$filename","$filename.1")
			or &error_exit("Failed to rotate files: $filename --> $filename.1.");
	}
}

#
# ◆ ファイルを読込む
#
#	$_[0]: ファイルのパス
#	  OUT: 文字列
sub read_file {
	my $filename = $_[0];
	my $lines = '';
	if (-f $filename) {
		open (IN, "< $filename") or &error_exit("Failed to open file: $filename.");
		while (my $line = <IN>) { $lines .= $line; }
		close (IN);
	} else {
		&error_exit("File not found: $filename.");
	}
	return $lines;
}

#
# ◆ 対象ファイルをローテートする
#
sub rotate_files {
	my $tm = localtime;
	my $dt = sprintf("%04d%02d%02d-%02d%02d%02d", 
		$tm->year+1900, $tm->mon+1, $tm->mday, $tm->hour, $tm->min, $tm->sec);
	# 対象ファイルをコピーする
	if (copy($conf->{TARGET_FILE}, "$conf->{TARGET_FILE}_$dt")) {
		&write_log('INFO', "Copied file: $conf->{TARGET_FILE} --> $conf->{TARGET_FILE}_$dt.");
	} else {
		&error_exit("Failed to copy file: $conf->{TARGET_FILE} --> $conf->{TARGET_FILE}_$dt.");
	}
	# 対象ファイルをクリアする
	if (truncate($conf->{TARGET_FILE}, 0)) {
		&write_log('INFO', "Truncated file: $conf->{TARGET_FILE}.");
	} else {
		&error_exit("Failed to truncate file: $conf->{TARGET_FILE}.");
	}
}

#
# ◆ 保持期限を過ぎたファイルを削除する
#
sub delete_expired_files {
	my $dir = dirname($conf->{TARGET_FILE});
	my @files = glob("$dir/*");
	my $tm = localtime(time - ($conf->{TARGET_FILE_RETENTION_POLICY} * 60 * 60 * 24));
	my $expiry_date = sprintf("%04d%02d%02d", $tm->year+1900, $tm->mon+1, $tm->mday);
	foreach my $filename (@files) {
		if (my($file_date) = $filename =~ /([0-9]{8})-[0-9]{6}$/g) {
			if ($file_date < $expiry_date) {
				if (unlink("$filename")) {
					&write_log('INFO', "Deleted file: $filename.");
				} else {
					&error_exit("Failed to open file: $filename.");
				}

			}
		}
	}
}

#
# ◆ ログに書き込む
#
#	$_[0]: エラーレベル(INFO/ERROR)
#	$_[1]: メッセージ
sub write_log {
	my $tm = localtime;
	my $dt = sprintf("%04d-%02d-%02d %02d:%02d:%02d",
		$tm->year+1900, $tm->mon+1, $tm->mday, $tm->hour, $tm->min, $tm->sec);
	print OUT "[$dt] [$_[0]] $_[1]\n";
}

#
# ◆ 異常終了する
#
#	$_[0]: メッセージ(省略可)
sub error_exit {
	write_log('ERROR', $_[0]) if ($#_ >= 0);
	exit(1);
}