From 54cf30be9bcfa8f34519de9d2958046aa327a2dc Mon Sep 17 00:00:00 2001 From: Tobias Brumhard Date: Tue, 31 Aug 2021 10:36:29 +0200 Subject: [PATCH 01/11] Add HealthCheck endpoint option to ServeMux --- runtime/mux.go | 54 ++++++++++++++++ runtime/mux_test.go | 153 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 207 insertions(+) diff --git a/runtime/mux.go b/runtime/mux.go index a282169e621..6cfee14b62a 100644 --- a/runtime/mux.go +++ b/runtime/mux.go @@ -9,6 +9,10 @@ import ( "regexp" "strings" + "google.golang.org/grpc/health/grpc_health_v1" + + "google.golang.org/grpc" + "github.com/grpc-ecosystem/grpc-gateway/v2/internal/httprule" "google.golang.org/grpc/codes" "google.golang.org/grpc/grpclog" @@ -204,6 +208,56 @@ func WithDisablePathLengthFallback() ServeMuxOption { } } +// WithHealthCheckEnabled returns a ServeMuxOption that will add a /healthz endpoint to the created ServeMux. +// When called the handler will forward the request to the upstream grpc service health check (defined in the +// gRPC Health Checking Protocol). +// See here https://grpc-ecosystem.github.io/grpc-gateway/docs/operations/health_check/ for more information on how +// to setup the protocol in the grpc server. +// If you define a service as query parameter, this will also be forwarded as service in the HealthCheckRequest. +// TODO: refine reponses +// TODO: add test cases +func WithHealthCheckEnabled(conn grpc.ClientConnInterface) ServeMuxOption { + healthCheckClient := grpc_health_v1.NewHealthClient(conn) + + return func(serveMux *ServeMux) { + // error can be ignored since pattern is definitely valid + _ = serveMux.HandlePath( + http.MethodGet, "/healthz", func(w http.ResponseWriter, r *http.Request, _ map[string]string, + ) { + serviceQueryParam := r.URL.Query().Get("service") + resp, err := healthCheckClient.Check(r.Context(), &grpc_health_v1.HealthCheckRequest{ + Service: serviceQueryParam, + }) + + if err != nil { + if stat, ok := status.FromError(err); ok { + switch stat.Code() { + case codes.Unimplemented: + http.Error(w, err.Error(), http.StatusNotImplemented) + case codes.DeadlineExceeded: + http.Error(w, err.Error(), http.StatusBadGateway) + case codes.NotFound: + http.Error(w, err.Error(), http.StatusNotFound) + default: + } + } + + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + switch resp.GetStatus() { + case grpc_health_v1.HealthCheckResponse_SERVING: + _, _ = fmt.Fprintf(w, "%v", resp) + case grpc_health_v1.HealthCheckResponse_NOT_SERVING, grpc_health_v1.HealthCheckResponse_UNKNOWN: + http.Error(w, fmt.Sprintf("%v", resp), http.StatusBadGateway) + case grpc_health_v1.HealthCheckResponse_SERVICE_UNKNOWN: + http.Error(w, fmt.Sprintf("%v", resp), http.StatusNotFound) + } + }) + } +} + // NewServeMux returns a new ServeMux whose internal mapping is empty. func NewServeMux(opts ...ServeMuxOption) *ServeMux { serveMux := &ServeMux{ diff --git a/runtime/mux_test.go b/runtime/mux_test.go index 0bdc72c2e23..46e1f5c8da4 100644 --- a/runtime/mux_test.go +++ b/runtime/mux_test.go @@ -2,13 +2,25 @@ package runtime_test import ( "bytes" + "context" "fmt" + "log" + "net" "net/http" "net/http/httptest" "net/url" "strconv" + "strings" "testing" + "google.golang.org/grpc/status" + + "google.golang.org/grpc/codes" + + "google.golang.org/grpc" + "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/test/bufconn" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" ) @@ -598,3 +610,144 @@ func TestServeMux_HandlePath(t *testing.T) { }) } } + +var healthCheckTests = []struct { + name string + code codes.Code + status grpc_health_v1.HealthCheckResponse_ServingStatus + httpStatusCode int +}{ + { + "Test codes.Unimplemented", + codes.Unimplemented, + grpc_health_v1.HealthCheckResponse_UNKNOWN, + http.StatusNotImplemented, + }, + { + "Test codes.DeadlineExceeded", + codes.DeadlineExceeded, + grpc_health_v1.HealthCheckResponse_UNKNOWN, + http.StatusBadGateway, + }, + { + "Test codes.NotFound", + codes.NotFound, + grpc_health_v1.HealthCheckResponse_UNKNOWN, + http.StatusNotFound, + }, + { + "Test any other code", + codes.Canceled, + grpc_health_v1.HealthCheckResponse_UNKNOWN, + http.StatusInternalServerError, + }, + { + "Test HealthCheckResponse_SERVING", + codes.OK, + grpc_health_v1.HealthCheckResponse_SERVING, + http.StatusOK, + }, + { + "Test HealthCheckResponse_NOT_SERVING", + codes.OK, + grpc_health_v1.HealthCheckResponse_NOT_SERVING, + http.StatusBadGateway, + }, + { + "Test HealthCheckResponse_UNKNOWN", + codes.OK, + grpc_health_v1.HealthCheckResponse_UNKNOWN, + http.StatusBadGateway, + }, + { + "Test HealthCheckResponse_SERVICE_UNKNOWN", + codes.OK, + grpc_health_v1.HealthCheckResponse_SERVICE_UNKNOWN, + http.StatusNotFound, + }, +} + +func TestWithHealthCheckEnabled_codes(t *testing.T) { + for _, tt := range healthCheckTests { + t.Run(tt.name, func(t *testing.T) { + conn, err := dummyGrpcServer(tt.status, tt.code) + if err != nil { + t.Errorf("connection to dummy grpc server failed: %v", err) + } + + mux := runtime.NewServeMux(runtime.WithHealthCheckEnabled(conn)) + + r := httptest.NewRequest(http.MethodGet, "/healthz", nil) + rr := httptest.NewRecorder() + + mux.ServeHTTP(rr, r) + + if rr.Code != tt.httpStatusCode { + t.Errorf( + "result http status code for grpc code %q and status %q should be %d, got %d", + tt.code, tt.status, tt.httpStatusCode, rr.Code, + ) + } + }) + } +} + +func TestWithHealthCheckEnabled_serviceParam(t *testing.T) { + service := "test" + + // set codes.UNKNOWN to trigger error response with service in msg + conn, err := dummyGrpcServer(grpc_health_v1.HealthCheckResponse_UNKNOWN, codes.Unknown) + if err != nil { + t.Errorf("connection to dummy grpc server failed: %v", err) + } + + mux := runtime.NewServeMux(runtime.WithHealthCheckEnabled(conn)) + + r := httptest.NewRequest(http.MethodGet, "/healthz?service="+service, nil) + rr := httptest.NewRecorder() + + mux.ServeHTTP(rr, r) + + if !strings.Contains(rr.Body.String(), service) { + t.Errorf( + "service query parameter should be translated to HealthCheckRequest: expected %s to contain %s", + rr.Body.String(), service, + ) + } +} + +type grpcServer struct { + status grpc_health_v1.HealthCheckResponse_ServingStatus + code codes.Code +} + +func (g *grpcServer) Check(ctx context.Context, r *grpc_health_v1.HealthCheckRequest) (*grpc_health_v1.HealthCheckResponse, error) { + if g.code != codes.OK { + return nil, status.Error(g.code, r.GetService()) + } + + return &grpc_health_v1.HealthCheckResponse{Status: g.status}, nil +} + +func (g *grpcServer) Watch(r *grpc_health_v1.HealthCheckRequest, s grpc_health_v1.Health_WatchServer) error { + panic("implement me") +} + +func dummyGrpcServer(status grpc_health_v1.HealthCheckResponse_ServingStatus, code codes.Code) (grpc.ClientConnInterface, error) { + s := grpc.NewServer() + grpc_health_v1.RegisterHealthServer(s, &grpcServer{status: status, code: code}) + + ctx := context.Background() + lis := bufconn.Listen(1024 * 1024) + go func() { + if err := s.Serve(lis); err != nil { + log.Fatalf("Server exited with error: %v", err) + } + }() + + return grpc.DialContext(ctx, + "bufnet", + grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { return lis.Dial() }), + grpc.WithInsecure(), + ) +} From 18fe9c7de1ec498bf865353bfd28298098a6e729 Mon Sep 17 00:00:00 2001 From: Tobias Brumhard Date: Tue, 31 Aug 2021 10:52:46 +0200 Subject: [PATCH 02/11] Use outboundMarshaler for HealthCheck reponses --- runtime/mux.go | 42 ++++++++++++++++++------------------------ runtime/mux_test.go | 24 +++--------------------- 2 files changed, 21 insertions(+), 45 deletions(-) diff --git a/runtime/mux.go b/runtime/mux.go index 6cfee14b62a..fdf96c11242 100644 --- a/runtime/mux.go +++ b/runtime/mux.go @@ -214,46 +214,40 @@ func WithDisablePathLengthFallback() ServeMuxOption { // See here https://grpc-ecosystem.github.io/grpc-gateway/docs/operations/health_check/ for more information on how // to setup the protocol in the grpc server. // If you define a service as query parameter, this will also be forwarded as service in the HealthCheckRequest. -// TODO: refine reponses -// TODO: add test cases func WithHealthCheckEnabled(conn grpc.ClientConnInterface) ServeMuxOption { healthCheckClient := grpc_health_v1.NewHealthClient(conn) - return func(serveMux *ServeMux) { + return func(s *ServeMux) { // error can be ignored since pattern is definitely valid - _ = serveMux.HandlePath( + _ = s.HandlePath( http.MethodGet, "/healthz", func(w http.ResponseWriter, r *http.Request, _ map[string]string, ) { + _, outboundMarshaler := MarshalerForRequest(s, r) + serviceQueryParam := r.URL.Query().Get("service") + resp, err := healthCheckClient.Check(r.Context(), &grpc_health_v1.HealthCheckRequest{ Service: serviceQueryParam, }) - if err != nil { - if stat, ok := status.FromError(err); ok { - switch stat.Code() { - case codes.Unimplemented: - http.Error(w, err.Error(), http.StatusNotImplemented) - case codes.DeadlineExceeded: - http.Error(w, err.Error(), http.StatusBadGateway) - case codes.NotFound: - http.Error(w, err.Error(), http.StatusNotFound) - default: - } + s.errorHandler(r.Context(), s, outboundMarshaler, w, r, err) + return + } + + if resp.GetStatus() != grpc_health_v1.HealthCheckResponse_SERVING { + var err error + switch resp.GetStatus() { + case grpc_health_v1.HealthCheckResponse_NOT_SERVING, grpc_health_v1.HealthCheckResponse_UNKNOWN: + err = status.Error(codes.Unavailable, resp.String()) + case grpc_health_v1.HealthCheckResponse_SERVICE_UNKNOWN: + err = status.Error(codes.NotFound, resp.String()) } - http.Error(w, err.Error(), http.StatusInternalServerError) + s.errorHandler(r.Context(), s, outboundMarshaler, w, r, err) return } - switch resp.GetStatus() { - case grpc_health_v1.HealthCheckResponse_SERVING: - _, _ = fmt.Fprintf(w, "%v", resp) - case grpc_health_v1.HealthCheckResponse_NOT_SERVING, grpc_health_v1.HealthCheckResponse_UNKNOWN: - http.Error(w, fmt.Sprintf("%v", resp), http.StatusBadGateway) - case grpc_health_v1.HealthCheckResponse_SERVICE_UNKNOWN: - http.Error(w, fmt.Sprintf("%v", resp), http.StatusNotFound) - } + _ = outboundMarshaler.NewEncoder(w).Encode(resp) }) } } diff --git a/runtime/mux_test.go b/runtime/mux_test.go index 46e1f5c8da4..9c751c08681 100644 --- a/runtime/mux_test.go +++ b/runtime/mux_test.go @@ -618,29 +618,11 @@ var healthCheckTests = []struct { httpStatusCode int }{ { - "Test codes.Unimplemented", - codes.Unimplemented, - grpc_health_v1.HealthCheckResponse_UNKNOWN, - http.StatusNotImplemented, - }, - { - "Test codes.DeadlineExceeded", - codes.DeadlineExceeded, - grpc_health_v1.HealthCheckResponse_UNKNOWN, - http.StatusBadGateway, - }, - { - "Test codes.NotFound", + "Test grpc error code", codes.NotFound, grpc_health_v1.HealthCheckResponse_UNKNOWN, http.StatusNotFound, }, - { - "Test any other code", - codes.Canceled, - grpc_health_v1.HealthCheckResponse_UNKNOWN, - http.StatusInternalServerError, - }, { "Test HealthCheckResponse_SERVING", codes.OK, @@ -651,13 +633,13 @@ var healthCheckTests = []struct { "Test HealthCheckResponse_NOT_SERVING", codes.OK, grpc_health_v1.HealthCheckResponse_NOT_SERVING, - http.StatusBadGateway, + http.StatusServiceUnavailable, }, { "Test HealthCheckResponse_UNKNOWN", codes.OK, grpc_health_v1.HealthCheckResponse_UNKNOWN, - http.StatusBadGateway, + http.StatusServiceUnavailable, }, { "Test HealthCheckResponse_SERVICE_UNKNOWN", From b44f7572264d03fb1fd92d5046ecff750154fddc Mon Sep 17 00:00:00 2001 From: Tobias Brumhard Date: Tue, 31 Aug 2021 11:08:05 +0200 Subject: [PATCH 03/11] Rename WithHealthCheckEnabled to WithHealthzEndpoint --- runtime/mux.go | 4 ++-- runtime/mux_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/runtime/mux.go b/runtime/mux.go index fdf96c11242..a0f67dbde38 100644 --- a/runtime/mux.go +++ b/runtime/mux.go @@ -208,13 +208,13 @@ func WithDisablePathLengthFallback() ServeMuxOption { } } -// WithHealthCheckEnabled returns a ServeMuxOption that will add a /healthz endpoint to the created ServeMux. +// WithHealthzEndpoint returns a ServeMuxOption that will add a /healthz endpoint to the created ServeMux. // When called the handler will forward the request to the upstream grpc service health check (defined in the // gRPC Health Checking Protocol). // See here https://grpc-ecosystem.github.io/grpc-gateway/docs/operations/health_check/ for more information on how // to setup the protocol in the grpc server. // If you define a service as query parameter, this will also be forwarded as service in the HealthCheckRequest. -func WithHealthCheckEnabled(conn grpc.ClientConnInterface) ServeMuxOption { +func WithHealthzEndpoint(conn grpc.ClientConnInterface) ServeMuxOption { healthCheckClient := grpc_health_v1.NewHealthClient(conn) return func(s *ServeMux) { diff --git a/runtime/mux_test.go b/runtime/mux_test.go index 9c751c08681..9e60c1e9e49 100644 --- a/runtime/mux_test.go +++ b/runtime/mux_test.go @@ -649,7 +649,7 @@ var healthCheckTests = []struct { }, } -func TestWithHealthCheckEnabled_codes(t *testing.T) { +func TestWithHealthzEndpoint_codes(t *testing.T) { for _, tt := range healthCheckTests { t.Run(tt.name, func(t *testing.T) { conn, err := dummyGrpcServer(tt.status, tt.code) @@ -657,7 +657,7 @@ func TestWithHealthCheckEnabled_codes(t *testing.T) { t.Errorf("connection to dummy grpc server failed: %v", err) } - mux := runtime.NewServeMux(runtime.WithHealthCheckEnabled(conn)) + mux := runtime.NewServeMux(runtime.WithHealthzEndpoint(conn)) r := httptest.NewRequest(http.MethodGet, "/healthz", nil) rr := httptest.NewRecorder() @@ -674,7 +674,7 @@ func TestWithHealthCheckEnabled_codes(t *testing.T) { } } -func TestWithHealthCheckEnabled_serviceParam(t *testing.T) { +func TestWithHealthzEndpoint_serviceParam(t *testing.T) { service := "test" // set codes.UNKNOWN to trigger error response with service in msg @@ -683,7 +683,7 @@ func TestWithHealthCheckEnabled_serviceParam(t *testing.T) { t.Errorf("connection to dummy grpc server failed: %v", err) } - mux := runtime.NewServeMux(runtime.WithHealthCheckEnabled(conn)) + mux := runtime.NewServeMux(runtime.WithHealthzEndpoint(conn)) r := httptest.NewRequest(http.MethodGet, "/healthz?service="+service, nil) rr := httptest.NewRecorder() From 22944144aeee1c1c3f33dacbe5453491a90e6a1f Mon Sep 17 00:00:00 2001 From: Tobias Brumhard Date: Tue, 31 Aug 2021 11:17:37 +0200 Subject: [PATCH 04/11] Add docs for WithHealthzEndpoint --- docs/docs/operations/health_check.md | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/docs/operations/health_check.md b/docs/docs/operations/health_check.md index df332804f51..e5257f0460e 100644 --- a/docs/docs/operations/health_check.md +++ b/docs/docs/operations/health_check.md @@ -1,8 +1,5 @@ --- -layout: default -title: Health check -nav_order: 1 -parent: Operations +layout: default title: Health check nav_order: 1 parent: Operations --- # Health check @@ -36,3 +33,21 @@ func (s *serviceServer) Watch(in *health.HealthCheckRequest, _ health.Health_Wat ``` 3. You can test the functionality with [GRPC health probe](https://github.com/grpc-ecosystem/grpc-health-probe). + +## Adding `/healthz` endpoint to runtime.ServeMux + +To automatically register a `/healthz` endpoint in your `ServeMux` you can use +the `ServeMuxOption` `WithHealthzEndpoint` +which takes in a connection to your registered gRPC server. + +This endpoint will forward a request to the `Check` method described above to really check the health of the +whole system, not only the gateway itself. If your server doesn't implement the health checking protocol each request +to `/healthz` will result in the following: + +```json +{"code":12,"message":"unknown service grpc.health.v1.Health","details":[]} +``` + +If you've implemented multiple services in your server you can target specific services with the `?service=` +query parameter. This will then be added to the `health.HealthCheckRequest` in the `Service` property. With that you can +write your own logic to handle that in the health checking methods. \ No newline at end of file From 1546d5f056f0be07ac64de40eea2512180b27ba5 Mon Sep 17 00:00:00 2001 From: Tobias Brumhard Date: Tue, 31 Aug 2021 11:33:20 +0200 Subject: [PATCH 05/11] Cleanup health_check.md and imports --- docs/docs/operations/health_check.md | 5 ++++- runtime/mux.go | 6 ++---- runtime/mux_test.go | 11 ++++------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/docs/docs/operations/health_check.md b/docs/docs/operations/health_check.md index e5257f0460e..95185c5e1c6 100644 --- a/docs/docs/operations/health_check.md +++ b/docs/docs/operations/health_check.md @@ -1,5 +1,8 @@ --- -layout: default title: Health check nav_order: 1 parent: Operations +layout: default +title: Health +check nav_order: 1 +parent: Operations --- # Health check diff --git a/runtime/mux.go b/runtime/mux.go index a0f67dbde38..98830b69bb2 100644 --- a/runtime/mux.go +++ b/runtime/mux.go @@ -9,13 +9,11 @@ import ( "regexp" "strings" - "google.golang.org/grpc/health/grpc_health_v1" - - "google.golang.org/grpc" - "github.com/grpc-ecosystem/grpc-gateway/v2/internal/httprule" + "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" diff --git a/runtime/mux_test.go b/runtime/mux_test.go index 9e60c1e9e49..e7f67ab5e91 100644 --- a/runtime/mux_test.go +++ b/runtime/mux_test.go @@ -13,16 +13,13 @@ import ( "strings" "testing" - "google.golang.org/grpc/status" - - "google.golang.org/grpc/codes" - + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/status" "google.golang.org/grpc/test/bufconn" - - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" ) func TestMuxServeHTTP(t *testing.T) { From a8020d76dc0c2b5dc4f33a7fa3da38375c3f8bd1 Mon Sep 17 00:00:00 2001 From: Tobias Brumhard Date: Tue, 31 Aug 2021 11:34:07 +0200 Subject: [PATCH 06/11] Fix health_check.md header --- docs/docs/operations/health_check.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/operations/health_check.md b/docs/docs/operations/health_check.md index 95185c5e1c6..2301077d3ae 100644 --- a/docs/docs/operations/health_check.md +++ b/docs/docs/operations/health_check.md @@ -1,7 +1,7 @@ --- layout: default -title: Health -check nav_order: 1 +title: Health check +nav_order: 1 parent: Operations --- From 2de8285cac5dca4104b2d0e8dedf550a8ca0dd80 Mon Sep 17 00:00:00 2001 From: Tobias Brumhard Date: Tue, 31 Aug 2021 11:34:36 +0200 Subject: [PATCH 07/11] Fix health_check.md header whitespace --- docs/docs/operations/health_check.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/operations/health_check.md b/docs/docs/operations/health_check.md index 2301077d3ae..c66f05da49f 100644 --- a/docs/docs/operations/health_check.md +++ b/docs/docs/operations/health_check.md @@ -1,6 +1,6 @@ --- layout: default -title: Health check +title: Health check nav_order: 1 parent: Operations --- From bc9f925c92bade69327a28f2b5874895b1eb2ef5 Mon Sep 17 00:00:00 2001 From: Tobias Brumhard Date: Wed, 1 Sep 2021 07:51:28 +0200 Subject: [PATCH 08/11] Replace panic with error return in test grpc server Co-authored-by: Johan Brandhorst-Satzkorn --- runtime/mux_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/mux_test.go b/runtime/mux_test.go index e7f67ab5e91..fdf84cdd103 100644 --- a/runtime/mux_test.go +++ b/runtime/mux_test.go @@ -709,7 +709,7 @@ func (g *grpcServer) Check(ctx context.Context, r *grpc_health_v1.HealthCheckReq } func (g *grpcServer) Watch(r *grpc_health_v1.HealthCheckRequest, s grpc_health_v1.Health_WatchServer) error { - panic("implement me") + return status.Error(codes.Unimplemented, "unimplemented") } func dummyGrpcServer(status grpc_health_v1.HealthCheckResponse_ServingStatus, code codes.Code) (grpc.ClientConnInterface, error) { From c02f9915b581582d4c8d4441457654c7b15c47c5 Mon Sep 17 00:00:00 2001 From: Tobias Brumhard Date: Wed, 1 Sep 2021 08:07:10 +0200 Subject: [PATCH 09/11] Update runtime/BUILD.bazel --- runtime/BUILD.bazel | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index a7a039816ea..b0b6ddca45d 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -28,8 +28,10 @@ go_library( "//utilities", "@go_googleapis//google/api:httpbody_go_proto", "@io_bazel_rules_go//proto/wkt:field_mask_go_proto", + "@org_golang_google_grpc//:go_default_library", "@org_golang_google_grpc//codes", "@org_golang_google_grpc//grpclog", + "@org_golang_google_grpc//health/grpc_health_v1", "@org_golang_google_grpc//metadata", "@org_golang_google_grpc//status", "@org_golang_google_protobuf//encoding/protojson", @@ -71,9 +73,12 @@ go_test( "@go_googleapis//google/rpc:errdetails_go_proto", "@go_googleapis//google/rpc:status_go_proto", "@io_bazel_rules_go//proto/wkt:field_mask_go_proto", + "@org_golang_google_grpc//:go_default_library", "@org_golang_google_grpc//codes", + "@org_golang_google_grpc//health/grpc_health_v1", "@org_golang_google_grpc//metadata", "@org_golang_google_grpc//status", + "@org_golang_google_grpc//test/bufconn", "@org_golang_google_protobuf//encoding/protojson", "@org_golang_google_protobuf//proto", "@org_golang_google_protobuf//testing/protocmp", From 9fead79176c94f56bc2d03dc3c942fcc1fb2c195 Mon Sep 17 00:00:00 2001 From: Tobias Brumhard Date: Wed, 2 Mar 2022 08:22:28 +0100 Subject: [PATCH 10/11] Use HealthClient for WithHealthzEndpoint option Signed-off-by: Tobias Brumhard --- runtime/mux.go | 5 +---- runtime/mux_test.go | 49 +++++++++------------------------------------ 2 files changed, 11 insertions(+), 43 deletions(-) diff --git a/runtime/mux.go b/runtime/mux.go index 98830b69bb2..8a3238b048d 100644 --- a/runtime/mux.go +++ b/runtime/mux.go @@ -10,7 +10,6 @@ import ( "strings" "github.com/grpc-ecosystem/grpc-gateway/v2/internal/httprule" - "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/grpclog" "google.golang.org/grpc/health/grpc_health_v1" @@ -212,9 +211,7 @@ func WithDisablePathLengthFallback() ServeMuxOption { // See here https://grpc-ecosystem.github.io/grpc-gateway/docs/operations/health_check/ for more information on how // to setup the protocol in the grpc server. // If you define a service as query parameter, this will also be forwarded as service in the HealthCheckRequest. -func WithHealthzEndpoint(conn grpc.ClientConnInterface) ServeMuxOption { - healthCheckClient := grpc_health_v1.NewHealthClient(conn) - +func WithHealthzEndpoint(healthCheckClient grpc_health_v1.HealthClient) ServeMuxOption { return func(s *ServeMux) { // error can be ignored since pattern is definitely valid _ = s.HandlePath( diff --git a/runtime/mux_test.go b/runtime/mux_test.go index fdf84cdd103..66d0ecb2972 100644 --- a/runtime/mux_test.go +++ b/runtime/mux_test.go @@ -4,8 +4,6 @@ import ( "bytes" "context" "fmt" - "log" - "net" "net/http" "net/http/httptest" "net/url" @@ -19,7 +17,6 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/status" - "google.golang.org/grpc/test/bufconn" ) func TestMuxServeHTTP(t *testing.T) { @@ -649,12 +646,7 @@ var healthCheckTests = []struct { func TestWithHealthzEndpoint_codes(t *testing.T) { for _, tt := range healthCheckTests { t.Run(tt.name, func(t *testing.T) { - conn, err := dummyGrpcServer(tt.status, tt.code) - if err != nil { - t.Errorf("connection to dummy grpc server failed: %v", err) - } - - mux := runtime.NewServeMux(runtime.WithHealthzEndpoint(conn)) + mux := runtime.NewServeMux(runtime.WithHealthzEndpoint(&dummyHealthCheckClient{status: tt.status, code: tt.code})) r := httptest.NewRequest(http.MethodGet, "/healthz", nil) rr := httptest.NewRecorder() @@ -674,13 +666,9 @@ func TestWithHealthzEndpoint_codes(t *testing.T) { func TestWithHealthzEndpoint_serviceParam(t *testing.T) { service := "test" - // set codes.UNKNOWN to trigger error response with service in msg - conn, err := dummyGrpcServer(grpc_health_v1.HealthCheckResponse_UNKNOWN, codes.Unknown) - if err != nil { - t.Errorf("connection to dummy grpc server failed: %v", err) - } - - mux := runtime.NewServeMux(runtime.WithHealthzEndpoint(conn)) + // trigger error to output service in body + dummyClient := dummyHealthCheckClient{status: grpc_health_v1.HealthCheckResponse_UNKNOWN, code: codes.Unknown} + mux := runtime.NewServeMux(runtime.WithHealthzEndpoint(&dummyClient)) r := httptest.NewRequest(http.MethodGet, "/healthz?service="+service, nil) rr := httptest.NewRecorder() @@ -695,12 +683,14 @@ func TestWithHealthzEndpoint_serviceParam(t *testing.T) { } } -type grpcServer struct { +var _ grpc_health_v1.HealthClient = (*dummyHealthCheckClient)(nil) + +type dummyHealthCheckClient struct { status grpc_health_v1.HealthCheckResponse_ServingStatus code codes.Code } -func (g *grpcServer) Check(ctx context.Context, r *grpc_health_v1.HealthCheckRequest) (*grpc_health_v1.HealthCheckResponse, error) { +func (g *dummyHealthCheckClient) Check(ctx context.Context, r *grpc_health_v1.HealthCheckRequest, opts ...grpc.CallOption) (*grpc_health_v1.HealthCheckResponse, error) { if g.code != codes.OK { return nil, status.Error(g.code, r.GetService()) } @@ -708,25 +698,6 @@ func (g *grpcServer) Check(ctx context.Context, r *grpc_health_v1.HealthCheckReq return &grpc_health_v1.HealthCheckResponse{Status: g.status}, nil } -func (g *grpcServer) Watch(r *grpc_health_v1.HealthCheckRequest, s grpc_health_v1.Health_WatchServer) error { - return status.Error(codes.Unimplemented, "unimplemented") -} - -func dummyGrpcServer(status grpc_health_v1.HealthCheckResponse_ServingStatus, code codes.Code) (grpc.ClientConnInterface, error) { - s := grpc.NewServer() - grpc_health_v1.RegisterHealthServer(s, &grpcServer{status: status, code: code}) - - ctx := context.Background() - lis := bufconn.Listen(1024 * 1024) - go func() { - if err := s.Serve(lis); err != nil { - log.Fatalf("Server exited with error: %v", err) - } - }() - - return grpc.DialContext(ctx, - "bufnet", - grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { return lis.Dial() }), - grpc.WithInsecure(), - ) +func (g *dummyHealthCheckClient) Watch(ctx context.Context, r *grpc_health_v1.HealthCheckRequest, opts ...grpc.CallOption) (grpc_health_v1.Health_WatchClient, error) { + return nil, status.Error(codes.Unimplemented, "unimplemented") } From 4b624dffd3e51f5b57121410cd9096457062233a Mon Sep 17 00:00:00 2001 From: Tobias Brumhard Date: Wed, 2 Mar 2022 08:52:58 +0100 Subject: [PATCH 11/11] Regenerate bazel files Signed-off-by: Tobias Brumhard --- runtime/BUILD.bazel | 2 -- 1 file changed, 2 deletions(-) diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index b0b6ddca45d..76d23634162 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -28,7 +28,6 @@ go_library( "//utilities", "@go_googleapis//google/api:httpbody_go_proto", "@io_bazel_rules_go//proto/wkt:field_mask_go_proto", - "@org_golang_google_grpc//:go_default_library", "@org_golang_google_grpc//codes", "@org_golang_google_grpc//grpclog", "@org_golang_google_grpc//health/grpc_health_v1", @@ -78,7 +77,6 @@ go_test( "@org_golang_google_grpc//health/grpc_health_v1", "@org_golang_google_grpc//metadata", "@org_golang_google_grpc//status", - "@org_golang_google_grpc//test/bufconn", "@org_golang_google_protobuf//encoding/protojson", "@org_golang_google_protobuf//proto", "@org_golang_google_protobuf//testing/protocmp",