diff --git a/README.md b/README.md index af5ce1c..bc27517 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,117 @@ # connect-go-prometheus -Prometheus interceptors for connect-go +[Prometheus](https://prometheus.io/) monitoring for [connect-go](https://github.com/bufbuild/connect-go). + +## Interceptors +This library defines [interceptors](https://connect.build/docs/go/interceptors) to observe both client-side and server-side calls. + +## Install +```bash +go get -u github.com/easyCZ/connect-go-prometheus +``` + +## Usage +```golang +import ( + "github.com/easyCZ/connect-go-prometheus" +) + +// Construct the interceptor. The same intereceptor is used for both client-side and server-side. +interceptor := connect_go_prometheus.NewInterceptor() + +// Use the interceptor when constructing a new connect-go handler +_, _ := your_connect_package.NewServiceHandler(handler, connect.WithInterceptors(interceptor)) + +// Or with a client +client := your_connect_package.NewServiceClient(http.DefaultClient, serverURL, connect.WithInterceptors(interceptor)) +``` +For configuration, and more advanced use cases see [Configuration](#Configuration) + +## Metrics + +Metrics exposed use the following labels: +* `type` - one of `unary`, `client_stream`, `server_stream` or `bidi` +* `service` - name of the service, for example `myservice.greet.v1` +* `method` - name of the method, for example `SayHello` +* `code` - the resulting outcome of the RPC. The codes match [connect-go Error Codes](https://connect.build/docs/protocol#error-codes) with the addition of `ok` for succesful RPCs. + + +### Server-side metrics +* Counter `connect_server_started_total` with `(type, service, method)` labels +* Counter `connect_server_handled_total` with `(type, service, method, code)` labels +* (optionally) Histogram `connect_server_handled_seconds` with `(type, service, method, code)` labels + +### Client-side metrics +* Counter `connect_client_started_total` with `(type, service, method)` labels +* Counter `connect_client_handled_total` with `(type, service, method, code)` labels +* (optionally) Histogram `connect_client_handled_seconds` with `(type, service, method, code)` labels + +## Configuration + +### Customizing client/server metrics reported +```golang +import ( + "github.com/easyCZ/connect-go-prometheus" + prom "github.com/prometheus/client_golang/prometheus" +) + +options := []connect_go_prometheus.MetricOption{ + connect_go_prometheus.WithHistogram(true), + connect_go_prometheus.WithNamespace("namespace"), + connect_go_prometheus.WithSubsystem("subsystem"), + connect_go_prometheus.WithConstLabels(prom.Labels{"component": "foo"}), + connect_go_prometheus.WithHistogramBuckets([]float64{1, 5}), +} + +// Construct client metrics +clientMetrics := connect_go_prometheus.NewClientMetrics(options...) + +// Construct server metrics +serverMetrics := connect_go_prometheus.NewServerMetrics(options...) + +// When you construct either client/server metrics with options, you must also register the metrics with your Prometheus Registry +prom.MustRegister(clientMetrics, serverMetrics) + +// Construct the interceptor with our configured metrics +interceptor := connect_go_prometheus.NewInterceptor( + connect_go_prometheus.WithClientMetrics(clientMetrics), + connect_go_prometheus.WithServerMetrics(serverMetrics), +) +``` + +### Registering metrics against a Registry +You may want to register metrics against a [Prometheus Registry](https://pkg.go.dev/github.com/prometheus/client_golang/prometheus#Registry). You can do this with the following: +```golang +import ( + "github.com/easyCZ/connect-go-prometheus" + prom "github.com/prometheus/client_golang/prometheus" +) + +clientMetrics := connect_go_prometheus.NewClientMetrics() +serverMetrics := connect_go_prometheus.NewServerMetrics() + +registry := prom.NewRegistry() +registry.MustRegister(clientMetrics, serverMetrics) + +interceptor := connect_go_prometheus.NewInterceptor( + connect_go_prometheus.WithClientMetrics(clientMetrics), + connect_go_prometheus.WithServerMetrics(serverMetrics), +) +``` + +### Disabling client/server metrics reporting +To disable reporting of either client or server metrics, pass `nil` as an option. +```golang +import ( + "github.com/easyCZ/connect-go-prometheus" +) + +// Disable client-side metrics +interceptor := connect_go_prometheus.NewInterceptor( + connect_go_prometheus.WithClientMetrics(nil), +) + +// Disable server-side metrics +interceptor := connect_go_prometheus.NewInterceptor( + connect_go_prometheus.WithServerMetrics(nil), +) +``` diff --git a/interceptor.go b/interceptor.go index 81f53fa..2829f73 100644 --- a/interceptor.go +++ b/interceptor.go @@ -29,6 +29,11 @@ type Interceptor struct { func (i *Interceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { return connect.UnaryFunc(func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + // Short-circuit, not configured to report for either client or server. + if i.client == nil && i.server == nil { + return next(ctx, req) + } + now := time.Now() callType := steamTypeString(req.Spec().StreamType) callPackage, callMethod := procedureToPackageAndMethod(req.Spec().Procedure) diff --git a/interceptor_test.go b/interceptor_test.go index 48d920c..eed2efa 100644 --- a/interceptor_test.go +++ b/interceptor_test.go @@ -32,12 +32,12 @@ func TestInterceptor_WithClient_WithServer_Histogram(t *testing.T) { reg.MustRegister(clientMetrics, serverMetrics) - intereceptor := NewInterceptor(WithClientMetrics(clientMetrics), WithServerMetrics(serverMetrics)) + interceptor := NewInterceptor(WithClientMetrics(clientMetrics), WithServerMetrics(serverMetrics)) - _, handler := greetconnect.NewGreetServiceHandler(greetconnect.UnimplementedGreetServiceHandler{}, connect.WithInterceptors(intereceptor)) + _, handler := greetconnect.NewGreetServiceHandler(greetconnect.UnimplementedGreetServiceHandler{}, connect.WithInterceptors(interceptor)) srv := httptest.NewServer(handler) - client := greetconnect.NewGreetServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(intereceptor)) + client := greetconnect.NewGreetServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(interceptor)) _, err := client.Greet(context.Background(), connect.NewRequest(&greet.GreetRequest{ Name: "elza", })) @@ -59,12 +59,12 @@ func TestInterceptor_WithClient_WithServer_Histogram(t *testing.T) { } func TestInterceptor_Default(t *testing.T) { - intereceptor := NewInterceptor() + interceptor := NewInterceptor() - _, handler := greetconnect.NewGreetServiceHandler(greetconnect.UnimplementedGreetServiceHandler{}, connect.WithInterceptors(intereceptor)) + _, handler := greetconnect.NewGreetServiceHandler(greetconnect.UnimplementedGreetServiceHandler{}, connect.WithInterceptors(interceptor)) srv := httptest.NewServer(handler) - client := greetconnect.NewGreetServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(intereceptor)) + client := greetconnect.NewGreetServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(interceptor)) _, err := client.Greet(context.Background(), connect.NewRequest(&greet.GreetRequest{ Name: "elza", })) @@ -88,25 +88,30 @@ func TestInterceptor_WithClientMetrics(t *testing.T) { clientMetrics := NewClientMetrics(testMetricOptions...) require.NoError(t, reg.Register(clientMetrics)) - intereceptor := NewInterceptor(WithClientMetrics(clientMetrics)) + interceptor := NewInterceptor(WithClientMetrics(clientMetrics), WithServerMetrics(nil)) - _, handler := greetconnect.NewGreetServiceHandler(greetconnect.UnimplementedGreetServiceHandler{}, connect.WithInterceptors(intereceptor)) + _, handler := greetconnect.NewGreetServiceHandler(greetconnect.UnimplementedGreetServiceHandler{}, connect.WithInterceptors(interceptor)) srv := httptest.NewServer(handler) - client := greetconnect.NewGreetServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(intereceptor)) + client := greetconnect.NewGreetServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(interceptor)) _, err := client.Greet(context.Background(), connect.NewRequest(&greet.GreetRequest{ Name: "elza", })) require.Error(t, err) require.Equal(t, connect.CodeOf(err), connect.CodeUnimplemented) - expectedMetrics := []string{ + possibleMetrics := []string{ + "namespace_subsystem_connect_client_handled_seconds", "namespace_subsystem_connect_client_handled_total", "namespace_subsystem_connect_client_started_total", + + "namespace_subsystem_connect_server_handled_seconds", + "namespace_subsystem_connect_server_handled_total", + "namespace_subsystem_connect_server_started_total", } - count, err := testutil.GatherAndCount(reg, expectedMetrics...) + count, err := testutil.GatherAndCount(reg, possibleMetrics...) require.NoError(t, err) - require.Equal(t, len(expectedMetrics), count) + require.Equal(t, 3, count, "must report only 3 metrics, as server side is disabled") } func TestInterceptor_WithServerMetrics(t *testing.T) { @@ -114,23 +119,28 @@ func TestInterceptor_WithServerMetrics(t *testing.T) { serverMetrics := NewServerMetrics(testMetricOptions...) require.NoError(t, reg.Register(serverMetrics)) - intereceptor := NewInterceptor(WithServerMetrics(serverMetrics)) + interceptor := NewInterceptor(WithServerMetrics(serverMetrics), WithClientMetrics(nil)) - _, handler := greetconnect.NewGreetServiceHandler(greetconnect.UnimplementedGreetServiceHandler{}, connect.WithInterceptors(intereceptor)) + _, handler := greetconnect.NewGreetServiceHandler(greetconnect.UnimplementedGreetServiceHandler{}, connect.WithInterceptors(interceptor)) srv := httptest.NewServer(handler) - client := greetconnect.NewGreetServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(intereceptor)) + client := greetconnect.NewGreetServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(interceptor)) _, err := client.Greet(context.Background(), connect.NewRequest(&greet.GreetRequest{ Name: "elza", })) require.Error(t, err) require.Equal(t, connect.CodeOf(err), connect.CodeUnimplemented) - expectedMetrics := []string{ + possibleMetrics := []string{ + "namespace_subsystem_connect_client_handled_seconds", + "namespace_subsystem_connect_client_handled_total", + "namespace_subsystem_connect_client_started_total", + + "namespace_subsystem_connect_server_handled_seconds", "namespace_subsystem_connect_server_handled_total", "namespace_subsystem_connect_server_started_total", } - count, err := testutil.GatherAndCount(reg, expectedMetrics...) + count, err := testutil.GatherAndCount(reg, possibleMetrics...) require.NoError(t, err) - require.Equal(t, len(expectedMetrics), count) + require.Equal(t, 3, count, "must report only server side metrics, client-side is disabled") }