🙆

Golang + MySQL構成のアプリケーションでNewrelicを使う方法

2022/12/20に公開

目的

  • NewrelicでWebTransactionを作成する。
  • 作成したWebTransactionにDBのSegmentを付与する。
  • 関連するログも付与する(logs in context)。

前提

  • Newrelicのアカウントを持っていて、License Keyを持っている。
  • WebTransaction / Segment / LogなどNewrelic内のオブジェクトについて知っている。
  • アプリケーションで使うライブラリは以下を前提とする。
    • chi
    • logrus
  • コードは擬似的な部分があるので、正常に動作しないと考えてください。

執筆背景

  • あまり日本語でGolang + MySQL + Newrelic構成の初心者向け設定方法がなかったので、書いてみる。
  • 備忘録として残す。

やること

  • WebTransactionを開始するmiddlewareを実装する。
  • logs in contextを使ってロギングできるようにする。
  • データベースドライバをNewrelicが用意するものに変更する。

middleware実装

middlewareでやりたいことは以下の2つ。

  1. リクエスト毎にWebTransactionを開始する。
  2. WebTransactionにlogを紐付けるように設定する。

まず、1を実装する。
これを実装することでエンドポイントごとのレスポンスタイム、エラーレートなどが分かる。
このデータをもとに、アラートを飛ばすこともできる。

ここで参考にした資料は以下の通りである。

上記資料に記載されている通り、go-agentを追加する。

$ cd /path/to/your-project-name
$ go get github.com/newrelic/go-agent/v3/newrelic

以下のように*newrelic.Applicationを返す関数を定義する。
このポインタ構造体にStartTransactionメソッドが定義されており、これをmiddlewareで呼び出すことになる。

func NewApplication() (*newrelic.Application, func(timeout time.Duration), error) {
  app, err := newrelic.NewApplication(
    // newrelicの管理画面で表示されるアプリケーション名
    newrelic.ConfigAppName("sample-app"),
    newrelic.ConfigLicense("LICENSE_KEY"),
    newrelic.ConfigAppLogForwardingEnabled(true),
    func(cfg *newrelic.Config) {
    // デフォルトのIgnoreStatusCodesに404が含まれているため、
    // 空スライスを代入することでデフォルト設定を書き換えている。
    // 404を検知する必要がないなら、この処理は必要ない。
      cfg.ErrorCollector.IgnoreStatusCodes = []int{}
    },
  )
  return app, app.Shutdown, err
}

次にmiddlewareの実装をする。
以下のように実装することで、エンドポイント(APIのパス)毎にWebTrancationを開始する。
例えば、アプリケーションに users/:id というパスがある場合、newrelic上ではsample-app/users/:idというTransactionの各種データを収集 / 閲覧したいはず。そのような実装にしている。

以下のmiddleware実装はこの辺りを参考にした。

type Middleware struct {
  NewrelicApp *newrelic.Application
}

func (api *API) StartNewrelicTransaction() func(next http.Handler) http.Handler {
  return func(next http.Handler) http.Handler {
    fn := func(w http.ResponseWriter, r *http.Request) {
      ctx := r.Context()
      // Transactionを開始する。引数にTransactionの名前を取るが、ここでは空文字として、別で命名する。
      txn := api.NewrelicApp.StartTransaction("")
      defer txn.End()
      
      // リクエストをWeb Transactionとしてマークする。
      txn.SetWebRequestHTTP(r)
      // Transactionにレスポンスのデータを計装させる。これを呼び出すことでnewrelicの管理画面でレスポンスの各種データが分かる(っぽい)。
      w = txn.SetWebResponse(w)
      
      ctx = newrelic.NewContext(ctx, txn)
      r = r.WithContext(ctx)

      next.ServeHTTP(w, r)
      
      // Transactionの名前はpathよりもpatternの方が都合が良さそうだと判断したため、こうした。
      // chiは[ここ](https://pkg.go.dev/github.com/go-chi/chi#Context.RoutePattern)に記載されている通り、handler.ServeHTTPを呼び出したあとにRoutePatternメソッドを呼び出すことで、pathのパターンを取得できる。このパターンをTransactionの命名に使う。
      pattern := chi.RouteContext(ctx).RoutePattern()
      txn.SetName("sample-app" + pattern)
    }
    return http.HandlerFunc(fn)
  }
}

次にもう一つlogs in contextをアプリケーション内で使えるようにするためのmiddlewareを実装する。

type Middleware struct {
  Logger *logrus.Logger
}

func (middleware *Middleware) StartNewrelicLogsInContext() func(next http.Handler) http.Handler {
  return func(next http.Handler) http.Handler {
    fn := func(w http.ResponseWriter, r *http.Request) {
      ctx := r.Context()

      // default_logsを出力する際にlogs in contextを使いたいので、loggerにcontextを含める。
      log := middleware.Logger.WithContext(ctx)
      // 他のパッケージ内でログ出力する際に、contextに埋め込んだloggerを使うことでWebTransactionにログを付与できる(logs in contextできる)      
      ctx = context.WithValue(ctx, contextutil.Logger{}, log)
      r = r.WithContext(ctx)

      ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)

      defer func() {
        d := newrelicLog{
	  Message:    "default_logs",
          Timestamp:  time.Now().UTC().Format(time.RFC3339),
          RequestID:  middleware.GetReqID(r.Context()),
          Schema:     r.URL.Scheme,
          Proto:      r.Proto,
          Method:     r.Method,
          RemoteAddr: r.RemoteAddr,
          UserAgent:  r.UserAgent(),
          URI:        fmt.Sprintf("%s://%s%s", r.URL.Scheme, r.Host, r.RequestURI),
          Status:     ww.Status(),
          ByteLength: ww.BytesWritten(),
        }
        j, err := json.Marshal(d)
        if err != nil {
          next.ServeHTTP(ww, r)
        }

        dst := &bytes.Buffer{}
        json.Compact(dst, j)
        log.Infoln(dst.String())
      }()

      next.ServeHTTP(ww, r)
    }

    return http.HandlerFunc(fn)
  }
}

上記2つのmiddlewareを呼び出すことで、やりたいことが実現できる。

データベースドライバ変更

newrelic(に限らないと思うが)では、Transaction内でどんな処理に、どのくらいの時間がかかっているのか計測できる。
Webアプリケーションの場合、リクエスト内で以下のようなデータを収集したい場合がある。

  • 特定の関数 / メソッド
  • RDBへの問い合わせ
  • 検索エンジン、キャッシュサーバ、外部APIへの問い合わせ

今回は2つ目のRDBへの問い合わせにどの程度時間がかかっているのか計測する方法を紹介する。
紹介するといっても、やることはここに書いてあり、この通りにやれば問題ないはずである。
ここで紹介されているのはgormだが、他のライブラリでもmysql driverを使っている場合はリンクのブログどおりにやれば動くはずである。

まとめ

ここではGolang + MySQL構成のアプリケーションでNewrelicを使う方法を実際のコード例を提示しながら、紹介した。
特にchiでTransactionの名前にパターンを使う方法は見かけなかったので、自分自身勉強になりました。
コード例、説明に拙い箇所が多いと思います。拙いどころか何か間違いがあるかもしれません。コメントいただければ、修正したいと思います。その時は優しく教えて下さい。

参考リンク

Discussion