私が歌川です

@utgwkk が書いている

【追記あり】echoでNew Relicのエージェントを使えるようにするミドルウェアを書いた

追記

nrechoというライブラリが公式から提供されているのでこっちを使ったほうがよいと思います。わざわざ自前実装をしてから見つけてしまったけど見つけられたのはよかったということでここはひとつ……。

そういうわけなので、以下の文章は読まなくてもよいことになりましたが、記録のために残しておきます。

(追記ここまで)


あらすじ

練習でISUCON9予選のコードにNew Relicエージェントを導入したときは、gojiを使っていて、 mux.HandleFunc でhandlerを登録する形式だったのでちょっと書き換えればすぐ導入できたのですが、echoだとひと工夫必要でした。

そもそも公式のGoエージェントではどうやっているのか、gojiの場合は

newrelic.WrapHandleFuncといった関数を使うことでGoエージェントを有効にできます(使用例のコードを引用)。

   http.HandleFunc(newrelic.WrapHandleFunc(app, "/users", func(w http.ResponseWriter, req *http.Request) {
        txn := newrelic.FromContext(req.Context())
        txn.AddAttribute("customerLevel", "gold")
        io.WriteString(w, "users page")
    }))

gojiの場合はこんな感じで導入できます。goji.io/patを使っていたので、軽いwrapperを噛ませて使っていました。

func wrapHandleFuncGet(path string, f func(w http.ResponseWriter, r *http.Request)) (*pat.Pattern, func(w http.ResponseWriter, r *http.Request)) {
    pathStr, wrapped := newrelic.WrapHandleFunc(newRelicApp, path, f)
    return pat.Get(pathStr), wrapped
}

func wrapHandleFuncPost(path string, f func(w http.ResponseWriter, r *http.Request)) (*pat.Pattern, func(w http.ResponseWriter, r *http.Request)) {
    pathStr, wrapped := newrelic.WrapHandleFunc(newRelicApp, path, f)
    return pat.Post(pathStr), wrapped
}

func main() {
    // (snip)
    mux.HandleFunc(wrapHandleFuncGet("/users/:user_id.json", getUserItems))
    mux.HandleFunc(wrapHandleFuncPost("/buy", postBuy))
    // (snip)
}

echoミドルウェア選定

GitHubecho newrelic というクエリでGo言語のリポジトリに絞って検索したところ、2020/09/13 時点で以下の5つのリポジトリがヒットしました。

エンドポイントごとにレスポンスを返すのにかかった時間を計測するのはどれでもできそうですが、Transactionオブジェクトをcontextで引き回さないと、DBアクセスにかかった時間までは測定できません。github.com/jessie-codes/echo-relic 以外は そのような実装になっています。

github.com/dafiti/echo-middleware は、ライブラリ側でNewRelicエージェントを初期化しており、また初期化に失敗したらpanicするので、エージェントを無効にしてアプリケーションを動かしたいというユースケースには合わなさそうです。

github.com/notyim/echo-middleware-newrelic は、importしたさいに環境変数の情報をもとにNew Relicエージェントを初期化しています。またエージェント初期化処理のエラーハンドリングがライブラリに委ねられています。ログを吐く処理とか環境変数をどうするか、みたいなのは自分でコントロールしたいので使わなさそうです。 runable 変数はどこで使っているのだろう……。

github.com/phacops/echorelic はNew Relicエージェントを渡してミドルウェアを作るかたちになっていてよさそうですが、c.Request().URL.Path をもとにトランザクション名を決めているのはよくなさそうでした。GET /api/chair/:id へのリクエストが :id ごとに散らばることになってしまいます。

github.com/golang-common-packages/monitoring (名前の範囲が広い!!) は、これもライブラリ側でNewRelicエージェントを初期化してpanicしているので、github.com/dafiti/echo-middleware と同様に採用できなさそうです。

いろいろ見てきましたが、どのライブラリも自分のユースケースには合わなさそうです。ISUCON10予選本番では、echoとNew Relicを組み合わせる例をその場で探して出てきたものを試した結果、 github.com/jessie-codes/echo-relic を採用してしまったのでなかなか大変でした。

エンドポイントにかかった時間は計測できたけど、分散トレーシングとか、SQLにかかった時間が取れてない!!! なんでなの!! と言いながらechorelicのコードを読みに行きました。どうもcontextが渡ってない予感がする? echoのミドルウェアを書くのか? と思いつつ echo.WrapMiddleware っていうのを使うとなんか書けそう、ということでガッと試しました。

ISUCON10 予選突破した #isucon - 私が歌川です

作った

  • e.Use(なんとか(app)) ってやるだけでエージェントが有効になってほしい
    • 全てのエンドポイントを計測したいので、エンドポイントごとにwrapするよりミドルウェアにしたほうが早い
  • エージェントの初期化処理はライブラリを使う側で行いたい
    • エージェントの設定を自由に行いたい
    • エラーハンドリングはこっちでやりたい (panicしないでほしい)
  • トランザクションの名前は c.Path() をもとにしたい
    • GET /api/chair/1 ではなく GET /api/chair/:id にしたい
  • できるだけNew Relicの公式ライブラリが提供する仕組みに乗っかりたい

以上の要件を満たすようなechoミドルウェアはなさそう、そしてちょっと実装すれば欲しいものはできそう、ISUCON10本戦でもNew Relicを使うことになるだろう、ということで作りました。go get github.com/utgwkk/echo-newrelic/v3 して今すぐご利用いただけます。ISUCON10予選の感想戦がてらちょっと試してみて、New Relicにデータが送信されていることは確かめました。

github.com