KubernetesにデプロイするDockerコンテナのログ形式を選ぶ

## TL;DR;

プログラム的にparseやすい、構造化されたログメッセージを採用するとよいです。

また、構造化されたログメッセージにするためには、

- ndjson形式(俗称? jsonlog)
- bunyan形式
- logfmt形式

のいずれかをおすすめします。

## なぜ構造化されたログメッセージがよいか?

アプリケーションによって、デバッグや稼働確認、障害調査等の目的でログに含めたいデータは異なります。

- アプリケーションの部分部分を担当したエンジニアによって、ログに含まれるデータのフォーマットが異なると、それが利用しづらい状態になってしまいます。

また、

- KubernetesのCluster-Level Loggingにはfluentdが使われるということと、
- アプリケーションが後の分析のためにログメッセージに付与しておきたいメタデータと、アプリケーションが動作している環境との関連付けのためにKubernetesが付与するメタデータの両方が検索・分析可能であってほしい

の２点を考慮しておくと後で困りません。

### フォーマットが部分分で違うとどうなるか

例) Aさんは、

`An error occurred while processing the foo request req_id=... req_params=... context_id=...`

のような形式、Bさんは

`{"msg":An error occurred while processing the foo request", "req_id":...,"req_params":...,"context_id":...}`

のような形式で同じアプリから異なる形式でログメッセージを出力するようにしたとします。また、そのログをFluentdで収集することとします。その場合、大きくわけて以下の様な構成になります。

* KubernetesでアプリのPod内に配備したサイドカーコンテナに配置Fluentdを置き、それぞれのログフォーマットに対応したfluentd.confを渡す
* ノード毎にデプロイしたFluentdに、アプリ各ログフォーマットに対応したfluentd.confを渡す

ログフォーマットを複数持ってしまったがために、前者はアプリ毎に（おそらくはアプリエンジニアが）Fluentdをメンテすることになってしまいます。後者はインフラエンジニアが各アプリのログフォーマットを理解してFluentdをメンテするか、またはアプリエンジニアがログフォーマットの対応のためだけにアプリではなくクラスタのこと（＝ノード毎に用意しているFluentd）を理解してFluentdに手をいれることになってしまいます。

各アプリのログ要件の最大公約数になれるような十分に汎用的な構造化されたログメッセージの形式を決めておけば、このようなひずみ・手間がなくなることが期待できます。

### Cluster-Level Logging

ログに付与されるアプリ起因のメタデータと、Kubernetes起因のメタデータの両方を捨てずにマージする、というのをfluentdでやることになるので、結果的にアプリが出力するのはFluentdで簡単にparseできるしかも構造化されたログフォーマットが良いです。例えばmulti-line textはそういう意味では最悪です（人間からすると読みやすいですが）

## 各形式の解説

### logfmt

- Heroku界隈で使われているシンプルなログ形式
- 機械も人間も解釈しやすいログ形式
- さらに人間が解釈しやすいように、[lviz](https://github.com/brandur/hutils)にパイプしてログを閲覧する、ということもできる
- 気になるところ
  - logfmtで唯一気になるのは、ログの要件によっては「logfmtがネストできない」という点がネックになりそうなこと

### ndjson (Newline delimited JSON) ~~jsonlog~~

※ ~~便宜上「jsonlog形式」と表現していますが、ログメッセージ構造化のための１行1JSONのログ形式の正式な仕様はありません。~~[^1]

http://ndjson.org/

- 各行1 JSONオブジェクトのログ形式
- ndjson形式…とは明言・明記されていませんが、Docker、Concourse、Fluentd界隈で使われている
  - Fluentd: https://blog.treasuredata.com/blog/2012/04/26/log-everything-as-json/
  - Docker: https://github.com/docker/docker/blob/master/pkg/jsonlog/jsonlog.go
- 私見ですが、JSONの表現力、機械による解釈のしやすさ、人間にとってはビュワー・変換用ツールの充実ぷりが魅力
  - i.e. 人間がそのまま読むにはつらいが、jqで様々なログを統一的な方法で変換・フィルタリングしたり、pretty-printerのようなものをかませば問題なく読める

### bunyan形式

[^1]: ndjsonという仕様があるそうです! 

bunyan形式はjsonlog形式に必要なフィールドをいくつか規定したものです。

- `"v":0`
- `"level":ログレベルを表す数値`
- `"name":"アプリ名"`
- `"hostname":"ホスト名"`
- `"pid":pidを表す数値`
- `"time":"日時文字列"`
- `"msg":"ログメッセージ本文"`

bunyan形式に対応したログであれば、bunyan cliで変換・閲覧することが可能です。bunyan形式はndjson形式そのものではありませんが、ndjson形式と同じメリット・デメリットが存在します。例えば、（当然ですが）bunyan形式のログをjqで変換することができたり、Fluentdで構造を壊さずに収集することができたりします。

bunyan形式のndjson形式に対する優位性は、ゆるくても仕様が存在して、その仕様の周りにツールが少しでも存在する、ということです。bunyan形式はndjson形式そのものではありませんが、ndjson形式と同じメリット・デメリットが存在します。

なおndjson形式のサブセットで、周辺ツールが少しでも存在するものはbunyanだけという認識です。他にもあればぜひ教えてください!

## 実装例

Kubernetes + Fluentd + GCP Stackdriver Logging

https://github.com/mumoshu/kube-fluentd/blob/master/fluent.conf

* /var/log/containersにkubernetesが全podのログをはくので、それをホスト経由でfluentdpodが読み取って、Kubernetesのメタデータ（pod名、ノード名等）を付与してstackdriverに流していく
* そのとき、podがndjson形式のログをはいている場合、もともとのアプリログに含まれる構造化されたデータと、td-agentが付与するメタデータがマージされて、アプリ起因のフィールドでもtd-agent起因のフィールドでも検索できるようになる

## Thanks!

- ndjsonについては[@eiel](hhttp://qiita.com/eielh)に教えてもらいました。ありがとうございます!