🐕

New Relic で connect-go のトレースを URL パスごとに取得する

2024/07/14に公開

はじめに

New Relic で connect-go のトレースを URL パスごとに取得する方法についてです。

https://newrelic.com/jp

https://github.com/connectrpc/connect-go

New Relic や connect-go の細かい説明は省略します。

課題

New Relic は HTTP リクエストに関するトレース情報を簡単に取得することができます。
しかし connect-go を利用している場合、以下のように URL パスに関する情報が可視化されず、全てのリクエストが /XXX_SERVICE/ のように集約されます。

目標

以下のように URL パスごとにトレース情報を取得することが目標です。

原因

課題に挙げたように URL パスが集約される原因は New Relic の SDK と connect-go で生成されたコードの相性が悪いためです。

New Relic を HTTP ミドルウェアに仕込む際は通常以下のような実装を行います。

mux := http.NewServeMux()
path, handler := v1connect.NewXXXServiceHandler(...)
mux.Handle(newrelic.WrapHandle(nr, path, handler))

WrapHandle 内部で自動的にトレース名が生成されます。
https://github.com/newrelic/go-agent/blob/cab3b3180facc6ffcbecb745ae4ded16f76ee225/v3/newrelic/instrumentation.go#L73

ここで connect-go で自動生成されている NewXXXServiceHandler を確認してみます。
詳細は省きますが、戻り値は以下のようになっています。

func NewXXXServiceHandler(svc XXXServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
	return "/XXXService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case XXXServiceGetUserProcedure:
			XXXServiceGetUserHandler.ServeHTTP(w, r)
		case XXXServiceCreateUserProcedure:
			XXXServiceCreateUserHandler.ServeHTTP(w, r)
		default:
			http.NotFound(w, r)
		}
	})
}

ここで問題になるのが、 New Relic の SDK 側でトレース名に利用される path が connect-go 側で /XXXService/ に固定されてしまっているためです。
connect-go 側ではパスを一括で受け取った後に関数内部で switch を利用して分岐しています。

これらの組み合わせによって connect-go の 1 つの Service はパスによらず全て同一名で New Relic 側に表示されます。

対策

対策としては mux.Handle() に渡す関数を更にもう 1 段階ラップすることで、パスごとにトレース情報を取得することができます。
ラップ関数では http.Reqest からパスを取得し、そのパスの情報を New Relic に渡しているだけです。
New Relic 側ではパス情報はトレース名の生成にしか利用していないため、このような実装で問題なくパスごとにトレース情報を取得することができます。
mux.Handle() に渡すパスは connect-go 側の NewXXXServiceHandler から取得したものをそのまま利用しています。

func main() {
    mux := http.NewServeMux()
    path, handler := v1connect.NewXXXServiceHandler(...)
    mux.Handle(path, customNewRelicHandler(nr, handler))
}

func customNewRelicHandler(app *newrelic.Application, handler http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, handler = newrelic.WrapHandle(app, r.URL.Path, handler)
		handler.ServeHTTP(w, r)
	})
}

Discussion