Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ linters:
# No zero values for param structs.
explicit-mode: true
enforce-patterns:
- connectrpc\.com/grpchealth\..*[pP]arams
- connectrpc\.com/grpchealth/v2\..*[pP]arams
forbidigo:
forbid:
- pattern: ^fmt\.Print
Expand Down
28 changes: 16 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ grpchealth
==========

[![Build](https://github.com/connectrpc/grpchealth-go/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/connectrpc/grpchealth-go/actions/workflows/ci.yaml)
[![GoDoc](https://pkg.go.dev/badge/connectrpc.com/grpchealth.svg)](https://pkg.go.dev/connectrpc.com/grpchealth)
[![GoDoc](https://pkg.go.dev/badge/connectrpc.com/grpchealth/v2.svg)](https://pkg.go.dev/connectrpc.com/grpchealth/v2)

`connectrpc.com/grpchealth` adds support for gRPC-style health checks to any
`net/http` server — including those built with [Connect][connect]. By
polling this API, load balancers, container orchestrators, and other
infrastructure systems can respond to changes in your HTTP server's health.
`connectrpc.com/grpchealth/v2` adds support for gRPC-style health checks to
servers built with [Connect][connect]. By polling this API, load balancers,
container orchestrators, and other infrastructure systems can respond to
changes in your server's health.

The exposed health checking API is wire compatible with Google's gRPC
implementations, so it works with [grpcurl], [grpc-health-probe], and
Expand All @@ -25,7 +25,9 @@ package main
import (
"net/http"

"connectrpc.com/grpchealth"
"connectrpc.com/connect/v2"
"connectrpc.com/connect/v2/connecthttp"
"connectrpc.com/grpchealth/v2"
)

func main() {
Expand All @@ -37,7 +39,9 @@ func main() {
// for these fully-qualified protobuf service names, so you'd more likely
// reference userv1.UserServiceName and groupv1.GroupServiceName.
)
mux.Handle(grpchealth.NewHandler(checker))
connectServer := connect.NewServer()
grpchealth.Register(connectServer, checker)
connecthttp.Mount(mux, connectServer)
// If you don't need to support HTTP/2 without TLS (h2c), you can use
// http.ListenAndServeTLS instead.
protocols := new(http.Protocols)
Expand All @@ -52,17 +56,17 @@ func main() {
}
```

## Status: Stable
## Status: Unstable

This module is stable. It supports:
This module is unstable while connect-go v2 is in alpha. Expect breaking
changes as we iterate toward a stable v2 release.

It supports:

* The two most recent major releases of Go (the same version of Go that continue to
[eceive security patches][go-support-policy]).
* [APIv2] of Protocol Buffers in Go (`google.golang.org/protobuf`).

Within those parameters, `grpchealth` follows semantic versioning.
We will _not_ make breaking changes in the 1.x series of releases.

## Legal

Offered under the [Apache 2 license][license].
Expand Down
6 changes: 4 additions & 2 deletions buf.gen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ managed:
enabled: true
override:
- file_option: go_package_prefix
value: connectrpc.com/grpchealth/internal/gen/go
value: connectrpc.com/grpchealth/v2/internal/gen/go
plugins:
- local: protoc-gen-go
out: internal/gen/go
opt: paths=source_relative
opt:
- paths=source_relative
- default_api_level=API_OPAQUE
67 changes: 35 additions & 32 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ package grpchealth

import (
"context"
"strings"
"errors"
"io"

"connectrpc.com/connect"
healthv1 "connectrpc.com/grpchealth/internal/gen/go/connectext/grpc/health/v1"
"connectrpc.com/connect/v2"
healthv1 "connectrpc.com/grpchealth/v2/internal/gen/go/connectext/grpc/health/v1"
)

// Compile-time assertion: *Client implements Checker.
Expand All @@ -28,44 +29,42 @@ var _ Checker = (*Client)(nil)
// Client calls the gRPC health-checking API. It implements [Checker], so it
// can be used anywhere a Checker is expected. It is safe to use concurrently.
type Client struct {
check *connect.Client[healthv1.HealthCheckRequest, healthv1.HealthCheckResponse]
watch *connect.Client[healthv1.HealthCheckRequest, healthv1.HealthCheckResponse]
client *connect.Client
check connect.Spec
watch connect.Spec
}

// NewClient constructs a new Client for the gRPC health-checking API.
//
// The URL supplied should be the base URL of the Connect or gRPC server (for
// example, https://api.example.com). By default the client uses the Connect
// protocol with the binary Protobuf codec. To use the gRPC protocol, supply
// [connect.WithGRPC] as an option.
func NewClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) *Client {
baseURL = strings.TrimRight(baseURL, "/")
// By default, transports use the Connect protocol with the binary Protobuf
// codec. To use the gRPC protocol, configure the transport with
// [connectrpc.com/connect/v2/connecthttp.WithGRPC].
func NewClient(client *connect.Client) *Client {
return &Client{
check: connect.NewClient[healthv1.HealthCheckRequest, healthv1.HealthCheckResponse](
httpClient,
baseURL+checkProcedure,
opts...,
),
watch: connect.NewClient[healthv1.HealthCheckRequest, healthv1.HealthCheckResponse](
httpClient,
baseURL+watchProcedure,
opts...,
),
client: client,
check: connect.Spec{
StreamType: connect.StreamTypeUnary,
Procedure: checkProcedure,
},
watch: connect.Spec{
StreamType: connect.StreamTypeServer,
Procedure: watchProcedure,
},
}
}

// Check reports the health of the named service. An empty service name
// requests the health of the whole server process. If the service is unknown,
// the returned error will have [connect.CodeNotFound].
func (c *Client) Check(ctx context.Context, req *CheckRequest) (*CheckResponse, error) {
res, err := c.check.CallUnary(ctx, connect.NewRequest(&healthv1.HealthCheckRequest{
var res healthv1.HealthCheckResponse
if err := c.client.CallUnary(ctx, c.check, healthv1.HealthCheckRequest_builder{
Service: req.Service,
}))
if err != nil {
}.Build(), &res); err != nil {
return nil, err
}
return &CheckResponse{
Status: Status(res.Msg.GetStatus()), //nolint:gosec // Conversion is safe; Status and ServingStatus share the same value space.
Status: Status(res.GetStatus()), //nolint:gosec // Conversion is safe; Status and ServingStatus share the same value space.
}, nil
}

Expand Down Expand Up @@ -107,25 +106,29 @@ func (c *Client) Watch(ctx context.Context, req *CheckRequest) <-chan WatchEvent
return true
}
}
stream, err := c.watch.CallServerStream(ctx, connect.NewRequest(&healthv1.HealthCheckRequest{
stream, err := c.client.CallServerStream(ctx, c.watch, healthv1.HealthCheckRequest_builder{
Service: req.Service,
}))
}.Build())
if err != nil {
send(WatchEvent{Err: err})
return
}
defer stream.Close()
for stream.Receive() {
for {
var res healthv1.HealthCheckResponse
if err := stream.Receive(&res); err != nil {
if !errors.Is(err, io.EOF) {
send(WatchEvent{Err: err})
}
return
}
resp := &CheckResponse{
Status: Status(stream.Msg().GetStatus()), //nolint:gosec // Conversion is safe; Status and ServingStatus share the same value space.
Status: Status(res.GetStatus()), //nolint:gosec // Conversion is safe; Status and ServingStatus share the same value space.
}
if !send(WatchEvent{Response: resp}) {
return
}
}
if err := stream.Err(); err != nil {
send(WatchEvent{Err: err})
}
}()
return events
}
33 changes: 26 additions & 7 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import (
"net/http/httptest"
"testing"

"connectrpc.com/connect"
"connectrpc.com/connect/v2"
"connectrpc.com/connect/v2/connecthttp"
)

func TestClient_Check(t *testing.T) {
Expand All @@ -32,13 +33,19 @@ func TestClient_Check(t *testing.T) {
t.Parallel()
checker := NewStaticChecker(userFQN)
mux := http.NewServeMux()
mux.Handle(NewHandler(checker))
connectServer := connect.NewServer()
Register(connectServer, checker)
connecthttp.Mount(mux, connectServer)
server := httptest.NewUnstartedServer(mux)
server.EnableHTTP2 = true
server.StartTLS()
t.Cleanup(server.Close)

client := NewClient(server.Client(), server.URL, connect.WithGRPC())
client := NewClient(connect.NewClient(connecthttp.NewTransport(
server.Client(),
server.URL,
connecthttp.WithGRPC(),
)))

assertStatus := func(t *testing.T, service string, expect Status) {
t.Helper()
Expand Down Expand Up @@ -82,13 +89,19 @@ func TestClient_Watch(t *testing.T) {
t.Parallel()
checker := NewStaticChecker(userFQN)
mux := http.NewServeMux()
mux.Handle(NewHandler(checker))
connectServer := connect.NewServer()
Register(connectServer, checker)
connecthttp.Mount(mux, connectServer)
server := httptest.NewUnstartedServer(mux)
server.EnableHTTP2 = true
server.StartTLS()
t.Cleanup(server.Close)

client := NewClient(server.Client(), server.URL, connect.WithGRPC())
client := NewClient(connect.NewClient(connecthttp.NewTransport(
server.Client(),
server.URL,
connecthttp.WithGRPC(),
)))

ctx, cancel := context.WithCancel(t.Context())
defer cancel()
Expand Down Expand Up @@ -128,13 +141,19 @@ func TestClient_Watch_unknownService(t *testing.T) {
t.Parallel()
checker := NewStaticChecker()
mux := http.NewServeMux()
mux.Handle(NewHandler(checker))
connectServer := connect.NewServer()
Register(connectServer, checker)
connecthttp.Mount(mux, connectServer)
server := httptest.NewUnstartedServer(mux)
server.EnableHTTP2 = true
server.StartTLS()
t.Cleanup(server.Close)

client := NewClient(server.Client(), server.URL, connect.WithGRPC())
client := NewClient(connect.NewClient(connecthttp.NewTransport(
server.Client(),
server.URL,
connecthttp.WithGRPC(),
)))

ctx, cancel := context.WithCancel(t.Context())
defer cancel()
Expand Down
8 changes: 3 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
module connectrpc.com/grpchealth
module connectrpc.com/grpchealth/v2

go 1.26.0

retract v1.1.1 // module cache poisoned, use v1.1.2

require (
connectrpc.com/connect v1.11.0
google.golang.org/protobuf v1.33.0
connectrpc.com/connect/v2 v2.0.0-alpha.1
google.golang.org/protobuf v1.36.11
)
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
connectrpc.com/connect v1.11.0 h1:Av2KQXxSaX4vjqhf5Cl01SX4dqYADQ38eBtr84JSUBk=
connectrpc.com/connect v1.11.0/go.mod h1:3AGaO6RRGMx5IKFfqbe3hvK1NqLosFNP2BxDYTPmNPo=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
connectrpc.com/connect/v2 v2.0.0-alpha.1 h1:T0Fk30RdHUG8Z8K5/ezkhGclsuoPoNp7ekK/YWjuB9Q=
connectrpc.com/connect/v2 v2.0.0-alpha.1/go.mod h1:h651FA//hXdrOxDQ1Hiwte02sVqSPt+XY1Mj0kpLToI=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
Loading
Loading