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

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

よくあるcgiサイトにある
訪問カウンターを[nginx](http://nginx.org/) + [lua-nginx-module](https://github.com/chaoslawful/lua-nginx-module)を利用して
作成したいと思います。

こんな感じなの。

![welcome-to-underground](https://dl.dropboxusercontent.com/u/614755/qiita/lua-nginx-module-3/screen-11.png)

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

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

[nginx及びlua-nginx-moduleの環境構築はこちらを参照してください。](http://qiita.com/items/4185432667569b6b3f35)

動作環境ですが、

- 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](http://wiki.nginx.org/HttpLuaModule#ngx.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](https://github.com/agentzh/lua-resty-string) を利用する。

lua-resty-stringを利用するにはLuaJITを利用しないといけない。
[FFI ライブラリ](http://luajit.org/ext_ffi.html) を利用してCの構造体を宣言するからだ…
しかもlua-resty-stringを利用するためにはopensslオプションを有効にした形で
nginxをビルドしないといけないので、ビルドを以下のビルドオプションを渡して
ビルドし直す。

opensslなどのバージョンはopenssl及びhomebrewのFormulaが
更新されたら変わると思うので都度変更。

(brew --cellar opensslでCellarの場所がわかる)

```sh
> 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…

```sh
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ライブラリを利用できるか確認する。


```nginx
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](https://dl.dropboxusercontent.com/u/614755/qiita/lua-nginx-module-3/screen-01.png)

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を以下の形に書き換える。


```nginx
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](https://dl.dropboxusercontent.com/u/614755/qiita/lua-nginx-module-3/screen-02.png)

きちんとIPアドレス+UserAgent+日付(YYYY-mm-dd)で文字列が連結されている！こいつをsha1でメッセージダイジェストを生成すればキーとして使える。

##単純なアクセスカウンターを作る

ただただカウントアップするカウンターを仮で作ってみる。
share DICTでのcountでも作ろう。
nginx.confについて以下の形に書き換えよう。


```nginx
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](https://dl.dropboxusercontent.com/u/614755/qiita/lua-nginx-module-3/screen-03.png)

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

![count-up-reaload](https://dl.dropboxusercontent.com/u/614755/qiita/lua-nginx-module-3/screen-04.gif)


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

##一度カウントしたらその日は同じIPアドレス、UAであればリロードしてもカウントアップしないようにする

そこで、IPアドレス+UA+日付(YYYY-mm-dd)な文字列から
ダイジェスト文字列を作成して、既に今日時点で
クライアントがアクセスしていたのか？を確認するようにする。

nginx.confを以下の形に書き換える。


```nginx
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](https://dl.dropboxusercontent.com/u/614755/qiita/lua-nginx-module-3/screen-05.gif)


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

![count-up-reload-firefox](https://dl.dropboxusercontent.com/u/614755/qiita/lua-nginx-module-3/screen-06.png)

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

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

![next-day-setting](https://dl.dropboxusercontent.com/u/614755/qiita/lua-nginx-module-3/screen-07.gif)

![next-day-access](https://dl.dropboxusercontent.com/u/614755/qiita/lua-nginx-module-3/screen-08.png)

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

前日のカウント数を得たいんで、
試すために新しくpathのroutingを書いて、
試しに前日の日付を取れるようにしてみる。

```nginx
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](https://dl.dropboxusercontent.com/u/614755/qiita/lua-nginx-module-3/screen-09.png)


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

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

![100-days-ago](https://dl.dropboxusercontent.com/u/614755/qiita/lua-nginx-module-3/screen-10.png)


5/5の100日前である、2013/1/25の日付が取れている。
今後、この日付をキーにしてcount(date)のテーブルからカウント数を取れば良い。

これで大丈夫そうだ。
日付をキーとしていたから、`count:get(specific_day)`という形に書き直す。

#完成...

完成したnginx.confは以下の形になる。

/count/upにアクセスすれば今日の訪問者数を、
/1/days/agoにアクセスすれば1日前の訪問者数を返してくれる。

```nginx
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ファイルに練り込みつつ書くか。

```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ディレクトリ
([この記事の通り](http://qiita.com/items/4185432667569b6b3f35)にビルドした場合はhtmlディレクトリ以下)
にコピーし、ブラウザからアクセスする。

![welcome-to-underground](https://dl.dropboxusercontent.com/u/614755/qiita/lua-nginx-module-3/screen-11.png)

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

#問題点

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