GoアプリにNew Relic APMトレースも自動挿入する
はじめに
こちらの記事にインスパイアされたアンサーソングです
New Relic のSpanを自動挿入するツールはすでにbudougumi0617さんの nrseg[1] がありますが、計装用のラッパーを自動生成する試みに惹かれたためNew Relic版を作ってみました
nrdeco
go install github.com/miyamo2/nrdeco/cmd/nrdeco@latest
# またはtoolディレクティブで管理
go get -tool github.com/miyamo2/nrdeco/cmd/nrdeco
nrdeco -s <生成元のGoファイル> [-d <出力先ファイル>]
nrdecoは生成元のGoファイル内で context.Context
を引数に持つメソッドが1つ以上定義されているインターフェイスに対して計装機能付の実装を自動生成します
nrseg では *http.Request
を引数に持つメソッド/関数に対しても計装が行われますが、nrdeco では対象外としています
生成元とできるのは一度の実行で一ファイルのみのため、基本的にはgo generate
と組み合わせて利用することを推奨します
以下のファイルを生成元として実行した場合、
//go:generate nrdeco -s $GOFILE
package repository
import (
"context"
"github.com/miyamo2/nrdeco/examples/domain/model"
)
type UserRepository interface {
GetUserByID(string) (*model.User, error) // これは実装されない
GetAllUsers() ([]model.User, error) // これは実装されない
GetUserByIDWithContext(context.Context, string) (*model.User, error) // これは実装される
GetAllUsersWithContext(context.Context) ([]model.User, error) // これは実装される
}
生成されるファイルは以下のようになります
<生成元のファイル名>.nrdeco.go
context.Context を引数に持たないメソッドに対しては計装用メソッドは実装されないため、呼び出された場合は埋め込まれた型のメソッドがそのまま実行されます
// Code generated by nrdeco@; DO NOT EDIT.
//
// See here for more information on nrdeco: https://github.com/miyamo2/nrdeco
package repository
import (
"context"
"github.com/miyamo2/nrdeco/examples/domain/model"
"github.com/newrelic/go-agent/v3/newrelic"
"os"
"strings"
)
// NRUserRepository implements repository.UserRepository with New Relic instrumentation.
type NRUserRepository struct {
UserRepository
}
func (n *NRUserRepository) GetUserByIDWithContext(ctx context.Context, arg1 string) (*model.User, error) {
if strings.ToLower(os.Getenv("NRDECO_ENABLED")) == "true" {
defer newrelic.FromContext(ctx).StartSegment("repository.UserRepository.GetUserByIDWithContext").End()
}
return n.UserRepository.GetUserByIDWithContext(ctx, arg1)
}
func (n *NRUserRepository) GetAllUsersWithContext(ctx context.Context) ([]model.User, error) {
if strings.ToLower(os.Getenv("NRDECO_ENABLED")) == "true" {
defer newrelic.FromContext(ctx).StartSegment("repository.UserRepository.GetAllUsersWithContext").End()
}
return n.UserRepository.GetAllUsersWithContext(ctx)
}
生成された構造体を使ったサンプルコードがこちら
package repository
import (
"context"
"github.com/miyamo2/nrdeco/examples/domain/repository"
"github.com/miyamo2/nrdeco/examples/infra/inmemory"
"github.com/newrelic/go-agent/v3/newrelic"
)
func main() {
// newrelic.Application の初期化
app, err := newrelic.NewApplication(/* your config */)
if err != nil {
panic(err)
}
// 元となる実装
originalRepo := &inmemory.UserRepository{}
// 計装用のデコレータでラップ
instrumentedRepo := &repository.NRUserRepository{
UserRepository: originalRepo,
}
// トランザクションの開始
txn := app.StartTransaction("test_transaction")
ctx := newrelic.NewContext(context.Background(), txn)
// 計装されたメソッドを呼び出す
user, err := instrumentedRepo.GetUserByIDWithContext(ctx, "123")
...
}
repoの.examplesディレクトリに google/wire を用いたサンプルコードがあるのでより具体的な使い方については是非そちらをご覧ください
NRDECO_ENABLED
nrdeco によって生成された計装は 環境変数 NRDECO_ENABLED
が true
の場合にのみ有効になります
フラグ
フラグ | 概要 | デフォルト値 | 備考 |
---|---|---|---|
-s , --source
|
計装するインターフェイスが含まれるファイル | - |
--version といずれか必須。 |
-d , --dest
|
出力先 | <source>.nrdeco.go |
|
--version |
nrdecoのバージョンを出力 | - |
--source といずれか必須。 |
-h , --help
|
ヘルプ | - |
おわりに
nrdeco では元記事で紹介されていた
- 生成対象をDIコンテナの依存関係に含まれるインターフェイスのみにフィルタする
- ラッパーだけをまとめたDIコンテナを自動生成
- ビルドタグによる制御で必要に応じてラッパーを剥がす
といった仕組みは提供できていませんが、nrdeco で生成されたコードを用いて 2, 3 と同様のアプローチを取ること自体は可能かと思います
nrdecoにgit-styleプラグインの機構を取り入れて、 uber-go/dig や google/wire のための拡張を開発してくのも楽しいかも
Discussion