ソーシャル経済メディア「NewsPicks」のSREをしている飯野です。
サービスの開発は試行錯誤の連続です。サービスの成長とともに機能はどんどん増えていきます。追加される機能はサービスに不可欠な重要な機能だけではなく、サービスの方向性や前提が変わり不要になってしまったり、思ったように価値が提供できずに使われない機能もたくさん登場します。 このような機能が登場してしまうことは仕方のないことですが、ずっとメンテナンスしていくコストは払えません。定期的にお掃除したいものです。
というわけで、使われなくなってしまった機能(APIサーバーのREST API)をNew Relicを使って探し出してみましょう。
使われていないAPIを探す
2023年6月にNew RelicにCSVのデータをNRQLから利用できるルックアップ*1という機能が追加されました。ここにAPI一覧を登録すれば使われていないAPIを探し出せそうです。
アクセスログとしてはNew RelicのTransactionを利用します。Transactionの情報はサンプリングされており確実にアクセスがないとは言えませんが、傾向は掴めるので削除や廃止の意思決定の参考になる情報は得られると思います。
ルックアップテーブルに登録するAPI一覧の作成
使われていないAPIを探すためにはAPI一覧が必要です。各プロダクトで用意してあるAPI定義*2から次のようなCSVを作成します。ここで「パス」と「Springのコントローラー名とアクション名」の二つの情報を含めておきます。
Path | Method | Controller And Action |
---|---|---|
/live-movies/{id} | get | LiveMovieController/getLiveMovie |
NewsPicksはAPIサーバーにSpringFrameworkを利用しています。このフレームワークの場合、New RelicのTransactionのname部分は次の二つの形式のどちらかになります。
- A:
WebTransaction/SpringController/[Path] ([HttpMethod])
- B:
WebTransaction/SpringController/[ControllerName]/[ActionName] ([HttpMethod])
どちらの形式になるかは@Controller
と@RequestMapping
の指定方法によって決まっているようなのですが*3、newrelic-agentの更新などをきっかけに形式が入れ替わってしまうことがありました。今回はどちらが入っても対応できるようにします。
ルックアップテーブルを登録する
CSVが作成できたらNew Relicに登録します。ルックアップテーブルは Logs > Lookup Tablesから登録できます。
NRQLからは登録時の名前を使って参照するのでわかりやすい名前をつけてください。
今回は ApiEndpoints
として登録します。
ルックアップテーブルは lookup
関数*4を利用してアクセスします。Query Builderに次のNRQL*5を実行してみます。
FROM lookup(ApiEndpoints) SELECT * WHERE path LIKE '/live-movies/%' AND method = 'get' LIMIT 1
結果はこちら。問題なく利用できるようです。
NRQLでTransactionルックアップテーブルを検索する
NRQLからAPI一覧を扱う準備ができたので、使われていないAPIを探してみましょう。Transaction.name
から Path
部分や ControllerName/ActionName
部分を取り出してNOT IN
で絞り込めば良さそうです。
文字列の一部を取り出すには capture
関数*6が使えます。
FROM lookup(ApiEndpoints) -- (1) lookup tableでAPI一覧を指定する SELECT path, method, controllerAndAction WHERE concat(path, ' (', upper(method), ')') NOT IN ( -- (2) PATH部分を抽出 SELECT uniques(capture(name, r'WebTransaction/SpringController(?P<path_or_action>.*)')) FROM Transaction WHERE appName = 'newspicks-api' LIMIT MAX ) AND controllerAndAction NOT IN ( -- (3) コントローラーとメソッド部分を抽出 SELECT uniques(capture(name, r'WebTransaction/SpringController/(?P<path_or_action>.*)')) FROM Transaction WHERE appName = 'newspicks-api' LIMIT MAX ) AND controllerAndAction NOT LIKE 'DefaultController%' -- (4) DefaultControllerは対象外にする SINCE 1 WEEK AGO
早速実行してみましょう。
現在は「NewsPicks Learning」としてリニューアルされている 「NewsPicks Academia」に関連する更新系APIが出てきました。過去1週間の間にコンテンツの更新は行われていないのでクエリに問題なさそうです。
利用頻度が低いAPIを探す
次は利用頻度が低いAPIを探しましょう。NRQLではFACET
句*7を使うことでグルーピングが行えます。Transaction.name
からcaputre
関数でpath部分を抜き出すと少し見栄えが良くなります。
FROM ( -- (1) Transaction.name, request.methodごとに数をカウントする FROM Transaction SELECT count(*) as total_count WHERE appName = 'newspicks-api' AND name like 'WebTransaction/SpringController/%' AND request.method != 'HEAD' FACET -- (2) captureで必要な部分だけ取り出す capture(name, r'WebTransaction/SpringController(?P<path_or_action>.*)'), request.method LIMIT MAX ) SELECT path_or_action, request.method as 'Method' LIMIT MAX ORDER BY total_count ASC -- (3) サブクエリの結果を並び替える SINCE 1 WEEK AGO
Transactionからクライアントのバージョンを取り出す
利用頻度が低いAPIをクライアントごとに分類できるとより利用実態がわかりそうです。NewsPicksのモバイルクライアントの場合、UserAgentに NewsPicks/<バージョン番号>
という文字列が含まれています。<バージョン番号>
からリリース時期を調べることができるので、古いモバイルクライアントからしか利用されていないAPIがわかりそうです。capture
関数で取り出してみましょう。
FROM Transaction SELECT -- (2) NewsPicks/NNNNのNNNNを取り出す capture(request.headers.userAgent, r'.*NewsPicks/(?P<version>[0-9]+).*') as 'Version', if(request.headers.userAgent LIKE '%Android%', 1, 0) as 'Android', if(request.headers.userAgent RLIKE r'.*(iPhone|iPad|CFNetwork).*', 1, 0) as 'iOS' WHERE appName = 'newspicks-api' AND name like 'WebTransaction/SpringController/%' AND request.method != 'HEAD' -- (1) モバイルのUAにはNewsPicksが含まれる AND request.headers.userAgent LIKE '%NewsPicks/%' LIMIT MAX
実行すると、、
うまく取り出せているようです。
利用頻度が低いAPIを分類してみる。
Transactionからクライアントのバージョンを取り出せたので、利用頻度が低いAPIを分類してみましょう。filter
関数*8を利用するとFacetでまとめたレコードをさらに分類できます。
FROM ( FROM Transaction SELECT filter(count(*), WHERE 1=1) as total_count, -- (1) ブラウザアクセス filter(count(*), WHERE request.headers.userAgent NOT LIKE '%NewsPicks/%') as browser_requests_count, -- (2) サポート対象のAndroidのアクセス filter(count(*), WHERE request.headers.userAgent LIKE '%NewsPicks/%' AND request.headers.userAgent LIKE '%Android%' AND numeric( capture(request.headers.userAgent, r'.*NewsPicks/(?P<version>[0-9]+).*') ) >= 3000 ) as android_requests_count, -- (3) サポート対象外のAndroidのアクセス filter(count(*), WHERE request.headers.userAgent LIKE '%NewsPicks/%' AND request.headers.userAgent LIKE '%Android%' AND numeric( capture(request.headers.userAgent, r'.*NewsPicks/(?P<version>[0-9]+).*') ) < 3000 ) as old_android_requests_count, -- (4) サポート対象のiOSのアクセス filter(count(*), WHERE request.headers.userAgent LIKE '%NewsPicks/%' AND request.headers.userAgent RLIKE r'.*(iPhone|iPad|CFNetwork).*' AND numeric( capture(request.headers.userAgent, r'.*NewsPicks/(?P<version>[0-9]+).*') ) >= 3000 ) as ios_requests_count, -- (5) サポート対象外のiOSのアクセス filter(count(*), WHERE request.headers.userAgent LIKE '%NewsPicks/%' AND request.headers.userAgent RLIKE r'.*(iPhone|iPad|CFNetwork).*' AND numeric( capture(request.headers.userAgent, r'.*NewsPicks/(?P<version>[0-9]+).*') ) < 3000 ) as old_ios_requests_count WHERE appName = 'newspicks-api' AND name like 'WebTransaction/SpringController/%' AND request.method != 'HEAD' FACET capture(name, r'WebTransaction/SpringController(?P<path_or_action>.*)'), request.method LIMIT MAX ) SELECT path_or_action, request.method as 'Method', total_count, browser_requests_count AS 'ブラウザ', android_requests_count AS 'Android', old_android_requests_count AS 'Androidサポート対象外', ios_requests_count AS 'iOS', old_ios_requests_count AS 'iOSサポート対象外' ORDER BY total_count ASC LIMIT MAX
今回の閾値の 3000
は仮の値です。
良さそうですね。古いクライアントからしかアクセスがないAPIは廃止に向けて動けそうです。
テンプレート変数でクエリを動的に変更する
先ほど作成したクエリは閾値が固定値でした。このままでは不便なので閾値をダッシュボードのテンプレート変数*9で動的に変更できるようにしてみましょう。 ダッシュボードを新規作成してWidgetを一つ作成すると変数を追加するボタンが表示されます*10。
今回はandroid
, ios
という名前で作成します。
変数はダッシュボードのWidgetからは {{変数名}}
で参照できます。Widgetを作成して埋め込んでみましょう。
FROM ( FROM Transaction SELECT filter(count(*), WHERE 1=1) as total_count, filter(count(*), WHERE request.headers.userAgent NOT LIKE '%NewsPicks/%') as browser_requests_count, filter(count(*), WHERE request.headers.userAgent LIKE '%NewsPicks/%' AND request.headers.userAgent LIKE '%Android%' AND numeric( capture(request.headers.userAgent, r'.*NewsPicks/(?P<version>[0-9]+).*') ) >= {{android}} -- (1) テンプレート変数 android ) as android_requests_count, filter(count(*), WHERE request.headers.userAgent LIKE '%NewsPicks/%' AND request.headers.userAgent LIKE '%Android%' AND numeric( capture(request.headers.userAgent, r'.*NewsPicks/(?P<version>[0-9]+).*') ) < {{android}} -- (1) テンプレート変数 android ) as old_android_requests_count, filter(count(*), WHERE request.headers.userAgent LIKE '%NewsPicks/%' AND request.headers.userAgent RLIKE r'.*(iPhone|iPad|CFNetwork).*' AND numeric( capture(request.headers.userAgent, r'.*NewsPicks/(?P<version>[0-9]+).*') ) >= {{ios}} -- (2) テンプレート変数 ios ) as ios_requests_count, filter(count(*), WHERE request.headers.userAgent LIKE '%NewsPicks/%' AND request.headers.userAgent RLIKE r'.*(iPhone|iPad|CFNetwork).*' AND numeric( capture(request.headers.userAgent, r'.*NewsPicks/(?P<version>[0-9]+).*') ) < {{ios}} -- (2) テンプレート変数 ios ) as old_ios_requests_count WHERE appName = 'newspicks-api' AND name like 'WebTransaction/SpringController/%' AND request.method != 'HEAD' FACET capture(name, r'WebTransaction/SpringController(?P<path_or_action>.*)'), request.method LIMIT MAX ) SELECT path_or_action, request.method as 'Method', total_count, browser_requests_count AS 'ブラウザ', android_requests_count AS 'Android', old_android_requests_count AS 'Androidサポート対象外', ios_requests_count AS 'iOS', old_ios_requests_count AS 'iOSサポート対象外' ORDER BY total_count ASC LIMIT MAX
参照がうまくいくと次のようにテンプレート変数が表示されます。
これで条件を動的に変更できるようになりました。
発展:テンプレート変数の値をわかりやすくする工夫
実際の運用ではてテンプレート変数の値にリリース日や説明を含めて(例: yyyy/mm/dd:nnnn
)nnnn
部分(値部分)を取り出すようにするとより親切になります。
numeric(capture({{android}}, r'.*:(?P<version>[0-9]+)'))
まとめ
New RelicでルックアップテーブルとNRQLを駆使して使われていないAPIや利用頻度が低いAPIを探し出してみました。
- ルックアップテーブルを利用することでNew Relicにないデータを使った分析が行えるようになりました。
- ルックアップテーブル以前ならNRQLの結果をExportして集計する必要があったものがNew Relic上で完結できます。
- NRQLの
capture
関数、FACET
句とfilter
関数を使うことで目的に値を取り出せたり、絞り込んだデータをさらに深掘りすることができました。- NRQLは効率が悪そうなクエリでも現実的な応答速度で結果が得られるのでストレスなく試行錯誤できてよかったです。
- テンプレート変数で動的なダッシュボードが作成できました。
- この活動をきっかけに使っていないAPIを10個以上削除することができました。
- お掃除をすると気持ちいいものですね。
NRQLでの試行錯誤や可視化は楽しいです。New Relicを導入している場合はぜひ試してみてください。
*1:https://newrelic.com/jp/blog/nerdlog/new-relic-update-202306
*2:NewsPicksの場合はシステムが出力したswagger.json
*3:固定する方法を紹介したドキュメントもあります。https://github.com/newrelic/newrelic-java-examples/tree/main/newrelic-java-agent/instrumentation/spring-inheritance
*4:https://docs.newrelic.com/jp/docs/nrql/using-nrql/lookups/
*5:NRQLはFROM句→SELECT句の順にクエリが書けるのが嬉しいです。
*6:https://docs.newrelic.com/jp/docs/nrql/nrql-syntax-clauses-functions/#func-capture
*7:https://docs.newrelic.com/jp/docs/nrql/nrql-syntax-clauses-functions/#sel-facet
*8:https://docs.newrelic.com/jp/docs/nrql/nrql-syntax-clauses-functions/#func-filter
*9:https://docs.newrelic.com/jp/docs/query-your-data/explore-query-data/dashboards/dashboard-template-variables/
*10:Widgetが0個の場合は変数を追加するボタンが表示されないようです