New Relic Go AgentでGoアプリの計測をやってみた

2020.04.27

はじめまして、New Relic株式会社でシニアテクニカルサポートエンジニアをしている田中です。今回からDevelopers.IOにお邪魔してブログ記事を投稿することになりました。

最初の記事として、New Relic Go Agentの使い方を説明するために、非常に簡単なWebアプリをGo Agentで計測してみました。

New Relic Go Agentで何が見える?

New Relic Goエージェントは、Javaや.NETといった他の言語のエージェントとは異なり設定不要の自動インストゥルメンテーションを行いません。その理由はGo 言語の性質によるもので、動的なランタイムやメソッドの置き換えを許可していないためです。つまり、計測を行うためには常にコード変更が必要になりますが、その分、コードの中で何を計測するかを100%制御でき、高度にカスタマイズできます。さらに、New Relic Goエージェントにはパッケージの依存関係やバージョンがありません。Goエージェントに直接変更を加えることもできます。

常にコード変更が必要になるが故に、とっつきづらさも感じるかもしれません。そこで単純なWebサーバーにNew Relic Go Agentを設定する手順を紹介して、コード変更のイメージをつかんでみることにします。対象となるWebサーバーのコードはGistに公開しています。

sleepして応答するだけのindexhogeという2つのエンドポイントと、外部サービスを呼び出すexternalというエンドポイントがあります。New Relicを使って次のような計測ができるようにしましょう。

  • エンドポイントごとに呼び出し数と処理時間を計測する
  • customerLevelというクエリ文字列で渡される顧客クラスをエンドポイントの統計に追加する(クエリ文字列を使うのはブラウザでテストしやすくするため)
  • hogeエンドポイントでは無名関数内の処理の経過時間を個別に計測する
  • 外部呼び出しを計測する
  • エラーが発生した時に記録する

最終的なコードも同じGistに公開しています

コード変更が必要ではありますが、思ったよりは少ないという印象があるのではないでしょうか(あって欲しいです)。このコードで計測するとNew Relic UI上では次のスクリーンショットのように見えます。New Relicではエンドポイントごとの処理をトランザクションとして計測することができます。トップ画面ではトランザクションの平均時間、スループット、エラー率、およびトランザクションの種類ごとの時間がわかります。処理時間の色分けは水色がコード処理、緑色が外部呼び出しの待ち時間を意味しています。

また、トランザクションに追加した属性はトランザクションの詳細画面で確認できます。

外部サービスの呼び出しはExternal Serviceの画面で確認できます。呼び出し先のドメインごとに分類されています。

では、コードについて一つずつ説明してみます。

Go Agentの設定

まずGo Agentの初期化は"github.com/newrelic/go-agent/v3/newrelic"を追加します。そしてmain関数の最初でnewrelic.NewApplicationを使って初期化します。

var (
	app *newrelic.Application
)

func main() {
	var err error
	app, err = newrelic.NewApplication(
		newrelic.ConfigAppName("lab1"),
		newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")),
		newrelic.ConfigDebugLogger(os.Stdout),
	)

	if (err != nil) {
		panic(err)
	}

	//略
}

この時の返り値である*newrelic.Applicationはこの後利用することがあるので変数に格納しています。アプリ名やライセンスキーなどの設定はnewrelic.ConfigAppName("lab1")と文字列で指定したり、newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY"))と環境変数経由で指定します。また、導入初期はnewrelic.ConfigDebugLogger(os.Stdout)とデバッグレベルのAgentログを出力する(ここでは標準出力に出力している)ことをおすすめします。

トランザクションを計測する

次にエンドポイントごとの計測を可能にするために、トランザクションを設定します。このためにエンドポイントになっている関数に計測用のコードを設定しますが、ラッパーを用意することで全てのエンドポイントのコードを変更することなく設定できます。今回はServeMuxを使っているので、replacementMuxという構造体とHandleFuncという関数をラッパーとして用意します。

type replacementMux struct {
	app *newrelic.Application
	*http.ServeMux
}

func (mux *replacementMux) HandleFunc(pattern string, fn func(http.ResponseWriter, *http.Request)) {
	mux.ServeMux.HandleFunc(newrelic.WrapHandleFunc(mux.app, pattern, fn))
}

main関数内での変更はmuxを生成する行だけです。

func main() {
	//略
	mux := replacementMux{ServeMux: http.NewServeMux(), app: app}
	mux.HandleFunc("/", index)
	mux.HandleFunc("/hoge", hoge)
	mux.HandleFunc("/external", external)

	http.ListenAndServe(":8123", mux)
}

このラッパーにより、HandleFunc関数内でnewrelic.WrapHandleFuncを挟み込めるようになっています。もしServeMuxとか使っていないよという場合でも心配は要りません。New Relicは多くのライブラリに対応した計測用のライブラリをインテグレーションとして提供しています。また、ここにない場合でもラッパーを書くことで対応できます。

https://github.com/newrelic/go-agent/blob/master/README.md#integrations

トランザクションに属性情報を追加する

クエリ文字列やHTTPヘッダー、場合によってはボディの情報に基づいて何かしらの情報をトランザクションに追加したい場合、TransactionAddAttributeを使ってキーと値のペアを追加できます。Transactionはnewrelic.FromContext(req.Context())により取得することができます。

func index (rw http.ResponseWriter, req *http.Request) {
	txn := newrelic.FromContext(req.Context())
	txn.AddAttribute("customerLevel", req.URL.Query().Get("customerLevel"))

	time.Sleep(15 * time.Millisecond)
	rw.Write([]byte("Hello World"))
}

トランザクション内を細分化して計測する

トランザクション内でより詳細に経過時間を計測したい場合は、経過時間を計測したい区間でSegmentを使います。一つの関数を丸ごと計測したい場合は、関数の最初にdefer txn.StartSegment("segment1").End()とすることでSegmentを開始し、関数終了時に終了できます。

func() {
	defer txn.StartSegment("segment1").End()
	time.Sleep(10 * time.Millisecond)
}()

もしくはStartSegementで開始し、そこで取得したSegmentをEndすることでそこで終了させることもできます。

s2 := txn.StartSegment("segment2")
time.Sleep(15 * time.Millisecond)
s2.End()

外部サービスの呼び出しを計測する

最後に外部サービスの呼び出しの計測です。HTTPのClientを計測するためにはRoundTripperを設定します。Transportフィールドにnewrelic.NewRoundTripper(nil)を設定します。

var (
	client = &http.Client{
		Transport: newrelic.NewRoundTripper(nil),
	}
)

この時どのトランザクションから呼び出されたかをClient側で認識させるためにreq = req.WithContext(r.Context())と記述してRequestオブジェクトにContextを渡しておきます。

txn := newrelic.FromContext(r.Context())

req = req.WithContext(r.Context())
	resp, err := client.Do(req)

まとめ

New RelicのGo Agentはこの様に設定することで、アプリケーションを計測することができます。実際のアプリケーションでは、データベースをはじめとするミドルウェアの呼び出し処理などまだまだ計測するべき処理があります。これらの計測も今回のサンプルコードの様にトランザクションを設定し、セグメントに分け、必要な情報を渡す処理を書けば実現できます。そして、広く使われてるライブラリについてはNew RelicがIntegrationを提供しており、より簡単に設定できます。Goでアプリケーションを開発運用している方は、ぜひ一度New RelicのGo Agentを試していただき、その感想をフィードバックしてもらえれる嬉しいです。