diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b66dec5..17c2c34c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Thanks to @chenlujjj for [the pull request](https://github.com/VictoriaMetrics/victoriametrics-datasource/pull/243). * BUGFIX: fix issue with variables not working in adhoc filters. See [this issue](https://github.com/VictoriaMetrics/victoriametrics-datasource/issues/235). +* BUGFIX: fix query type switching when creating alerts in Grafana. See [this issue](https://github.com/VictoriaMetrics/victoriametrics-datasource/issues/237) ## [v0.10.3](https://github.com/VictoriaMetrics/victoriametrics-datasource/releases/tag/v0.10.3) diff --git a/pkg/plugin/datasource.go b/pkg/plugin/datasource.go index a0586e56..bafab5e2 100644 --- a/pkg/plugin/datasource.go +++ b/pkg/plugin/datasource.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "strconv" "strings" "sync" "time" @@ -20,6 +21,9 @@ import ( const ( defaultScrapeInterval = 15 * time.Second health = "/-/healthy" + // it is weird logic to pass an identifier for an alert request in the headers + // but Grafana decided to do so, so we need to follow this + requestFromAlert = "FromAlert" ) // NewDatasource creates a new datasource instance. @@ -62,14 +66,20 @@ func (d *Datasource) Dispose() { // contains Frames ([]*Frame). func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { response := backend.NewQueryDataResponse() + headers := req.Headers + + forAlerting, err := d.checkAlertingRequest(headers) + if err != nil { + return nil, err + } var wg sync.WaitGroup for _, q := range req.Queries { wg.Add(1) - go func(q backend.DataQuery) { + go func(q backend.DataQuery, forAlerting bool) { defer wg.Done() - response.Responses[q.RefID] = d.query(ctx, q) - }(q) + response.Responses[q.RefID] = d.query(ctx, q, forAlerting) + }(q, forAlerting) } wg.Wait() @@ -77,7 +87,7 @@ func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataReques } // query process backend.Query and return response -func (d *Datasource) query(ctx context.Context, query backend.DataQuery) backend.DataResponse { +func (d *Datasource) query(ctx context.Context, query backend.DataQuery, forAlerting bool) backend.DataResponse { var q Query if err := json.Unmarshal(query.JSON, &q); err != nil { err = fmt.Errorf("failed to parse query json: %s", err) @@ -152,6 +162,8 @@ func (d *Datasource) query(ctx context.Context, query backend.DataQuery) backend return newResponseError(err, backend.StatusInternal) } + r.ForAlerting = forAlerting + frames, err := r.getDataFrames() if err != nil { err = fmt.Errorf("failed to prepare data from response: %w", err) @@ -196,6 +208,23 @@ func (d *Datasource) CheckHealth(ctx context.Context, _ *backend.CheckHealthRequ }, nil } +func (d *Datasource) checkAlertingRequest(headers map[string]string) (bool, error) { + var forAlerting bool + if val, ok := headers[requestFromAlert]; ok { + if val == "" { + return false, nil + } + + boolValue, err := strconv.ParseBool(val) + if err != nil { + return false, fmt.Errorf("failed to parse %s header value: %s", requestFromAlert, val) + } + + forAlerting = boolValue + } + return forAlerting, nil +} + // newHealthCheckErrorf returns a new *backend.CheckHealthResult with its status set to backend.HealthStatusError // and the specified message, which is formatted with Sprintf. func newHealthCheckErrorf(format string, args ...interface{}) *backend.CheckHealthResult { diff --git a/pkg/plugin/datasource_test.go b/pkg/plugin/datasource_test.go index 4826c8df..018efc91 100644 --- a/pkg/plugin/datasource_test.go +++ b/pkg/plugin/datasource_test.go @@ -367,3 +367,56 @@ func TestDatasourceQueryRequestWithRetry(t *testing.T) { expValue(2) // 1 - fail, 2 - retry expErr("EOF") // 3, 4 - retries } + +func TestDatasource_checkAlertingRequest(t *testing.T) { + tests := []struct { + name string + headers map[string]string + want bool + wantErr bool + }{ + { + name: "no alerting header", + headers: map[string]string{}, + want: false, + wantErr: false, + }, + { + name: "alerting header", + headers: map[string]string{"FromAlert": "true"}, + want: true, + wantErr: false, + }, + { + name: "invalid alerting header", + headers: map[string]string{"FromAlert": "invalid"}, + want: false, + wantErr: true, + }, + { + name: "false alerting header", + headers: map[string]string{"FromAlert": "false"}, + want: false, + wantErr: false, + }, + { + name: "irrelevant header", + headers: map[string]string{"SomeOtherHeader": "true"}, + want: false, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &Datasource{} + got, err := d.checkAlertingRequest(tt.headers) + if (err != nil) != tt.wantErr { + t.Errorf("checkAlertingRequest() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("checkAlertingRequest() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/plugin/response.go b/pkg/plugin/response.go index 31a20c62..3902de92 100644 --- a/pkg/plugin/response.go +++ b/pkg/plugin/response.go @@ -36,8 +36,9 @@ type Data struct { // Response contains fields from query response type Response struct { - Status string `json:"status"` - Data Data `json:"data"` + Status string `json:"status"` + Data Data `json:"data"` + ForAlerting bool `json:"-"` } type promInstant struct { @@ -61,6 +62,21 @@ func (pi promInstant) dataframes() (data.Frames, error) { return frames, nil } +func (pi *promInstant) alertingDataFrames() (data.Frames, error) { + frames := make(data.Frames, len(pi.Result)) + for i, res := range pi.Result { + f, err := strconv.ParseFloat(res.Value[1].(string), 64) + if err != nil { + return nil, fmt.Errorf("metric %v, unable to parse float64 from %s: %w", res, res.Value[1], err) + } + + frames[i] = data.NewFrame("", + data.NewField(data.TimeSeriesValueFieldName, data.Labels(res.Labels), []float64{f})) + } + + return frames, nil +} + type promRange struct { Result []Result `json:"result"` } @@ -120,6 +136,9 @@ func (r *Response) getDataFrames() (data.Frames, error) { if err := json.Unmarshal(r.Data.Result, &pi.Result); err != nil { return nil, fmt.Errorf("unmarshal err %s; \n %#v", err, string(r.Data.Result)) } + if r.ForAlerting { + return pi.alertingDataFrames() + } return pi.dataframes() case matrix: var pr promRange diff --git a/pkg/plugin/response_test.go b/pkg/plugin/response_test.go index 47201ffb..d1e65fea 100644 --- a/pkg/plugin/response_test.go +++ b/pkg/plugin/response_test.go @@ -9,25 +9,21 @@ import ( ) func TestResponse_getDataFrames(t *testing.T) { - type fields struct { - Status string - Data Data - } tests := []struct { - name string - fields fields - query Query - want func() data.Frames - wantErr bool + name string + status string + data Data + forAlerting bool + query Query + want func() data.Frames + wantErr bool }{ { - name: "empty data", - fields: fields{ - Status: "", - Data: Data{ - ResultType: "", - Result: nil, - }, + name: "empty data", + status: "", + data: Data{ + ResultType: "", + Result: nil, }, query: Query{}, want: func() data.Frames { @@ -36,13 +32,11 @@ func TestResponse_getDataFrames(t *testing.T) { wantErr: true, }, { - name: "incorrect result type", - fields: fields{ - Status: "success", - Data: Data{ - ResultType: "abc", - Result: nil, - }, + name: "incorrect result type", + status: "success", + data: Data{ + ResultType: "abc", + Result: nil, }, query: Query{LegendFormat: "legend {{app}}"}, want: func() data.Frames { @@ -51,13 +45,11 @@ func TestResponse_getDataFrames(t *testing.T) { wantErr: true, }, { - name: "bad json", - fields: fields{ - Status: "success", - Data: Data{ - ResultType: "success", - Result: []byte("{"), - }, + name: "bad json", + status: "success", + data: Data{ + ResultType: "success", + Result: []byte("{"), }, query: Query{LegendFormat: "legend {{app}}"}, want: func() data.Frames { @@ -66,13 +58,11 @@ func TestResponse_getDataFrames(t *testing.T) { wantErr: true, }, { - name: "scalar response", - fields: fields{ - Status: "success", - Data: Data{ - ResultType: "scalar", - Result: []byte(`[1583786142, "1"]`), - }, + name: "scalar response", + status: "success", + data: Data{ + ResultType: "scalar", + Result: []byte(`[1583786142, "1"]`), }, query: Query{LegendFormat: "legend {{app}}"}, want: func() data.Frames { @@ -86,13 +76,11 @@ func TestResponse_getDataFrames(t *testing.T) { wantErr: false, }, { - name: "vector response", - fields: fields{ - Status: "success", - Data: Data{ - ResultType: "vector", - Result: []byte(`[{"metric":{"__name__":"vm_rows"},"value":[1583786142,"13763"]},{"metric":{"__name__":"vm_requests"},"value":[1583786140,"2000"]}]`), - }, + name: "vector response", + status: "success", + data: Data{ + ResultType: "vector", + Result: []byte(`[{"metric":{"__name__":"vm_rows"},"value":[1583786142,"13763"]},{"metric":{"__name__":"vm_requests"},"value":[1583786140,"2000"]}]`), }, query: Query{LegendFormat: "legend {{app}}"}, want: func() data.Frames { @@ -110,13 +98,11 @@ func TestResponse_getDataFrames(t *testing.T) { wantErr: false, }, { - name: "matrix response", - fields: fields{ - Status: "success", - Data: Data{ - ResultType: "matrix", - Result: []byte(`[{"metric":{"__name__":"ingress_nginx_request_qps","status":"100"},"values":[[1670324477.542,"1"]]}, {"metric":{"__name__":"ingress_nginx_request_qps","status":"500"},"values":[[1670324477.542,"2"]]}, {"metric":{"__name__":"ingress_nginx_request_qps","status":"200"},"values":[[1670324477.542,"3"]]}]`), - }, + name: "matrix response", + status: "success", + data: Data{ + ResultType: "matrix", + Result: []byte(`[{"metric":{"__name__":"ingress_nginx_request_qps","status":"100"},"values":[[1670324477.542,"1"]]}, {"metric":{"__name__":"ingress_nginx_request_qps","status":"500"},"values":[[1670324477.542,"2"]]}, {"metric":{"__name__":"ingress_nginx_request_qps","status":"200"},"values":[[1670324477.542,"3"]]}]`), }, query: Query{LegendFormat: "legend {{app}}"}, want: func() data.Frames { @@ -137,12 +123,34 @@ func TestResponse_getDataFrames(t *testing.T) { }, wantErr: false, }, + { + name: "vector response for alerting", + status: "success", + data: Data{ + ResultType: "vector", + Result: []byte(`[{"metric":{"__name__":"vm_rows"},"value":[1583786142,"13763"]},{"metric":{"__name__":"vm_requests"},"value":[1583786140,"2000"]}]`), + }, + forAlerting: true, + query: Query{LegendFormat: "legend {{app}}"}, + want: func() data.Frames { + return []*data.Frame{ + data.NewFrame("", + data.NewField(data.TimeSeriesValueFieldName, data.Labels{"__name__": "vm_rows"}, []float64{13763}), + ), + data.NewFrame("", + data.NewField(data.TimeSeriesValueFieldName, data.Labels{"__name__": "vm_requests"}, []float64{2000}), + ), + } + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Response{ - Status: tt.fields.Status, - Data: tt.fields.Data, + Status: tt.status, + Data: tt.data, + ForAlerting: tt.forAlerting, } got, err := r.getDataFrames() if (err != nil) != tt.wantErr { diff --git a/src/querybuilder/state.ts b/src/querybuilder/state.ts index 00812a6f..8ad0577e 100755 --- a/src/querybuilder/state.ts +++ b/src/querybuilder/state.ts @@ -57,9 +57,10 @@ export function getQueryWithDefaults(query: PromQuery, app: CoreApp | undefined) } // Unified Alerting does not support "both" for query type – fall back to "range". - if (app === CoreApp.UnifiedAlerting) { - const isBothInstantAndRange = query.instant && query.range; - result = { ...result, range: isBothInstantAndRange, instant: !isBothInstantAndRange }; + const isBothInstantAndRange = query.instant && query.range; + if (app === CoreApp.UnifiedAlerting && isBothInstantAndRange) { + result = { ...result, instant: false, range: true }; } + return result; }