My External Storage

Jan 17, 2021 - 4 minute read - Comments - go

GoのアプリにNew Relic APMを導入する時とても便利なCLIを作った

GoのWebアプリコードにNew Relic APMでSpan(Segment)を計測するコードを自動挿入するOSSを作った。
New RelicをGoのアプリケーションへ導入するときに利用をオススメする。

TL;DR

  • New Relic APMは分散トレーシングなどにも対応しているAPM
  • Spanを計測するためには、関数ごとにSegmentの処理を差し込む必要がある
  • 指定ディレクトリ以下にあるGoのコードにFunction segmentsを自動挿入するOSSを作った
  • https://github.com/budougumi0617/nrseg

New Relic APM

New Relic APMはNew Relic Oneに含まれている分散トレーシングなどにも対応したモニタリングツールだ。

個人でも業務でも使い始めた1のだが、エージェントを挿入すれば簡単にアプリケーションを丸裸にできる。

GoのWebアプリケーションにNew Relic APMを導入する手順

APMだけならば導入もすぐ完了する。

ざっくりとまとめると、まずアプリ起動時にNew Relicと接続するためのApplicationオブジェクトを生成する。

app, err := newrelic.NewApplication(
    newrelic.ConfigAppName("Your Application Name"),
    newrelic.ConfigLicense("__YOUR_NEW_RELIC_LICENSE_KEY__"),
)

そして各種ハンドラーのエンドポイントを次のようにラップする。

http.HandleFunc(newrelic.WrapHandleFunc(app, "/users", usersHandler))

主要なフレームワーク、MySQLなどならば専用のラッパーも用意されている。

Spanの計測が大変

これでNew Relic上からTraceを確認できるようになる。
ここまでの対応ならば、すぐにできるだろう。しかし、ここまでだとTraceは取得できるようになっても、Span2が取得できない。

Goのアプリケーション上でSpanを取得するには、関数やメソッドごとにFunction segmentsを明示的に埋め込んでいく必要がある。

defer txn.StartSegment("mySegmentName").End()

これを中規模以上の既存のアプリケーションに実施しようとすると、関数/メソッド1つ1つを修正していく必要があり非常に面倒なことになる。

nrseg

上記の面倒くささを解消するためのコマンドラインツールがnrsegだ。

Macの場合はBrewでインストールすることができる。

$ brew install budougumi0617/tap/nrseg

このような既存のコードがあったとする。

package input

import (
  "context"
  "fmt"
  "net/http"
)

type FooBar struct{}

func (f *FooBar) SampleMethod(ctx context.Context) {
  fmt.Println("Hello, playground")
  fmt.Println("end function")
}

func SampleFunc(ctx context.Context) {
  fmt.Println("Hello, playground")
  fmt.Println("end function")
}

func SampleHandler(w http.ResponseWriter, req *http.Request) {
  fmt.Fprintf(w, "Hello, %q", req.URL.Path)
}

// nrseg:ignore you can be ignored if you want to not insert segment.
func IgnoreHandler(w http.ResponseWriter, req *http.Request) {
  fmt.Fprintf(w, "Hello, %q", req.URL.Path)
}

上記のコードを含んだディレクトリにnrsegを実行する。

$ nrseg .

そうすると、上記のようなコードが次のようになる。

package input

import (
  "context"
  "fmt"
  "net/http"

  "github.com/newrelic/go-agent/v3/newrelic"
)

type FooBar struct{}

func (f *FooBar) SampleMethod(ctx context.Context) {
  defer newrelic.FromContext(ctx).StartSegment("foo_bar_sample_method").End()
  fmt.Println("Hello, playground")
  fmt.Println("end function")
}

func SampleFunc(ctx context.Context) {
  defer newrelic.FromContext(ctx).StartSegment("sample_func").End()
  fmt.Println("Hello, playground")
  fmt.Println("end function")
}

func SampleHandler(w http.ResponseWriter, req *http.Request) {
  defer newrelic.FromContext(req.Context()).StartSegment("sample_handler").End()
  fmt.Fprintf(w, "Hello, %q", req.URL.Path)
}

// nrseg:ignore you can be ignored if you want to not insert segment.
func IgnoreHandler(w http.ResponseWriter, req *http.Request) {
  fmt.Fprintf(w, "Hello, %q", req.URL.Path)
}

nrseg:ignoreとコメントした関数以外のすべての関数/メソッドに対してSpanを生成するためのNew Relicライブラリの呼び出しが自動挿入される。
nrsegコマンドはディレクトリの下のすべてのGoコード3に対して上記の修正を行なう。 既存アプリケーションのコード量が多ければ多いほど便利!

技術的なところ

やっていることはディレクトリをトラバースしながら各コードのASTに無理やり該当コードを挿入してファイルを更新している。
挿入時にシグネチャの中にあるcontext.Contextの変数名を取得したり、細かいASTのパースも行なっている。
めんどくさかったところはおいおい個別の記事でメモを公開したいと思う。

終わりに

今年の目標(2020年のTry)として「ひとつ突き詰めて実装してみる」を掲げている。 nrsegはGoのASTと仲良くなれるし業務で利用しはじめたため、目標とマッチしつつモチベーションも保ちやすい。
また、New Relicを使って今年のISUCONを出ることを想定して「(サブミット時に)挿入したコードをすべて削除する」サブコマンドなども用意したいと思う。 引き続き機能強化していきたい。

参考資料


  1. https://newrelic.com/jp/press-release/20201217 ↩︎

  2. 1つのTraceの中にあるオペレーションの1つ1つの実行処理時間などを計測したもの ↩︎

  3. testdataディレクトリや-iオプションで指定されたディレクトリは除く ↩︎

関連記事