TT Lab
はじめる
学ぶ 学習パス コース

Loki — ログを索引しないログストア

パーサーが黙って何も取り出せないとき

TT Labで続きを見る

一言でいうと

Lokiは本文をインデックス化しないので、本文の中の値で絞り込むには、クエリのたびにパーサーで取り出す必要があります。パーサーは失敗すると__error__ラベルを付けますが、何も取り出せなくてもエラーを出さないパーサーがあります。

なぜ必要なのか

あるチームが、決済サービスのエラー率をLokiで測っていました。クエリは{app="pay"} | logfmt | status="500"で、ダッシュボードは数か月間、平穏でした。ところが、障害対応の会議で、同じ時間帯の実際の5xxの件数を数えてみたところ、ダッシュボードの数字の3倍でした。

原因は単純でした。そのサービスの一部の経路が、JSONでログを出力していたのです。logfmtパーサーは、JSONの行に出会ってもエラーを出しません。ただ何のラベルも取り出せないまま通り過ぎます。ラベルがなければstatus="500"は偽になり、その行は静かに抜け落ちます。ダッシュボードには「データなし」も「パースエラー」も表示されません。ただ数字が小さくなるだけです。

どう動くのか

LogQLのパーサーは4つです。すべてセレクターと行フィルターの後ろに付き、クエリのたびにその行を読み直してラベルを作ります。

パーサー 読む形式 失敗したとき
logfmt 키=값(プレースホルダーはキーと値です)が空白でつながった行 エラーなしで何も取り出しません
json JSONオブジェクト1行 __error__="JSONParserErr"を付けます
pattern <이름>(プレースホルダーは名前です)で取り出す位置を示した固定の形 形が違うと取り出しません
regexp 名前付きキャプチャグループを持つRE2 合わないと取り出しません

そのため、調査は常に2方向で行う必要があります。まず| json | __error__!=""で、壊れた行がどれだけあるかを数え、次に| logfmt | status=""で、エラーはないのに値が出なかった行がどれだけあるかを数えます。2つの数字がどちらも0であって初めて、そのストリームのパーサーを信頼できます。

パースエラーが出た行を消したければ| __error__=""を付けます。しかし、習慣的に付けてはいけません。その行こそが「形式の異なるログが混ざっている」というサインだからです。まず数え、原因を知り、そのあとで消します。

抽出されたラベルは、そのクエリの中でだけ生きます。保存もされず、インデックス化もされません。そのため、パーサーを変えると、過去のデータも一緒に解釈し直されます。メトリクスとは正反対です。メトリクスは計装を間違えると、過去が永遠に失われますが、ログは本文さえ残っていれば、あとから別の見方で読み直せます。これが「インデックス化しない」という選択がもたらす最大の利点です。

値は常に文字列です。| dur_ms > 500のように比較しようとすると、LogQLが数値に変換してくれますが、単位が混ざっていると(ミリ秒と秒が1つのフィールドに)、静かに間違った答えが出ます。数値を入れるフィールドは、名前に単位を入れておくほうが安全です。

現場での姿

最もよくある事故は「1つのストリームに2つの形式」です。アプリケーションはJSONで出力しているのに、その前のプロキシやランタイムが、平文の警告を同じstdoutに混ぜて入れてきます。ストリームラベルは同じなので1つのストリームであり、クエリは1つのパーサーしか使えません。

解決策はクエリではなく配置にあります。形式の異なるログは、ラベルを変えて別のストリームに送ります。コレクターで一度分けておけば、クエリは単純になり、パーサーエラーも消えます。それができないレガシーなら、少なくともパーサーごとに別々に数えて合算するクエリを作っておき、その事実をダッシュボードの横に書いておきます。

もう1つ。patternパーサーはregexpより速くて読みやすいので、アクセスログのように位置が固定された行には、ほとんど常にこちらのほうが優れています。正規表現は、位置が揺れる行にだけ使います。

次のラボですること

3つの形式が混ざったストリーム1つと、形式がきれいなストリーム1つを、Pod内の本物のLokiに入れ、4つのパーサーを順に付けてみます。jsonがエラーを出す行数と、logfmtが静かに通り過ぎた行数をそれぞれ数え、patternとregexpでレガシーの行から値を取り出したあと、最後に、形式ごとに別々に数えて合算して初めて出てくる、本当の5xx件数を求めます。