きょこみのーと

元六本木でGo書いてました。今はVRでGo書いてます。

goa v3をそろそろ検証してみる

はじめに

github.com

今回は以下について

  • とりあえず触ってみた & 感想
  • Middlewareについて調査
  • Panic時の挙動とPanicをMiddlewareでハンドリングする

とりあえずGetting Startedを参考に触ってみた

https://goa.design/learn/getting-started/ 見ながらやっていく。

スッと動いた。良き!!

 [calcapi] 16:16:15 HTTP "Add" mounted on GET /add/{a}/{b}
 [calcapi] 16:16:15 HTTP "./gen/http/openapi.json" mounted on GET /openapi.json
 [calcapi] 16:16:15 serving gRPC method calc.Calc/Add
 [calcapi] 16:16:15 HTTP server listening on "localhost:8000"
 [calcapi] 16:16:15 gRPC server listening on "localhost:8080"
 [calcapi] 16:16:17  id=sGdiU14f req=GET /add/1/2 from=127.0.0.1

generateされたclientもあるけど、あえてcurlで叩いてみる。

 curl -i -X GET http://localhost:8000/add/1/2
 HTTP/1.1 200 OK
 Content-Type: application/json
 Date: Sun, 20 Oct 2019 07:20:22 GMT
 Content-Length: 2
 
 3

よさそう

v1 -> v3の移行手順はこれらしい

https://goa.design/learn/upgrading/

designは基本的には、これにしたがって移行していけばよさそう。

また今度実際に移行したら色々書く予定

midlewareについて

goa.design

gRPCに対応したことで、プロトコル関係なく実行するmiddlewareとプロトコル単位で設定できるmiddlewareの2種類になったようす。

Endpoint Middleware

Useを使えば今まで通りすべてのpathに対してのmiddlewareになるみたい。 calcEndpoints.Add に対してだけ設定するみたいなやりかたもあるみたい。

calc/main.go.diff

     // Wrap the services in endpoints that can be invoked from other services
  // potentially running in different processes.
  var (
      calcEndpoints *calc.Endpoints
  )
  {
      calcEndpoints = calc.NewEndpoints(calcSvc)
 
      // Apply ErrorLogger to all endpoints.
      calcEndpoints.Use(middleware.ErrorLogger(logger, "calc"))
 
      // Or apply ErrorLogger to specific endpoint.
      //calcEndpoints.Add = middleware.ErrorLogger(logger, "add")(calcEndpoints.Add)
  }

Transport MIddleware

HTTPだけgRPCだけ実行みたいなのがやりたいときに使うぽい。

calc/middleware/auth.go

 package middleware
 
 import (
  "context"
  "net/http"
  "strings"
 )
 
 // WithAuthToken is a HTTP server middleware that reads the value of the
 // Authorization header and if present writes it in the request context.
 func WithAuthToken() func(http.Handler) http.Handler {
  return func(h http.Handler) http.Handler {
      // A HTTP handler is a function.
      return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
          req := r
          // Grab Authorization header and initialize request context with it.
          if bearerToken := r.Header.Get("Authorization"); bearerToken != "" {
              ctx := context.WithValue(r.Context(), AuthTokenKey, strings.Replace(bearerToken, "Bearer ", "", 1))
              req = r.WithContext(ctx)
          }
 
          // Call initial handler.
          h.ServeHTTP(w, req)
      })
  }
 }

goa.Context じゃなくて普通の context.Context がくるのでそこからGetすればOK

calc.go

 s.logger.Print("Token = ", ctx.Value(middleware.AuthTokenKey))

PanicHandler

httpパッケージのserver.goのserveメソッド側のrecoverでhandlingされる。

 2019/10/20 21:16:16 http: panic serving 127.0.0.1:57525: test
 goroutine 21 [running]:
 net/http.(*conn).serve.func1(0xc00017e280)
         /usr/local/opt/go/libexec/src/net/http/server.go:1767 +0x139
 panic(0x1507000, 0x168e4c0)
         /usr/local/opt/go/libexec/src/runtime/panic.go:679 +0x1b2
 calc.(*calcsrvc).Add(0xc0000b0550, 0x16a6860, 0xc0001d43f0, 0xc0001d2130, 0xc0001ec101, 0xc0001d2130, 0xc0000eb760)
         /Users/kyokomi/workspace/ghq/github.com/kyokomi-sandbox/sandbox/golang/goav3/calc/calc.go:29 +0x11c

これはこれでとりあえず助かるんですが、responseも以下みたいな感じになってしまい...

共通の500エラーとか返したいなぁと...

 $ curl -i -X GET -H "Authorization: Bearer hogehogehogehoge" http://localhost:8000/add/1/2
 curl: (52) Empty reply from server

v1のようにEndpoint Middlewareでrecoverを入れてみる

注意点としては、recoverしたときにreturnのerrorをちゃんとdefer内で差し替えないと、正常処理に入ってしまいそっちで別のpanic等が起きてしまうことがあります(encodeとかdecodeのあたりで)

calc/middleware/panic_handler.go

 package middleware

 import (
  "context"
  "fmt"
  "log"
 
  goa "goa.design/goa/v3/pkg"
 )
 
 // PanicRecover panic recover middleware
 func PanicRecover(l *log.Logger) func(goa.Endpoint) goa.Endpoint {
  return func(e goa.Endpoint) goa.Endpoint {
      return func(ctx context.Context, req interface{}) (response interface{}, err error) {
          defer func() {
              if exception := recover(); exception != nil {
                  panicErr, ok := exception.(error)
                  if !ok {
                      panicErr = fmt.Errorf("recover error %v", exception)
                  }
                  l.Printf("[PANIC] %v", panicErr)
                  err = panicErr
              }
          }()
 
          response, err = e(ctx, req)
          return
      }
  }
 }

適当にhandlerでpanicさせてときのログ

 [calcapi] 21:19:33 calc.add
 [calcapi] 21:19:33 Token = hogehogehogehoge
 [calcapi] 21:19:33 [PANIC] recover error test
 [calcapi] 21:19:33 [ERROR] calc: recover error test
 [calcapi] 21:19:33  id=IGTAuYo3 status=500 bytes=111 time=247.777µs

curlの結果もこんな感じになる

$ curl -i -X GET -H "Authorization: Bearer hogehogehogehoge" http://localhost:8000/add/1/2
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
Date: Sun, 20 Oct 2019 12:19:33 GMT
Content-Length: 111

{"name":"fault","id":"Aghc9GSY","message":"recover error test","temporary":false,"timeout":false,"fault":true}