高速ログフォーマットと簡単な解析機能の実装

先日の高速ログフォーマットの提案を実際に実装してみました。
ログを MessagePackシリアライズしてバイナリ形式で出力することで、生のバイト列をBase64などでエンコードしなくてもログに書き出せるようにし、また高速に解析できるようにしようというアイディアです。


twitterやコメントでいただいたアイディアを参考に、いちばんシンプルなフォーマットにしてみました:

ログサイズ [アクセスログ, {"時刻": 1230415655, "URL": ["index.html"], "UA": "Mozilla/5.0 ... Firefox 3.0.5"}]
ログサイズ [アクセスログ, {"時刻": 1230415656, "URL": ["index.html"], "UA": "Mozilla/5.0 ... Safari/525.13"}]
ログサイズ [アクセスログ, {"時刻": 1230415656, "URL": ["top", "login.png"], "UA": "Mozilla/5.0 ... Safari/525.13"}]
ログサイズ:4bytes [ログ種別:String, {フィールド名:String: フィールド値:*, ...}:Map]

「ログ種別」と「フィールド名」に使える文字列は、最大8バイトに制限しています。ログに出力するときはこの文字列を64ビットの整数に変換して格納します。
MessagePackでは小さい整数ほど少ないバイト数でシリアライズできるので、「ログ種別」や「フィールド名」の文字列が短いほど(1バイト, 2バイト, 4バイト以下, 8バイト以下)ログのサイズが小さくなります。


各ログの先頭には4バイトでログのサイズを付加しています。これによってログファイルに余分なデータが入り込んだりログが破損したときでも、壊れたログだけをスキップしてそれ以降の正常なログは問題なく解析し続けることができます。


ログ解析のプログラムはRubyで簡単に実装してみました。
使い方は以下の通りで、MapReduceのようにmapとreduceの二つの関数を渡してログの解析ができます。

# 「len」フィールドの値が3より大きいログの「msg」フィールドを表示
$ logparse.rb logfile.mapack 'self["msg"] if self["len"] > 3'  'self'

# 「msg」フィールドの値が"target"であるログの総数をカウント
$ logparse.rb logfile.msgpack '1 if self["msg"] == "target"'  'count'

mapとreduceの引数はRubyのスクリプトとして解釈され、instance_evalで実行されます。


ログ出力のAPIC++で実装しています。

logpack lpk("logfile.mpack");
lpk.log("access", "key1", 1, "key2", std::string("value2"));
// lpk.log(ログ種別, フィールド名1, フィールド値1, フィールド名2, フィールド値2, ...);

型安全な可変長のtemplateで実装しており、ログ種別やフィールド名が8バイトより大きいとコンパイルエラーになります。
フィールド値にstd::string型の変数を渡せば文字列としてシリアライズされ、int型の変数を渡せば数値としてシリアライズされるというように、渡された変数に従って型が推論されます。