Labeled Tab Separated Values (LTSV) ノススメ

追記(2/8 11:30)

追記ここまで

Labeled Tab Separated Values (LTSV) というのは、はてなで使っているログフォーマットのことで、広く使われているTSV(Tab Separated Value)フォーマットにラベルを付けて扱い易くしたものです。はてなでは、もう3年以上、このフォーマットでログを残していて、one-linerからfluentd、Apache Hiveまで幅広く便利に使えています。

ログフォーマットに期待されることは、

  • フォーマットが統一されている → 共通のツールで集計し易い
  • 新しいフィールドの追加が容易 → サービス固有のデータをログに残したいことはよくある。かつ、複数サービスに渡って、別々のエンジニアが触っても混乱しない
  • one linerで操作しやすい → grepした後でsortなり、uniqなりで集計するのはよくあること

の3点だと考えています。

これまでにも、ログフォーマットを扱い易くする取り組みとして、SyslogでもRFC5425で構造化の仕様が入ったり*1していますが、いまいち普及しているとは言えません。Apache標準のCombined Log Formatを独自拡張して頑張る、というのがよくある光景なんじゃないかと思います。ただ、サービスごとの要求に応じて、どんどん独自拡張していると個別にParserの定義を書かないといけなくなったり、fluentdで扱う際にも頑張ってサービスごとの定義を正規表現で書かないといけない羽目になって、どんどん辛くなります。

Labeled Tab Separated Values (LTSV) とは

そこではてなでは、3年ほど前*2からLabeled Tab Separated Values (LTSV) によるログフォーマットを全面的に採用しています。

Labeled Tab Separated Valueは、

time:[28/Feb/2013:12:00:00 +0900]	host:192.168.0.1	req:GET /list HTTP/1.1	status:200	size:5316	referer:- ua:Mozilla/5.0	taken:9789	isrobot:1	dos:-	harddos:-	cache:-

というもので、各フィールドがタブで区切られ、フィールドの頭にラベルを付けています。

ApacheのLogFormat定義は

LogFormat "host:%h\ttime:%t\treq:%r\tstatus:%>s\tsize:%b\treferer:%{Referer}i\tua:%{User-Agent}i\ttaken:%D\tisbot:%{Isbot}e\tdos:%{SuspectDoS}e\tharddos:%{SuspectHardDoS}ecache:%{X-Cache}o" ltsv

となり、nginxでは

log_format tsv "time:$time_local\thost:$remote_addr\treq:$request\tstatus:$status\tsize:$body_bytes_sent\treferer:$http_referer\tua:$http_user_agent\treqtime:$request_time\tcache:$upstream_http_x_cache";

となります。

このLabeled Tab Separeted Valueフォーマットのメリットは、まずログを定義する時に各フィールドの順序を意識しなくてよく、各プロダクトで容易に独自拡張できる、ということにあります。上の例では、X-Cacheヘッダやmod_dosdetectorの判定結果を残したりしています。はてなでの運用では、ラベルをWikiで管理しており、新しいラベルを作る際には記録してもらうようにしています。

Labeled Tab Separated Valueを扱う

LTSVはTSVの拡張ですので、cutコマンドで簡単に切り分けられます。例えば、ステータスコードごとに数えるとすると以下のようになります。

% tail -n 100 /var/log/httpd/access_log.ltsv | cut -f 5 | sort | uniq -c
     97 status:200
      2 status:301
      1 status:404

ただ、これだけだと、ラベルが出て、ちょっと分かり易いぐらいですかね。

では、次にfluentd pluginを見てみます。コードは、https://github.com/stanaka/fluent-plugin-tail-labeled-tsvにあります(近々Gem化します)。fluentdでの定義は、

<source>
  type tail_labeled_tsv
  path /var/log/nginx/access_log.ltsv
  tag access_log
  pos_file /tmp/fluent.log.pos
</source>

のようになります。in_tail pluginのように非標準のフォーマットでも頑張って定義を正規表現で書く必要がありません。Webサーバー側での定義を変更してもfluentd側の設定を触らずにすむのも便利です。

さらにApache HadoopのHive用のSerDe(シリアライザ・デシリアライザ)も用意しています。コードは、 https://github.com/hatena/KeyValuePairsDeserializer にあります。

これにより、Amazon EMR上のHiveで簡単にテーブルを定義することができます。

ADD JAR s3://sample/_lib/hadoop/hive/serde/hatena-serde.jar;

CREATE EXTERNAL TABLE logs (
  time string, host string,
  req string, status string, size string,
  referer string, ua string
) 
PARTITIONED BY (dt string)
ROW FORMAT SERDE 'jp.ne.hatena.hadoop.hive.serde.KeyValuePairsDeserializer';

ALTER TABLE sample ADD PARTITION (dt='2013-01-01') LOCATION 's3://sample/sample_log.ltsv.gz';

その後、Hiveにより、MapReduceでログ解析を行うことができます。例えば、

select count(distinct referer) from logs

のようなHiveQLを流すことで、1年分のログでも、がりっと処理することができます。

まとめ

はてなでは、Labeled Tab Separated Valueによるログフォーマットで統一していて、one-linerからfluentd、Amazon EMR上のHiveまで使えて便利!という話でした。

*1:ところで、Parser書いている人もいますね。 http://blog.kentarok.org/entry/20101217/1292606627

*2:確認すると2009年10月頃からでした