diff --git a/cmd/query/app/default_params.go b/cmd/query/app/default_params.go new file mode 100644 index 00000000000..58104493879 --- /dev/null +++ b/cmd/query/app/default_params.go @@ -0,0 +1,32 @@ +// Copyright (c) 2021 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Contains default parameter values used by handlers when optional request parameters are missing. + +package app + +import ( + "time" + + "github.com/jaegertracing/jaeger/proto-gen/api_v2/metrics" +) + +var ( + defaultDependencyLookbackDuration = time.Hour * 24 + defaultTraceQueryLookbackDuration = time.Hour * 24 * 2 + defaultMetricsQueryLookbackDuration = time.Hour + defaultMetricsQueryStepDuration = 5 * time.Second + defaultMetricsQueryRateDuration = 10 * time.Minute + defaultMetricsSpanKinds = []string{metrics.SpanKind_SPAN_KIND_SERVER.String()} +) diff --git a/cmd/query/app/grpc_handler.go b/cmd/query/app/grpc_handler.go index bc223230dae..33d5e701b00 100644 --- a/cmd/query/app/grpc_handler.go +++ b/cmd/query/app/grpc_handler.go @@ -16,6 +16,8 @@ package app import ( "context" + "errors" + "time" "github.com/opentracing/opentracing-go" "go.uber.org/zap" @@ -24,8 +26,11 @@ import ( "github.com/jaegertracing/jaeger/cmd/query/app/querysvc" "github.com/jaegertracing/jaeger/model" - _ "github.com/jaegertracing/jaeger/pkg/gogocodec" //force gogo codec registration + _ "github.com/jaegertracing/jaeger/pkg/gogocodec" // force gogo codec registration + "github.com/jaegertracing/jaeger/plugin/metrics/disabled" "github.com/jaegertracing/jaeger/proto-gen/api_v2" + "github.com/jaegertracing/jaeger/proto-gen/api_v2/metrics" + "github.com/jaegertracing/jaeger/storage/metricsstore" "github.com/jaegertracing/jaeger/storage/spanstore" ) @@ -35,22 +40,20 @@ const ( msgTraceNotFound = "trace not found" ) +var ( + errGRPCMetricsQueryDisabled = status.Error(codes.Unimplemented, "metrics querying is currently disabled") + errNilRequest = status.Error(codes.InvalidArgument, "a nil argument is not allowed") + errMissingServiceNames = status.Error(codes.InvalidArgument, "please provide at least one service name") + errMissingQuantile = status.Error(codes.InvalidArgument, "please provide a quantile between (0, 1]") +) + // GRPCHandler implements the gRPC endpoint of the query service. type GRPCHandler struct { - queryService *querysvc.QueryService - logger *zap.Logger - tracer opentracing.Tracer -} - -// NewGRPCHandler returns a GRPCHandler -func NewGRPCHandler(queryService *querysvc.QueryService, logger *zap.Logger, tracer opentracing.Tracer) *GRPCHandler { - gH := &GRPCHandler{ - queryService: queryService, - logger: logger, - tracer: tracer, - } - - return gH + queryService *querysvc.QueryService + metricsQueryService querysvc.MetricsQueryService + logger *zap.Logger + tracer opentracing.Tracer + nowFn func() time.Time } // GetTrace is the gRPC handler to fetch traces based on trace-id. @@ -177,3 +180,135 @@ func (g *GRPCHandler) GetDependencies(ctx context.Context, r *api_v2.GetDependen return &api_v2.GetDependenciesResponse{Dependencies: dependencies}, nil } + +// GetLatencies is the gRPC handler to fetch latency metrics. +func (g *GRPCHandler) GetLatencies(ctx context.Context, r *metrics.GetLatenciesRequest) (*metrics.GetMetricsResponse, error) { + bqp, err := g.newBaseQueryParameters(r) + if err := g.handleErr("failed to build parameters", err); err != nil { + return nil, err + } + // Check for cases where clients do not provide the Quantile, which defaults to the float64's zero value. + if r.Quantile == 0 { + return nil, errMissingQuantile + } + queryParams := metricsstore.LatenciesQueryParameters{ + BaseQueryParameters: bqp, + Quantile: r.Quantile, + } + m, err := g.metricsQueryService.GetLatencies(ctx, &queryParams) + if err := g.handleErr("failed to fetch latencies", err); err != nil { + return nil, err + } + return &metrics.GetMetricsResponse{Metrics: *m}, nil +} + +// GetCallRates is the gRPC handler to fetch call rate metrics. +func (g *GRPCHandler) GetCallRates(ctx context.Context, r *metrics.GetCallRatesRequest) (*metrics.GetMetricsResponse, error) { + bqp, err := g.newBaseQueryParameters(r) + if err := g.handleErr("failed to build parameters", err); err != nil { + return nil, err + } + queryParams := metricsstore.CallRateQueryParameters{ + BaseQueryParameters: bqp, + } + m, err := g.metricsQueryService.GetCallRates(ctx, &queryParams) + if err := g.handleErr("failed to fetch call rates", err); err != nil { + return nil, err + } + return &metrics.GetMetricsResponse{Metrics: *m}, nil +} + +// GetErrorRates is the gRPC handler to fetch error rate metrics. +func (g *GRPCHandler) GetErrorRates(ctx context.Context, r *metrics.GetErrorRatesRequest) (*metrics.GetMetricsResponse, error) { + bqp, err := g.newBaseQueryParameters(r) + if err := g.handleErr("failed to build parameters", err); err != nil { + return nil, err + } + queryParams := metricsstore.ErrorRateQueryParameters{ + BaseQueryParameters: bqp, + } + m, err := g.metricsQueryService.GetErrorRates(ctx, &queryParams) + if err := g.handleErr("failed to fetch error rates", err); err != nil { + return nil, err + } + return &metrics.GetMetricsResponse{Metrics: *m}, nil +} + +// GetMinStepDuration is the gRPC handler to fetch the minimum step duration supported by the underlying metrics store. +func (g *GRPCHandler) GetMinStepDuration(ctx context.Context, _ *metrics.GetMinStepDurationRequest) (*metrics.GetMinStepDurationResponse, error) { + minStep, err := g.metricsQueryService.GetMinStepDuration(ctx, &metricsstore.MinStepDurationQueryParameters{}) + if err := g.handleErr("failed to fetch min step duration", err); err != nil { + return nil, err + } + return &metrics.GetMinStepDurationResponse{MinStep: minStep}, nil +} + +func (g *GRPCHandler) handleErr(msg string, err error) error { + if err == nil { + return nil + } + g.logger.Error(msg, zap.Error(err)) + + // Avoid wrapping "expected" errors with an "Internal Server" error. + if errors.Is(err, disabled.ErrDisabled) { + return errGRPCMetricsQueryDisabled + } + if _, ok := status.FromError(err); ok { + return err + } + + // Received an "unexpected" error. + return status.Errorf(codes.Internal, "%s: %v", msg, err) +} + +func (g *GRPCHandler) newBaseQueryParameters(r interface{}) (bqp metricsstore.BaseQueryParameters, err error) { + if r == nil { + return bqp, errNilRequest + } + var baseRequest *metrics.MetricsQueryBaseRequest + switch v := r.(type) { + case *metrics.GetLatenciesRequest: + baseRequest = v.BaseRequest + case *metrics.GetCallRatesRequest: + baseRequest = v.BaseRequest + case *metrics.GetErrorRatesRequest: + baseRequest = v.BaseRequest + } + if baseRequest == nil || len(baseRequest.ServiceNames) == 0 { + return bqp, errMissingServiceNames + } + + // Copy non-nullable params. + bqp.GroupByOperation = baseRequest.GroupByOperation + bqp.ServiceNames = baseRequest.ServiceNames + + // Initialize nullable params with defaults. + defaultEndTime := g.nowFn() + bqp.EndTime = &defaultEndTime + bqp.Lookback = &defaultMetricsQueryLookbackDuration + bqp.RatePer = &defaultMetricsQueryRateDuration + bqp.SpanKinds = defaultMetricsSpanKinds + bqp.Step = &defaultMetricsQueryStepDuration + + // ... and override defaults with any provided request params. + if baseRequest.EndTime != nil { + bqp.EndTime = baseRequest.EndTime + } + if baseRequest.Lookback != nil { + bqp.Lookback = baseRequest.Lookback + } + if baseRequest.Step != nil { + bqp.Step = baseRequest.Step + } + if baseRequest.RatePer != nil { + bqp.RatePer = baseRequest.RatePer + } + if len(baseRequest.SpanKinds) > 0 { + spanKinds := make([]string, len(baseRequest.SpanKinds)) + for i, v := range baseRequest.SpanKinds { + spanKinds[i] = v.String() + } + bqp.SpanKinds = spanKinds + } + return bqp, nil +} diff --git a/cmd/query/app/grpc_handler_test.go b/cmd/query/app/grpc_handler_test.go index 98d922e6c11..9e2d3e2900e 100644 --- a/cmd/query/app/grpc_handler_test.go +++ b/cmd/query/app/grpc_handler_test.go @@ -34,8 +34,12 @@ import ( "github.com/jaegertracing/jaeger/cmd/query/app/querysvc" "github.com/jaegertracing/jaeger/model" + "github.com/jaegertracing/jaeger/plugin/metrics/disabled" "github.com/jaegertracing/jaeger/proto-gen/api_v2" + "github.com/jaegertracing/jaeger/proto-gen/api_v2/metrics" depsmocks "github.com/jaegertracing/jaeger/storage/dependencystore/mocks" + "github.com/jaegertracing/jaeger/storage/metricsstore" + metricsmocks "github.com/jaegertracing/jaeger/storage/metricsstore/mocks" "github.com/jaegertracing/jaeger/storage/spanstore" spanstoremocks "github.com/jaegertracing/jaeger/storage/spanstore/mocks" ) @@ -118,27 +122,40 @@ var ( }, Warnings: []string{}, } + + now = time.Now() ) type grpcServer struct { - server *grpc.Server - lisAddr net.Addr - spanReader *spanstoremocks.Reader - depReader *depsmocks.Reader - archiveSpanReader *spanstoremocks.Reader - archiveSpanWriter *spanstoremocks.Writer + server *grpc.Server + lisAddr net.Addr + spanReader *spanstoremocks.Reader + depReader *depsmocks.Reader + metricsQueryService querysvc.MetricsQueryService + archiveSpanReader *spanstoremocks.Reader + archiveSpanWriter *spanstoremocks.Writer } type grpcClient struct { api_v2.QueryServiceClient + metrics.MetricsQueryServiceClient conn *grpc.ClientConn } -func newGRPCServer(t *testing.T, q *querysvc.QueryService, logger *zap.Logger, tracer opentracing.Tracer) (*grpc.Server, net.Addr) { +func newGRPCServer(t *testing.T, q *querysvc.QueryService, mq querysvc.MetricsQueryService, logger *zap.Logger, tracer opentracing.Tracer) (*grpc.Server, net.Addr) { lis, _ := net.Listen("tcp", ":0") grpcServer := grpc.NewServer() - grpcHandler := NewGRPCHandler(q, logger, tracer) + grpcHandler := &GRPCHandler{ + queryService: q, + metricsQueryService: mq, + logger: logger, + tracer: tracer, + nowFn: func() time.Time { + return now + }, + } api_v2.RegisterQueryServiceServer(grpcServer, grpcHandler) + metrics.RegisterMetricsQueryServiceServer(grpcServer, grpcHandler) go func() { err := grpcServer.Serve(lis) @@ -155,18 +172,34 @@ func newGRPCClient(t *testing.T, addr string) *grpcClient { require.NoError(t, err) return &grpcClient{ - QueryServiceClient: api_v2.NewQueryServiceClient(conn), - conn: conn, + QueryServiceClient: api_v2.NewQueryServiceClient(conn), + MetricsQueryServiceClient: metrics.NewMetricsQueryServiceClient(conn), + conn: conn, } } -func initializeTestServerGRPCWithOptions(t *testing.T) *grpcServer { +type testOption func(*testQueryService) +type testQueryService struct { + // metricsQueryService is used when creating a new GRPCHandler. + metricsQueryService querysvc.MetricsQueryService +} + +func withMetricsQuery() testOption { + reader := &metricsmocks.Reader{} + return func(ts *testQueryService) { + ts.metricsQueryService = reader + } +} + +func initializeTestServerGRPCWithOptions(t *testing.T, options ...testOption) *grpcServer { archiveSpanReader := &spanstoremocks.Reader{} archiveSpanWriter := &spanstoremocks.Writer{} spanReader := &spanstoremocks.Reader{} dependencyReader := &depsmocks.Reader{} + disabledReader, err := disabled.NewMetricsReader() + require.NoError(t, err) q := querysvc.NewQueryService(spanReader, dependencyReader, querysvc.QueryServiceOptions{ @@ -174,23 +207,32 @@ func initializeTestServerGRPCWithOptions(t *testing.T) *grpcServer { ArchiveSpanWriter: archiveSpanWriter, }) + tqs := &testQueryService{ + // Disable metrics query by default. + metricsQueryService: disabledReader, + } + for _, opt := range options { + opt(tqs) + } + logger := zap.NewNop() tracer := opentracing.NoopTracer{} - server, addr := newGRPCServer(t, q, logger, tracer) + server, addr := newGRPCServer(t, q, tqs.metricsQueryService, logger, tracer) return &grpcServer{ - server: server, - lisAddr: addr, - spanReader: spanReader, - depReader: dependencyReader, - archiveSpanReader: archiveSpanReader, - archiveSpanWriter: archiveSpanWriter, + server: server, + lisAddr: addr, + spanReader: spanReader, + depReader: dependencyReader, + metricsQueryService: tqs.metricsQueryService, + archiveSpanReader: archiveSpanReader, + archiveSpanWriter: archiveSpanWriter, } } -func withServerAndClient(t *testing.T, actualTest func(server *grpcServer, client *grpcClient)) { - server := initializeTestServerGRPCWithOptions(t) +func withServerAndClient(t *testing.T, actualTest func(server *grpcServer, client *grpcClient), options ...testOption) { + server := initializeTestServerGRPCWithOptions(t, options...) client := newGRPCClient(t, server.lisAddr.String()) defer server.server.Stop() defer client.conn.Close() @@ -511,3 +553,307 @@ func TestSendSpanChunksError(t *testing.T) { }) assert.EqualError(t, err, expectedErr.Error()) } + +func TestGetMetricsSuccessGRPC(t *testing.T) { + withServerAndClient(t, func(server *grpcServer, client *grpcClient) { + baseQueryParam := &metrics.MetricsQueryBaseRequest{ + ServiceNames: []string{"foo"}, + } + for _, tc := range []struct { + mockMethod string + mockParamType string + testFn func(client *grpcClient) (*metrics.GetMetricsResponse, error) + }{ + { + mockMethod: "GetLatencies", + mockParamType: "*metricsstore.LatenciesQueryParameters", + testFn: func(client *grpcClient) (*metrics.GetMetricsResponse, error) { + return client.GetLatencies(context.Background(), &metrics.GetLatenciesRequest{Quantile: 0.95, BaseRequest: baseQueryParam}) + }, + }, + { + mockMethod: "GetCallRates", + mockParamType: "*metricsstore.CallRateQueryParameters", + testFn: func(client *grpcClient) (*metrics.GetMetricsResponse, error) { + return client.GetCallRates(context.Background(), &metrics.GetCallRatesRequest{BaseRequest: baseQueryParam}) + }, + }, + { + mockMethod: "GetErrorRates", + mockParamType: "*metricsstore.ErrorRateQueryParameters", + testFn: func(client *grpcClient) (*metrics.GetMetricsResponse, error) { + return client.GetErrorRates(context.Background(), &metrics.GetErrorRatesRequest{BaseRequest: baseQueryParam}) + }, + }, + } { + t.Run(tc.mockMethod, func(t *testing.T) { + expectedMetrics := &metrics.MetricFamily{Name: "foo"} + m := server.metricsQueryService.(*metricsmocks.Reader) + m.On(tc.mockMethod, mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType(tc.mockParamType)). + Return(expectedMetrics, nil).Once() + + res, err := tc.testFn(client) + require.NoError(t, err) + assert.Equal(t, expectedMetrics, &res.Metrics) + }) + } + }, withMetricsQuery()) +} + +func TestGetMetricsReaderDisabledGRPC(t *testing.T) { + withServerAndClient(t, func(server *grpcServer, client *grpcClient) { + baseQueryParam := &metrics.MetricsQueryBaseRequest{ + ServiceNames: []string{"foo"}, + } + for _, tc := range []struct { + name string + testFn func(client *grpcClient) (*metrics.GetMetricsResponse, error) + }{ + { + name: "GetLatencies", + testFn: func(client *grpcClient) (*metrics.GetMetricsResponse, error) { + return client.GetLatencies(context.Background(), &metrics.GetLatenciesRequest{Quantile: 0.95, BaseRequest: baseQueryParam}) + }, + }, + { + name: "GetCallRates", + testFn: func(client *grpcClient) (*metrics.GetMetricsResponse, error) { + return client.GetCallRates(context.Background(), &metrics.GetCallRatesRequest{BaseRequest: baseQueryParam}) + }, + }, + { + name: "GetErrorRates", + testFn: func(client *grpcClient) (*metrics.GetMetricsResponse, error) { + return client.GetErrorRates(context.Background(), &metrics.GetErrorRatesRequest{BaseRequest: baseQueryParam}) + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + res, err := tc.testFn(client) + require.Error(t, err) + assert.Nil(t, res) + + assertGRPCError(t, err, codes.Unimplemented, "metrics querying is currently disabled") + }) + } + }) +} + +func TestGetMetricsUseDefaultParamsGRPC(t *testing.T) { + withServerAndClient(t, func(server *grpcServer, client *grpcClient) { + baseQueryParam := &metrics.MetricsQueryBaseRequest{ + ServiceNames: []string{"foo"}, + } + request := &metrics.GetCallRatesRequest{ + BaseRequest: baseQueryParam, + } + expectedMetrics := &metrics.MetricFamily{Name: "foo"} + expectedParams := &metricsstore.CallRateQueryParameters{ + BaseQueryParameters: metricsstore.BaseQueryParameters{ + ServiceNames: []string{"foo"}, + EndTime: &now, + Lookback: &defaultMetricsQueryLookbackDuration, + Step: &defaultMetricsQueryStepDuration, + RatePer: &defaultMetricsQueryRateDuration, + SpanKinds: defaultMetricsSpanKinds, + }, + } + m := server.metricsQueryService.(*metricsmocks.Reader) + m.On("GetCallRates", mock.AnythingOfType("*context.valueCtx"), expectedParams). + Return(expectedMetrics, nil).Once() + + res, err := client.GetCallRates(context.Background(), request) + require.NoError(t, err) + assert.Equal(t, expectedMetrics, &res.Metrics) + }, withMetricsQuery()) +} + +func TestGetMetricsOverrideDefaultParamsGRPC(t *testing.T) { + loc, _ := time.LoadLocation("UTC") + endTime := time.Now().In(loc) + lookback := time.Minute + step := time.Second + ratePer := time.Hour + spanKinds := []metrics.SpanKind{metrics.SpanKind_SPAN_KIND_CONSUMER} + expectedSpanKinds := []string{metrics.SpanKind_SPAN_KIND_CONSUMER.String()} + withServerAndClient(t, func(server *grpcServer, client *grpcClient) { + baseQueryParam := &metrics.MetricsQueryBaseRequest{ + ServiceNames: []string{"foo"}, + EndTime: &endTime, + Lookback: &lookback, + Step: &step, + RatePer: &ratePer, + SpanKinds: spanKinds, + } + request := &metrics.GetCallRatesRequest{ + BaseRequest: baseQueryParam, + } + expectedMetrics := &metrics.MetricFamily{Name: "foo"} + expectedParams := &metricsstore.CallRateQueryParameters{ + BaseQueryParameters: metricsstore.BaseQueryParameters{ + ServiceNames: baseQueryParam.ServiceNames, + EndTime: &endTime, + Lookback: &lookback, + Step: &step, + RatePer: &ratePer, + SpanKinds: expectedSpanKinds, + }, + } + m := server.metricsQueryService.(*metricsmocks.Reader) + m.On("GetCallRates", mock.AnythingOfType("*context.valueCtx"), expectedParams). + Return(expectedMetrics, nil).Once() + + res, err := client.GetCallRates(context.Background(), request) + require.NoError(t, err) + assert.Equal(t, expectedMetrics, &res.Metrics) + }, withMetricsQuery()) +} + +func TestGetMetricsFailureGRPC(t *testing.T) { + withServerAndClient(t, func(server *grpcServer, client *grpcClient) { + baseQueryParam := &metrics.MetricsQueryBaseRequest{ + ServiceNames: []string{"foo"}, + } + for _, tc := range []struct { + mockMethod string + mockParamType string + testFn func(client *grpcClient) (*metrics.GetMetricsResponse, error) + wantErr string + }{ + { + mockMethod: "GetLatencies", + mockParamType: "*metricsstore.LatenciesQueryParameters", + testFn: func(client *grpcClient) (*metrics.GetMetricsResponse, error) { + return client.GetLatencies(context.Background(), &metrics.GetLatenciesRequest{Quantile: 0.95, BaseRequest: baseQueryParam}) + }, + wantErr: "failed to fetch latencies: storage error", + }, + { + mockMethod: "GetCallRates", + mockParamType: "*metricsstore.CallRateQueryParameters", + testFn: func(client *grpcClient) (*metrics.GetMetricsResponse, error) { + return client.GetCallRates(context.Background(), &metrics.GetCallRatesRequest{BaseRequest: baseQueryParam}) + }, + wantErr: "failed to fetch call rates: storage error", + }, + { + mockMethod: "GetErrorRates", + mockParamType: "*metricsstore.ErrorRateQueryParameters", + testFn: func(client *grpcClient) (*metrics.GetMetricsResponse, error) { + return client.GetErrorRates(context.Background(), &metrics.GetErrorRatesRequest{BaseRequest: baseQueryParam}) + }, + wantErr: "failed to fetch error rates: storage error", + }, + } { + t.Run(tc.mockMethod, func(t *testing.T) { + m := server.metricsQueryService.(*metricsmocks.Reader) + m.On(tc.mockMethod, mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType(tc.mockParamType)). + Return(nil, errStorageGRPC).Once() + + res, err := tc.testFn(client) + require.Nil(t, res) + require.Error(t, err) + + assertGRPCError(t, err, codes.Internal, tc.wantErr) + }) + } + }, withMetricsQuery()) +} + +func TestGetMinStepDurationSuccessGRPC(t *testing.T) { + withServerAndClient(t, func(server *grpcServer, client *grpcClient) { + m := server.metricsQueryService.(*metricsmocks.Reader) + m.On("GetMinStepDuration", mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("*metricsstore.MinStepDurationQueryParameters")). + Return(time.Hour, nil).Once() + + res, err := client.GetMinStepDuration(context.Background(), &metrics.GetMinStepDurationRequest{}) + require.NoError(t, err) + require.Equal(t, time.Hour, res.MinStep) + }, withMetricsQuery()) +} + +func TestGetMinStepDurationFailureGRPC(t *testing.T) { + withServerAndClient(t, func(server *grpcServer, client *grpcClient) { + m := server.metricsQueryService.(*metricsmocks.Reader) + m.On("GetMinStepDuration", mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("*metricsstore.MinStepDurationQueryParameters")). + Return(time.Duration(0), errStorageGRPC).Once() + + res, err := client.GetMinStepDuration(context.Background(), &metrics.GetMinStepDurationRequest{}) + require.Nil(t, res) + require.Error(t, err) + + assertGRPCError(t, err, codes.Internal, "failed to fetch min step duration: storage error") + }, withMetricsQuery()) +} + +func TestGetMetricsInvalidParametersGRPC(t *testing.T) { + withServerAndClient(t, func(server *grpcServer, client *grpcClient) { + for _, tc := range []struct { + name string + mockMethod string + mockParamType string + testFn func(client *grpcClient) (*metrics.GetMetricsResponse, error) + wantErr string + }{ + { + name: "GetLatencies missing service names", + mockMethod: "GetLatencies", + mockParamType: "*metricsstore.LatenciesQueryParameters", + testFn: func(client *grpcClient) (*metrics.GetMetricsResponse, error) { + return client.GetLatencies(context.Background(), &metrics.GetLatenciesRequest{Quantile: 0.95}) + }, + wantErr: "please provide at least one service name", + }, + { + name: "GetLatencies missing quantile", + mockMethod: "GetLatencies", + mockParamType: "*metricsstore.LatenciesQueryParameters", + testFn: func(client *grpcClient) (*metrics.GetMetricsResponse, error) { + return client.GetLatencies(context.Background(), &metrics.GetLatenciesRequest{ + BaseRequest: &metrics.MetricsQueryBaseRequest{ + ServiceNames: []string{"foo"}, + }, + }) + }, + wantErr: "please provide a quantile between (0, 1]", + }, + { + name: "GetCallRates missing service names", + mockMethod: "GetCallRates", + mockParamType: "*metricsstore.CallRateQueryParameters", + testFn: func(client *grpcClient) (*metrics.GetMetricsResponse, error) { + return client.GetCallRates(context.Background(), &metrics.GetCallRatesRequest{}) // Test + }, + wantErr: "please provide at least one service name", + }, + { + name: "GetErrorRates nil request", + mockMethod: "GetErrorRates", + mockParamType: "*metricsstore.ErrorRateQueryParameters", + testFn: func(client *grpcClient) (*metrics.GetMetricsResponse, error) { + return client.GetErrorRates(context.Background(), &metrics.GetErrorRatesRequest{}) + }, + wantErr: "please provide at least one service name", + }, + } { + t.Run(tc.name, func(t *testing.T) { + m := server.metricsQueryService.(*metricsmocks.Reader) + m.On(tc.mockMethod, mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType(tc.mockParamType)). + Times(0) + + res, err := tc.testFn(client) + require.Nil(t, res) + require.Error(t, err) + + assertGRPCError(t, err, codes.InvalidArgument, tc.wantErr) + }) + } + }, withMetricsQuery()) +} + +func TestMetricsQueryNilRequestGRPC(t *testing.T) { + grpcHandler := &GRPCHandler{} + bqp, err := grpcHandler.newBaseQueryParameters(nil) + assert.Empty(t, bqp) + assert.EqualError(t, err, errNilRequest.Error()) +} diff --git a/cmd/query/app/http_handler.go b/cmd/query/app/http_handler.go index 1280ab6f8ab..04d3bea2a9b 100644 --- a/cmd/query/app/http_handler.go +++ b/cmd/query/app/http_handler.go @@ -42,9 +42,7 @@ const ( endTsParam = "endTs" lookbackParam = "lookback" - defaultDependencyLookbackDuration = time.Hour * 24 - defaultTraceQueryLookbackDuration = time.Hour * 24 * 2 - defaultAPIPrefix = "api" + defaultAPIPrefix = "api" ) // HTTPHandler handles http requests diff --git a/cmd/query/app/server.go b/cmd/query/app/server.go index 5918dc7ca88..be9a562d7b7 100644 --- a/cmd/query/app/server.go +++ b/cmd/query/app/server.go @@ -19,6 +19,7 @@ import ( "net" "net/http" "strings" + "time" "github.com/gorilla/handlers" "github.com/opentracing/opentracing-go" @@ -32,6 +33,7 @@ import ( "github.com/jaegertracing/jaeger/pkg/netutils" "github.com/jaegertracing/jaeger/pkg/recoveryhandler" "github.com/jaegertracing/jaeger/proto-gen/api_v2" + "github.com/jaegertracing/jaeger/proto-gen/api_v2/metrics" ) // Server runs HTTP, Mux and a grpc server @@ -110,10 +112,15 @@ func createGRPCServer(querySvc *querysvc.QueryService, metricsQuerySvc querysvc. server := grpc.NewServer(grpcOpts...) - handler := NewGRPCHandler(querySvc, logger, tracer) - - // TODO: Register MetricsQueryService + handler := &GRPCHandler{ + queryService: querySvc, + metricsQueryService: metricsQuerySvc, + logger: logger, + tracer: tracer, + nowFn: time.Now, + } api_v2.RegisterQueryServiceServer(server, handler) + metrics.RegisterMetricsQueryServiceServer(server, handler) return server, nil } diff --git a/model/proto/metrics/metricsquery.proto b/model/proto/metrics/metricsquery.proto index ad18f3dba7d..a3b2d8d4601 100644 --- a/model/proto/metrics/metricsquery.proto +++ b/model/proto/metrics/metricsquery.proto @@ -84,7 +84,7 @@ message MetricsQueryBaseRequest { message GetLatenciesRequest { MetricsQueryBaseRequest baseRequest = 1; // quantile is the quantile to compute from latency histogram metrics. - // Valid range: 0 - 1 (inclusive). + // Valid range: (0, 1] // // e.g. 0.99 will return the 99th percentile or P99 which is the worst latency // observed from 99% of all spans for the given service (and operation). diff --git a/proto-gen/api_v2/metrics/metricsquery.pb.go b/proto-gen/api_v2/metrics/metricsquery.pb.go index e4cad11ee40..6725a182733 100644 --- a/proto-gen/api_v2/metrics/metricsquery.pb.go +++ b/proto-gen/api_v2/metrics/metricsquery.pb.go @@ -150,7 +150,7 @@ func (m *MetricsQueryBaseRequest) GetSpanKinds() []SpanKind { type GetLatenciesRequest struct { BaseRequest *MetricsQueryBaseRequest `protobuf:"bytes,1,opt,name=baseRequest,proto3" json:"baseRequest,omitempty"` // quantile is the quantile to compute from latency histogram metrics. - // Valid range: 0 - 1 (inclusive). + // Valid range: (0, 1] // // e.g. 0.99 will return the 99th percentile or P99 which is the worst latency // observed from 99% of all spans for the given service (and operation).