commit 68434feb2550a24d4b3cd540374cbcde2a187f4f Author: Nikolay Bondarenko Date: Fri Jan 25 17:27:27 2019 +0300 Add prometheus requests processed and request latencies metric gathering wrapper diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f741eb --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Prometheus + +Wrappers are a form of middleware that can be used with go-micro services. They can wrap both the Client and Server handlers. +This plugin implements the HandlerWrapper interface to provide automatic prometheus metric handling +for each microservice method execution time and operation count for success and failed cases. + +This handler will export two metrics to prometheus: +* **go_micro_requests_total**. How many go-miro requests processed, partitioned by method and status. +* **go_micro_request_durations_microseconds**. Service method request latencies in microseconds, partitioned by method. + +# Usage + +When creating your service, add the wrapper like so. + +```go + service := micro.NewService( + micro.Name("service name"), + micro.Version("latest"), + micro.WrapHandler(prometheus.NewHandlerWrapper()), + ) + + service.Init() +``` diff --git a/prometheus.go b/prometheus.go new file mode 100644 index 0000000..4d2d001 --- /dev/null +++ b/prometheus.go @@ -0,0 +1,49 @@ +package prometheus + +import ( + "context" + "github.com/micro/go-micro/server" + "github.com/prometheus/client_golang/prometheus" +) + +func NewHandlerWrapper() server.HandlerWrapper { + opsCounter := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "go_micro_requests_total", + Help: "How many go-miro requests processed, partitioned by method and status", + }, + []string{"method", "status"}, + ) + + timeCounter := prometheus.NewSummaryVec( + prometheus.SummaryOpts{ + Name: "go_micro_request_durations_microseconds", + Help: "Service method request latencies in microseconds", + }, + []string{"method"}, + ) + + prometheus.MustRegister(opsCounter) + prometheus.MustRegister(timeCounter) + + return func(fn server.HandlerFunc) server.HandlerFunc { + return func(ctx context.Context, req server.Request, rsp interface{}) error { + name := req.Endpoint() + + timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { + us := v * 1000000 // make microseconds + timeCounter.WithLabelValues(name).Observe(us) + })) + defer timer.ObserveDuration() + + err := fn(ctx, req, rsp) + if err == nil { + opsCounter.WithLabelValues(name, "success").Inc() + } else { + opsCounter.WithLabelValues(name, "fail").Inc() + } + + return err + } + } +} diff --git a/prometheus_test.go b/prometheus_test.go new file mode 100644 index 0000000..ecaa0da --- /dev/null +++ b/prometheus_test.go @@ -0,0 +1,105 @@ +package prometheus + +import ( + "context" + "fmt" + "github.com/micro/go-micro/client" + "github.com/micro/go-micro/registry/memory" + "github.com/micro/go-micro/selector" + "github.com/micro/go-micro/server" + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" + "github.com/stretchr/testify/assert" + "testing" +) + +type Test interface { + Method(ctx context.Context, in *TestRequest, opts ...client.CallOption) (*TestResponse, error) +} + +type TestRequest struct { + IsError bool +} +type TestResponse struct{} + +type testHandler struct{} + +func (t *testHandler) Method(ctx context.Context, req *TestRequest, rsp *TestResponse) error { + if req.IsError { + return fmt.Errorf("test error") + } + return nil +} + +func TestPrometheusMetrics(t *testing.T) { + // setup + registry := memory.NewRegistry() + sel := selector.NewSelector(selector.Registry(registry)) + name := "test" + + c := client.NewClient(client.Selector(sel)) + s := server.NewServer( + server.Name(name), + server.Registry(registry), + server.WrapHandler(NewHandlerWrapper()), + ) + + type Test struct { + *testHandler + } + + s.Handle( + s.NewHandler(&Test{new(testHandler)}), + ) + + if err := s.Start(); err != nil { + t.Fatalf("Unexpected error starting server: %v", err) + } + + if err := s.Register(); err != nil { + t.Fatalf("Unexpected error registering server: %v", err) + } + + req := c.NewRequest(name, "Test.Method", &TestRequest{IsError: false}, client.WithContentType("application/json")) + rsp := TestResponse{} + + assert.NoError(t, c.Call(context.TODO(), req, &rsp)) + + req = c.NewRequest(name, "Test.Method", &TestRequest{IsError: true}, client.WithContentType("application/json")) + assert.Error(t, c.Call(context.TODO(), req, &rsp)) + + list, _ := prometheus.DefaultGatherer.Gather() + + metric := findMetricByName(list, dto.MetricType_SUMMARY, "go_micro_request_durations_microseconds") + assert.Equal(t, *metric.Metric[0].Label[0].Name, "method") + assert.Equal(t, *metric.Metric[0].Label[0].Value, "Test.Method") + assert.Equal(t, *metric.Metric[0].Summary.SampleCount, uint64(2)) + assert.True(t, *metric.Metric[0].Summary.SampleSum > 0) + + metric = findMetricByName(list, dto.MetricType_COUNTER, "go_micro_requests_total") + + assert.Equal(t, *metric.Metric[0].Label[0].Name, "method") + assert.Equal(t, *metric.Metric[0].Label[0].Value, "Test.Method") + assert.Equal(t, *metric.Metric[0].Label[1].Name, "status") + assert.Equal(t, *metric.Metric[0].Label[1].Value, "fail") + assert.Equal(t, *metric.Metric[0].Counter.Value, float64(1)) + + assert.Equal(t, *metric.Metric[1].Label[0].Name, "method") + assert.Equal(t, *metric.Metric[1].Label[0].Value, "Test.Method") + assert.Equal(t, *metric.Metric[1].Label[1].Name, "status") + assert.Equal(t, *metric.Metric[1].Label[1].Value, "success") + assert.Equal(t, *metric.Metric[1].Counter.Value, float64(1)) + + s.Deregister() + s.Stop() +} + +func findMetricByName(list []*dto.MetricFamily, tp dto.MetricType, name string) *dto.MetricFamily { + for _, metric := range list { + if *metric.Name == name && *metric.Type == tp { + return metric + } + } + + return nil +}