lua-resty-woothee というモジュールを書きました

Lua Advent Calendar 2014 7日目の記事です。


wootheeというUA解析プロジェクトを最近知ったのですが、
Lua版が無さそうだったので書いてみました。
本当はLua単体で動くようにしたかったんですが、諸事情によりOpenresty依存となっております。
理由は後述。

ちなみに書いた動機など。

  • ちょうど、エンドユーザのUA解析してDB登録したりゴニョゴニョ出来たらいいなーと思っていた
  • Nginx+Luaを書き始めているが、がっつりLuaを触っていたわけではないのでライブラリっぽいのも練習がてら書いてみたかった
  • 期待された https://twitter.com/songmu/status/525610905946447872

使い方

READMEそのままですが…

インストール
luarocks install https://raw.githubusercontent.com/toritori0318/lua-resty-woothee/master/lua-resty-woothee-dev-1.rockspec

または git clone でリポジトリダウンロード後に
lua_package_path をよしなに指定すると使えるようになります。
アップデートしたい場合は、再インストールしてNginxをリスタートするだけで行けるはず。

基本
server {
    location /test {
        content_by_lua '
            local woothee = require "resty.woothee"

            -- parse
            local r = woothee.parse(ngx.var.http_user_agent)
            --  => {"name": "xxx", "category": "xxx", "os": "xxx", "version": "xxx", "vendor": "xxx"}

            -- crawler?
            local crawler = woothee.is_crawler(ngx.var.http_user_agent)
            --  => true

            ngx.header.content_type = "text/plain"
            ngx.say(r.name)
        ';
    }
}

あとはparseメソッドにUserAgent渡せば結果がtableで返ってきます。簡単。

応用編

parseした結果をアクセスログに直接埋め込んだり、プロキシしているバックエンドサーバに渡したりと
いろいろ応用が効きそうですね。
この辺りのサンプルもREADMEに書いてあるので参考にどうぞ。
アクセスログ出力した時はこんな感じでparseした情報を付与できます。

xxx.xxx.xxx.xxx - - [01/Nov/2014:15:47:38 +0000] "GET /test HTTP/1.1" 200 17 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36" "Chrome" "pc" "Mac OSX" "38.0.2125.111" "Google" "10.9.5"
xxx.xxx.xxx.xxx - - [01/Nov/2014:15:47:41 +0000] "GET /test HTTP/1.1" 200 18 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:33.0) Gecko/20100101 Firefox/33.0" "Firefox" "pc" "Mac OSX" "33.0" "Mozilla" "10.9"
xxx.xxx.xxx.xxx - - [01/Nov/2014:15:48:01 +0000] "GET /test HTTP/1.1" 200 17 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/600.1.17 (KHTML, like Gecko) Version/7.1 Safari/537.85.10" "Safari" "pc" "Mac OSX" "7.1" "Apple" "10.9.5"

Openresty依存になった理由など

実は最初はLua単体で動くように書いていました。
実装がだいたい終わってよっしゃーテストやるぞーとテスト通し始めたら

で、あとで知ったんですが(オイ) Luaのは正規表現というか独自パターンマッチングなのでそりゃ通らないわけですね。
最初は独自の方に寄せようとしたんですが「(a|b)」とか「()?」とか使えなくてだいぶ辛い…

正規表現ライブラリ使うことも考えたんですが
あまり他のライブラリに依存するとユーザの導入障壁が高くなるでしょうし、
自分は(そして多分殆どの人が)Openrestyでしか使わないでしょうし、
OpenrestyはPCREの正規表現実装を含んでいるのでそれ使えば解決するし、
という感じでOpenresty依存にしました。
(関連する会話 https://twitter.com/toritori0318/status/527151195719077891


この辺り、うまく共存してOpenresty依存無くすことも出来るかもしれないですが
自分は必要なかったのでそこは頑張りませんでした。
もしLua単体で動かしたいという要件があれば挑戦してみてください。



まとめ

Nginx+luaで解析することによって
高いパフォーマンスで、かつ汎用的に解析結果を使えるようになりました。
大抵はアプリケーションサーバよりNginxのほうがリソース空いていることが多いと思いますし、
そちらによけいな処理をお任せしてしまうのは悪いことではないと思います。
よろしければ使ってみてください〜

おまけ1:luayamlモジュールでハマった

http://yaml.luaforge.net/
これの最新版使ったらバグなのかわからないけどtestsetsのparse結果がおかしくてめっちゃハマった。つらい*1

おまけ2:Nginxのset変数はLuaから定義出来ない

Nginxのset変数もライブラリの中からよしなに出来ればもう少しシンプルに書けるかなと思いましたが
"That is, nginx variables cannot be created on-the-fly." とのことでした:(

おまけ3:Openrestyモジュールのテストについて

Openrestyモジュールのテスト、coreから野良モジュールまでほとんどperlのTest::Nginxで書かれています。
perl...うっ」となるかもしれないですが、実際にperlを知らなくても書けます。
なかなか面白い手法だったので紹介してみます。

# vim:set ft= ts=4 sw=4 et:

use Test::Nginx::Socket::Lua;
use Cwd qw(cwd);

repeat_each(1);

plan tests => repeat_each() * (3 * blocks());

my $pwd = cwd();

our $HttpConfig = qq{
    lua_package_path "$pwd/lib/?.lua;;";
};

$ENV{TEST_NGINX_RESOLVER} = '8.8.8.8';

no_long_string();
#no_diff();

run_tests();

__DATA__

=== TEST 1: basic
--- http_config eval: $::HttpConfig
--- config
    location /t {
        content_by_lua '
            local woothee = require "resty.woothee"
            ngx.say("OK")
        ';
    }
--- request
    GET /t
--- response_body
OK
--- no_error_log
[error]

たとえばこちら、lua-resty-wootheeのテストです。
__DATA__ 以下のところだけ変えればOKです。
見たらわかりますが、Nginxコンフィグをそのまま記述して、期待するレスポンスを書く感じです。簡単ですね。
(ただ、printデバッグなどが非常に面倒なのが少し辛い…)

おまけ4:ベンチマーク

単純なLuaスクリプトと比較し、どの程度性能劣化するかを検証してみました。

now()を返すだけのLuaスクリプト
ab -c 10 -n 50000 -H "User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)" http://localhost/now
Requests per second:    10291.40 [#/sec] (mean)
woothee.parse ON
ab -c 10 -n 50000 -H "User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)" http://localhost/test
Requests per second:    8541.17 [#/sec] (mean)

たぶんUser-Agentによって多少は性能変化すると思いますが、許容範囲ではないでしょうか。


*1:datasetのテストをするときのみyamlモジュールに依存している