Skip to content

Commit d5d2ecb

Browse files
committed
[baseserver] Expose gRPC prometheus metrics
1 parent a766836 commit d5d2ecb

File tree

3 files changed

+73
-14
lines changed

3 files changed

+73
-14
lines changed

components/common-go/baseserver/options.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,13 @@ type config struct {
3434

3535
func defaultConfig() *config {
3636
return &config{
37-
logger: log.New(),
38-
hostname: "localhost",
39-
httpPort: 9000,
40-
grpcPort: 9001,
41-
closeTimeout: 5 * time.Second,
42-
healthHandler: healthcheck.NewHandler(),
37+
logger: log.New(),
38+
hostname: "localhost",
39+
httpPort: 9000,
40+
grpcPort: 9001,
41+
closeTimeout: 5 * time.Second,
42+
healthHandler: healthcheck.NewHandler(),
43+
metricsRegistry: prometheus.NewRegistry(),
4344
}
4445
}
4546

components/common-go/baseserver/server.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
gitpod_grpc "github.com/gitpod-io/gitpod/common-go/grpc"
1111
"github.com/gitpod-io/gitpod/common-go/pprof"
1212
grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus"
13+
grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
14+
"github.com/prometheus/client_golang/prometheus"
1315
"github.com/prometheus/client_golang/prometheus/promhttp"
1416
"github.com/sirupsen/logrus"
1517
"google.golang.org/grpc"
@@ -165,6 +167,10 @@ func (s *Server) GRPC() *grpc.Server {
165167
return s.grpc
166168
}
167169

170+
func (s *Server) MetricsRegistry() *prometheus.Registry {
171+
return s.cfg.metricsRegistry
172+
}
173+
168174
func (s *Server) close(ctx context.Context) error {
169175
if s.listening == nil {
170176
return fmt.Errorf("server is not running, invalid close operation")
@@ -221,14 +227,9 @@ func (s *Server) newHTTPMux() *http.ServeMux {
221227
mux.HandleFunc("/live", s.cfg.healthHandler.LiveEndpoint)
222228
s.Logger().WithField("protocol", "http").Debug("Serving liveliness handler on /live")
223229

224-
// Metrics endpoint
225-
metricsHandler := promhttp.Handler()
226-
if s.cfg.metricsRegistry != nil {
227-
metricsHandler = promhttp.InstrumentMetricHandler(
228-
s.cfg.metricsRegistry, promhttp.HandlerFor(s.cfg.metricsRegistry, promhttp.HandlerOpts{}),
229-
)
230-
}
231-
mux.Handle("/metrics", metricsHandler)
230+
mux.Handle("/metrics", promhttp.InstrumentMetricHandler(
231+
s.cfg.metricsRegistry, promhttp.HandlerFor(s.cfg.metricsRegistry, promhttp.HandlerOpts{}),
232+
))
232233
s.Logger().WithField("protocol", "http").Debug("Serving metrics on /metrics")
233234

234235
mux.Handle(pprof.Path, pprof.Handler())
@@ -240,11 +241,19 @@ func (s *Server) newHTTPMux() *http.ServeMux {
240241
func (s *Server) initializeGRPC() error {
241242
gitpod_grpc.SetupLogging()
242243

244+
grpcMetrics := grpc_prometheus.NewServerMetrics()
245+
grpcMetrics.EnableHandlingTimeHistogram()
246+
if err := s.MetricsRegistry().Register(grpcMetrics); err != nil {
247+
return fmt.Errorf("failed to register grpc metrics: %w", err)
248+
}
249+
243250
unary := []grpc.UnaryServerInterceptor{
244251
grpc_logrus.UnaryServerInterceptor(s.Logger()),
252+
grpcMetrics.UnaryServerInterceptor(),
245253
}
246254
stream := []grpc.StreamServerInterceptor{
247255
grpc_logrus.StreamServerInterceptor(s.Logger()),
256+
grpcMetrics.StreamServerInterceptor(),
248257
}
249258

250259
s.grpc = grpc.NewServer(gitpod_grpc.ServerOptionsWithInterceptors(stream, unary)...)

components/common-go/baseserver/server_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@
55
package baseserver_test
66

77
import (
8+
"context"
89
"fmt"
910
"github.com/gitpod-io/gitpod/common-go/baseserver"
1011
"github.com/gitpod-io/gitpod/common-go/pprof"
1112
"github.com/prometheus/client_golang/prometheus"
1213
"github.com/stretchr/testify/require"
14+
"google.golang.org/grpc"
15+
"google.golang.org/grpc/credentials/insecure"
16+
"google.golang.org/grpc/health/grpc_health_v1"
1317
"net/http"
1418
"testing"
1519
)
@@ -77,3 +81,48 @@ func TestServer_ServesPprof(t *testing.T) {
7781
require.NoError(t, err)
7882
require.Equalf(t, http.StatusOK, resp.StatusCode, "must serve pprof on %s", pprof.Path)
7983
}
84+
85+
func TestServer_Metrics_gRPC(t *testing.T) {
86+
ctx := context.Background()
87+
srv := baseserver.NewForTests(t)
88+
89+
// At this point, there must be metrics registry available for use
90+
require.NotNil(t, srv.MetricsRegistry())
91+
92+
// To actually get gRPC metrics, we need to invoke an RPC, let's use a built-in health service as a mock
93+
grpc_health_v1.RegisterHealthServer(srv.GRPC(), &HealthService{})
94+
95+
// Let's start our server up
96+
baseserver.StartServerForTests(t, srv)
97+
98+
// We need a client to be able to invoke the RPC, let's construct one
99+
conn, err := grpc.DialContext(ctx, srv.GRPCAddress(), grpc.WithTransportCredentials(insecure.NewCredentials()))
100+
require.NoError(t, err)
101+
client := grpc_health_v1.NewHealthClient(conn)
102+
103+
// Invoke the RPC
104+
_, err = client.Check(ctx, &grpc_health_v1.HealthCheckRequest{})
105+
require.NoError(t, err)
106+
107+
// Finally, we can assert that some metrics were produced.
108+
registry := srv.MetricsRegistry()
109+
metrics, err := registry.Gather()
110+
require.NoError(t, err)
111+
112+
// We expect at least the following. It's not the full set, but a good baseline to sanity check.
113+
expected := []string{"grpc_server_handled_total", "grpc_server_handling_seconds", "grpc_server_started_total"}
114+
var actual []string
115+
for _, metric := range metrics {
116+
actual = append(actual, *metric.Name)
117+
}
118+
119+
require.Subset(t, actual, expected)
120+
}
121+
122+
type HealthService struct {
123+
grpc_health_v1.UnimplementedHealthServer
124+
}
125+
126+
func (h *HealthService) Check(_ context.Context, _ *grpc_health_v1.HealthCheckRequest) (*grpc_health_v1.HealthCheckResponse, error) {
127+
return &grpc_health_v1.HealthCheckResponse{Status: grpc_health_v1.HealthCheckResponse_SERVING}, nil
128+
}

0 commit comments

Comments
 (0)