Datadog Logsでklogをよしなに扱う
TL;DR
klog_parser %{regex("[A-Z]"):klog_level}%{date("MMdd HH:mm:ss.SSSSSS"):date}\s+%{integer:thread_num}\s+%{data:file_name}\:%{integer:line}\]\s+%{data:msg}
これで
I0413 11:04:16.323941 1 main.go:176] Communication with server successful
これが
{ "klog_level": "I", "date": 1586775856323, "thread_num": 1, "file_name": "main.go", "line": 176, "msg": "Communication with server successful" }
こうなる。
Datadog Logsとk8sコンポーネントのログ
Datadog Agentを適当に動かしてk8sのコンポーネントのログを回収しようとすると以下のように全てErrorログとして出力される場合がある。
ログがIから始まっていることからわかるように、これらのログは本来全てInfoログある。 この現象の原因はstderrに出力していることである。 なぜかはよく知らないが、k8sのエコシステムではログをファイルに吐くのが標準でstderrに吐くためのオプションが用意されていることが多い。 少なくともmetrics-server, cluster-autoscaler, external-dns, alb-ingress-controller , nginx-ingress-controllerあたりはexampleのmanifestを動かした時にstderr(ファイルにも)にログを出力する。 なお、stdoutやstderrは実質ファイルじゃんというツッコミは今回は受け付けないものとします。
さて、ファイルでなくstderrに吐くオプションがあるのはおそらくコンテナをステートレスに保つための工夫であると思われる(なんでstdoutじゃないかは知らないので教えてくださいmm)。 一方でInfoログがErrorログとしてたまるとアラートも発砲しにくいし正直困ってしまう。よってこれに対処する。
ログフォーマット
Kubernetesのエコシステムでは多くの場合klogというログ形式が利用される1。 klogは以下のリポジトリで管理されているもので、glogがメンテされなくなったのを受けてk8sコミュニティで命名を変えて管理するようになったものである。
ちなみにフォーマットはこんな感じである。
I0413 11:04:16.323941 1 main.go:176] Communication with server successful
先頭の文字が (I, W, E, F)
になっていてそれぞれ (Info, Warning, Error, Fatal)
のログレベルを示している。
続けて日時、スレッドID、ファイル名、行、メッセージと並ぶ。
いわゆる構造化されていないログである。パッとみる分にはフォーマットが定まっているのでわかりやすいが機械が適切に情報を抜き出すにはちょっとして小細工をする必要がある。
なお、例えばcluster-autoscalerにはJSONのログを出して欲しいというISSUEがあったりする。 klogを利用している背景については詳しくないが緩く期待したいところである。
klogをDatadog Logsでよしなに扱う
やることは3つである。
- klogをJSONに変換
- log_levelをDatadog Logsが理解できる形式に変更
- log statusの書き換え
変換方法については詳しくは公式ドキュメントを見るといい
klogをJSONに変換
Grok Parserを利用する。 Grok Parserは様々なフォーマットのログをJSON形式に変換するために用意されている。 正規表現などを利用してなんやかんやログをマッチさせてJSONに変換することができる。 ログルールは上に記述したものから順に適用されていくため、複数のログフォーマットが混在していても問題なく構造化を行うことができる。 詳しい書き方は公式ドキュメントを見て欲しい。
具体的にklogをJSONにパースするための記述はこれである。
klog_parser %{regex("[A-Z]"):klog_level}%{date("MMdd HH:mm:ss.SSSSSS"):date}\s+%{integer:thread_num}\s+%{data:file_name}\:%{integer:line}\]\s+%{data:msg}
まず、先頭の1文字をklogのログレベルとして回収する( {I, W, E, F}
に絞ってもいい)。
次に日付やスレッド番号などをフォーマットに合わせて適当にパースして最後に余ったものを全てmsgとして扱う。
ここまで処理をすると結果として
{ "klog_level": "I", "date": 1586775856323, "thread_num": 1, "file_name": "main.go", "line": 176, "msg": "Communication with server successful" }
こうなる。
正直構造化さえできてしまえばあとはこっちのものなのでイージーだ。
log_levelをDatadog Logsが理解できる形式に変更
次に{I, W, E, F}
のログレベルが取れたので今度はlog statusを変更したいわけだが、そうは問屋が下さない。
log statusを変更するLog Status Remapperは下記のものを認識してログレベルを変更する。
- Integers from 0 to 7 map to the Syslog severity standards
- Strings beginning with emerg or f (case-insensitive) map to emerg (0)
- Strings beginning with a (case-insensitive) map to alert (1)
- Strings beginning with c (case-insensitive) map to critical (2)
- Strings beginning with err (case-insensitive) map to error (3)
- Strings beginning with w (case-insensitive) map to warning (4)
- Strings beginning with n (case-insensitive) map to notice (5)
- Strings beginning with i (case-insensitive) map to info (6)
- Strings beginning with d, trace or verbose (case-insensitive) map to debug (7)
- Strings beginning with o or matching OK or Success (case-insensitive) map to OK
- All others map to info (6)
つまり、このままだと E
がInfoにマッピングされてしまう。
大文字小文字は無視するらしいので (I, W, F)
はそれぞれ (info, warning, emerg)
にマッピングされるのでまあ問題なさそうだがErrorがInfoになるのは困る。
正直Fatalは落ちるレベルの時しか発生しないので普段使いはWarningとErrorになってくる。
したがってklog_levelをこれに合わせて変更する必要がある。 その時に利用できるのがLookup Processorである。
変換元と出力をそれぞれ以下のように設定し、
source: klog_level target: log_level
list of elementsを以下のようにする。
I,Info W,Warn E,Error F,Fatal
ここまで処理をすると結果として
{ "klog_level": "I", "klog_level": "Info", "date": 1586775856323, "thread_num": 1, "file_name": "main.go", "line": 176, "msg": "Communication with server successful" }
こうなる。
log statusの書き換え
先述のLog Status Remapperを利用する。
Set status attribute: log_level
とすれば良い。
ここまでの作業を終えると晴れてInfoログとして出力が得られる。
とりあえず満足である。
終わりに
Grok Parserを使うと概ねどんなログでも、またログ形式が混ざっていても構造化することが可能である。 ちょっとGrok Parser職人としての俗人化の香りを感じたがとりあえず満足である。
なお、stderrをstdoutに追記すると全てがInfoログになるのでそれはそれは辛く、おすすめできない。 Datadogも結構難しいよね。