ルモーリン
ホーム 更新 Perl Sample サービス 雑談 鉄ゲタ Linux リンク 連絡先

アクセスカウンターを設置~第2弾~

2020-03-06

第1弾のアクセスカウンター(アクセスカウンターを設置)は同じクライアントからのアクセスを有効期限1時間のクッキーで判断していました。 これはクライアントがクッキーを既に持っていれば同じクライアントだからカウントアップをしない仕組みです。 上手い事行ったと安心していたらGoogleのクロール(検索用のページ収集)はクッキーを持ちません(推測)。 これは個別のアクセスと判定、ページ毎にカウントアップすることに気付きました。

クライアントのIPアドレスとアクセス日を記録、アクセス日が更新される際にカウントアップします。 本当はアクセス日時を記録して24時間経過後に再びアクセスがあればカウントアップしたいけれど、処理を簡単にします。 なので深夜0時を跨いで再びサイトを見るとカウントアップします(笑)。

ウチのサイトではMySQLも稼働していますけれど、こいつは大げさな(のと他のデーターベース処理と独立させたい)のでredisを使います。 WebサーバーのMojoliciousが複数プロセスで稼働してもredisサーバーは単一なのでカウントアップ処理が競合しても処理できます。 redisにはPerlのハッシュみたいなデーターベースがあり、ハッシュの名前を指定して、キー名/値の組み合わせを登録/更新できます。
ハッシュ型 — redis 2.0.3 documentation
キーをIPアドレス、値をアクセス日にして記録します。 redisのコマンドで「HSET remote_ip, 123.45.67.89, 2020-03-06」(クォートを除く)といった感じです。 読み出しはIPアドレスを指定するとアクセス日が返ります。 redisのコマンドで「HGET remote_ip, 123.45.67.89」で「2020-03-06」(クォートを除く)が返る訳です。

redisはスーパーユーザーで起動しても良いのですが一般ユーザーが起動しておけば万が一クラックを食らってもそのユーザーの権限しか使えないので保険になります。 redisのサーバーをユーザーサービスとして登録、常駐させます。
一般ユーザーの常駐プログラムをサービス化 - Linux

#!/bin/bash

THIS_FILE_PATH=`readlink -e $0`
FILE_DIRECTORY=${THIS_FILE_PATH%/*}
THIS_FILE=${THIS_FILE_PATH##*/}
# SCRIPT_FILE=${THIS_FILE/%.sh/.pl}
ERROR_LOG_FILE=${THIS_FILE%.sh}_error.log

# スクリプトがあるディレクトリに移動
cd ${FILE_DIRECTORY}

# オプションなし
if [ 0 -eq $# ]; then
        # コマンドラインから起動(ログアウト後も継続)
        echo start redis server.
        nohup redis-server </dev/null >/dev/null 2>>${ERROR_LOG_FILE} &

        sleep 3
        pgrep -fa redis-server
        echo done.
elif [ "start" = $1 ]; then
        # 起動
        redis-server
elif [ "stop" = $1 ]; then
        # 停止
        redis-cli shutdown save
else
        echo "使い方:$0 [start|stop]"
fi

Mojolicousで使うプラグインを作成します。 3秒インターバルでredisからカウント値を読み出す処理、 Mojolicousのどこからでもカウント値を取得できるヘルパー、 3秒インターバルでカウントアップ値をredisに反映させる処理、 カウントアップするヘルパー(IPアドレスとアクセス日でカウントアップすべきか判定)を用意します。

package PpAccessCounter;
use Mojo::Base "Mojolicious::Plugin";

use Mojo::Redis;

use constant COUNT_KEY => "access_counter";
use constant REMOTE_IP => "remote_ip";

sub register {
	my ($self, $app, $conf) = @_;

	$app->plugin("DateTime");
	my $db = Mojo::Redis->new->db;

	my $count = 0;
	Mojo::IOLoop->recurring(3 => sub {
		my $loop = shift;

		# 一度切断すると再接続しないので都度接続
		$db->get(COUNT_KEY, sub {
			my ($db, $err, $res) = @_;

			# $app->log->debug("count err: $err, res: $res");
			$count = $res if defined $res;
		});
	});

	$app->helper(PpAccessCounter_get => sub {
		my $self = shift;

		return $count;
	});

	my $increment = 0;
	Mojo::IOLoop->recurring(3 => sub {
		my $loop = shift;

		if ($increment) {
			# redisがインクリメント
			# 一度切断すると再接続しないので都度接続
			$db->incrby(COUNT_KEY, $increment, sub {
				my ($db, $err, $res) = @_;

				# $app->log->debug("increment err: $err, res: $res");
				# 上手く行ったら刈り取り
				$increment = 0 if !$err;
			});
		}
	});

	$app->helper(PpAccessCounter_count => sub {
		my $self = shift;

		my $ip = $self->tx->remote_address;
		my $last_date = $db->hget(REMOTE_IP, $ip);
		my $today = $self->now(time_zone => "local")->strftime("%F");

		if (!defined $last_date or $last_date ne $today) {
			$db->hset(REMOTE_IP, $ip, $today);

			# インクリメント後、タイマーが刈り取る
			$increment++;
			$self->app->log->debug("PpAccessCounter_count $increment");

		}
	});
}

1;

Mojolicousはクライアントからのアクセスを網羅できるので、とりあえず全部拾ってカウントアップ処理へ回します。 アプリケーションのstartupでルーターを設定する冒頭に次のコードを入れます。 リクエストのURLでドメイン名の次が「/」で始まる場合に(つまり全部)コールバックを登録してヘルパー関数のカウント処理を呼び、1を返してルーター処理を継続させます。 サブルーチンの中はコントローラーなので$selfで良いのですが、startupの$selfと混同しそうなので$cにしています。 このコードに続いてルーターの設定があるので$rを書き替えています。

# アクセスカウンター
$r = $r->under("/" => sub {
	my $c = shift;

	$c->PpAccessCounter_count;
	return 1;
});

トップページのテンプレートでカウント値を表示します。 一見すると昔なつかしSSI(サーバーサイドインクルード)のように見えますけれど、そこは我らがMojoliciousです。 なのでテンプレートからページを生成する際にヘルパーがカウント値を返してカウント結果に置き換わります。 続く2行はサイト全体のページ数及び最終更新日を表示する処理です。 HTMLとPerlが混合したテンプレートはシンタックスハイライトが誤認しますのでご了承ください。

<h1>
        ルモーリンのサイトにようこそ
</h1>
<p>
あなたは<%== PpAccessCounter_get() %>人目のお客様です。<br />
% my ($count, $dt) = PpUpdate_summary();
<%= $dt->strftime("%F") %> 現在で <%= $count %> ページあります。
</p>
<h2>
        更新情報
</h2>

こんな感じに見えます。