WebSiteの生存確認モジュール(DeadOrAlive.pm)を作った

下に20091005の追記あり

より新しいのは、http://d.hatena.ne.jp/smeghead/20091005/deadoralive2 にあります。

管理とか運用しているWebSiteが落ちていたりすると、良くないので、自動で生存確認するモジュールを作った。このDeadOrAlive.pmを使うスクリプトをcronで回して結果をメールするように書いておけば、もしサーバが落ちてたときにも早く対処ができるようになる。


こうゆうことは、大勢の人がやっていることだと思ったので作る前にgoogleで調べてみたけど、Webサイトの生存確認に特化したモジュールというの見付からなかった。WWW::Mechanizedを使えば簡単にできそうだったけど、機能がちょっとtoo muchに感じたのと、自由にモジュールインストールできないサーバもあるので、最小限のチェックに特化したモジュールを作成してみた。

このモジュールを使うための最小のコードは以下です。

http_test.pl

#!/usr/bin/perl
use strict;
use warnings;
use utf8;
use DeadOrAlive;
#チェック対象のサイト定義
my $sites = [
{
name => 'www.example.com',
charset => 'utf-8',
tests => [
{
url => 'http://www.example.com/',
test => '<TITLE>Example Web Page</TITLE>'
},
]
},
];
#WebSiteの生存確認を行なう。
DeadOrAlive::check_sites($sites);

やっていることは、

  1. チェック対象のWebサイトの名前、文字コード、確認するURLとそのページに含まれるべき文字列を定義する。
  2. DeadOrAlive::check_sitesを呼び出す

1の部分の定義は、DSLっぽい定義にすると今風かもしれないけど、単純にperlのデータ構造を構築するだけにしてある。

このスクリプトを実行すると、下のように出力される。

実行結果(成功例)

$ ./http_test.pl
HTTP test for servers.
ok 1 - http://www.example.com/ status code.
ok 2 - http://www.example.com/

実行結果(失敗例)

$ ./http_test.pl
HTTP test for servers. [TEST ERROR]
not ok 1 - http://www.example.com/ status code.
#   Failed test 'http://www.example.com/ status code.'
#   at lib/DeadOrAlive.pm line 40.
#          got: '500'
#     expected: '200'
not ok 2 - http://www.example.com/
#   Failed test 'http://www.example.com/'
#   at lib/DeadOrAlive.pm line 42.

エラーがある場合は、1行目に[TEST ERROR]が出力される。

DeadOrAlive.pmの内容は下です。

内部的には、標準モジュールだけしか使ってないはずなので、モジュール1ファイル(DeadOrAlive.pm)と、呼び出しスクリプト(http_test.pl)を持っていけば動くはず。処理内容は、LWPでコンテンツを取得して、Test::Moreのテストを自動生成してるだけです。だから、出力内容は、Test::Moreそのまんま。

DeadOrAlive.pm

