📝

Go, NewRelic 複数アプリケーションでの分散トレーシング

2024/04/09に公開

Go言語を使ったNewRelicAPMの計装は自前でやらないといけないのは皆さんご存知だと思いますが
サービス間の分散トレーシングがうまく記録されなかったので実装メモを残そうと思います

前提

基本的な実装は省きます。重要なポイントだけ

アプリケーション構成

App1 Frontend => App2 Backend => (http.client) => App3 Backend

Frontend Application (App1)

Frontend(Browser)はApplicationSettingでDistributedTracingを有効にすれば
AjaxのRequestHeaderに newrelic,traceparent,tracestate が付加される
※ サーバー側でAllowHeaderしておかないとCROS制約でエラーになるので注意
https://docs.newrelic.com/jp/docs/browser/new-relic-browser/browser-pro-features/browser-data-distributed-tracing/

Backend Application (App2)

Frontからリクエストを受けるBackendApp
ドキュメントに従い、CROS対応(Access-Control-Allow-Headers)の対応をする
これをやるだけでFrontendとBackend(App2)はすんなり繋がる

Backend Application (App3)

Backend(App2)からhttp.clientを使って呼び出されるBackendApp
こちらはサーバー間通信なのでCROS設定は不要
特に実装上で必要なことはない

Backend間の分散トレース

https://docs.newrelic.com/jp/docs/apm/agents/go-agent/configuration/distributed-tracing-go-agent/#guidelines
アウトバウンドのHTTPリクエストを計測すれば基本OK

ここで注意が必要なのが、フロントエンドから受け取ったTraceHeaderだけを渡してしまうと
バックエンド間通信をしているにもかかわらず、Frontから並列でApp2、App3を呼び出しているように記録されてしまいます。

backend_app_2.go
req, _ := http.NewRequest("GET", backend_app3_url, nil)
for key, values := range r.Header {
    for _, value := range values {
        req.Header.Set(key, value)
    }
}

c := new(http.Client)
resp, _ := c.Do(req)
defer resp.Body.Close()

そうじゃない...
これは、単純にParentとなるTraceIDがフロントエンドで生成されたIDに両方紐づくからというだけの話なので
適切に処理をします

backend_app_2.go
req, _ := http.NewRequest("GET", backend_app3_url, nil)
// 対応①
req = newrelic.RequestWithTransactionContext(req, newrelic.FromContext(r.Context()))
for key, values := range r.Header {
    for _, value := range values {
        req.Header.Set(key, value)
    }
}

c := new(http.Client)
// 対応②
c.Transport = newrelic.NewRoundTripper(c.Transport)
resp, _ := c.Do(req)
defer resp.Body.Close()

Headerをそのまま転送してもいいが、NewRelicのRequest設定 ①、② を実装する必要があります

これが欲しかった!

StartExternalSegmentの場合はもう少しシンプル

req, _ := http.NewRequest("GET", backend_app3_url, nil)
for key, values := range r.Header {
    for _, value := range values {
        req.Header.Set(key, value)
    }
}
s := newrelic.StartExternalSegment(newrelic.FromContext(r.Context()), req)
defer s.End()
response, _ := http.DefaultClient.Do(req)
s.Response = response

まとめ

  • すべてのEntityで分散トレースを有効にする
  • Frontend(Browser)から受け取るバックエンドで気をつけるポイント
    • Allow Header
  • Backend間 通信で気をつけるポイント
    • HTTPRequestに適切にNewRelicの設定を含める事
    • newrelic.StartExternalSegment or newrelic.NewRoundTripper
  • EntityとEntityがTraceで 直列 に繋がるよう 実装するためにリクエストを意識する

Discussion