LoginSignup
8
10

More than 5 years have passed since last update.

nginx + lua-nginx-module でホームページ訪問カウンターを作る

Last updated at Posted at 2013-05-05

ホームページ訪問カウンター

よくあるcgiサイトにある
訪問カウンターをnginx + lua-nginx-moduleを利用して
作成したいと思います。

こんな感じなの。

welcome-to-underground

レッツPHPさんのサンプルにあるナンカイメカウンターのようなものです。
昔良く見たアレです。ユーザーが全体で何回そのサイトにアクセスしたか、
を記録する奴。

というのをnginxとlua-nginx-moduleで実装したいと思います。

nginx及びlua-nginx-moduleの環境構築はこちらを参照してください。

動作環境ですが、

  • MacOS X 10.8
  • nginx 1.4.0
  • lua-nginx-module v0.8.1

となります。

訪問カウンターの仕様

1) 特定の人をIPアドレス+UAで切り分けして判別する
2) 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 にアクセス。

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 にアクセスしよう。

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 にアクセスする

count-up

おお。
Command + Rでリロードしまくろう

count-up-reaload

カウンターはこれでよし。
問題はリロードすると延々とカウントアップしてしまう点だ。

一度カウントしたらその日は同じ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を再起動しよう。
そして先程のようにリロードしまくってみる。

count-up-reload-2

おお…!!
UA変えたいからFirefoxでロードして試してみよう
カウントアップはされるはず...

count-up-reload-firefox

2である。カウントアップができている。

システムの日付を一つ進めて、
改めてアクセスする。

next-day-setting

next-day-access

どうせなら前日のカウント数を得たい

前日のカウント数を得たいんで、
試すために新しく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だ。

2013-05-04

おお。と、ついでに100日前についてもきちんと
導き出せるか確認する。

http://localhost:8080/100/days/ago にアクセス。

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ディレクトリ以下)
にコピーし、ブラウザからアクセスする。

welcome-to-underground

これで目的が達成できた。
アクセスカウンターが実装できたし、
1日の楽しみが増えることになる。

問題点

nginxのプロセスを再起動すると
カウントしていた値が消えるし(オンメモリなわけで...)、
そもそも準備するのがだるい。

8
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
10