ログなどのデータに含まれるJSON文字列をNRQLを使って解析する方法を解説します。改めて構造化したログを送りたいけど手が加えられない、でもなんとかして今のログは上手く使いたい、そんなときはご参考にしてください。
はじめに
New Relicにログなどのデータを送る際にメッセージフィールドがJSONの場合は、自動的にKey/Valueに分解されて保存されるので、特に意識する必要なくログデータを簡単に分析することができます。
しかし、すでに出力しているログの中にJSON以外の情報も付与されている場合などは妥当なJSONとして認識されず自動的な解析が行われません。本来、ログを構造化して送るように変更するのが綺麗な対応方法ではありますが、諸事情で改変が行えない、影響範囲が大きすぎるという場合もあるでしょう。
その場合は、ログの取り込み後にログに含まれるJSONを抽出し、解析する方法が必要ですが、New Relicのクエリ言語であるNRQL(New Relic Query Language)を利用すればそれを実現できます。この記事ではNRQLがサポートするJSON解析のための関数を使ってどのように情報が抽出できるかみていきたいと思います。
ログからのJSONの抽出
まず始めに、単純なJSONではないログ(ログの中の一部にJSONが含まれる)からJSONテキストの部分を抽出します。
例えば以下のログをみてみましょう。前半に時刻やその他の情報があり、その後にJSON文字列が続きます。
(1) 2024/12/03 23:33:10.419116 {\"level\":\"debug\",\"msg\":\"transaction ended\",
\"context\":{\"app_connected\":true,\"duration_ms\":1.100159,\"ignored\":false,
\"name\":\"WebTransaction/Go/hipstershop.ShippingService/GetQuote\"}}
このような文字列の場合妥当なJSONとは判断される自動的に解析はされないのでNew RelicのUI上でも以下のような形で見えるので検索性が良いとは言えません。
この文字列から後半のJSON文字列を抽出してみます。文字列から特定の部分だけを抽出するためにはcapture関数やaparse関数を利用します。それらの関数の詳細に関しては以下をご参照ください。
今回はJSON部分を抽出するのにcapture関数を使ってみます。capture関数は、ログのフォーマットを正規表現込みで指定することで欲しい部分だけを抽出できます。今回は、ログの前半が「(1) 2024/12/04 04:24:16.2222 」のような形になっているのでその部分を正規表現で指定し、後半部分を全てJSONとして抽出するため、capture(message, r'\(\d\) [\d/]+ [\d:\.]+ (?P<json>.*)')
としています。
WITH capture(message, r'\(\d\) [\d/]+ [\d:\.]+ (?P<json>.*)') as json
SELECT message, json
FROM Log
WHERE json is not null
NRQLを実行した結果が以下です。Messageがオリジナルのログメッセージ、JSONがJSON部分だけ抽出したものです。意図通り抽出できていることがわかりました。今回はcapture関数を利用しましたが、もっと簡単なフォーマットであればaparse関数を使っても良いです。
これで準備は完了です。JSONから情報を取り出す様々な方法を試してみます。
JSONからの情報抽出
JSONからの情報抽出にはjparseという関数を利用します。jparseは、jparse(<属性>, ‘<jsonパス>’)
という構文になっており、jsonパスの部分を変えることにより、<属性>に指定したJSON文字列から様々な要素を取り出すことができます。jparse関数の詳細は以下のドキュメントを参照ください。
今回は、ドキュメントにある例を実際に動かしてみてどのような結果が得られるかをみていきます。
前提
今回簡単のため、以下のようなJSON文字列を前提として情報を抽出してみます。実際にはLogなどのイベントからJSONのデータを引っ張ってきますが、試験的に動かすのでNRQLのWITH句で該当のJSON文字列を直指定しちゃいます。
{
"valueA": "test",
"valueB": {
"nestedValue1": [1, 2, 3],
"nestedValue2": 100
},
"valueC": [
{ "id": 1, "label": "A", "other": 7 },
{ "id": 2, "label": "B", "other": 9 },
{ "id": 3, "label": "C", "other": 13 }
]
}
指定したキーの値を取るNRQL
WITH '{"valueA": "test", "valueB": {"nestedValue1": [1, 2, 3], "nestedValue2": 100}, "valueC": [{ "id": 1, "label": "A", "other": 7 }, { "id": 2, "label": "B", "other": 9 }, { "id": 3, "label": "C", "other": 13 }]}'
as json
SELECT jparse(json, 'valueA') as 'Value'
キーvalueAに対応する値であるtestが取得できました。
指定したキーの値を取るNRQL(ネストされている場合)
では、値がネストしている場合はどうでしょうか?valueBを指定してみます。
WITH '{"valueA": "test", "valueB": {"nestedValue1": [1, 2, 3], "nestedValue2": 100}, "valueC": [{ "id": 1, "label": "A", "other": 7 }, { "id": 2, "label": "B", "other": 9 }, { "id": 3, "label": "C", "other": 13 }]}'
as json
SELECT jparse(json, 'valueB') as 'Value'
valueBの値のJSONが取り出せました。
ちなみに結果をJSON形式で確認してみると結果は文字列ではなくJSONで返ってきていることがわかります。
指定したキーの値を取るNRQL(ネストされている場合#2)
キーvalueBの値はJSONでしたが、さらにその先のキーを指定してみます。valueBのJSONのnestedValue1です。その場合は、'valueB.nestedValue1'のようにドット (.) でキー名を連結させます。
WITH '{"valueA": "test", "valueB": {"nestedValue1": [1, 2, 3], "nestedValue2": 100}, "valueC": [{ "id": 1, "label": "A", "other": 7 }, { "id": 2, "label": "B", "other": 9 }, { "id": 3, "label": "C", "other": 13 }]}'
as json
SELECT jparse(json, 'valueB.nestedValue1') as 'Value'
nestedValue1の値 [1,2,3]が取得できました。
指定したキーの値を取るNRQL(配列の場合#1)
どの程度頻繁に使うか、というのはありますが、値が配列の場合はその要素や一部分を抽出できます。配列から要素を抽出する際は、一般的なプログラミング言語によくみられるように [0] や [1:2] などで要素の位置や範囲を指定します。
WITH '{"valueA": "test", "valueB": {"nestedValue1": [1, 2, 3], "nestedValue2": 100}, "valueC": [{ "id": 1, "label": "A", "other": 7 }, { "id": 2, "label": "B", "other": 9 }, { "id": 3, "label": "C", "other": 13 }]}'
as json
SELECT jparse(json, 'valueB.nestedValue1[0]') as 'Value'
先ほどは配列全体[1,2,3]が返ってきましたが、今回は最初の要素[0]を指定しているので1だけが返ってきています。
WITH '{"valueA": "test", "valueB": {"nestedValue1": [1, 2, 3], "nestedValue2": 100}, "valueC": [{ "id": 1, "label": "A", "other": 7 }, { "id": 2, "label": "B", "other": 9 }, { "id": 3, "label": "C", "other": 13 }]}'
as json
SELECT jparse(json, 'valueB.nestedValue1[0:2]') as 'Value'
[0:2]とした場合は、0番目以上から2番目未満(つまり1番目)の要素を抽出します。[1, 2]が返ってきました。
指定したキーの値を取るNRQL(配列の場合#2)
配列の各要素がJSONだったらどうでしょうか?それぞれのJSONの中から特定のキーだけを集めてきたいことはあるでしょう。
今回のデータだとvalueCには{ "id": 1, "label": "A", "other": 7 }という形のJSONが配列で入っていますが、各要素からlabelだけを抜き取ってみます。その場合は、'valueC[*].label'のような形になります。valueCの配列の全要素を対象とし、そのうちlabel属性だけを抽出する感じです。
WITH '{"valueA": "test", "valueB": {"nestedValue1": [1, 2, 3], "nestedValue2": 100}, "valueC": [{ "id": 1, "label": "A", "other": 7 }, { "id": 2, "label": "B", "other": 9 }, { "id": 3, "label": "C", "other": 13 }]}'
as json
SELECT jparse(json, 'valueC[*].label') as 'Value'
配列の各要素からlabelの値だけを抽出することで、[‘A’, ‘B’, ‘C’]という値が返ってきました。
色々JSONからのデータの抽出方法をみてきました。この辺りを押さえておけばほぼほぼ間違いないでしょう🙌🙌
応用パターン
これまではJSONから抽出した値をSELECT句で返す例を中心にみてきましたが、抽出した値はSELECT句だけでなくWHERE句の絞り込みに利用したり、FACET句でグルーピングで利用したりできます。これを応用することで、単に一行のログを評価するだけでなく複数のログ横断して可視化ができるのでより効率的に面での分析が可能になります。
例えば、冒頭のログにはJSONの中のcontextというキーに、URL (name)の情報やレイテンシ (duration_ms)の情報が入っています。これを抽出した後、URL(name)事にレイテンシーの平均(average(duration))を出したりできます。
WITH capture(message, r'\(\d\) [\d/]+ [\d:\.]+ (?P<json>.*)') as json,
numeric(jparse(json, 'context.duration_ms')) as duration,
jparse(json, 'context.name') as name
SELECT average(duration) FROM Log
WHERE json is not null
FACET name
以下の例のようにURL毎にレイテンシーの平均が出せました。
上記の例はログメッセージに含まれるJSONを解析することで複雑な分析にも利用できる例を示したものですが、例として出したものはAPMがデータとして取るので、いちいちログから抽出して分析する必要はありません。
この例はログファーストの分析をお勧めするものではなく、あくまでテレメトリーデータとしてログしか取得する方法がない場合の例であることをご認識ください。例えばCDNのログを統合した場合などが該当するでしょう。
まとめ
今回はログのデータに含まれるJSON文字列を抽出し、解析する方法をご紹介しました。すでにログが出力されているが有効な解析手段がない場合にご利用をご検討ください。
今回ご紹介した関数も含めNRQLの使い方の詳細については公式ドキュメントを参照してください。
その他
New Relicでは、新しい機能やその活用方法について、QiitaやXで発信しています!
無料でアカウント作成も可能なのでぜひお試しください!
New Relic株式会社のX(旧Twitter) や Qiita OrganizationOrganizationでは、
新機能を含む活用方法を公開していますので、ぜひフォローをお願いします。
無料のアカウントで試してみよう!
New Relic フリープランで始めるオブザーバビリティ!
NRQLマスターになろうシリーズはこちら