🍩

Goの ORM である Bun を New Relic で計測する

2023/10/05に公開

本記事では Golang の ORM である Bun を使って New Relic APM で計測をするための紹介記事です。(巷で流行っている Bunとは別物です)

Bun 自体の紹介は他の記事でも紹介されているので詳細は省きますが database/sql 上で動作して SQL クエリを自動的に Bun 式にコンパイルするというもののようです。

Gorm よりも高速で動くということですが ORM としてできることなどに制限は多少ありそうですが普段の用途としては問題なく利用できそうです。

Opentelemetry で計測する方法は紹介されていますが今回は New Relic で計測するための方法をご紹介したいと思います。

環境情報

以下の環境で動作をさせています。
実際に利用した Go のライブラリなどは完成物のリポジトリがあるのでそちらをご参照ください

  • Apple M1 Max
  • Go v1.20.3
  • Docker version 23.0.5

New Relic アカウントの作成

New Relic のアカウントを持っていない場合は New Relic のアカウントを作成する必要があります。New Relic は1ユーザーと月間100GBのデータ転送料までなら完全無料で使えますので今回の検証は無料で実施可能です。

https://newrelic.com/jp/sign-up-japan

必要なライブラリのインストール

以下のライブラリを利用してきます

github.com/go-sql-driver/mysql
github.com/newrelic/go-agent/v3
github.com/newrelic/go-agent/v3/integrations/nrmysql
github.com/uptrace/bun
github.com/uptrace/bun/dialect/mysqldialect

New Relic に APM を登録する

New Relic に対して APM を登録します。

newrelic.NewApplication を呼び出すことで New Relic 側に APM として認識されます。

	app, err := newrelic.NewApplication(
		newrelic.ConfigAppName("go-bun-newrelic-example"),
		newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")),
		newrelic.ConfigAppLogEnabled(false),
		func(config *newrelic.Config) {
			config.Labels = map[string]string{
				"Env": "test",
			}
		},
	)
	if err != nil {
		return nil, err

今回は単純な http サーバーを用意しているのでなにかの path にアクセスする際に New Relic の情報をラップして渡すことで処理を記録することができるのでそのように実装します。

http.HandleFunc(newrelic.WrapHandleFunc(app, "/", excuteQueryRoute))

データベースへ接続をする

Bun は database/sql上で動作するので先に sql.DB を作成する必要があります。
またここで New Relic がクエリを記録できるように nrmysql を利用します。

	user := os.Getenv("DB_USER")
	password := os.Getenv("DB_PASSWORD")
	host := os.Getenv("DB_HOST")
	port := os.Getenv("DB_PORT")
	database_name := os.Getenv("DB_DATABASE_NAME")

	dsn := user + ":" + password + "@tcp(" + host + ":" + port + ")/" + database_name + "?charset=utf8mb4"
	mysqlDB, err := sql.Open("nrmysql", dsn)
	if err != nil {
		log.Fatalf("Error on creating database connection: %s", err.Error())
	}
	defer mysqlDB.Close()

次に bun.NewDB を呼び出すことで準備が完了です。
mysqldialect.New() は利用するデータベースドライバーによって変更してください。

	db = bun.NewDB(mysqlDB, mysqldialect.New())

クエリを呼び出す

最後にクエリを実行する実装をします。

type Employees struct {
	ID         int
	Name       string
	Age        int
	Department string
}

func excuteQueryRoute(w http.ResponseWriter, r *http.Request) {
	txn := newrelic.FromContext(r.Context())
	defer txn.StartSegment("excuteQueryRoute").End()

	var employees []Employees

	err := db.NewSelect().Model(&employees).Scan(r.Context())
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(employees)
}

New Relic は newrelic.FromContext を呼び出すことでコンテキストからトランザクション情報を取得することができそのトランザクション情報を使って Span の記録やデータストアへのアクセスを記録します。

今回は作成した employees というテーブルに対して単純に SELECT を実行しているだけになります。 Scan か Exec を呼び出すことで実行ができますが SELECT の際は Scan を利用できるので Scan を利用します。

Or use Scan which does the same but omits the sql.Result (only available for selects):

動作検証

実際に動作検証して値が返ってくることと New Relic に記録されているかを確認します。

docker comopse up --build
curl localhost:8080
go-bun-newrelic-example-app-1  | [{1 Alice 30 Engineering} {2 Bob 35 Marketing} {3 Charlie 25 Sales} {4 David 45 HR} {5 Eve 40 Finance}]

このような形で結果が返ってくるはずです。
New Relic 側も確認してみましょう。

問題なくクエリが記録されることがわかりました。
このようにして Bun のクエリのパフォーマンスなども New Relic で記録することができるので ORM 選定の際に Bun を選んでも問題なく利用できそうなことがわかりました。

複雑なクエリなどは検証していませんが記録できるかと思いますので時間がある際に試してみようと思います。

参考

https://bun.uptrace.dev/guide/

https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/introduction-new-relic-go/

https://zenn.dev/a_ichi1/articles/f0509170611b46#はじめに

https://zenn.dev/tkithrta/articles/c6074e0f09eb90

Discussion