diff --git a/pkg/tsdb/loki/api.go b/pkg/tsdb/loki/api.go index 2bbe18aba30..79501b11146 100644 --- a/pkg/tsdb/loki/api.go +++ b/pkg/tsdb/loki/api.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "net/url" + "path" "strconv" "github.com/grafana/grafana-plugin-sdk-go/data" @@ -68,12 +69,12 @@ func makeDataRequest(ctx context.Context, lokiDsUrl string, query lokiQuery, oau // precise, as Loki does not support step with float number // and time-specifier, like "1.5s" qs.Set("step", fmt.Sprintf("%dms", query.Step.Milliseconds())) - lokiUrl.Path = "/loki/api/v1/query_range" + lokiUrl.Path = path.Join(lokiUrl.Path, "/loki/api/v1/query_range") } case QueryTypeInstant: { qs.Set("time", strconv.FormatInt(query.End.UnixNano(), 10)) - lokiUrl.Path = "/loki/api/v1/query" + lokiUrl.Path = path.Join(lokiUrl.Path, "/loki/api/v1/query") } default: return nil, fmt.Errorf("invalid QueryType: %v", query.QueryType) @@ -169,18 +170,22 @@ func (api *LokiAPI) DataQuery(ctx context.Context, query lokiQuery) (data.Frames return res.Frames, nil } -func makeRawRequest(ctx context.Context, lokiDsUrl string, resourceURL string, oauthToken string) (*http.Request, error) { +func makeRawRequest(ctx context.Context, lokiDsUrl string, resourcePath string, oauthToken string) (*http.Request, error) { lokiUrl, err := url.Parse(lokiDsUrl) if err != nil { return nil, err } - url, err := lokiUrl.Parse(resourceURL) + resourceUrl, err := url.Parse(resourcePath) if err != nil { return nil, err } - req, err := http.NewRequestWithContext(ctx, "GET", url.String(), nil) + // we take the path and the query-string only + lokiUrl.RawQuery = resourceUrl.RawQuery + lokiUrl.Path = path.Join(lokiUrl.Path, resourceUrl.Path) + + req, err := http.NewRequestWithContext(ctx, "GET", lokiUrl.String(), nil) if err != nil { return nil, err @@ -191,8 +196,8 @@ func makeRawRequest(ctx context.Context, lokiDsUrl string, resourceURL string, o return req, nil } -func (api *LokiAPI) RawQuery(ctx context.Context, resourceURL string) ([]byte, error) { - req, err := makeRawRequest(ctx, api.url, resourceURL, api.oauthToken) +func (api *LokiAPI) RawQuery(ctx context.Context, resourcePath string) ([]byte, error) { + req, err := makeRawRequest(ctx, api.url, resourcePath, api.oauthToken) if err != nil { return nil, err } diff --git a/pkg/tsdb/loki/api_mock.go b/pkg/tsdb/loki/api_mock.go index a48633677e7..7ffa7e9a9aa 100644 --- a/pkg/tsdb/loki/api_mock.go +++ b/pkg/tsdb/loki/api_mock.go @@ -33,9 +33,13 @@ func (mockedRT *mockedRoundTripper) RoundTrip(req *http.Request) (*http.Response } func makeMockedAPI(statusCode int, contentType string, responseBytes []byte, requestCallback mockRequestCallback) *LokiAPI { + return makeMockedAPIWithUrl("http://localhost:9999", statusCode, contentType, responseBytes, requestCallback) +} + +func makeMockedAPIWithUrl(url string, statusCode int, contentType string, responseBytes []byte, requestCallback mockRequestCallback) *LokiAPI { client := http.Client{ Transport: &mockedRoundTripper{statusCode: statusCode, contentType: contentType, responseBytes: responseBytes, requestCallback: requestCallback}, } - return newLokiAPI(&client, "http://localhost:9999", log.New("test"), "") + return newLokiAPI(&client, url, log.New("test"), "") } diff --git a/pkg/tsdb/loki/api_test.go b/pkg/tsdb/loki/api_test.go new file mode 100644 index 00000000000..8b84d5d7553 --- /dev/null +++ b/pkg/tsdb/loki/api_test.go @@ -0,0 +1,126 @@ +package loki + +import ( + "context" + "fmt" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestApiLogVolume(t *testing.T) { + response := []byte(` + { + "status": "success", + "data": { + "resultType" : "matrix", + "result": [] + } + } + `) + + t.Run("log-volume queries should set log-volume http header", func(t *testing.T) { + called := false + api := makeMockedAPI(200, "application/json", response, func(req *http.Request) { + called = true + require.Equal(t, "Source=logvolhist", req.Header.Get("X-Query-Tags")) + }) + + _, err := api.DataQuery(context.Background(), lokiQuery{Expr: "", VolumeQuery: true, QueryType: QueryTypeRange}) + require.NoError(t, err) + require.True(t, called) + }) + + t.Run("non-log-volume queries should not set log-volume http header", func(t *testing.T) { + called := false + api := makeMockedAPI(200, "application/json", response, func(req *http.Request) { + called = true + require.Equal(t, "", req.Header.Get("X-Query-Tags")) + }) + + _, err := api.DataQuery(context.Background(), lokiQuery{Expr: "", VolumeQuery: false, QueryType: QueryTypeRange}) + require.NoError(t, err) + require.True(t, called) + }) +} + +func TestApiUrlHandling(t *testing.T) { + response := []byte(` + { + "status": "success", + "data": { + "resultType" : "matrix", + "result": [] + } + } + `) + + queryTestData := []struct { + name string + dsUrl string + queryPrefix string + metaUrl string + }{ + { + name: "no path in datasource-config", + dsUrl: "http://localhost:3100", + queryPrefix: "http://localhost:3100/loki/api/v1/query_range?", + metaUrl: "http://localhost:3100/loki/api/v1/labels?start=1&end=2", + }, + { + name: "just a slash path in datasource-config", + dsUrl: "http://localhost:3100/", + queryPrefix: "http://localhost:3100/loki/api/v1/query_range?", + metaUrl: "http://localhost:3100/loki/api/v1/labels?start=1&end=2", + }, + { + name: "when path-without-end-slash in datasource-config", + dsUrl: "http://localhost:3100/a/b/c", + queryPrefix: "http://localhost:3100/a/b/c/loki/api/v1/query_range?", + metaUrl: "http://localhost:3100/a/b/c/loki/api/v1/labels?start=1&end=2", + }, + { + name: "path-with-end-slash in datasource-config", + dsUrl: "http://localhost:3100/a/b/c/", + queryPrefix: "http://localhost:3100/a/b/c/loki/api/v1/query_range?", + metaUrl: "http://localhost:3100/a/b/c/loki/api/v1/labels?start=1&end=2", + }, + } + + for _, test := range queryTestData { + t.Run("Loki should build the query URL correctly when "+test.name, func(t *testing.T) { + called := false + api := makeMockedAPIWithUrl(test.dsUrl, 200, "application/json", response, func(req *http.Request) { + called = true + urlString := req.URL.String() + wantedPrefix := test.queryPrefix + failMessage := fmt.Sprintf(`wanted prefix: [%s], got string [%s]`, wantedPrefix, urlString) + require.True(t, strings.HasPrefix(urlString, wantedPrefix), failMessage) + }) + + query := lokiQuery{ + QueryType: QueryTypeRange, + } + + _, err := api.DataQuery(context.Background(), query) + require.NoError(t, err) + require.True(t, called) + }) + } + + for _, test := range queryTestData { + t.Run("Loki should build the metadata query URL correctly when "+test.name, func(t *testing.T) { + called := false + api := makeMockedAPIWithUrl(test.dsUrl, 200, "application/json", response, func(req *http.Request) { + called = true + require.Equal(t, test.metaUrl, req.URL.String()) + }) + + _, err := api.RawQuery(context.Background(), "/loki/api/v1/labels?start=1&end=2") + require.NoError(t, err) + require.True(t, called) + }) + } +}