My External Storage

Mar 22, 2022 - 6 minute read - Comments - newrelic

[New Relic] FACET CASE句を使って外部サービスのエンドポイントのレスポンスタイムを集計する

New Relicを使って外部サービスのエンドポイント別のレスポンスタイムを可視化した。
IDのようなパスパラメータを含むエンドポイントがあるときはFACET CASE句とcapture関数を使うとよい。

TL;DR

  • New Relicで外部APIのレスポンスタイムを計測するときはFROM Span WHERE category = 'http'という条件でNRQLを書く。
  • 単純なFACET句だと/users/${USER_ID}のようなパスパラメータを含むエンドポイントの計測を集約できない。
  • パスパラメータを含むエンドポイントはFACET CASE句を使って結果をまとめる
  • その他のエンドポイントはcapture関数を使って結果を分類する。

次のサンプルNRQLはhttps://example.com/external_apiという外部サービスのエンドポイント別レスポンスタイムの中央値を時系列データとして可視化するグラフ。

SELECT percentile(duration, 50)
FROM Span
WHERE category = 'http' AND appName = 'my-application'
AND http.url LIKE 'https://example.com/external_api%'
FACET http.method, CASES(
    -- https://example.com/external_api/users/${USER_ID}/icon を想定
    WHERE http.url LIKE '%icon' AS '/users/${USER_ID}/icon', 
    -- https://example.com/external_api/users/${USER_ID} を想定
    WHERE http.url LIKE '%users/%' AS '/users/${USER_ID}'
  -- その他のパスパラメータを含まないエンドポイント
) OR capture(http.url, r'https://example.com/external_api(?P<path>.*)')
SINCE 10 day ago TIMESERIES AUTO

HTTPメソッドとURLを使ってたとえば次のようなプロットで時系列データをグラフにできる。

  1. PUT /users/${USER_ID}
  2. GET /users/${USER_ID}
  3. GET /users/${USER_ID}/icon
  4. GET /auth
  5. GET /health

New Relic Oneで外部APIのレスポンスタイムを計測したい

New Relic Oneを導入し、分散トレースも確認できる状態のアプリケーションがある。 このアプリケーションが依存している外部APIのレスポンスタイムをエンドポイント別に確認したかった。
本記事では外部APIは以下のようなエンドポイントを持っている前提で記載する。

IDによってレスポンスタイムに大きな違いはないので、「指定されたIDのユーザー情報を返すエンドポイント」としてのレスポンスタイムの集計データを確認したかった。

MetricではなくSpanを使う

New Relic One上で書くサービスのサマリーページを見ると「Web transactions time」というMetricを使ったグラフがあり「Web external」の情報も確認できる。

Web transactions time
(参考画像はNew Relic Explorers HubのTopicから拝借)

ここを見るとこのグラフの元データとなるMetricからやりたいことができそうだが、Metricはプロパティがわかりづらく、NRQLが試行錯誤できなかった。

そこで今回はトランザクショントレースのSpanからグラフを作成する。 とうぜんアプリケーションでAPMが取得できていて外部Webサービスへのリクエストのメトリクスも取得できている必要がある。

Goアプリケーションで外部Webサービスへのリクエストのメトリクスを取得する方法

Goで該当メトリクスを取得するにはNew Relic Go Agentのnewrelic.NewRoundTripperを使う。

import (
  "net/http"
  "time"

  "github.com/newrelic/go-agent/v3/newrelic"
)

type roundTripperFunc func(*http.Request) (*http.Response, error)

func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
  return f(r)
}

func HttpClient() *http.Client {
  cli := &http.Client{
    Timeout:   5 * time.Second,
  }

  cli.Transport = func(rt http.RoundTripper) http.RoundTripper {
    return roundTripperFunc(func(req *http.Request) (*http.Response, error) {
      nrt := newrelic.NewRoundTripper(rt)
      nreq := newrelic.RequestWithTransactionContext(req, newrelic.FromContext(req.Context()))
      return nrt.RoundTrip(nreq)
    })
  }(cli.Transport)

  return cli
}

このように作ったhttp.Clientから外部サービスへリクエストを送信すると、NRQLでFROM Span WHERE category = 'http'という条件で外部サービスとの通信結果のメトリクスが取得できる。

単純なFACET句だと/users/${USER_ID}のようなパスパラメータを含むエンドポイントの計測を集約できない。

