ApacheのアクセスログをMessagePack形式で出力するためのモジュールを作りました
Apacheモジュールのログ出力、こんどはMessagePack版を作成しました。続いてはこちらをご紹介します。
Apacheのアクセスログを使い、ユーザアクセスの集計やパターン解析などというのは一般にどこでもやられていることだと思います。通常のアクセスログはテキストファイルなので、集計を行うためにスクリプト上で扱える変数・オブジェクト化が必要になりますね。1行ごとの各ログ項目を取り出すのに正規表現を使ったり、cutやawkなどを使い空白で分割するなど、色々工夫されていることと思います。
今回、MessagePack版のアクセスログ出力をやってみようと思い立ったのは、アクセスログをあらかじめ構造化済みの状態で保存しておければ、読み込みの際の解析する手間を省くことで解析処理の高速化が期待できるのではないか、そう考えたためです。MessagePackであれば、PythonやRubyはじめ様々なスクリプト言語でのバインディングを備えており、取り扱いも簡単ですから、ファイル読み込みからログデータの取り出しまでを速やかに行い、その分集計処理の実装に集中することができるわけです。また、時折イレギュラー的に長かったり、予期しない文字がリクエストに入る等したためにログ解析の正規表現がエラーになったりすることがありますが、構造化済みの形式で保存するのであれば、この問題も回避できます。
何より、元々構造化されたデータがアクセスログ上では一度文字列になり、これを解析する時に再 度解釈処理されるという二度手間になっているわけで、テキストデータ化せずデータ構造を保った バイナリとしておくのはこの手間を回避する目的も持っています。
ソースはこちらです。
→http://log.blog.klab.org/support/mod_log_msgpack/mod_log_msgpack-0.1.0.tar.gz
それでは、実際にどのような変更を加えたのでしょうか。モジュールの中身を見てみましょう。
mod_log_msgpackは、従来型のログ出力モジュールであるmod_log_configからその多くを受け継いでいます。mod_log_configの構造がそのまま、mod_log_msgpackにも当てはまるとお考えください。
mod_log_configは、起動直後にLogFormatで定義された書式文字列を読み込み、アクセスログに出力する一行分の内容を定義します。例えば、
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" combined
このようなログ書式の指定があったとします。mod_log_configは、このディレクティブを解釈して下記のような内部的なデータ構造に分解して落とすのです。http: //httpd.apache.org/docs/2.2/ja/mod/mod_log_config.html#formats を参考にしながら、当てはめてみるとこうなります。
(mod_log_config)%h | リモートホスト(を出力する関数をコールバック) |
" " | 空白 |
%l | (identdからもし提供されていれば)リモートログ名 |
" " | 空白 |
%u | リモートユーザ |
" " | 空白 |
%t | リクエストを受け付けた時刻 |
" \"" | 空白+「"」(ダブルクオテーション) |
%r | リクエストの最初の行 |
"\" " | 「"」+空白 |
%>s | 最後のステータス |
" " | 空白 |
%b | レスポンスのバイト数。HTTPヘッダは除く。 |
" \"" | 空白+「"」(ダブルクオテーション) |
%{Referer}i | Referer:ヘッダの内容 |
"\" \"" | 「"」+空白+「"」 |
%{User-agent}i | User-agent:ヘッダの内容 |
"\"" | 「"」 |
このように、%?の箇所とそれ以外(空白等、置き換えない文字列)とでそれぞれ区切り、小片に分けられます。%で始まる項目は対応するコールバック関数を通し、指定のリクエスト情報を文字列に変換します。つまり、リクエスト情報は一旦LogFormatの項目数分(今回18個)の部分文字列に変換され、最後にこれらをすべて連結してログ1行分として出力しています。
mod_log_msgpackではこの部分を利用、アクセスログ1行をMessagePackのarray1個として置き換えて下記の項目のみ保持することとしました。
(mod_log_msgpack)%h | リモートホスト(を出力する関数をコールバック) |
%l | (identdからもし提供されていれば)リモートログ名 |
%u | リモートユーザ |
%t | リクエストを受け付けた時刻 |
%r | リクエストの最初の行 |
%>s | 最後のステータス |
%b | レスポンスのバイト数。HTTPヘッダは除く。 |
%{Referer}i | Referer:ヘッダの内容 |
%{User-agent}i | User-agent:ヘッダの内容 |
つまり、用があるのは%?の項目のみということにして、それ以外の固定文字列に関しては無視して います。%rや%iについていた「"」での囲みも、項目の内容が空白を含みうることを想定して区切 りとしての空白と区別がつかなくなることを回避するためのものなので、arrayとして分割できて あれば、不要なものとなるわけです。
さらに、これらのコールバック関数はrequest_rec構造体(と、%{…}?となったときの{}の中身)を引数にとり、部分文字列を返値とするわけですが、msgpack_packer構造体を渡す引数を1つ追加、ここにログの内容を順次packしていく処理に切り替えています。
static const char *log_remote_host(request_rec *r, char *a) { return ap_escape_logitem(r->pool, ap_get_remote_host(r->connection, r->per_dir_config, REMOTE_NAME, NULL)); }
こちらが元々のmod_log_config版。"%h"に対応するコールバックです。mod_log_msgpackになりま すと下のように変更になりました。
static int _strpack(const char* s, msgpack_packer* pk) { if (s) { size_t n = strlen(s); msgpack_pack_raw(pk, n); msgpack_pack_raw_body(pk, s, n); } else { msgpack_pack_nil(pk); } return 0; } static int log_remote_host(request_rec *r, char *a, msgpack_packer* pk) { const char* str = ap_escape_logitem(r->pool, ap_get_remote_host(r->connection, r->per_dir_config, REMOTE_NAME, NULL)); return _strpack(str, pk); }
log_remot_host()の第3引数が追加され、msgpack_pack_xxxの関数が呼び出されていることが確 認できると思います。
あとはここまでログ内容を集めたmsgpack_packer構造体を、ファイルに書き出す関数 apr_file_write()に渡して書き出しを行います。渡す際、mod_log_msgpackであれば行っている部分文字列の連結の箇所がありますが、mod_log_msgpackでは不要なので省略しています。
static apr_status_t ap_default_log_writer( request_rec *r, void *handle, const char **strs, int *strl, int nelts, apr_size_t len) { char *str; char *s; int i; apr_status_t rv; #if 0 // ここから、部分文字列の連結になるので str = apr_palloc(r->pool, len + 1); for (i = 0, s = str; i < nelts; ++i) { memcpy(s, strs[i], strl[i]); s += strl[i]; } // ここまでスキップ。 #else str = *strs; #endif rv = apr_file_write((apr_file_t*)handle, str, &len); return rv; }***
おおよその変更の概要は以上です。あとは、mod_log_configとの併存を可能にするためにいくつか細かい変更を加えていますが、次回以降に続くということで^^;。