From 73ee09cd4be62d0f46fff629072900a610f55c88 Mon Sep 17 00:00:00 2001 From: Raphael Ludwig Date: Thu, 5 May 2022 10:19:58 +0200 Subject: [PATCH] fix: Queries with no data points (#297) * fix: Queries with no data points Signed-off-by: Raphael Ludwig * refactor: Apply suggestions from code review Signed-off-by: Raphael Ludwig --- eventhandling/getSliEvent.go | 133 ++++---- eventhandling/getSliEvent_test.go | 170 ++++++++++ eventhandling/handler.go | 4 +- go.mod | 7 +- go.sum | 14 +- main.go | 16 +- utils/prometheus/fake/prometheusapi_mock.go | 341 ++++++++++++++++++++ utils/prometheus/prometheus.go | 48 ++- utils/prometheus/prometheus_test.go | 113 +++++++ 9 files changed, 762 insertions(+), 84 deletions(-) create mode 100644 eventhandling/getSliEvent_test.go create mode 100644 utils/prometheus/fake/prometheusapi_mock.go create mode 100644 utils/prometheus/prometheus_test.go diff --git a/eventhandling/getSliEvent.go b/eventhandling/getSliEvent.go index 1a07ad5..db51d94 100644 --- a/eventhandling/getSliEvent.go +++ b/eventhandling/getSliEvent.go @@ -7,23 +7,23 @@ import ( "github.com/keptn-contrib/prometheus-service/utils/prometheus" "gopkg.in/yaml.v2" "log" - "math" "net/url" "strings" cloudevents "github.com/cloudevents/sdk-go/v2" "github.com/keptn-contrib/prometheus-service/utils" + keptnv2 "github.com/keptn/go-utils/pkg/lib/v0_2_0" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" v1 "k8s.io/client-go/kubernetes/typed/core/v1" - "k8s.io/client-go/rest" ) // GetSliEventHandler is responsible for processing configure monitoring events type GetSliEventHandler struct { event cloudevents.Event keptnHandler *keptnv2.Keptn + kubeClient *kubernetes.Clientset } type prometheusCredentials struct { @@ -47,33 +47,82 @@ func (eh GetSliEventHandler) HandleEvent() error { // send started event _, err = eh.keptnHandler.SendTaskStartedEvent(eventData, utils.ServiceName) - if err != nil { - errMsg := fmt.Sprintf("Failed to send task started CloudEvent (%s), aborting...", err.Error()) - log.Println(errMsg) + errMsg := fmt.Errorf("failed to send task started CloudEvent: %w", err) + log.Println(errMsg.Error()) return err } - // create SLI Results - var sliResults []*keptnv2.SLIResult + // helper function to log an error and send an appropriate finished event + sendFinishedErrorEvent := func(err error) error { + log.Printf("sending errored finished event: %s", err.Error()) - // 2: try to fetch metrics into sliResults - if sliResults, err = retrieveMetrics(eventData, eh.keptnHandler); err != nil { - // failed to fetch metrics, send a finished event with the error - _, err = eh.keptnHandler.SendTaskFinishedEvent(&keptnv2.EventData{ + _, sendError := eh.keptnHandler.SendTaskFinishedEvent(&keptnv2.EventData{ Status: keptnv2.StatusErrored, Result: keptnv2.ResultFailed, Message: err.Error(), }, utils.ServiceName) - return err + // TODO: Maybe log error to console + + return sendError + } + + // get prometheus API URL for the provided Project from Kubernetes Config Map + prometheusAPIURL, err := getPrometheusAPIURL(eventData.Project, eh.kubeClient.CoreV1()) + if err != nil { + return sendFinishedErrorEvent(fmt.Errorf("unable to get prometheus api URL: %w", err)) + } + + // create a new Prometheus Handler + prometheusHandler := prometheus.NewPrometheusHandler( + prometheusAPIURL, + &eventData.EventData, + eventData.Deployment, // "canary", "primary" or "" (or "direct" or "user_managed") + eventData.Labels, + eventData.GetSLI.CustomFilters, + ) + + // get SLI queries (from SLI.yaml) + projectCustomQueries, err := getCustomQueries(eh.keptnHandler, eventData.Project, eventData.Stage, eventData.Service) + if err != nil { + return sendFinishedErrorEvent( + fmt.Errorf("unable to retrieve custom queries for project %s: %w", eventData.Project, err), + ) + } + + // only apply queries if they contain anything + if projectCustomQueries != nil { + prometheusHandler.CustomQueries = projectCustomQueries + } + + // retrieve metrics from prometheus + sliResults := retrieveMetrics(prometheusHandler, eventData) + + // If we hand any problem retrieving an SLI value, we set the result of the overall .finished event + // to Warning, if all fail ResultFailed is set for the event + finalSLIEventResult := keptnv2.ResultPass + + if len(sliResults) > 0 { + sliResultsFailed := 0 + for _, sliResult := range sliResults { + if !sliResult.Success { + sliResultsFailed++ + } + } + + if sliResultsFailed > 0 && sliResultsFailed < len(sliResults) { + finalSLIEventResult = keptnv2.ResultWarning + } else if sliResultsFailed == len(sliResults) { + finalSLIEventResult = keptnv2.ResultFailed + } } // construct finished event data getSliFinishedEventData := &keptnv2.GetSLIFinishedEventData{ EventData: keptnv2.EventData{ Status: keptnv2.StatusSucceeded, - Result: keptnv2.ResultPass, + Result: finalSLIEventResult, }, GetSLI: keptnv2.GetSLIFinished{ IndicatorValues: sliResults, @@ -82,9 +131,12 @@ func (eh GetSliEventHandler) HandleEvent() error { }, } - // send get-sli.finished event with SLI DAta - _, err = eh.keptnHandler.SendTaskFinishedEvent(getSliFinishedEventData, utils.ServiceName) + if getSliFinishedEventData.EventData.Result == keptnv2.ResultFailed { + getSliFinishedEventData.EventData.Message = "unable to retrieve metrics" + } + // send get-sli.finished event with SLI DATA + _, err = eh.keptnHandler.SendTaskFinishedEvent(getSliFinishedEventData, utils.ServiceName) if err != nil { errMsg := fmt.Sprintf("Failed to send task finished CloudEvent (%s), aborting...", err.Error()) log.Println(errMsg) @@ -94,48 +146,9 @@ func (eh GetSliEventHandler) HandleEvent() error { return nil } -func retrieveMetrics(eventData *keptnv2.GetSLITriggeredEventData, keptnHandler *keptnv2.Keptn) ([]*keptnv2.SLIResult, error) { +func retrieveMetrics(prometheusHandler *prometheus.Handler, eventData *keptnv2.GetSLITriggeredEventData) []*keptnv2.SLIResult { log.Printf("Retrieving Prometheus metrics") - clusterConfig, err := rest.InClusterConfig() - if err != nil { - log.Println("could not create Kubernetes cluster config") - return nil, errors.New("could not create Kubernetes client") - } - - kubeClient, err := kubernetes.NewForConfig(clusterConfig) - if err != nil { - log.Println("could not create Kubernetes client") - return nil, errors.New("could not create Kubernetes client") - } - - // get prometheus API URL for the provided Project from Kubernetes Config Map - prometheusAPIURL, err := getPrometheusAPIURL(eventData.Project, kubeClient.CoreV1()) - if err != nil { - return nil, err - } - - // Create a new Prometheus Handler - prometheusHandler := prometheus.NewPrometheusHandler( - prometheusAPIURL, - &eventData.EventData, - eventData.Deployment, // "canary", "primary" or "" (or "direct" or "user_managed") - eventData.Labels, - eventData.GetSLI.CustomFilters, - ) - - // get SLI queries (from SLI.yaml) - projectCustomQueries, err := getCustomQueries(keptnHandler, eventData.Project, eventData.Stage, eventData.Service) - if err != nil { - log.Println("retrieveMetrics: Failed to get custom queries for project " + eventData.Project) - log.Println(err.Error()) - return nil, err - } - - if projectCustomQueries != nil { - prometheusHandler.CustomQueries = projectCustomQueries - } - var sliResults []*keptnv2.SLIResult for _, indicator := range eventData.GetSLI.Indicators { @@ -148,13 +161,6 @@ func retrieveMetrics(eventData *keptnv2.GetSLITriggeredEventData, keptnHandler * Success: false, Message: err.Error(), }) - } else if math.IsNaN(sliValue) { - sliResults = append(sliResults, &keptnv2.SLIResult{ - Metric: indicator, - Value: 0, - Success: false, - Message: "SLI value is NaN", - }) } else { sliResults = append(sliResults, &keptnv2.SLIResult{ Metric: indicator, @@ -163,7 +169,8 @@ func retrieveMetrics(eventData *keptnv2.GetSLITriggeredEventData, keptnHandler * }) } } - return sliResults, nil + + return sliResults } func getCustomQueries(keptnHandler *keptnv2.Keptn, project string, stage string, service string) (map[string]string, error) { diff --git a/eventhandling/getSliEvent_test.go b/eventhandling/getSliEvent_test.go new file mode 100644 index 0000000..fd3b374 --- /dev/null +++ b/eventhandling/getSliEvent_test.go @@ -0,0 +1,170 @@ +package eventhandling + +import ( + "encoding/json" + cloudevents "github.com/cloudevents/sdk-go/v2" + "github.com/golang/mock/gomock" + prometheusAPI "github.com/prometheus/client_golang/api/prometheus/v1" + prometheusModel "github.com/prometheus/common/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "math/rand" + "testing" + + prometheusUtils "github.com/keptn-contrib/prometheus-service/utils/prometheus" + prometheusfake "github.com/keptn-contrib/prometheus-service/utils/prometheus/fake" + + keptnv2 "github.com/keptn/go-utils/pkg/lib/v0_2_0" +) + +const eventJSON = ` +{ + "data": { + "deployment": "canary", + "get-sli": { + "end": "2022-04-06T14:36:19.667Z", + "sliProvider": "prometheus", + "start": "2022-04-06T14:35:03.762Z", + "indicators": ["throughput"] + }, + "project": "sockshop", + "service": "carts", + "stage": "staging" + }, + "gitcommitid": "c8a40997599180a338d72504541c00057550a3dc", + "id": "585cb332-7198-4605-a0ef-28199268b91d", + "shkeptncontext": "37a580f4-96ef-4594-b62a-1235b91ed7f6", + "shkeptnspecversion": "0.2.4", + "source": "lighthouse-service", + "specversion": "1.0", + "time": "2022-04-06T14:36:19.887Z", + "type": "sh.keptn.event.get-sli.triggered" +} +` + +func Test_retrieveMetrics(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + incomingEvent := &cloudevents.Event{} + + err := json.Unmarshal([]byte(eventJSON), incomingEvent) + require.NoError(t, err) + + eventData := &keptnv2.GetSLITriggeredEventData{} + err = incomingEvent.DataAs(eventData) + require.NoError(t, err) + + apiMock := prometheusfake.NewMockPrometheusAPI(mockCtrl) + handler := prometheusUtils.Handler{ + Project: eventData.Project, + Stage: eventData.Stage, + Service: eventData.Service, + PrometheusAPI: apiMock, + } + + sliValue := rand.Float64() + returnValue := prometheusModel.Vector{ + { + Value: prometheusModel.SampleValue(sliValue), + }, + } + + apiMock.EXPECT().Query(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return( + returnValue, prometheusAPI.Warnings{}, nil, + ) + + sliResults := retrieveMetrics(&handler, eventData) + + assert.Len(t, sliResults, 1) + assert.Contains(t, sliResults, &keptnv2.SLIResult{ + Metric: Throughput, + Value: sliValue, + ComparedValue: 0, + Success: true, + Message: "", + }) +} + +func Test_retrieveMetricsWithMultipleValues(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + incomingEvent := &cloudevents.Event{} + + err := json.Unmarshal([]byte(eventJSON), incomingEvent) + require.NoError(t, err) + + eventData := &keptnv2.GetSLITriggeredEventData{} + err = incomingEvent.DataAs(eventData) + require.NoError(t, err) + + apiMock := prometheusfake.NewMockPrometheusAPI(mockCtrl) + handler := prometheusUtils.Handler{ + Project: eventData.Project, + Stage: eventData.Stage, + Service: eventData.Service, + PrometheusAPI: apiMock, + } + + returnValue := prometheusModel.Vector{ + { + Value: prometheusModel.SampleValue(8.12830), + }, + { + Value: prometheusModel.SampleValue(0.28384), + }, + } + + apiMock.EXPECT().Query(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return( + returnValue, prometheusAPI.Warnings{}, nil, + ) + + sliResults := retrieveMetrics(&handler, eventData) + + assert.Len(t, sliResults, 1) + assert.Contains(t, sliResults, &keptnv2.SLIResult{ + Metric: Throughput, + Value: 0, + ComparedValue: 0, + Success: false, + Message: prometheusUtils.ErrMultipleValues.Error(), + }) +} + +func Test_retrieveMetricsWithNoValue(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + incomingEvent := &cloudevents.Event{} + + err := json.Unmarshal([]byte(eventJSON), incomingEvent) + require.NoError(t, err) + + eventData := &keptnv2.GetSLITriggeredEventData{} + err = incomingEvent.DataAs(eventData) + require.NoError(t, err) + + apiMock := prometheusfake.NewMockPrometheusAPI(mockCtrl) + handler := prometheusUtils.Handler{ + Project: eventData.Project, + Stage: eventData.Stage, + Service: eventData.Service, + PrometheusAPI: apiMock, + } + + apiMock.EXPECT().Query(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return( + prometheusModel.Vector{}, prometheusAPI.Warnings{}, nil, + ) + + sliResults := retrieveMetrics(&handler, eventData) + + assert.Len(t, sliResults, 1) + assert.Contains(t, sliResults, &keptnv2.SLIResult{ + Metric: Throughput, + Value: 0, + ComparedValue: 0, + Success: false, + Message: prometheusUtils.ErrNoValues.Error(), + }) +} diff --git a/eventhandling/handler.go b/eventhandling/handler.go index ad81d8a..4ae5069 100644 --- a/eventhandling/handler.go +++ b/eventhandling/handler.go @@ -6,6 +6,7 @@ import ( "github.com/keptn-contrib/prometheus-service/utils" "github.com/keptn/go-utils/pkg/lib/keptn" keptnv2 "github.com/keptn/go-utils/pkg/lib/v0_2_0" + "k8s.io/client-go/kubernetes" ) // PrometheusEventHandler defines a handler for events @@ -25,7 +26,7 @@ func (e NoOpEventHandler) HandleEvent() error { var env utils.EnvConfig // NewEventHandler creates a new Handler for an incoming event -func NewEventHandler(event cloudevents.Event, logger *keptn.Logger, keptnHandler *keptnv2.Keptn) PrometheusEventHandler { +func NewEventHandler(event cloudevents.Event, logger *keptn.Logger, keptnHandler *keptnv2.Keptn, kubeClient *kubernetes.Clientset) PrometheusEventHandler { logger.Debug("Received event: " + event.Type()) if err := envconfig.Process("", &env); err != nil { @@ -42,6 +43,7 @@ func NewEventHandler(event cloudevents.Event, logger *keptn.Logger, keptnHandler return &GetSliEventHandler{ event: event, keptnHandler: keptnHandler, + kubeClient: kubeClient, } } diff --git a/go.mod b/go.mod index 8a4b4d0..c28f3b1 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.17 require ( github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 github.com/cloudevents/sdk-go/v2 v2.5.0 + github.com/golang/mock v1.6.0 github.com/google/uuid v1.3.0 github.com/kelseyhightower/envconfig v1.4.0 github.com/keptn/go-utils v0.13.0 @@ -59,14 +60,14 @@ require ( go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee // indirect go.uber.org/zap v1.13.0 // indirect golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect - golang.org/x/mod v0.4.2 // indirect + golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect - golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e // indirect + golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect - golang.org/x/tools v0.1.5 // indirect + golang.org/x/tools v0.1.10 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.27.1 // indirect diff --git a/go.sum b/go.sum index bf6b487..ab8c105 100644 --- a/go.sum +++ b/go.sum @@ -275,6 +275,8 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -381,10 +383,10 @@ github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaR github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= -github.com/keptn/go-utils v0.12.0 h1:iB/2qDJLt0V2OwkSAD4rH0lKEGlKyYNLzS/ZZ6HKZfY= -github.com/keptn/go-utils v0.12.0/go.mod h1:yJM7pnCUj23VHKa2az9eWUTAmLDv94f6DVHON9qV1kU= github.com/keptn/go-utils v0.13.0 h1:jQ8EoWWa4EPamu4dis+AMzVD4YG2Yu/FEwvpgwslFrE= github.com/keptn/go-utils v0.13.0/go.mod h1:yJM7pnCUj23VHKa2az9eWUTAmLDv94f6DVHON9qV1kU= +github.com/keptn/go-utils v0.14.0 h1:1EDbYjKdQdhcvp6ErbDyyR/pd7pa4dksT509/GRzQ24= +github.com/keptn/go-utils v0.14.0/go.mod h1:CIRwnEp/QYaSBa/r146x3h4yqWB4FS3YNKHzftoyhVA= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -524,7 +526,6 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -649,6 +650,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -789,6 +792,8 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e h1:XMgFehsDnnLGtjvjOfqWSUzt0alpTR1RSEuznObga2c= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -867,8 +872,11 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go index f1216c4..92df3c0 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,8 @@ import ( "github.com/keptn-contrib/prometheus-service/utils" keptn "github.com/keptn/go-utils/pkg/lib" "io/ioutil" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" "log" "net/http" "os" @@ -78,7 +80,19 @@ func gotEvent(event cloudevents.Event) error { return fmt.Errorf("could not create Keptn handler: %v", err) } - return eventhandling.NewEventHandler(event, logger, keptnHandler).HandleEvent() + clusterConfig, err := rest.InClusterConfig() + if err != nil { + // TODO: Send Error log event to Keptn + return fmt.Errorf("unable to create kubernetes cluster config: %w", err) + } + + kubeClient, err := kubernetes.NewForConfig(clusterConfig) + if err != nil { + // TODO: Send Error log event to Keptn + return fmt.Errorf("unable to create kubernetes client: %w", err) + } + + return eventhandling.NewEventHandler(event, logger, keptnHandler, kubeClient).HandleEvent() } // HTTPGetHandler will handle all requests for '/health' and '/ready' diff --git a/utils/prometheus/fake/prometheusapi_mock.go b/utils/prometheus/fake/prometheusapi_mock.go new file mode 100644 index 0000000..1bf6d1f --- /dev/null +++ b/utils/prometheus/fake/prometheusapi_mock.go @@ -0,0 +1,341 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/keptn-contrib/prometheus-service/utils/prometheus (interfaces: PrometheusAPI) + +// Package fake is a generated GoMock package. +package fake + +import ( + context "context" + reflect "reflect" + time "time" + + gomock "github.com/golang/mock/gomock" + v1 "github.com/prometheus/client_golang/api/prometheus/v1" + model "github.com/prometheus/common/model" +) + +// MockPrometheusAPI is a mock of PrometheusAPI interface. +type MockPrometheusAPI struct { + ctrl *gomock.Controller + recorder *MockPrometheusAPIMockRecorder +} + +// MockPrometheusAPIMockRecorder is the mock recorder for MockPrometheusAPI. +type MockPrometheusAPIMockRecorder struct { + mock *MockPrometheusAPI +} + +// NewMockPrometheusAPI creates a new mock instance. +func NewMockPrometheusAPI(ctrl *gomock.Controller) *MockPrometheusAPI { + mock := &MockPrometheusAPI{ctrl: ctrl} + mock.recorder = &MockPrometheusAPIMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPrometheusAPI) EXPECT() *MockPrometheusAPIMockRecorder { + return m.recorder +} + +// AlertManagers mocks base method. +func (m *MockPrometheusAPI) AlertManagers(arg0 context.Context) (v1.AlertManagersResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AlertManagers", arg0) + ret0, _ := ret[0].(v1.AlertManagersResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AlertManagers indicates an expected call of AlertManagers. +func (mr *MockPrometheusAPIMockRecorder) AlertManagers(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AlertManagers", reflect.TypeOf((*MockPrometheusAPI)(nil).AlertManagers), arg0) +} + +// Alerts mocks base method. +func (m *MockPrometheusAPI) Alerts(arg0 context.Context) (v1.AlertsResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Alerts", arg0) + ret0, _ := ret[0].(v1.AlertsResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Alerts indicates an expected call of Alerts. +func (mr *MockPrometheusAPIMockRecorder) Alerts(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Alerts", reflect.TypeOf((*MockPrometheusAPI)(nil).Alerts), arg0) +} + +// Buildinfo mocks base method. +func (m *MockPrometheusAPI) Buildinfo(arg0 context.Context) (v1.BuildinfoResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Buildinfo", arg0) + ret0, _ := ret[0].(v1.BuildinfoResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Buildinfo indicates an expected call of Buildinfo. +func (mr *MockPrometheusAPIMockRecorder) Buildinfo(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Buildinfo", reflect.TypeOf((*MockPrometheusAPI)(nil).Buildinfo), arg0) +} + +// CleanTombstones mocks base method. +func (m *MockPrometheusAPI) CleanTombstones(arg0 context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CleanTombstones", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// CleanTombstones indicates an expected call of CleanTombstones. +func (mr *MockPrometheusAPIMockRecorder) CleanTombstones(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanTombstones", reflect.TypeOf((*MockPrometheusAPI)(nil).CleanTombstones), arg0) +} + +// Config mocks base method. +func (m *MockPrometheusAPI) Config(arg0 context.Context) (v1.ConfigResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Config", arg0) + ret0, _ := ret[0].(v1.ConfigResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Config indicates an expected call of Config. +func (mr *MockPrometheusAPIMockRecorder) Config(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Config", reflect.TypeOf((*MockPrometheusAPI)(nil).Config), arg0) +} + +// DeleteSeries mocks base method. +func (m *MockPrometheusAPI) DeleteSeries(arg0 context.Context, arg1 []string, arg2, arg3 time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteSeries", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteSeries indicates an expected call of DeleteSeries. +func (mr *MockPrometheusAPIMockRecorder) DeleteSeries(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSeries", reflect.TypeOf((*MockPrometheusAPI)(nil).DeleteSeries), arg0, arg1, arg2, arg3) +} + +// Flags mocks base method. +func (m *MockPrometheusAPI) Flags(arg0 context.Context) (v1.FlagsResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Flags", arg0) + ret0, _ := ret[0].(v1.FlagsResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Flags indicates an expected call of Flags. +func (mr *MockPrometheusAPIMockRecorder) Flags(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Flags", reflect.TypeOf((*MockPrometheusAPI)(nil).Flags), arg0) +} + +// LabelNames mocks base method. +func (m *MockPrometheusAPI) LabelNames(arg0 context.Context, arg1 []string, arg2, arg3 time.Time) ([]string, v1.Warnings, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LabelNames", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(v1.Warnings) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// LabelNames indicates an expected call of LabelNames. +func (mr *MockPrometheusAPIMockRecorder) LabelNames(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LabelNames", reflect.TypeOf((*MockPrometheusAPI)(nil).LabelNames), arg0, arg1, arg2, arg3) +} + +// LabelValues mocks base method. +func (m *MockPrometheusAPI) LabelValues(arg0 context.Context, arg1 string, arg2 []string, arg3, arg4 time.Time) (model.LabelValues, v1.Warnings, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LabelValues", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(model.LabelValues) + ret1, _ := ret[1].(v1.Warnings) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// LabelValues indicates an expected call of LabelValues. +func (mr *MockPrometheusAPIMockRecorder) LabelValues(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LabelValues", reflect.TypeOf((*MockPrometheusAPI)(nil).LabelValues), arg0, arg1, arg2, arg3, arg4) +} + +// Metadata mocks base method. +func (m *MockPrometheusAPI) Metadata(arg0 context.Context, arg1, arg2 string) (map[string][]v1.Metadata, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Metadata", arg0, arg1, arg2) + ret0, _ := ret[0].(map[string][]v1.Metadata) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Metadata indicates an expected call of Metadata. +func (mr *MockPrometheusAPIMockRecorder) Metadata(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Metadata", reflect.TypeOf((*MockPrometheusAPI)(nil).Metadata), arg0, arg1, arg2) +} + +// Query mocks base method. +func (m *MockPrometheusAPI) Query(arg0 context.Context, arg1 string, arg2 time.Time) (model.Value, v1.Warnings, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Query", arg0, arg1, arg2) + ret0, _ := ret[0].(model.Value) + ret1, _ := ret[1].(v1.Warnings) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Query indicates an expected call of Query. +func (mr *MockPrometheusAPIMockRecorder) Query(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockPrometheusAPI)(nil).Query), arg0, arg1, arg2) +} + +// QueryExemplars mocks base method. +func (m *MockPrometheusAPI) QueryExemplars(arg0 context.Context, arg1 string, arg2, arg3 time.Time) ([]v1.ExemplarQueryResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryExemplars", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].([]v1.ExemplarQueryResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryExemplars indicates an expected call of QueryExemplars. +func (mr *MockPrometheusAPIMockRecorder) QueryExemplars(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryExemplars", reflect.TypeOf((*MockPrometheusAPI)(nil).QueryExemplars), arg0, arg1, arg2, arg3) +} + +// QueryRange mocks base method. +func (m *MockPrometheusAPI) QueryRange(arg0 context.Context, arg1 string, arg2 v1.Range) (model.Value, v1.Warnings, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryRange", arg0, arg1, arg2) + ret0, _ := ret[0].(model.Value) + ret1, _ := ret[1].(v1.Warnings) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// QueryRange indicates an expected call of QueryRange. +func (mr *MockPrometheusAPIMockRecorder) QueryRange(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryRange", reflect.TypeOf((*MockPrometheusAPI)(nil).QueryRange), arg0, arg1, arg2) +} + +// Rules mocks base method. +func (m *MockPrometheusAPI) Rules(arg0 context.Context) (v1.RulesResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Rules", arg0) + ret0, _ := ret[0].(v1.RulesResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Rules indicates an expected call of Rules. +func (mr *MockPrometheusAPIMockRecorder) Rules(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rules", reflect.TypeOf((*MockPrometheusAPI)(nil).Rules), arg0) +} + +// Runtimeinfo mocks base method. +func (m *MockPrometheusAPI) Runtimeinfo(arg0 context.Context) (v1.RuntimeinfoResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Runtimeinfo", arg0) + ret0, _ := ret[0].(v1.RuntimeinfoResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Runtimeinfo indicates an expected call of Runtimeinfo. +func (mr *MockPrometheusAPIMockRecorder) Runtimeinfo(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Runtimeinfo", reflect.TypeOf((*MockPrometheusAPI)(nil).Runtimeinfo), arg0) +} + +// Series mocks base method. +func (m *MockPrometheusAPI) Series(arg0 context.Context, arg1 []string, arg2, arg3 time.Time) ([]model.LabelSet, v1.Warnings, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Series", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].([]model.LabelSet) + ret1, _ := ret[1].(v1.Warnings) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Series indicates an expected call of Series. +func (mr *MockPrometheusAPIMockRecorder) Series(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Series", reflect.TypeOf((*MockPrometheusAPI)(nil).Series), arg0, arg1, arg2, arg3) +} + +// Snapshot mocks base method. +func (m *MockPrometheusAPI) Snapshot(arg0 context.Context, arg1 bool) (v1.SnapshotResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Snapshot", arg0, arg1) + ret0, _ := ret[0].(v1.SnapshotResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Snapshot indicates an expected call of Snapshot. +func (mr *MockPrometheusAPIMockRecorder) Snapshot(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Snapshot", reflect.TypeOf((*MockPrometheusAPI)(nil).Snapshot), arg0, arg1) +} + +// TSDB mocks base method. +func (m *MockPrometheusAPI) TSDB(arg0 context.Context) (v1.TSDBResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TSDB", arg0) + ret0, _ := ret[0].(v1.TSDBResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TSDB indicates an expected call of TSDB. +func (mr *MockPrometheusAPIMockRecorder) TSDB(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSDB", reflect.TypeOf((*MockPrometheusAPI)(nil).TSDB), arg0) +} + +// Targets mocks base method. +func (m *MockPrometheusAPI) Targets(arg0 context.Context) (v1.TargetsResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Targets", arg0) + ret0, _ := ret[0].(v1.TargetsResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Targets indicates an expected call of Targets. +func (mr *MockPrometheusAPIMockRecorder) Targets(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Targets", reflect.TypeOf((*MockPrometheusAPI)(nil).Targets), arg0) +} + +// TargetsMetadata mocks base method. +func (m *MockPrometheusAPI) TargetsMetadata(arg0 context.Context, arg1, arg2, arg3 string) ([]v1.MetricMetadata, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TargetsMetadata", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].([]v1.MetricMetadata) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TargetsMetadata indicates an expected call of TargetsMetadata. +func (mr *MockPrometheusAPIMockRecorder) TargetsMetadata(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TargetsMetadata", reflect.TypeOf((*MockPrometheusAPI)(nil).TargetsMetadata), arg0, arg1, arg2, arg3) +} diff --git a/utils/prometheus/prometheus.go b/utils/prometheus/prometheus.go index 15d1dd3..9a5f114 100644 --- a/utils/prometheus/prometheus.go +++ b/utils/prometheus/prometheus.go @@ -30,6 +30,20 @@ const RequestLatencyP50 = "response_time_p50" const RequestLatencyP90 = "response_time_p90" const RequestLatencyP95 = "response_time_p95" +// ErrInvalidData indicates that the retrieved data from the prometheus api is invalid +var /* const */ ErrInvalidData = errors.New("query did not return valid values") + +// ErrNoValues indicates that no values where present in the prometheus api result +var /* const */ ErrNoValues = errors.New("query did not return any values") + +// ErrMultipleValues indicates that multiple values where present in the prometheus api result +var /* const */ ErrMultipleValues = errors.New("query did return multiple values") + +//go:generate mockgen -destination=fake/prometheusapi_mock.go -package=fake . PrometheusAPI + +// API PrometheusAPI is a type alias for the prometheus api interface +type API = apiv1.API + // Handler interacts with a prometheus API endpoint type Handler struct { ApiURL string @@ -40,7 +54,7 @@ type Handler struct { Service string DeploymentType string Labels map[string]string - prometheusAPI apiv1.API + PrometheusAPI API CustomFilters []*keptnv2.SLIFilter CustomQueries map[string]string } @@ -166,7 +180,7 @@ func NewPrometheusHandler(apiURL string, eventData *keptnv2.EventData, deploymen Service: eventData.Service, DeploymentType: deploymentType, Labels: labels, - prometheusAPI: v1api, + PrometheusAPI: v1api, CustomFilters: customFilters, } @@ -177,39 +191,47 @@ func NewPrometheusHandler(apiURL string, eventData *keptnv2.EventData, deploymen func (ph *Handler) GetSLIValue(metric string, start string, end string) (float64, error) { startUnix, err := parseUnixTimestamp(start) if err != nil { - return 0, err + return 0, fmt.Errorf("unable to parse start timestamp: %w", err) } endUnix, _ := parseUnixTimestamp(end) if err != nil { - return 0, err + return 0, fmt.Errorf("unable to parse end timestamp: %w", err) } query, err := ph.GetMetricQuery(metric, startUnix, endUnix) if err != nil { - return 0, err + return 0, fmt.Errorf("unable to get metriy query: %w", err) } log.Println("GetSLIValue: Generated query: /api/v1/query?query=" + query + "&time=" + strconv.FormatInt(endUnix.Unix(), 10)) - result, w, err := ph.prometheusAPI.Query(context.TODO(), query, endUnix) + result, w, err := ph.PrometheusAPI.Query(context.TODO(), query, endUnix) + if err != nil { + return 0, fmt.Errorf("unable to query prometheus api: %w", err) + } + if len(w) != 0 { log.Printf("Prometheus API returned warnings: %v", w) } - if err != nil { - return 0, err - } + // check if we can cast the result to a vector, it might be another data struct which we can't process resultVector, ok := result.(model.Vector) if !ok { - return 0, fmt.Errorf("prometheus response is not a Vector: %v", result) + return 0, fmt.Errorf("prometheus api response is not a Vector: %v", result) } + + // We are only allowed to return one value, if not the query may be malformed + // we are using two different errors to give the user more information about the result if len(resultVector) == 0 { - return 0, nil + return 0, ErrNoValues + } else if len(resultVector) > 1 { + return 0, ErrMultipleValues } + // parse the first entry as float and return the value if it's a valid float value resultValue := resultVector[0].Value.String() floatValue, err := strconv.ParseFloat(resultValue, 64) - if err != nil { - return 0, err + if err != nil || math.IsNaN(floatValue) { + return 0, ErrInvalidData } log.Printf(fmt.Sprintf("Prometheus Result is %v\n", floatValue)) diff --git a/utils/prometheus/prometheus_test.go b/utils/prometheus/prometheus_test.go new file mode 100644 index 0000000..e103c99 --- /dev/null +++ b/utils/prometheus/prometheus_test.go @@ -0,0 +1,113 @@ +package prometheus + +import ( + "errors" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + "strconv" + "testing" + "time" + + prometheusfake "github.com/keptn-contrib/prometheus-service/utils/prometheus/fake" + prometheusAPI "github.com/prometheus/client_golang/api/prometheus/v1" + prometheusModel "github.com/prometheus/common/model" +) + +func TestHandler_GetSLIValue(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + apiMock := prometheusfake.NewMockPrometheusAPI(mockCtrl) + handler := Handler{ + PrometheusAPI: apiMock, + } + + returnValue := prometheusModel.Vector{ + { + Value: 0, + }, + } + + apiMock.EXPECT().Query(gomock.Any(), gomock.Any(), gomock.Any()).Return(returnValue, prometheusAPI.Warnings{}, nil).Times(1) + + startTime := strconv.FormatInt(time.Now().UTC().UnixNano(), 10) + endTime := strconv.FormatInt(time.Now().UTC().UnixNano(), 10) + + value, err := handler.GetSLIValue(Throughput, startTime, endTime) + require.NoError(t, err) + + require.Equal(t, (float64)(0), value) +} + +func TestHandler_GetSLIValueNoResult(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + apiMock := prometheusfake.NewMockPrometheusAPI(mockCtrl) + handler := Handler{ + PrometheusAPI: apiMock, + } + + returnValue := prometheusModel.Vector{} + + apiMock.EXPECT().Query(gomock.Any(), gomock.Any(), gomock.Any()).Return(returnValue, prometheusAPI.Warnings{}, nil).Times(1) + + startTime := strconv.FormatInt(time.Now().UTC().UnixNano(), 10) + endTime := strconv.FormatInt(time.Now().UTC().UnixNano(), 10) + + _, err := handler.GetSLIValue(Throughput, startTime, endTime) + require.Error(t, err) + require.ErrorIs(t, err, ErrNoValues) +} + +func TestHandler_GetSLIValueMultipleValues(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + apiMock := prometheusfake.NewMockPrometheusAPI(mockCtrl) + handler := Handler{ + PrometheusAPI: apiMock, + } + + returnValue := prometheusModel.Vector{ + { + Value: 123, + }, + { + Value: 999, + }, + } + returnWarnings := prometheusAPI.Warnings{} + + apiMock.EXPECT().Query(gomock.Any(), gomock.Any(), gomock.Any()).Return(returnValue, returnWarnings, nil).Times(1) + + startTime := strconv.FormatInt(time.Now().UTC().UnixNano(), 10) + endTime := strconv.FormatInt(time.Now().UTC().UnixNano(), 10) + + _, err := handler.GetSLIValue(Throughput, startTime, endTime) + require.Error(t, err) + require.ErrorIs(t, err, ErrMultipleValues) +} + +func TestHandler_GetSLIValueError(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + apiMock := prometheusfake.NewMockPrometheusAPI(mockCtrl) + handler := Handler{ + PrometheusAPI: apiMock, + } + + returnValue := prometheusModel.Vector{} + returnWarnings := prometheusAPI.Warnings{} + apiError := errors.New("http Error XXX") + + apiMock.EXPECT().Query(gomock.Any(), gomock.Any(), gomock.Any()).Return(returnValue, returnWarnings, apiError).Times(1) + + startTime := strconv.FormatInt(time.Now().UTC().UnixNano(), 10) + endTime := strconv.FormatInt(time.Now().UTC().UnixNano(), 10) + + _, err := handler.GetSLIValue(Throughput, startTime, endTime) + require.Error(t, err) + require.ErrorIs(t, err, apiError) +}