NRQLマスターになろう - 文字列操作編

NRQLでNew Relicに収集したデータに含まれる文字列から特定部分を抽出する方法（aparse / capture）をご紹介します。いくつか方法はありますがその違いもみていきます。


# はじめに
[NRQL（New Relic Query Language）](https://docs.newrelic.com/jp/docs/nrql/nrql-syntax-clauses-functions/)はNew Relicに収集したデータをいい感じで可視化し、分析するためのクエリ言語です。ビルトインで提供されているUIで十分に分析をすることはできますが、複雑な条件でデータ分析をしたい場合やビルトインのUIではカバーされていないような可視化をしたい場合にはNRQLによる可視化は非常に強力な手段となります。

今回は収集したデータに含まれる文字から特定の部分を抽出する方法をご紹介します。以下のようなことが理解できるようにします。

- 文字から特定の部分を抽出する方法の理解
- 抽出したい部分が複数ある場合の抽出方法
- 抽出した後に数値などに変換して分析する方法
- 結局どっちがいいの？

# 想定ケース
New Relicに送信するデータが構造化されており、利用したいデータが全て独立した属性として格納されている場合はそのままそのデータを使えますが、場合によっては利用したいデータが構造化されていなかったり、特定の属性値の一部に含まれるということがあるでしょう。BrowserエージェントやAPMエージェントが収集するURLの中の一部を使いたい場合であったり、構造化されていない旧来のログを利用する必要があったり、3rd Partyのログをそのまま取り込んでいるようなケースです。

例えば、以下のようなパターンのURLがあったとして、商品毎の集計をしたい場合は1234の部分を抽出したいかもしれませんし、商品のカテゴリ毎の集計をしたい場合はsportsの部分を抽出したいケースもあるでしょう。

```
https://xxxxx/products/sports/1234
```

プログラムを修正して構造化したデータを送ったり、sportsや1234の部分を独立した属性として送ることも可能ですが、今回は上記のようなデータがそのまま送られた場合にNRQLを使って抽出する方法をご紹介します。

# 文字列から特定部分を抽出する方法
文字列から特定部分を抽出する方法としてNRQLでは二つの関数を提供しています。
一つが***capture***関数で、もう一つが***aparse***関数です。


capture関数は、RE2構文の正規表現を使用して属性から値を抽出するものです。正規表現が細かく指定できて柔軟性が高いので、複雑なパターンの文字列であっても対応できます。

https://docs.newrelic.com/jp/docs/nrql/nrql-syntax-clauses-functions/#func-capture

aparse関数は、capture関数の代替となるものです。構文としてはシンプルでcapture関数ほどの複雑な指定はできませんが、可読性はありメンテナンスのしやすさという意味ではaparseが良さそうです。また、capture関数よりは処理速度が早いというのが特徴です。

https://docs.newrelic.com/jp/docs/nrql/nrql-syntax-clauses-functions/#func-aparse

では、それぞれの関数での抽出方法をみてみましょう。

### capture関数
capture関数の基本的な構文は以下のような形式です。第一引数は抽出対象となる属性、第二引数は正規表現を指定します。パターンに該当する部分がマッチすると、抽出された部分がnameという変数に格納されます。
```sql:capture関数の構文 
capture(対象の属性名, r'... (?P<name>パターン ) ...')
```

では、PageViewイベントのpageUrlという属性にある`https://xxxxx/products/sports/1234`というデータを対象にcapture関数を使ってみます。
まずは末尾の数字を取り出したい場合です。

正規表現の書き方としては以下のような感じになります。URLの前半部分は固定だと仮定して、最後の部分で1文字以上の数値を抽出して、idという属性に入れています。数値の部分の指定方法は通常の正規表現と同じように、\dでもいいですし、[0-9]でも大丈夫です。
```
r'https://xxxxx/products/sports/(?P<id>\d+)'
```

NRQL全体をみてみると以下のような感じです。WITHは固定値を渡すために使っていて今回はそんなに重要じゃありません。またlimit 1も例として書いているので無視して大丈夫です。重要なのはcaptureの部分です。
```
WITH 'https://xxxxx/products/sports/1234' as pageUrl 
FROM PageView 
SELECT pageUrl as '元の文字列', 
       capture(pageUrl, r'https://xxxxx/products/sports/(?P<id>\d+)') as 'ID'
LIMIT 1
```

New RelicのQuery Builderで上記のクエリを試すと以下のような感じで出ると思います。

![capture_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535230/d76930fa-ffbd-ae2d-40bc-42594a38ccb8.png)



次に、URLの途中にあるsportsの部分だけ抽出してみます。正規表現の書き方としては、例えば以下のようにすれば取れるます。[a-zA-Z]+ は、アルファベット小文字か大文字1文字以上の文字列を表す正規表現です。この時点でアレルギー反応が出ている方は頑張ってください。
```sql:正規表現の指定方法
r'https://xxxxx/products/(?P<category>[a-zA-Z]+)/.*'
```

もうちょっと柔軟性を持たせるのであれば以下の正規表現でも大丈夫です。[^\/]+ は、/以外(^は以外)の1文字以上の文字列を表します。/sports/のスラッシュの間がどんな文字でも取れます。
```sql:正規表現の指定方法
r'https://xxxxx/products/(?P<category>[^\/]+)/.*'
```

ちょっと脱線しましたが、NRQL全体としては以下のようになります。
```sql:capture関数を利用して途中の文字列を抽出するNRQL
WITH 'https://xxxxx/products/sports/1234' as pageUrl 
FROM PageView 
SELECT pageUrl as '元の文字列', 
　　　　capture(pageUrl, r'https://xxxxx/products/(?P<category>[a-zA-Z]+)/.*') as 'Category' 
LIMIT 1
```

結果はこのような感じになります。

![capture_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535230/ec059b9d-aef2-3d20-0433-ff9fb0f19362.png)



いかがでしょうか？構文さえ覚えてしまえば簡単に抽出することができます。


### aparse関数
次にaparse関数を使ってみます。aparse関数の構文は以下の通りです、第一引数はcapture関数と同様に抽出対象としたい属性名です。第二引数は、*や%といったワイルドカードを使ったパターンを指定します。
```sql:aparse関数の構文
aparse(対象の属性、パターン）
```

`https://xxxxx/products/sports/1234`から、1234部分を取り出したい場合は以下のようなパターンを指定します。ワイルドカード*で指定された部分が抽出されます。
```aparseのパターン指定方法
https://xxxxx/products/sports/* 
```

NRQLとしては以下のような感じです。
```sql:aparse関数を使った部分文字列抽出のNRQL
WITH 'https://xxxxx/products/sports/1234' as pageUrl 
FROM PageView 
SELECT pageUrl as '元の文字列', 
　　　　aparse(pageUrl, 'https://xxxxx/products/sports/*') as 'ID'
LIMIT 1
```

もっとラフにしたい場合は、 以下のように%のワイルドカードを指定して前半部分を軽くすることもできます。%のワイルドカードは抽出対象とはなりません。
```sql:aparseの指定方法
aparse(pageUrl, '%products/sports/*')
```

先ほどと同様にID部分が抽出できました。

![capture_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535230/89346a09-327c-5898-c14f-f8a98fa9fc99.png)


次に、`https://xxxxx/products/sports/1234`からsportsの部分だけを抽出してみます。
パターンの指定方法の一例としては以下のようになります。productsのすぐ後の部分（sportsの部分）だけ抽出し、スラッシュを含むそれ以降は無視する形です。
```sql:aparseの指定方法
aparse(pageUrl, '%products/*/%')
```

NRQLとしては以下のようになります。
```sql:aparse関数を使って部分文字列を抽出するNRQL
WITH 'https://xxxxx/products/sports/1234' as pageUrl 
FROM PageView 
SELECT pageUrl as '元の文字列', 
       aparse(pageUrl, '%products/*/%') as 'Category'
LIMIT 1
```

aparse関数でも同様に取得することができました。

![capture_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535230/14f6d02e-4adb-f63b-d66e-b2fd9d06f135.png)


aparse関数は正規表現をゴリゴリ書かなくて良いので簡単ですね。簡単なパターンであればaparse関数で十分そうです。

一方で、みてわかる通り、products/*/% の部分のパターンは非常に曖昧性があります。抽出したいデータの構造が複雑であったり、可変の部分が多い場合は抽出したい部分が一意にならないケースも出てくるでしょう。対象とする属性とそのパターンによってどちらが的しているのか選択するのがおすすめです。

以上が部分文字列を抽出するcapture関数とaparse関数の使い方になります。
この後はもう少し複雑なケースなどみていきます。

# 複数の値をとりたいとき
一つの属性から複数の値を取りたい時はどうすると良いでしょうか。capture関数もaparse関数も両方複数の値を取る方法をサポートしています。

複数の値を取る場合は、***WITH構文***を使って一度抽出した複数の値をそれぞれ別の属性値に格納します。以下のようなイメージです。
```sql:WITH構文を使った複数の文字列抽出
WITH aparse関数またはcapture関数 as (attr1, attr2, …) FROM … SELECT ….
```

例えば、`https://xxxxx/products/sports/1234`から、sportsというカテゴリと、1234というIDを取りたい場合を想定します。

capture関数だと以下のような書き方になります。
```sql:抽出対象が複数ある場合のNRQL (capture)
WITH capture(pageUrl, r'https://[^\/]+/products/(?P<category>[a-zA-Z]+)/(?P<id>\d+)') as (category, id) 
FROM PageView 
SELECT pageUrl as '元の文字列', category as 'Category', id as 'ID' 
```

aparse関数だと以下のような書き方になります。
```sql:抽出対象が複数ある場合のNRQL (aparse)
WITH aparse(pageUrl, '%products/*/*') as (category, id) 
FROM PageView 
SELECT pageUrl as '元の文字列', category as 'Category', id as 'ID' 
```

一度に複数の値を抽出できました。

![aparse.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535230/c7df2abb-210e-4b4c-bc75-ab8df04f9ab9.png)


いかがでしょうか。簡単です。


# 数値変換
抽出した文字を数値として扱いたい場合が度々あります。例えばCDNの生ログからキャッシュヒット率を計算したり、Time to First Byteを統計的に見たり。HerokuではCPUやリソース利用率がログとして出力されたりもします。

そのような時は、***numeric***関数を使ってみてください。文字列として保存されている数字を数値として認識させることができるので、例えばアベレージやパーセンタイルをとって統計的に評価することが可能です。

例えば、以下の例ではログに含まれるCPU Timeを、capture関数を使って文字列として抽出した後、numeric関数で数値に変換した上で平均をとっています。
```sql:抽出した文字の数値への変換
SELECT average(numeric(capture(message, r'.*CpuTime:\s(?P<cpuTime>\d+)'))) 
FROM Log 
WHERE message LIKE '%CpuTime:%' SINCE 1 hour ago
```

文字列としてだけでなく数値にもできると利用の幅は広がりますね。

その他変換の関数については公式ドキュメントをご参照ください。

https://docs.newrelic.com/jp/docs/nrql/nrql-syntax-clauses-functions/#type-conversion


# 処理時間の違い
aparse関数はcapture関数より早いという特徴があると[リリースノート](https://docs.newrelic.com/whats-new/2022/10/whats-new-10-13-nrql-productivity-improvements/)には書かれていますが実際はどうでしょうか。対象とするデータ量が多いログを使って測ってみます。

ログには有象無象のテキストが出力されますが、今回はその中で以下のようなパターンを持つログの中からuserId (xxxxxx)の部分を抽出し、ユニーク数をカウントする時間を比べてみます。
```
[GetCart\]: Get cart for user: xxxxxxx
```

実行するNRQLは雑に以下のような感じにしてみます。測定環境は4000万行ぐらいのログがありました。

```sql:capture関数を利用した場合のNRQL
WITH capture(message, r'.* user: (?P<user>.*)') as user 
FROM Log 
SELECT uniqueCount(user)  
since 1 day ago
```

```sql:aparse関数を利用した場合のNRQL
WITH aparse(message, '%user: *') as user 
FROM Log 
SELECT uniqueCount(user)  
since 1 day ago
```

NRQLの実行はコマンドライン (curl) からAPI (NerdGraph)経由でやってみます。コマンドラインから実行する方法は以下をご参照ください。curlに-wオプションをつけて最初のバイト転送までの時間を比べてみます (time_starttransfer)

https://newrelic.com/jp/blog/how-to-relic/commandline-nrql-with-nerdgraph

コマンドは以下のような感じです。
```console: curlとNerdGraph APIを使ったNRQLの実行
curl https://api.newrelic.com/graphql \
  -H 'Content-Type: application/json' \
  -H 'API-Key: <User Key>' \
  -w '\n%{time_starttransfer}\n' \
  --data-binary '{"query":"{\n  actor {\n    account(id: xxxxxx) {\n      nrql(\n        query: \"<NRQL>\"\n      ) {\n        results\n      }\n    }\n  }\n}", "variables":""}'
```

結果は以下の通りです。

```console:capture関数を使った場合の結果（秒）
{"data":{"actor":{"account":{"nrql":{"results":[{"uniqueCount.user":66114}]}}}}}
0.952087
```

```console: aparse関数を使った場合の結果（秒）
{"data":{"actor":{"account":{"nrql":{"results":[{"uniqueCount.user":66114}]}}}}}
0.518737
```

結果、何回か繰り返すと概ねaparse関数の方が早いということは言えそうです。どちらを使うかの判断材料の一つになるかと思います。なお、結果に関しては本記事執筆時点でのものであることや、ネットワークやバックエンドの負荷の違いもあった上での結果であり、未来永劫同じであるという保証はないことをご認識ください。

# その他の手段
今回はNRQLを使ってすでに格納されているデータからうまく抽出する方法をご紹介しましたが、***最初から構造化したデータを送ったり、送ったデータを構文解析した状態で格納できるとより効率的***ではあります。

その一つがカスタム属性です。カスタム属性とはAPMやBrowserなどの各種エージェントが送信するデータに任意の属性を付与できる機能です。例えば、User IDやテナントID、カテゴリ情報など様々な属性をプログラム中で付与しておくことによって、例えばどのテナントのアクセスが多いか、どのユーザーで問題が起きているかなど一瞬でわかるようになります。また、APMなどのビルトインのUIでもその属性を利用したパフォーマンス分析やエラー分析などが可能になりますのでこちらがオススメです。

カスタム属性の詳細は以下をご覧ください。

https://docs.newrelic.com/jp/docs/data-apis/custom-data/custom-events/collect-custom-attributes/


また、ログに関しては、取り込んだ時点で解析する方法もあります。ログの解析方法については以下のドキュメントもご参考にしてください。

https://docs.newrelic.com/jp/docs/logs/ui-data/parsing/


https://qiita.com/ume67026265/items/ce0afad4048c357a851b


# まとめ
NRQLでサポートされる様々な機能のうち、文字列のデータを抽出するaparse/capture関数について解説しました。


簡単な例で解説しましたがaparse/capture関数は色々な部分で使えます。SELECT句で値をとったり、WHERE句で条件の絞り込みに使ったり、FACET句でグルーピングして集計するのに使ったりできます。



うまくデータから情報を抽出できると、それをさらに集計して分析できるので活用の幅はさらに広まります。是非お試しください。

活用方法の例については公式ブログもご参照ください。


https://newrelic.com/jp/blog/how-to-relic/nrql-regrex-capture


# その他
New Relicでは、**新しい機能やその活用方法**について、QiitaやXで発信しています！
**無料でアカウント作成**も可能なのでぜひお試しください！

:::note info
New Relic株式会社の[X(旧Twitter)](https://x.com/NewRelicJapan) や [Qiita OrganizationOrganization](https://qiita.com/organizations/newrelic)では、
新機能を含む活用方法を公開していますので、ぜひフォローをお願いします。
:::

:::note info
無料のアカウントで試してみよう！
[New Relic フリープランで始めるオブザーバビリティ！](https://qiita.com/ktst79/items/5ee74b34e6adb8a7a6fa)
:::

### NRQLマスターになろうシリーズはこちら

https://qiita.com/ktst79/items/5d84d6a2ceca90efa108

https://qiita.com/ktst79/items/c1ddf33e18e9cfeba893

https://qiita.com/ktst79/items/88a80229d226b5c0abcf

https://qiita.com/ktst79/items/f743ad2207c435134615