NewRelic×Golangで好きなHTTPクライアント使いたい
この記事は、AEON Advent Calendar 2023の7日目です。
こんにちは。@pideohです。イオンスマートテクノロジーさんと一緒にお仕事させて頂いています。
皆さんNewRelic活用していますか?イオンスマートテクノロジーさんではNewRelicをフロントエンドからバックエンド、インフラレイヤまで全方位で導入し、サイロ化していたAPM(元はNewRelic含めて3種類!)・ログ分析・モニタリングを統合しました。この活動を@hikkie13と進めさせて頂いた訳ですが、Golangへの導入は他の言語と比べて少し苦労したのでTipsになればと思っています。
@hikkie13のFutureStack Tokyo 2023登壇記はコチラ↓↓
NewRelicさんにご紹介頂いた事例はコチラ↓↓
GolangでNewRelicAPMを始めるのは根気が必要。。。
Javaや.Netのようにエージェントを引数や環境変数で読み込ませるとWebのトレーシングが”いい感じ”にできるようにならないのがGolang。”いい感じ”に通信やメソッドを計測するためのインターフェースが提供されていないため、トレースしたい範囲は自分で実装する必要があります。
Webアプリケーションの場合、
- HTTPハンドラのラップ
- ファンクションコール等、"内部"トレース対象に対するセグメント実装
- APIコール等、"外部"トレース対象に対するセグメント実装
などの実装は最低限必要でしょう。他にもデータストアやMQ等アプリケーション要件によって実装したいものが出てくるはずです。
ここで壁になるのが 「HTTPハンドラのラップ」 と 「APIコール等、"外部"の計測対象に対するセグメント実装」 です。
HTTPハンドラ、HTTPクライアント共にGoエージェントの互換性と要件に記載のライブラリ(=インテグレーションパッケージ)を利用していればドキュメントの通り実装するだけですが、それ以外のライブラリを使っている場合は "なんとかする" か諦めるの2択です。
iAEONは超ざっくり以下のような構成を取っており、BFFにGolangを使用しています。
HTTPハンドラはgorilla/mux
、HTTPクライアントはgo-resty/resty
で実装されていますが、restyはインテグレーションパッケージとして提供されていません。
構成を見ておわかりでしょうか。BFFで諦めたら何もトレースできません。BFFを推進してきたのも私です。つまり "なんとかする" しかないのです。
※蛇足ですが、iAEONは100以上のMicroserviceで成り立っています
どうやってなんとかするか
外部セグメントの仕組み
「APIコール等、"外部"の計測対象に対するセグメント実装」、つまり外部セグメントの実装は標準であれば以下のような実装が推薦されています。
func external(txn *newrelic.Transaction, req *http.Request) (*http.Response, error) {
s := newrelic.StartExternalSegment(txn, req) // 1. 外部セグメントの開始と
// W3Cトレースコンテキスト標準、NewRelicヘッダの挿入
response, err := http.DefaultClient.Do(req) // 2. HTTPリクエスト
s.Response = response // 3. 外部セグメントへのHTTPレスポンス設定
s.End() // 4. 外部セグメントの終了
return response, err
}
引用元:Instrument Go セグメント - 外部セグメント
同じことをすれば良いじゃない
コードのコメントの通り、外部セグメントはトレース情報を他のアプリケーションに引き継ぐためにHTTPリクエストにトレース関連のヘッダを挿入していることと、レスポンスを連携しているだけということがわかりました。
つまり、
- 外部セグメントの開始
- W3Cトレースコンテキスト標準、NewRelicヘッダをどうにかこうにか挿入
- HTTPリクエスト
- 外部セグメントにレスポンス格納
- 外部セグメントの終了
を実装できればどんなHTTPクライアントでも使い放題です。WoW!
そして、課題となる2.のヘッダ挿入はAPIガイドに記載のない、InsertDistributedTraceHeaders
というファンクションで挿入できるのです(他のHTTPクライアントの実装みて真似てみた)。これで怖いものはなくなりました!
こうなった
結果、コードで実装するとこうなります。↑の説明の通りです。
func demo(txn *newrelic.Transaction) {
seg := txn.StartSegment("demo")
defer seg.End()
var hogeResponse HogeResponse
var url := "https://a-service"
res, err := external(txn, func(newReq *resty.Request) (*resty.Response, error) {
return newReq.SetResult(&hogeResponse).Get(url)
})
}
func external(txn *newrelic.Transaction, r func(*resty.Request) (*resty.Response, error)) (*resty.Response, error) {
client := resty.New()
req := client.R()
seg := &newrelic.ExternalSegment{ // 1. 外部セグメントの開始
StartTime: txn.StartSegmentNow(),
}
txn.InsertDistributedTraceHeaders(req.Header) // 2. W3Cトレースコンテキスト標準、NewRelicヘッダを挿入
res, err := r(req) // 3. HTTPリクエスト
seg.Response = res.RawResponse // 4. 外部セグメントにレスポンス格納
seg.End() // 5. 外部セグメントの終了
return res, err
}
フロントエンド・バックエンド含めてトレースできるようになったらこうなった
フロントエンドからBFF、バックエンドとこれで全てコンポーネントに導入が完了しました。
そして某画面のトレース結果がこちらです。数十のMicroservice、3,500近くのスパンが一気通貫でトレース可能になりました👏
今まで3つのAPMで分断していた情報から解析していたのと比べると効率がn倍になるのもうなずける結果ですね!
まとめ
- インテグレーションパッケージとして提供されていないHTTPクライアントでもトレーシングは可能
- GolangでNewRelicAPM、怖くないよ
- iAEONは実は100+のMicroserviceで動いている
Discussion