New RelicのルックアップテーブルとNRQLを駆使して使われていないAPIを探し出す

ソーシャル経済メディア「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

早速実行してみましょう。

使われていないAPIを探す

現在は「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

実行すると、、

Transactionからクライアントのバージョンを取り出す

うまく取り出せているようです。

利用頻度が低い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を分類する

良さそうですね。古いクライアントからしかアクセスがない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:nnnnnnnn部分(値部分)を取り出すようにするとより親切になります。

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を導入している場合はぜひ試してみてください。

Page top