Skip to content

Add support for /api/v1/format_query API #6893

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
16 changes: 16 additions & 0 deletions docs/api/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <prometheus-http-prefix>/api/v1/query` |
| [Range query](#range-query) | Querier, Query-frontend || `GET,POST <prometheus-http-prefix>/api/v1/query_range` |
| [Exemplar query](#exemplar-query) | Querier, Query-frontend || `GET,POST <prometheus-http-prefix>/api/v1/query_exemplars` |
| [Format query](#format-query) | Querier, Query-frontend || `GET,POST <prometheus-http-prefix>/api/v1/format-query` |
| [Get series by label matchers](#get-series-by-label-matchers) | Querier, Query-frontend || `GET,POST <prometheus-http-prefix>/api/v1/series` |
| [Get label names](#get-label-names) | Querier, Query-frontend || `GET,POST <prometheus-http-prefix>/api/v1/labels` |
| [Get label values](#get-label-values) | Querier, Query-frontend || `GET <prometheus-http-prefix>/api/v1/label/{name}/values` |
Expand Down Expand Up @@ -368,6 +369,21 @@ _For more information, please check out the Prometheus [exemplar query](https://

_Requires [authentication](#authentication)._

### Format query

```
GET,POST <prometheus-http-prefix>/api/v1/format_query

# Legacy
GET,POST <legacy-http-prefix>/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

```
Expand Down
127 changes: 127 additions & 0 deletions integration/format_query_api_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
2 changes: 2 additions & 0 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions pkg/api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Loading