このメトリクスを使ってエンドポイント別のグラフを作る。単純に考えるとNRQL上のGROUP BYであるFACET句を使うことになるだろう。

NRQLは次のようになる。

SELECT percentile(duration, 50)
FROM Span WHERE category = 'http' AND appName = 'my-application'
AND http.url LIKE 'https://example.com/external_api%'
-- HTTPメソッドとURL別にレスポンスタイムを集約する。
FACET http.method, http.url
SINCE 10 day ago TIMESERIES AUTO

しかし、これでは集計に失敗する。
FACET句だけだと、次のようにパスパラメータとしてIDなどが含まれるエンドポイントがID別に集計されてしまう。

  1. PUT https://example.com/external_api/users/123
  2. GET https://example.com/external_api/users/123
  3. GET https://example.com/external_api/users/567
  4. GET https://example.com/external_api/users/333/icon
  5. GET https://example.com/external_api/auth
  6. GET https://example.com/external_api/health

これではIDの数だけグラフにプロットされてしまうので意味があるグラフではなくなる。

パスパラメータを含むエンドポイントはFACET CASE句を使って結果をまとめる

このようなときはFACET CASE句を使うとパスパラメータを集約できる。

パスパラメータを含むエンドポイントはFACET CASE句の中でWHERE LIKEを使って集約する。 順番にパターンマッチングされているはずなので、一度WHERE条件にかかったデータは後続のWHEREにはマッチしない。 パスパラメータを含むエンドポイントはFACET CASE句にORでつなげるとよい。

SELECT percentile(duration, 50)
FROM Span
WHERE category = 'http' AND appName = 'my-application'
AND http.url LIKE 'https://example.com/external_api%'
FACET http.method, CASES(
    -- https://example.com/external_api/users/${USER_ID}/icon を想定
    WHERE http.url LIKE '%icon' AS '/users/${USER_ID}/icon', 
    -- https://example.com/external_api/users/${USER_ID} を想定
    WHERE http.url LIKE '%users/%' AS '/users/${USER_ID}'
  -- その他のパスパラメータを含まないエンドポイント
) OR http.url
SINCE 10 day ago TIMESERIES AUTO

こうすると次のような集約結果になる。

  1. PUT /users/${USER_ID}
  2. GET /users/${USER_ID}
  3. GET /users/${USER_ID}/icon
  4. GET https://example.com/external_api/auth
  5. GET https://example.com/external_api/health

その他のエンドポイントはcapture関数を使って表示をスッキリする。

capture関数を使うとグラフ表示時のラベルをスッキリさせることができる。 正規表現はGoなどと同じRe2形式が使える。

capture(http.url, r'https://example.com/external_api(?P<path>.*)')

capture関数も使うとNRQLは次のようになる。

SELECT percentile(duration, 50)
FROM Span
WHERE category = 'http' AND appName = 'my-application'
AND http.url LIKE 'https://example.com/external_api%'
FACET http.method, CASES(
    -- https://example.com/external_api/users/${USER_ID}/icon を想定
    WHERE http.url LIKE '%icon' AS '/users/${USER_ID}/icon', 
    -- https://example.com/external_api/users/${USER_ID} を想定
    WHERE http.url LIKE '%users/%' AS '/users/${USER_ID}'
  -- その他のパスパラメータを含まないエンドポイント
) OR capture(http.url, r'https://example.com/external_api(?P<path>.*)')
SINCE 10 day ago TIMESERIES AUTO

こうすると次のようなグラフのプロットが完成する。

  1. PUT /users/${USER_ID}
  2. GET /users/${USER_ID}
  3. GET /users/${USER_ID}/icon
  4. GET /auth
  5. GET /health

終わりに

New Relic実践入門という書籍も所持しているのだが、今回のやりかたは載っていないような気がしたので記事にした。
外部サービスのレスポンスタイムのレスポンスタイムを可視化することができたので、「なんとなく遅くなってません?」などと定性的な話ではなく、定量的に外部連携先と調整をしたり、 レスポンスタイムの悪化の原因部分をより詳細に把握できるようになった。

ちなみにAPMを導入しているサービス自体のエンドポイント別のデータを集計したいときはパスパラメータが入っていてもNRQLが提供するプレースホルダーのような機能で簡単に集約できる。

参考

関連記事