diff --git a/CHANGELOG.md b/CHANGELOG.md index 56f3e485ed9..bf147342b7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## master / unreleased +* [FEATURE] Query Frontend: Add support /api/v1/format_query API for formatting queries. #6893 * [CHANGE] StoreGateway/Alertmanager: Add default 5s connection timeout on client. #6603 * [CHANGE] Ingester: Remove EnableNativeHistograms config flag and instead gate keep through new per-tenant limit at ingestion. #6718 * [CHANGE] Validate a tenantID when to use a single tenant resolver. #6727 diff --git a/docs/api/_index.md b/docs/api/_index.md index 32047248216..64a6aab3f0c 100644 --- a/docs/api/_index.md +++ b/docs/api/_index.md @@ -37,6 +37,7 @@ For the sake of clarity, in this document we have grouped API endpoints by servi | [Instant query](#instant-query) | Querier, Query-frontend || `GET,POST /api/v1/query` | | [Range query](#range-query) | Querier, Query-frontend || `GET,POST /api/v1/query_range` | | [Exemplar query](#exemplar-query) | Querier, Query-frontend || `GET,POST /api/v1/query_exemplars` | +| [Format query](#format-query) | Querier, Query-frontend || `GET,POST /api/v1/format-query` | | [Get series by label matchers](#get-series-by-label-matchers) | Querier, Query-frontend || `GET,POST /api/v1/series` | | [Get label names](#get-label-names) | Querier, Query-frontend || `GET,POST /api/v1/labels` | | [Get label values](#get-label-values) | Querier, Query-frontend || `GET /api/v1/label/{name}/values` | @@ -368,6 +369,21 @@ _For more information, please check out the Prometheus [exemplar query](https:// _Requires [authentication](#authentication)._ +### Format query + +``` +GET,POST /api/v1/format_query + +# Legacy +GET,POST /api/v1/format_query +``` + +Prometheus-compatible format query endpoint. The endpoint formats a PromQL expression in a prettified way. + +_For more information, please check out the Prometheus [fomatting query expressions](https://prometheus.io/docs/prometheus/latest/querying/api/#formatting-query-expressions) documentation._ + +_Requires [authentication](#authentication)._ + ### Get series by label matchers ``` diff --git a/integration/format_query_api_test.go b/integration/format_query_api_test.go new file mode 100644 index 00000000000..cd07ae699aa --- /dev/null +++ b/integration/format_query_api_test.go @@ -0,0 +1,127 @@ +//go:build requires_docker +// +build requires_docker + +package integration + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cortexproject/cortex/integration/e2e" + e2edb "github.com/cortexproject/cortex/integration/e2e/db" + "github.com/cortexproject/cortex/integration/e2ecortex" +) + +func TestFormatQueryAPI(t *testing.T) { + s, err := e2e.NewScenario(networkName) + require.NoError(t, err) + defer s.Close() + + // Start dependencies. + consul := e2edb.NewConsul() + minio := e2edb.NewMinio(9000, bucketName) + require.NoError(t, s.StartAndWaitReady(consul, minio)) + + flags := mergeFlags(BlocksStorageFlags(), map[string]string{ + "-auth.enabled": "true", + }) + + // Start the query-frontend. + queryFrontend := e2ecortex.NewQueryFrontend("query-frontend", flags, "") + require.NoError(t, s.Start(queryFrontend)) + + querier := e2ecortex.NewQuerier("querier", e2ecortex.RingStoreConsul, consul.NetworkHTTPEndpoint(), mergeFlags(flags, map[string]string{ + "-querier.frontend-address": queryFrontend.NetworkGRPCEndpoint(), + }), "") + require.NoError(t, s.StartAndWaitReady(querier)) + + // Start querier without frontend. + querierDirect := e2ecortex.NewQuerier("querier-direct", e2ecortex.RingStoreConsul, consul.NetworkHTTPEndpoint(), flags, "") + require.NoError(t, s.StartAndWaitReady(querierDirect)) + + require.NoError(t, s.WaitReady(queryFrontend)) + + testCases := []struct { + name string + query string + expectedResp string + expectError bool + method string + useOnlyQuerier bool + }{ + { + name: "Valid query for GET method", + query: "foo/bar", + expectedResp: "foo / bar", + expectError: false, + method: "GET", + }, + { + name: "Valid query for POST method", + query: "foo/bar", + expectedResp: "foo / bar", + expectError: false, + method: "POST", + }, + { + name: "Invalid query for GET method", + query: "invalid_expression/", + expectError: true, + method: "GET", + }, + { + name: "Invalid query for POST method", + query: "invalid_expression/", + expectError: true, + method: "POST", + }, + { + name: "Valid query using only querier (GET)", + query: "foo/bar", + expectedResp: "foo / bar", + expectError: false, + method: "GET", + useOnlyQuerier: true, + }, + } + var parsed struct { + Data string `json:"data"` + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var endpoint string + if tc.useOnlyQuerier { + endpoint = fmt.Sprintf("http://%s/api/prom/api/v1/format_query?query=%s", querierDirect.HTTPEndpoint(), tc.query) + } else { + endpoint = fmt.Sprintf("http://%s/api/prom/api/v1/format_query?query=%s", queryFrontend.HTTPEndpoint(), tc.query) + } + + req, err := http.NewRequest(tc.method, endpoint, nil) + req.Header.Set("X-Scope-OrgID", "user-1") + require.NoError(t, err) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + if tc.expectError { + require.NotEqual(t, 200, resp.StatusCode) + return + } else { + require.Equal(t, 200, resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + require.NoError(t, json.Unmarshal(body, &parsed)) + require.Equal(t, tc.expectedResp, parsed.Data) + }) + } +} diff --git a/pkg/api/api.go b/pkg/api/api.go index 13843c3e64a..ec02f72e760 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -430,6 +430,7 @@ func (a *API) RegisterQueryAPI(handler http.Handler) { a.RegisterRoute(path.Join(a.cfg.PrometheusHTTPPrefix, "/api/v1/query"), hf, true, "GET", "POST") a.RegisterRoute(path.Join(a.cfg.PrometheusHTTPPrefix, "/api/v1/query_range"), hf, true, "GET", "POST") a.RegisterRoute(path.Join(a.cfg.PrometheusHTTPPrefix, "/api/v1/query_exemplars"), hf, true, "GET", "POST") + a.RegisterRoute(path.Join(a.cfg.PrometheusHTTPPrefix, "/api/v1/format_query"), hf, true, "GET", "POST") a.RegisterRoute(path.Join(a.cfg.PrometheusHTTPPrefix, "/api/v1/labels"), hf, true, "GET", "POST") a.RegisterRoute(path.Join(a.cfg.PrometheusHTTPPrefix, "/api/v1/label/{name}/values"), hf, true, "GET") a.RegisterRoute(path.Join(a.cfg.PrometheusHTTPPrefix, "/api/v1/series"), hf, true, "GET", "POST", "DELETE") @@ -440,6 +441,7 @@ func (a *API) RegisterQueryAPI(handler http.Handler) { a.RegisterRoute(path.Join(a.cfg.LegacyHTTPPrefix, "/api/v1/query"), hf, true, "GET", "POST") a.RegisterRoute(path.Join(a.cfg.LegacyHTTPPrefix, "/api/v1/query_range"), hf, true, "GET", "POST") a.RegisterRoute(path.Join(a.cfg.LegacyHTTPPrefix, "/api/v1/query_exemplars"), hf, true, "GET", "POST") + a.RegisterRoute(path.Join(a.cfg.LegacyHTTPPrefix, "/api/v1/format_query"), hf, true, "GET", "POST") a.RegisterRoute(path.Join(a.cfg.LegacyHTTPPrefix, "/api/v1/labels"), hf, true, "GET", "POST") a.RegisterRoute(path.Join(a.cfg.LegacyHTTPPrefix, "/api/v1/label/{name}/values"), hf, true, "GET") a.RegisterRoute(path.Join(a.cfg.LegacyHTTPPrefix, "/api/v1/series"), hf, true, "GET", "POST", "DELETE") diff --git a/pkg/api/handlers.go b/pkg/api/handlers.go index 5173affb197..9bcc6a6906e 100644 --- a/pkg/api/handlers.go +++ b/pkg/api/handlers.go @@ -290,6 +290,7 @@ func NewQuerierHandler( router.Path(path.Join(prefix, "/api/v1/query")).Methods("GET", "POST").Handler(queryAPI.Wrap(queryAPI.InstantQueryHandler)) router.Path(path.Join(prefix, "/api/v1/query_range")).Methods("GET", "POST").Handler(queryAPI.Wrap(queryAPI.RangeQueryHandler)) router.Path(path.Join(prefix, "/api/v1/query_exemplars")).Methods("GET", "POST").Handler(promRouter) + router.Path(path.Join(prefix, "/api/v1/format_query")).Methods("GET", "POST").Handler(promRouter) router.Path(path.Join(prefix, "/api/v1/labels")).Methods("GET", "POST").Handler(promRouter) router.Path(path.Join(prefix, "/api/v1/label/{name}/values")).Methods("GET").Handler(promRouter) router.Path(path.Join(prefix, "/api/v1/series")).Methods("GET", "POST", "DELETE").Handler(promRouter) @@ -303,6 +304,7 @@ func NewQuerierHandler( router.Path(path.Join(legacyPrefix, "/api/v1/query")).Methods("GET", "POST").Handler(queryAPI.Wrap(queryAPI.InstantQueryHandler)) router.Path(path.Join(legacyPrefix, "/api/v1/query_range")).Methods("GET", "POST").Handler(queryAPI.Wrap(queryAPI.RangeQueryHandler)) router.Path(path.Join(legacyPrefix, "/api/v1/query_exemplars")).Methods("GET", "POST").Handler(legacyPromRouter) + router.Path(path.Join(legacyPrefix, "/api/v1/format_query")).Methods("GET", "POST").Handler(legacyPromRouter) router.Path(path.Join(legacyPrefix, "/api/v1/labels")).Methods("GET", "POST").Handler(legacyPromRouter) router.Path(path.Join(legacyPrefix, "/api/v1/label/{name}/values")).Methods("GET").Handler(legacyPromRouter) router.Path(path.Join(legacyPrefix, "/api/v1/series")).Methods("GET", "POST", "DELETE").Handler(legacyPromRouter)