はじめに
このブログのマイクロサービスのうち1つをgoogle.golang.org/grpcからconnectrpc.com/connectに置き換えてみました
99designs/gqlgenをlabstack/echoでルーティングしているため自作したMiddlewareが流用できたら嬉しい、ぐらいのモチベですが
しばらくの間は
google.golang.org/grpc で動いているマイクロサービスと connectrpc.com/connect で動いているマイクロサービスが混在する状態で比較検討する材料にできればと考えています
ルーティング
connectのサンプルコードではnet/httpを使用していますが
func main() {
greeter := &GreetServer{}
mux := http.NewServeMux()
path, handler := greetv1connect.NewGreetServiceHandler(greeter)
mux.Handle(path, handler)
http.ListenAndServe(
"localhost:8080",
// Use h2c so we can serve HTTP/2 without TLS.
h2c.NewHandler(mux, &http2.Server{}),
)
}
愚直にechoに置き換えると以下のようになります
func main() {
greeter := &GreetServer{}
e := echo.New()
path, handler := greetv1connect.NewGreetServiceHandler(greeter)
e.Any(fmt.Sprintf("%s*", path), echo.WrapHandler(handler))
e.StartH2CServer(("localhost:8080"), &http2.Server{})
}
ワイルドカードを付け足している理由としては
NewXxxServiceHandler
が返すパス文字列は/{Service}/
となっていて
NewXxxServiceHandler
が返す http.Handler は内部にgRPC Method単位で http.Handler を保持し、
リクエストされたgRPC Methodに応じて実際に呼び出されるハンドラを振り分けているためです
func NewTagServiceHandler(
svc TagServiceHandler,
opts ...connect.HandlerOption,
) (string, http.Handler) {
// 省略
return "/tag.TagService/",
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case TagServiceGetTagByIdProcedure:
tagServiceGetTagByIdHandler.ServeHTTP(w, r)
case TagServiceGetAllTagsProcedure:
tagServiceGetAllTagsHandler.ServeHTTP(w, r)
case TagServiceGetNextTagsProcedure:
tagServiceGetNextTagsHandler.ServeHTTP(w, r)
case TagServiceGetPrevTagsProcedure:
tagServiceGetPrevTagsHandler.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
})
}
末尾スラッシュは http.ServeMux であれば子リソースのあるリクエストにもマッチしますが
echoの場合は明示的に*
を指定しなければマッチしません
Middleware etcを足してみる
ヘルスチェック、Server Reflection、New Relicの自動計装を足してこんな感じになりました
func main() {
greeter := &GreetServer{}
e := echo.New()
path, handler := greetv1connect.NewGreetServiceHandler(greeter)
e.POST(
fmt.Sprintf("%s*", path),
echo.WrapHandler(handler),
nrecho.Middleware(nr)))
reflector := grpcreflect.NewStaticReflector(greetv1connect.GreetServiceName)
reflectV1Path, reflectV1Handler := grpcreflect.NewHandlerV1(reflector)
e.POST(fmt.Sprintf("%s*", reflectV1Path), echo.WrapHandler(reflectV1Handler))
reflectV1AlpPath, reflectV1AlpHandler := grpcreflect.NewHandlerV1Alpha(reflector)
e.POST(fmt.Sprintf("%s*", reflectV1AlpPath),
echo.WrapHandler(reflectV1AlphaHandler))
healthPath, healthHandler := grpchealth.NewHandler(
grpchealth.NewStaticChecker(greetv1connect.GreetServiceName))
e.POST(fmt.Sprintf("%s*", healthPath), echo.WrapHandler(healthHandler))
e.StartH2CServer(("localhost:8080"), &http2.Server{})
}
またconnectで利用できるHTTPメソッドはデフォルトではPOSTのみのため
e.Any
-> e.POST
に変更しています
nrechoで行うconnectの計装とNew RelicのTransaction名
先ほどのコードではnrecho
で計装を行っていましたが
実際にNew Relicで確認してみるとgRPC Methodが*
となっています
Connectへの移行前、nrgrpcによって計装されたTransactionがこちらです
これは nrecho.Middleware の実装に理由があります
nrechoではTransaction名に使用するパス名の取得をecho.Context.Path()
で行っています
func transactionName(c echo.Context) (string, string) {
ptr := handlerPointer(c.Handler())
if ptr == handlerPointer(echo.NotFoundHandler) {
return "NotFoundHandler", ""
}
if ptr == handlerPointer(echo.MethodNotAllowedHandler) {
return "MethodNotAllowedHandler", ""
}
return c.Request().Method + " " + c.Path(), c.Path()
}
GoDocにも記載されている通りecho.Context.Path() が返すのは実際にリクエストされたパスではなく
ハンドラに登録されたパスとなるため、ここで返されるのは{Service}/{Method}
ではなく{Service}/*
となります
type Context interface {
// 省略
// Path returns the registered path for the handler.
Path() string
// 省略
}
そこで今回はConnect用にMiddlewareを実装しました
Connectでしか使わないのならConnectのInterceptorで良い気がしてきた...
このMiddlewareではnrechoによって開始されたTransactionを後から上書きしています
func NRConnect(app *newrelic.Application) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ctx := c.Request().Context()
nrtx := newrelic.FromContext(ctx)
if nrtx == nil {
nrtx = app.StartTransaction(c.Request().URL.Path)
defer nrtx.End()
} else {
nrtx.SetName(c.Request().URL.Path)
}
host := c.Request().Host
nrtx.SetWebRequest(newrelic.WebRequest{
Type: "ConnectRPC",
Host: host,
Header: c.Request().Header,
URL: &url.URL{
Scheme: "connectrpc",
Host: host,
Path: c.Request().URL.Path,
},
Method: "POST",
Transport: newrelic.TransportHTTP})
ctx = newrelic.NewContext(ctx, nrtx)
c.SetRequest(c.Request().WithContext(ctx))
return next(c)
}
}
}
実際にNew Relic上で確認してみると期待通りgRPC MethodがTransaction名に含まれています