use strict;
use warnings;
use utf8;
package DeadOrAlive;
=head1 WebSiteの生存確認モジュール
=cut
use Encode;
use Data::Dumper;
use Exporter;
use base qw(Exporter);
our @EXPORT_OK = qw(check_sites);
our @EXPORT_FAIL = qw(get_http_response);
use LWP;
my $ua = LWP::UserAgent->new(timeout => 2);
$ua->agent('Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)');
sub check_sites {
my ($sites, $result_expected) = @_;
use File::Temp qw(tempfile);
#出力結果をバッファする一時ファイル
my $file_temp = tempfile();
use Test::More qw(no_plan);
Test::More->builder->output($file_temp);
Test::More->builder->failure_output($file_temp);
foreach my $site (@$sites) {
foreach my $test (@{$site->{tests}}) {
my $post = $test->{post};
my $test_string = $test->{test};
if ($post) {
#TODO post
} else {
my $res = get_http_response($test, $site->{charset});
my $code = $test->{code} || 200;
is($res->{code}, $code, $test->{url} . ' status code.');
if ($test_string) {
ok($res->{html} =~ m{$test_string},
$test->{url});
}
}
}
}
#出力したテスト結果を取得する。
my $out = do {local $/; seek $file_temp, 0, 0; <$file_temp>;};
print 'HTTP test for servers.';
if ($out =~ m{^not ok}) {
print ' [TEST ERROR]';
}
if (defined $result_expected and $out ne $result_expected) {
print ' [RESULT ERROR]';
}
print "\n\n";
print $out;
}
sub get_http_response {
my ($test, $charset) = @_;
my $host;
my $url = $test->{url};
if ($url =~ m{https?://([^/]*)/}) {
$host = $1;
}
my $req = HTTP::Request->new(GET => $url);
$req->header('Accept' => 'text/html');
$req->header('Host' => $host);
$req->header('Accept-Language' => 'ja');
my $res = $ua->request($req);
my $content = $res->as_string;
$charset ||= 'utf-8';
$content = decode($charset, $content);
#print Dumper($res->headers);
return {
headers => $res->headers,
code => $res->code,
html => $content
};
}
1;

一般的な使用例

#!/usr/bin/perl
use strict;
use warnings;
use utf8;
use FindBin;
use lib "$FindBin::Bin/lib"; #libの下にDeadOrAlive.pmを配置した場合
use DeadOrAlive;
use Data::Dumper;
#チェック対象のサイト定義
my $sites = [
{
name => 'www.example.com',
charset => 'utf-8',
tests => [
{
url => 'http://www.example.com/',
test => '<TITLE>Example Web Page</TITLE>'
},
]
},
{
name => 'www.google.co.jp',
charset => 'utf-8',
tests => [
{
url => 'http://www.google.co.jp/',
test => '日本語のページを検索'
},
]
},
];
#期待するテスト結果を__DATA__の下から取得
my $result_expected = do {local $/;<DATA>};
#WebSiteの生存確認を行なう。
DeadOrAlive::check_sites($sites, $result_expected);
# vim: set sw=2 st=2 ts=2 expandtab:
__DATA__
ok 1 - http://www.example.com/ status code.
ok 2 - http://www.example.com/
ok 3 - http://www.google.co.jp/ status code.
ok 4 - http://www.google.co.jp/

テストが失敗した場合は、標準出力の1行目に[TEST ERROR]という文字列を出力します。

DeadOrAlive::check_sitesの第二引数に、期待されるテスト結果を渡すと、テスト結果が期待している物と違った場合は、標準出力の1行目に[RESULT ERROR]という文字列を出力するようになります。

cronの実行結果の標準出力の1行目を1行目をメールの件名にすると便利かもしれません。(うちでは、そうしてます)

追記 20091004

perlcodesample スクリプトの置いてあるディレクトリ名を取得するのにFindBinというモジュールがあります。

はてなブックマーク – WebSiteの生存確認モジュール(DeadOrAlive.pm)を作った – 週記くらい(BTS開発記)

id:perlcodesample さんに、スクリプトの存在するディレクトリ名を取得するためのモジュールを教えてもらいましたので、File::Basename::dirname($0) を使う方法から、FindBinを使う方法に修正してみました。この方が、目的を素直に表せるので良いとおもいます。ありがとうございます。

下は、元のソース。

#use File::Basename;
#use lib dirname($0) . '/lib'; #libの下にDeadOrAlive.pmを配置した場合
use FindBin qw($Bin);
use lib $Bin . '/lib'; #libの下にDeadOrAlive.pmを配置した場合

TODO

  • PODを書く。
  • サイトへログインできることを確認できるようにする。

今のところそれ以上の機能追加はしないつもり。



追記 20091005

id:tokuhiromさんに添削してもらえるなんて、恐縮してしまいます。ありがとうございます。

まず

自由にモジュールインストールできないサーバもあるので、最小限のチェックに特化したモジュールを作成してみた。

っていう点なんですが、基本的に pure perl のモジュールなら、そのままアップすればうごくので、再発明する必要はありません。とくに最近は local::lib で簡単に extlib/ ディレクトリを構築できるので、なおさらです。

Re: WebSiteの生存確認モジュール(DeadOrAlive.pm)を作った – TokuLog 改メ tokuhirom’s blog

そうかもしれません。今度は、まず local::lib 調べてみます。

my $content = $res->as_string;

$charset ||= ‘utf-8’;

$content = decode($charset, $content);

のように、charset を自分で指定できるようになっていますが、最近の LWP は $res->decoded_content でヘッダなどから適当に解釈して decode してくれるので、こんなことをする必要はありません。古いLWPをつかいたい場合は弾さんの HTTP::Response::Encoding を workaround として同梱してもいいかもしれません(ちいさいのでコピペでもよいとおもう)。

Re: WebSiteの生存確認モジュール(DeadOrAlive.pm)を作った – TokuLog 改メ tokuhirom’s blog

$res->decoded_content こんな便利なものがあったとは。直します。

sub check_sites {

my ($sites, $result_expected) = @_;

use File::Temp qw(tempfile);

#出力結果をバッファする一時ファイル

my $file_temp = tempfile();

use Test::More qw(no_plan);

これはよくない。use Test::More qw(no_plan) で設定されるプラン情報はグローバル変数なので、check_sites の中で勝手に設定するのはよろしくない。

そして、sub { } の中で use するのは基本的によくないです。これは sub { BEGIN { require Test::More; Test::More->import(‘no_plan’); } } のように解釈されるから、sub のよびだしに関係なくコンパイル時に評価されるので、見た目上の効果と実際の効果がことなってしまってうれしくない。

Re: WebSiteの生存確認モジュール(DeadOrAlive.pm)を作った – TokuLog 改メ tokuhirom’s blog

勉強になります。BEGINに展開されるというイメージはわかりやすいです。納得しました。

また、いちばん大きい問題点として、生存確認を cron でおこなうという瑣末な事象がメインロジックにうめこまれてるのがよくないとおもう。TAP をつかうならば、普通に TAP を吐くテストスクリプトとして書いてしまうのがいいとおもいます。その上で、cron に登録する際に

perl -MPOSIX -e ‘my $x = `perl main.pl`;print $x unless WIFEXITED($?) && WEXITSTATUS($?)==0’

のように書くなどするのがいいのではないか。つまり、「cron のためにエラー時以外はなにも STDOUT に書かない」という用件と「HTTPサーバの生存状況をテストする」という2つの事項を明示的に分離した方が使いまわしが効くとおもう。

Re: WebSiteの生存確認モジュール(DeadOrAlive.pm)を作った – TokuLog 改メ tokuhirom’s blog

ここがちょっとわからなかったのですが、cronの呼び出しは呼び出しスクリプトにも入ってないのですが、メインロジックに埋め込まれているというのはどこのことでしょうか?

個々は小さい役割を担当させるというのは納得です。WIFEXITED WEXITSTATUS は、知らなかったです。スクリプトを組合せる時に便利そうですね。

添削、ありがとうございましたー。

2 Comments

  • use FindBin;
    use lib "$FindBin::Bin/lib";
    とも書けます。

  • ありがとうございます。
    その方がいいですw また、直します。よろしければ他にも変なとこあったら教えてください。

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.