diff --git a/pkg/promlib/library.go b/pkg/promlib/library.go index 596a6f61c02..cbee4a19ffc 100644 --- a/pkg/promlib/library.go +++ b/pkg/promlib/library.go @@ -2,7 +2,6 @@ package promlib import ( "context" - "errors" "fmt" "strings" @@ -11,7 +10,6 @@ import ( sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/backend/log" - apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" "github.com/grafana/grafana/pkg/promlib/client" "github.com/grafana/grafana/pkg/promlib/instrumentation" @@ -138,18 +136,3 @@ func (s *Service) getInstance(ctx context.Context, pluginCtx backend.PluginConte in := i.(instance) return &in, nil } - -// IsAPIError returns whether err is or wraps a Prometheus error. -func IsAPIError(err error) bool { - // Check if the right error type is in err's chain. - var e *apiv1.Error - return errors.As(err, &e) -} - -func ConvertAPIError(err error) error { - var e *apiv1.Error - if errors.As(err, &e) { - return fmt.Errorf("%s: %s", e.Msg, e.Detail) - } - return err -} diff --git a/pkg/promlib/querydata/framing_bench_test.go b/pkg/promlib/querydata/framing_bench_test.go index b769df2b47d..411c8644e71 100644 --- a/pkg/promlib/querydata/framing_bench_test.go +++ b/pkg/promlib/querydata/framing_bench_test.go @@ -43,7 +43,7 @@ func BenchmarkExemplarJson(b *testing.B) { StatusCode: 200, Body: io.NopCloser(bytes.NewReader(responseBytes)), } - tCtx.httpProvider.setResponse(&res) + tCtx.httpProvider.setResponse(&res, &res) resp, err := tCtx.queryData.Execute(context.Background(), query) require.NoError(b, err) for _, r := range resp.Responses { @@ -74,7 +74,7 @@ func BenchmarkRangeJson(b *testing.B) { StatusCode: 200, Body: io.NopCloser(bytes.NewReader(body)), } - tCtx.httpProvider.setResponse(&res) + tCtx.httpProvider.setResponse(&res, &res) r, err = tCtx.queryData.Execute(context.Background(), q) require.NoError(b, err) } diff --git a/pkg/promlib/querydata/framing_test.go b/pkg/promlib/querydata/framing_test.go index 3668438ded4..3fa48bca92b 100644 --- a/pkg/promlib/querydata/framing_test.go +++ b/pkg/promlib/querydata/framing_test.go @@ -149,6 +149,6 @@ func runQuery(response []byte, q *backend.QueryDataRequest) (*backend.QueryDataR StatusCode: 200, Body: io.NopCloser(bytes.NewReader(response)), } - tCtx.httpProvider.setResponse(res) + tCtx.httpProvider.setResponse(res, res) return tCtx.queryData.Execute(context.Background(), q) } diff --git a/pkg/promlib/querydata/request_test.go b/pkg/promlib/querydata/request_test.go index 0977b9036d3..bfbde8dfb96 100644 --- a/pkg/promlib/querydata/request_test.go +++ b/pkg/promlib/querydata/request_test.go @@ -27,7 +27,6 @@ import ( func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { t.Run("exemplars response should be sampled and parsed normally", func(t *testing.T) { - t.Skip() exemplars := []apiv1.ExemplarQueryResult{ { SeriesLabels: p.LabelSet{ @@ -60,6 +59,22 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { }, } + values := []p.SamplePair{ + {Value: 1, Timestamp: 1000}, + {Value: 4, Timestamp: 4000}, + {Value: 6, Timestamp: 7000}, + {Value: 8, Timestamp: 1100}, + } + rangeResult := queryResult{ + Type: p.ValMatrix, + Result: p.Matrix{ + &p.SampleStream{ + Metric: p.Metric{"app": "Application", "tag2": "tag2"}, + Values: values, + }, + }, + } + tctx, err := setup() require.NoError(t, err) @@ -76,20 +91,20 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { RefID: "A", JSON: b, } - res, err := execute(tctx, query, exemplars) + res, err := execute(tctx, query, exemplars, rangeResult) require.NoError(t, err) // Test fields - require.Len(t, res, 1) - // require.Equal(t, res[0].Name, "exemplar") + require.Len(t, res, 2) + require.Equal(t, res[0].Name, "exemplar") require.Equal(t, res[0].Fields[0].Name, "Time") require.Equal(t, res[0].Fields[1].Name, "Value") require.Len(t, res[0].Fields, 6) // Test correct values (sampled to 2) - require.Equal(t, res[0].Fields[1].Len(), 2) + require.Equal(t, res[0].Fields[1].Len(), 4) require.Equal(t, res[0].Fields[1].At(0), 0.009545445) - require.Equal(t, res[0].Fields[1].At(1), 0.003535405) + require.Equal(t, res[0].Fields[1].At(3), 0.003535405) }) t.Run("matrix response should be parsed normally", func(t *testing.T) { @@ -128,7 +143,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { } tctx, err := setup() require.NoError(t, err) - res, err := execute(tctx, query, result) + res, err := execute(tctx, query, result, nil) require.NoError(t, err) require.Len(t, res, 1) @@ -177,7 +192,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { } tctx, err := setup() require.NoError(t, err) - res, err := execute(tctx, query, result) + res, err := execute(tctx, query, result, nil) require.NoError(t, err) require.Len(t, res, 1) @@ -222,7 +237,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { } tctx, err := setup() require.NoError(t, err) - res, err := execute(tctx, query, result) + res, err := execute(tctx, query, result, nil) require.NoError(t, err) require.Len(t, res, 1) @@ -266,7 +281,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { tctx, err := setup() require.NoError(t, err) - res, err := execute(tctx, query, result) + res, err := execute(tctx, query, result, nil) require.NoError(t, err) require.Equal(t, `{app="Application"}`, res[0].Fields[1].Config.DisplayNameFromDS) @@ -298,7 +313,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { } tctx, err := setup() require.NoError(t, err) - res, err := execute(tctx, query, qr) + res, err := execute(tctx, query, qr, nil) require.NoError(t, err) require.Len(t, res, 1) @@ -317,7 +332,6 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { }) t.Run("scalar response should be parsed normally", func(t *testing.T) { - t.Skip("TODO: implement scalar responses") qr := queryResult{ Type: p.ValScalar, Result: &p.Scalar{ @@ -339,14 +353,15 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { } tctx, err := setup() require.NoError(t, err) - res, err := execute(tctx, query, qr) + res, err := execute(tctx, query, qr, nil) require.NoError(t, err) require.Len(t, res, 1) require.Len(t, res[0].Fields, 2) require.Len(t, res[0].Fields[0].Labels, 0) require.Equal(t, res[0].Fields[0].Name, "Time") - require.Equal(t, "1", res[0].Fields[1].Name) + require.Equal(t, "Value", res[0].Fields[1].Name) + require.Equal(t, float64(1), res[0].Fields[1].At(0)) // Ensure the timestamps are UTC zoned testValue := res[0].Fields[0].At(0) @@ -360,22 +375,29 @@ type queryResult struct { Result any `json:"result"` } -func executeWithHeaders(tctx *testContext, query backend.DataQuery, qr any, headers map[string]string) (data.Frames, error) { +func executeWithHeaders(tctx *testContext, query backend.DataQuery, rqr any, eqr any, headers map[string]string) (data.Frames, error) { req := backend.QueryDataRequest{ Queries: []backend.DataQuery{query}, Headers: headers, } - promRes, err := toAPIResponse(qr) - defer func() { - if err := promRes.Body.Close(); err != nil { - fmt.Println(fmt.Errorf("response body close error: %v", err)) - } - }() + rangeRes, err := toAPIResponse(rqr) if err != nil { return nil, err } - tctx.httpProvider.setResponse(promRes) + exemplarRes, err := toAPIResponse(eqr) + if err != nil { + return nil, err + } + defer func() { + if err := rangeRes.Body.Close(); err != nil { + fmt.Println(fmt.Errorf("rangeRes body close error: %v", err)) + } + if err := exemplarRes.Body.Close(); err != nil { + fmt.Println(fmt.Errorf("exemplarRes body close error: %v", err)) + } + }() + tctx.httpProvider.setResponse(rangeRes, exemplarRes) res, err := tctx.queryData.Execute(context.Background(), &req) if err != nil { @@ -385,8 +407,8 @@ func executeWithHeaders(tctx *testContext, query backend.DataQuery, qr any, head return res.Responses[req.Queries[0].RefID].Frames, nil } -func execute(tctx *testContext, query backend.DataQuery, qr any) (data.Frames, error) { - return executeWithHeaders(tctx, query, qr, map[string]string{}) +func execute(tctx *testContext, query backend.DataQuery, rqr any, eqr any) (data.Frames, error) { + return executeWithHeaders(tctx, query, rqr, eqr, map[string]string{}) } type apiResponse struct { @@ -426,7 +448,11 @@ func setup() (*testContext, error) { opts: httpclient.Options{ Timeouts: &httpclient.DefaultTimeoutOptions, }, - res: &http.Response{ + rangeRes: &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewReader([]byte(`{}`))), + }, + exemplarRes: &http.Response{ StatusCode: 200, Body: io.NopCloser(bytes.NewReader([]byte(`{}`))), }, @@ -456,9 +482,10 @@ func setup() (*testContext, error) { type fakeHttpClientProvider struct { httpclient.Provider - opts httpclient.Options - req *http.Request - res *http.Response + opts httpclient.Options + req *http.Request + rangeRes *http.Response + exemplarRes *http.Response } func (p *fakeHttpClientProvider) New(opts ...httpclient.Options) (*http.Client, error) { @@ -476,11 +503,18 @@ func (p *fakeHttpClientProvider) GetTransport(opts ...httpclient.Options) (http. return http.DefaultTransport, nil } -func (p *fakeHttpClientProvider) setResponse(res *http.Response) { - p.res = res +func (p *fakeHttpClientProvider) setResponse(rangeRes *http.Response, exemplarRes *http.Response) { + p.rangeRes = rangeRes + p.exemplarRes = exemplarRes } func (p *fakeHttpClientProvider) RoundTrip(req *http.Request) (*http.Response, error) { p.req = req - return p.res, nil + switch req.URL.Path { + case "/api/v1/query_range", "/api/v1/query": + return p.rangeRes, nil + case "/api/v1/query_exemplars": + return p.exemplarRes, nil + } + return nil, fmt.Errorf("no such path: %s", req.URL.Path) } diff --git a/pkg/promlib/resource/resource.go b/pkg/promlib/resource/resource.go index 56988d10805..0673a5e0b41 100644 --- a/pkg/promlib/resource/resource.go +++ b/pkg/promlib/resource/resource.go @@ -83,15 +83,6 @@ func (r *Resource) Execute(ctx context.Context, req *backend.CallResourceRequest return callResponse, err } -func (r *Resource) DetectVersion(ctx context.Context, req *backend.CallResourceRequest) (*backend.CallResourceResponse, error) { - newReq := &backend.CallResourceRequest{ - PluginContext: req.PluginContext, - Path: "/api/v1/status/buildinfo", - } - - return r.Execute(ctx, newReq) -} - func getSelectors(expr string) ([]string, error) { parsed, err := parser.ParseExpr(expr) if err != nil { diff --git a/pkg/promlib/resource/resource_test.go b/pkg/promlib/resource/resource_test.go new file mode 100644 index 00000000000..a9b7ed21b60 --- /dev/null +++ b/pkg/promlib/resource/resource_test.go @@ -0,0 +1,109 @@ +package resource_test + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/promlib/resource" +) + +type mockRoundTripper struct { + Response *http.Response + Err error +} + +func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return m.Response, m.Err +} + +func setup() (*http.Client, backend.DataSourceInstanceSettings, log.Logger) { + // Mock HTTP Response + mockResponse := &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewReader([]byte(`{"message": "success"}`))), + Header: make(http.Header), + } + + // Create a mock RoundTripper + mockTransport := &mockRoundTripper{ + Response: mockResponse, + } + + // Create a mock HTTP client using the mock RoundTripper + mockClient := &http.Client{ + Transport: mockTransport, + } + + settings := backend.DataSourceInstanceSettings{ + ID: 1, + URL: "http://mock-server", + JSONData: []byte(`{}`), + } + + logger := log.DefaultLogger + + return mockClient, settings, logger +} + +func TestNewResource(t *testing.T) { + mockClient, settings, logger := setup() + res, err := resource.New(mockClient, settings, logger) + require.NoError(t, err) + assert.NotNil(t, res) +} + +func TestResource_Execute(t *testing.T) { + mockClient, settings, logger := setup() + res, err := resource.New(mockClient, settings, logger) + require.NoError(t, err) + + req := &backend.CallResourceRequest{ + URL: "/test", + } + ctx := context.Background() + + resp, err := res.Execute(ctx, req) + require.NoError(t, err) + assert.NotNil(t, resp) +} + +func TestResource_GetSuggestions(t *testing.T) { + mockClient, _, logger := setup() + settings := backend.DataSourceInstanceSettings{ + ID: 1, + URL: "http://localhost:9090", + JSONData: []byte(`{"httpMethod": "GET"}`), + } + + res, err := resource.New(mockClient, settings, logger) + require.NoError(t, err) + + suggestionReq := resource.SuggestionRequest{ + LabelName: "instance", + Queries: []string{"up"}, + Start: "1609459200", + End: "1609462800", + Limit: 10, + } + + body, err := json.Marshal(suggestionReq) + require.NoError(t, err) + + req := &backend.CallResourceRequest{ + Body: body, + } + ctx := context.Background() + + resp, err := res.GetSuggestions(ctx, req) + require.NoError(t, err) + assert.NotNil(t, resp) +}