gRPC -> Connect RPC(connect-go on echo)

#Connect#Go#echo#gRPC
2024/12/29
ArticleImage:01JG822KJXRAF63P5BTP8AZ0QD

はじめに

このブログのマイクロサービスのうち1つをgoogle.golang.org/grpcからconnectrpc.com/connectに置き換えてみました

99designs/gqlgenlabstack/echoでルーティングしているため自作したMiddlewareが流用できたら嬉しい、ぐらいのモチベですが

しばらくの間は
google.golang.org/grpc で動いているマイクロサービスと connectrpc.com/connect で動いているマイクロサービスが混在する状態で比較検討する材料にできればと考えています

ルーティング

connectのサンプルコードではnet/httpを使用していますが

Copy
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に置き換えると以下のようになります

Copy
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に応じて実際に呼び出されるハンドラを振り分けているためです

Copy
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の自動計装を足してこんな感じになりました

Copy
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が*となっています

nerechoで計装した場合

Connectへの移行前、nrgrpcによって計装されたTransactionがこちらです

gRPCをnrgrpcで計装した場合

これは nrecho.Middleware の実装に理由があります
nrechoではTransaction名に使用するパス名の取得をecho.Context.Path()で行っています

Copy
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}/*となります

Copy
type Context interface {
	// 省略

	// Path returns the registered path for the handler.
	Path() string

	// 省略
}

そこで今回はConnect用にMiddlewareを実装しました
Connectでしか使わないのならConnectのInterceptorで良い気がしてきた...

このMiddlewareではnrechoによって開始されたTransactionを後から上書きしています

Copy
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名に含まれています

俺々Middlewareで計装した場合


Recommend Articles

Gatsby.jsとgqlgenでブログを作った

はじめに 最初は単にGraphQL、gRPC、ECS、New Relic etc 自分が興味のある技術トピックでHello World Enterprise Editionをやるだけのつもりだったものの GraphQL -> ヘッドレスCMS -> ブログ という連想ゲームの結…

2024/12/23

ArticleImage:01JFT54WPPZ0ET3XTKZ0JTCMRE

S3 + CloudFrontで実現する独自ドメインGoパッケージ

はじめに go.uber.org/mock google.golang.org/grpc gorm.io/gorm 独自ドメインで配布されているGoパッケージはカッコいい 、 のような のPrefixがない分、importディレクティブがスッキリして見える 今回は当ブログのバ…

2025/01/26

ArticleImage:01JJHA5TFHVR4MP9ZWJSDD713A

Copyright © miyamo2 All rights reserved.