ホームページ訪問カウンター
よくあるcgiサイトにある
訪問カウンターをnginx + lua-nginx-moduleを利用して
作成したいと思います。
こんな感じなの。
レッツPHPさんのサンプルにあるナンカイメカウンターのようなものです。
昔良く見たアレです。ユーザーが全体で何回そのサイトにアクセスしたか、
を記録する奴。
というのをnginxとlua-nginx-moduleで実装したいと思います。
nginx及びlua-nginx-moduleの環境構築はこちらを参照してください。
動作環境ですが、
- MacOS X 10.8
- nginx 1.4.0
- lua-nginx-module v0.8.1
となります。
訪問カウンターの仕様
- 特定の人をIPアドレス+UAで切り分けして判別する
- incrementするのは1日1回かぎり
以上 2点が満足できれば目的を達成できるはず。
特定の人をIPアドレス+UAで切り分けして判別する
これをどうやるか。
僕はlua_shared_dictを利用することにした。
オンメモリなLuaのDictという。
documentのサンプルを見るに、以下のものを用意すればできそうだ。
count 訪問者のカウント。keyは日付でいいだろう。valueは数値で。
be_present_today 訪問者がその日のうちにアクセスしたか。keyはIPアドレス+UA+日付
IPアドレス+UAとか、空白含むしkeyはsha1でなんとかしたい。
lua-nginx-moduleのsha1アルゴリズムによるハッシュ出力、
バイナリ出力しかないし他のことやるのもだるいので
lua-resty-string を利用する。
lua-resty-stringを利用するにはLuaJITを利用しないといけない。
FFI ライブラリ を利用してCの構造体を宣言するからだ…
しかもlua-resty-stringを利用するためにはopensslオプションを有効にした形で
nginxをビルドしないといけないので、ビルドを以下のビルドオプションを渡して
ビルドし直す。
opensslなどのバージョンはopenssl及びhomebrewのFormulaが
更新されたら変わると思うので都度変更。
(brew --cellar opensslでCellarの場所がわかる)
> cd /usr/local/sandbox/nginx_lua_module
> make clean
>
> ./configure --prefix=/usr/local/nginx-1.4.0 \
> --add-module=/usr/local/sandbox/nginx_lua_module/ngx_devel_kit \
> --add-module=/usr/local/sandbox/nginx_lua_module/lua-nginx-module \
> --with-cc-opt='-O0 -I/usr/local/Cellar/pcre/8.32/include' \
> --with-ld-opt='-L/usr/local/Cellar/pcre/8.32/lib' \
> --with-openssl=/usr/local/Cellar/openssl/1.0.1e
>
> make -j2
> make install
ビルドし直しして且つmake installが行えたら、
/usr/local/nginx-1.4.0以下に、lua-nginx-moduleで利用するための
ライブラリを保存するためのディレクトリを作成する。
> mkdir -p /usr/local/nginx-1.4.0/lua-lib
作成したディレクトリの中に対しlua-resty-stringライブラリのリポジトリを
ローカルにcloneする。cloneし終えたら最新のリリースタグにcheckout…
cd /usr/local/sandbox/nginx-1.4.0/lua-lib
git clone git://github.com/agentzh/lua-resty-string.git
cd lua-resty-string
git checkout -b v0.08 v0.08
lua-resty-stringが使えるか確認する
lua_package_pathをnginx.confに追記し、
且つsha1でハッシュを求める簡易なスクリプトを書き、
lua-resty-stringライブラリを利用できるか確認する。
worker_processes 1;
error_log logs/error.log;
pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
lua_package_path "/usr/local/nginx-1.4.0/lua-lib/lua-resty-string/lib/?.lua;;";
include mime.types;
default_type text/html;
access_log logs/access.log;
sendfile on;
keepalive_timeout 65;
gzip on;
server {
listen 8080;
server_name localhost;
access_log logs/host.access.log;
location / {
root html;
try_files $uri $uri/ /index.html;
}
location /sha1 {
content_by_lua '
local resty_sha1 = require "resty.sha1"
local str = require "resty.string"
local sha1 = resty_sha1:new()
sha1:update("Hello world")
local digest = sha1:final()
ngx.say(str.to_hex(digest))
';
}
}
}
設定を書き換えたらnginxを起動し、
http://localhost:8080/sha1 にアクセス。
OK。
アクセスカウンターの実装
カウンターの実装を行う。
訪問日を覚える為のキーを生成するには...
カウンターのその日の値を覚えるためのキーを生成しないといけない
IPアドレス -> $remote_addr
UA -> $http_user_agent
時刻をどうにかしないといけない。
os.date("%Y-%m-%d")
で、日付を
整形済みな形で文字列として返してくれるのでこれを使う。
以下の形でconcatしよう。
$remote_addr + $http_user_agent + os.date("%Y-%m-%d")
これでいいはず。試そう。
$remote_addr
や$http_user_agent
などは
ngx.var.remote_addr
, ngx.var.user_agent
として
lua-nginx-module内で置換が可能なので、引数として
lua構文に渡す必要は無くなるので、置き換える。
nginx.confを以下の形に書き換える。
worker_processes 1;
error_log logs/error.log;
pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
lua_package_path "/usr/local/nginx-1.4.0/lua-lib/lua-resty-string/lib/?.lua;;";
include mime.types;
default_type text/html;
access_log logs/access.log;
sendfile on;
keepalive_timeout 65;
gzip on;
server {
listen 8080;
server_name localhost;
access_log logs/host.access.log;
location / {
root html;
try_files $uri $uri/ /index.html;
}
location /concat {
content_by_lua '
local date = os.date("%Y-%m-%d")
ngx.say(ngx.var.remote_addr .. "+" ..
ngx.req.get_headers()["User-Agent"] .. "+" ..
date)
';
}
}
}
nginxを再起動して、ブラウザーでhttp://localhost:8080/concat にアクセスしよう。
きちんとIPアドレス+UserAgent+日付(YYYY-mm-dd)で文字列が連結されている!こいつをsha1でメッセージダイジェストを生成すればキーとして使える。
単純なアクセスカウンターを作る
ただただカウントアップするカウンターを仮で作ってみる。
share DICTでのcountでも作ろう。
nginx.confについて以下の形に書き換えよう。
worker_processes 1;
error_log logs/error.log;
pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
lua_package_path "/usr/local/nginx-1.4.0/lua-lib/lua-resty-string/lib/?.lua;;";
include mime.types;
default_type text/html;
access_log logs/access.log;
sendfile on;
keepalive_timeout 65;
gzip on;
lua_shared_dict count 5m;
server {
listen 8080;
server_name localhost;
access_log logs/host.access.log;
location / {
root html;
try_files $uri $uri/ /index.html;
}
location /count/up {
content_by_lua '
local count = ngx.shared.count
val, flags = count:get("up")
if val == nil then
count:set("up", 0)
end
val, err = count:incr("up", 1)
ngx.say(val)
';
}
}
}
countという共有ディクショナリを作成し、
それにupというキーを設定し、最初にキーなどの
初期化が終わってなければ0で初期化し…とかそんな感じで書く。
変更し終えたらnginxを再起動しよう。
そして、http://localhost:8080/count/up にアクセスする
おお。
Command + Rでリロードしまくろう
カウンターはこれでよし。
問題はリロードすると延々とカウントアップしてしまう点だ。
一度カウントしたらその日は同じIPアドレス、UAであればリロードしてもカウントアップしないようにする
そこで、IPアドレス+UA+日付(YYYY-mm-dd)な文字列から
ダイジェスト文字列を作成して、既に今日時点で
クライアントがアクセスしていたのか?を確認するようにする。
nginx.confを以下の形に書き換える。
worker_processes 1;
error_log logs/error.log;
pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
lua_package_path "/usr/local/nginx-1.4.0/lua-lib/lua-resty-string/lib/?.lua;;";
include mime.types;
default_type text/html;
access_log logs/access.log;
sendfile on;
keepalive_timeout 65;
gzip on;
lua_shared_dict count 5m;
lua_shared_dict check_of_user_access 5m;
server {
listen 8080;
server_name localhost;
access_log logs/host.access.log;
location / {
root html;
try_files $uri $uri/ /index.html;
}
location /count/up {
content_by_lua '
local count = ngx.shared.count
local check_of_user_access = ngx.shared.check_of_user_access
local date = os.date("%Y-%m-%d")
is_exists_val, flags = count:get(date)
if is_exists_val == nil then
count:set(date, 0)
end
local user_access_key = (
ngx.var.remote_addr .. "+" ..
ngx.req.get_headers()["User-Agent"] .. "+" ..
date
)
is_access_val, flags = check_of_user_access:get(user_access_key)
if is_access_val == nil then
check_of_user_access:set(user_access_key, 1)
new_incr_val, err = count:incr(date, 1)
end
latest_count = count:get(date)
ngx.say(latest_count)
';
}
}
}
書き換えたらnginxを再起動しよう。
そして先程のようにリロードしまくってみる。
おお…!!
UA変えたいからFirefoxでロードして試してみよう
カウントアップはされるはず...
2である。カウントアップができている。
システムの日付を一つ進めて、
改めてアクセスする。
どうせなら前日のカウント数を得たい
前日のカウント数を得たいんで、
試すために新しくpathのroutingを書いて、
試しに前日の日付を取れるようにしてみる。
location ~ ^/([0-9]+)/days/ago {
content_by_lua '
local count = ngx.shared.count
local now = os.date("*t")
local point = tonumber(ngx.var[1])
local specific_day = os.date
"%Y-%m-%d",
os.time({
day=tonumber(now.day-point)
month=now.month,
year=now.year
}
ngx.say(specific_day)
';
}
早速 http://localhost:8080/1/days/ago にアクセス。
今日は2013/5/5だ。
おお。と、ついでに100日前についてもきちんと
導き出せるか確認する。
http://localhost:8080/100/days/ago にアクセス。
5/5の100日前である、2013/1/25の日付が取れている。
今後、この日付をキーにしてcount(date)のテーブルからカウント数を取れば良い。
これで大丈夫そうだ。
日付をキーとしていたから、count:get(specific_day)
という形に書き直す。
完成...
完成したnginx.confは以下の形になる。
/count/upにアクセスすれば今日の訪問者数を、
/1/days/agoにアクセスすれば1日前の訪問者数を返してくれる。
worker_processes 1;
error_log logs/error.log;
pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
lua_package_path "/usr/local/nginx-1.4.0/lua-lib/lua-resty-string/lib/?.lua;;";
include mime.types;
default_type text/html;
access_log logs/access.log;
sendfile on;
keepalive_timeout 65;
gzip on;
lua_shared_dict count 5m;
lua_shared_dict check_of_user_access 5m;
server {
listen 8080;
server_name localhost;
access_log logs/host.access.log;
location / {
root html;
try_files $uri $uri/ /index.html;
}
location /count/up {
content_by_lua '
local count = ngx.shared.count
local check_of_user_access = ngx.shared.check_of_user_access
local date = os.date("%Y-%m-%d")
is_exists_val, flags = count:get(date)
if is_exists_val == nil then
count:set(date, 0)
end
local user_access_key = (
ngx.var.remote_addr .. "+" ..
ngx.req.get_headers()["User-Agent"] .. "+" ..
date
)
is_access_val, flags = check_of_user_access:get(user_access_key)
if is_access_val == nil then
check_of_user_access:set(user_access_key, 1)
new_incr_val, err = count:incr(date, 1)
end
latest_count, flags = count:get(date)
ngx.say(latest_count)
';
}
location ~ ^/([0-9]+)/days/ago {
content_by_lua '
local count = ngx.shared.count
local now = os.date("*t")
local point = tonumber(ngx.var[1])
local specific_day = os.date(
"%Y-%m-%d",
os.time({
day=tonumber(now.day-point),
month=now.month,
year=now.year
}))
record, flag = count:get(specific_day)
if record == nil then
record = 0
end
ngx.say(record)
';
}
}
}
ついでに
jQueryという便利なモノを使いつつ、このAPI(?)にアクセスする
フロントサイドの処理でもhtmlファイルに練り込みつつ書くか。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<title>ようこそ僕のホームページへ</title>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$.get("/count/up", function(data) {
$("#today").html(data);
});
$.get("/1/days/ago", function(data) {
$("#one-days-ago").html(data);
});
});
</script>
</head>
<body>
<h1>ようこそ僕のホームページへ</h1>
<div>
<p>今日の訪問者数:<span id="today"></span></p>
<p>昨日の訪問者数:<span id="one-days-ago"></span></p>
</div>
</body>
</html>
このhtmlファイルをnginxのrootディレクトリ
(この記事の通りにビルドした場合はhtmlディレクトリ以下)
にコピーし、ブラウザからアクセスする。
これで目的が達成できた。
アクセスカウンターが実装できたし、
1日の楽しみが増えることになる。
問題点
nginxのプロセスを再起動すると
カウントしていた値が消えるし(オンメモリなわけで...)、
そもそも準備するのがだるい。