Skip to content

Commit 3d92801

Browse files
authored
Parse query (cortexproject#6978)
* Add support for /api/v1/parse_query API (experimental) Signed-off-by: Siddarth Gundu <[email protected]> * Integration: Add test for parse query API Signed-off-by: Siddarth Gundu <[email protected]> * docs: fix typo of format query api in the endpoints table Signed-off-by: Siddarth Gundu <[email protected]> * update changelog Signed-off-by: Siddarth Gundu <[email protected]> * move entry for proper grouping Signed-off-by: Siddarth Gundu <[email protected]> --------- Signed-off-by: Siddarth Gundu <[email protected]>
1 parent a776fe4 commit 3d92801

File tree

5 files changed

+157
-1
lines changed

5 files changed

+157
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
* [FEATURE] Querier: Allow choosing PromQL engine via header. #6777
2424
* [FEATURE] Querier: Support for configuring query optimizers and enabling XFunctions in the Thanos engine. #6873
2525
* [FEATURE] Query Frontend: Add support /api/v1/format_query API for formatting queries. #6893
26+
* [FEATURE] Query Frontend: Add support for /api/v1/parse_query API (experimental) to parse a PromQL expression and return it as a JSON-formatted AST (abstract syntax tree). #6978
2627
* [ENHANCEMENT] Ingester: Add `cortex_ingester_tsdb_wal_replay_unknown_refs_total` and `cortex_ingester_tsdb_wbl_replay_unknown_refs_total` metrics to track unknown series references during wal/wbl replaying. #6945
2728
* [ENHANCEMENT] Ruler: Emit an error message when the rule synchronization fails. #6902
2829
* [ENHANCEMENT] Querier: Support snappy and zstd response compression for `-querier.response-compression` flag. #6848

docs/api/_index.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ For the sake of clarity, in this document we have grouped API endpoints by servi
3737
| [Instant query](#instant-query) | Querier, Query-frontend || `GET,POST <prometheus-http-prefix>/api/v1/query` |
3838
| [Range query](#range-query) | Querier, Query-frontend || `GET,POST <prometheus-http-prefix>/api/v1/query_range` |
3939
| [Exemplar query](#exemplar-query) | Querier, Query-frontend || `GET,POST <prometheus-http-prefix>/api/v1/query_exemplars` |
40-
| [Format query](#format-query) | Querier, Query-frontend || `GET,POST <prometheus-http-prefix>/api/v1/format-query` |
40+
| [Format query](#format-query) | Querier, Query-frontend || `GET,POST <prometheus-http-prefix>/api/v1/format_query` |
41+
| [Parse query](#parse-query) | Querier, Query-frontend || `GET,POST <prometheus-http-prefix>/api/v1/parse_query` |
4142
| [Get series by label matchers](#get-series-by-label-matchers) | Querier, Query-frontend || `GET,POST <prometheus-http-prefix>/api/v1/series` |
4243
| [Get label names](#get-label-names) | Querier, Query-frontend || `GET,POST <prometheus-http-prefix>/api/v1/labels` |
4344
| [Get label values](#get-label-values) | Querier, Query-frontend || `GET <prometheus-http-prefix>/api/v1/label/{name}/values` |
@@ -384,6 +385,21 @@ _For more information, please check out the Prometheus [fomatting query expressi
384385

385386
_Requires [authentication](#authentication)._
386387

388+
### Parse query
389+
390+
```
391+
GET,POST <prometheus-http-prefix>/api/v1/parse_query
392+
393+
# Legacy
394+
GET,POST <legacy-http-prefix>/api/v1/parse_query
395+
```
396+
397+
Prometheus-compatible parse query endpoint. This endpoint is **experimental**, it parses a PromQL expression and returns it as a JSON-formatted AST (abstract syntax tree) representation.
398+
399+
_For more information, please check out the Prometheus [Parsing query expressions](https://prometheus.io/docs/prometheus/latest/querying/api/#parsing-a-promql-expressions-into-a-abstract-syntax-tree-ast) documentation._
400+
401+
_Requires [authentication](#authentication)._
402+
387403
### Get series by label matchers
388404

389405
```
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
//go:build requires_docker
2+
// +build requires_docker
3+
4+
package integration
5+
6+
import (
7+
"encoding/json"
8+
"fmt"
9+
"io"
10+
"net/http"
11+
"testing"
12+
13+
"github.com/stretchr/testify/require"
14+
15+
"github.com/cortexproject/cortex/integration/e2e"
16+
e2edb "github.com/cortexproject/cortex/integration/e2e/db"
17+
"github.com/cortexproject/cortex/integration/e2ecortex"
18+
)
19+
20+
func TestParseQueryAPIQuerier(t *testing.T) {
21+
s, err := e2e.NewScenario(networkName)
22+
require.NoError(t, err)
23+
defer s.Close()
24+
25+
// Start dependencies.
26+
consul := e2edb.NewConsul()
27+
minio := e2edb.NewMinio(9000, bucketName)
28+
require.NoError(t, s.StartAndWaitReady(consul, minio))
29+
30+
flags := mergeFlags(BlocksStorageFlags(), map[string]string{
31+
"-auth.enabled": "true",
32+
})
33+
34+
distributor := e2ecortex.NewDistributor("distributor", e2ecortex.RingStoreConsul, consul.NetworkHTTPEndpoint(), flags, "")
35+
ingester := e2ecortex.NewIngester("ingester", e2ecortex.RingStoreConsul, consul.NetworkHTTPEndpoint(), flags, "")
36+
require.NoError(t, s.StartAndWaitReady(distributor, ingester))
37+
38+
// Wait until the distributor has updated the ring.
39+
require.NoError(t, distributor.WaitSumMetrics(e2e.Equals(512), "cortex_ring_tokens_total"))
40+
41+
querier := e2ecortex.NewQuerier("querier", e2ecortex.RingStoreConsul, consul.NetworkHTTPEndpoint(), flags, "")
42+
require.NoError(t, s.StartAndWaitReady(querier))
43+
44+
// Wait until the querier has updated the ring.
45+
require.NoError(t, querier.WaitSumMetrics(e2e.Equals(512), "cortex_ring_tokens_total"))
46+
47+
endpoint := fmt.Sprintf("http://%s/api/prom/api/v1/parse_query?query=foo/bar", querier.HTTPEndpoint())
48+
49+
req, err := http.NewRequest("GET", endpoint, nil)
50+
require.NoError(t, err)
51+
req.Header.Set("X-Scope-OrgID", "user-1")
52+
53+
resp, err := http.DefaultClient.Do(req)
54+
require.NoError(t, err)
55+
defer resp.Body.Close()
56+
57+
body, err := io.ReadAll(resp.Body)
58+
require.NoError(t, err)
59+
60+
require.Equal(t, http.StatusOK, resp.StatusCode)
61+
62+
var parsed struct {
63+
Status string `json:"status"`
64+
Data json.RawMessage `json:"data"`
65+
}
66+
require.NoError(t, json.Unmarshal(body, &parsed))
67+
require.Equal(t, "success", parsed.Status)
68+
69+
// check for AST contents.
70+
require.Contains(t, string(parsed.Data), "\"op\":\"/\"")
71+
require.Contains(t, string(parsed.Data), `"lhs":{"matchers":[{"name":"__name__","type":"=","value":"foo"}]`)
72+
require.Contains(t, string(parsed.Data), `"rhs":{"matchers":[{"name":"__name__","type":"=","value":"bar"}]`)
73+
}
74+
75+
func TestParseQueryAPIQueryFrontend(t *testing.T) {
76+
s, err := e2e.NewScenario(networkName)
77+
require.NoError(t, err)
78+
defer s.Close()
79+
80+
// Start dependencies.
81+
consul := e2edb.NewConsul()
82+
minio := e2edb.NewMinio(9000, bucketName)
83+
require.NoError(t, s.StartAndWaitReady(consul, minio))
84+
85+
flags := mergeFlags(BlocksStorageFlags(), map[string]string{
86+
"-auth.enabled": "true",
87+
})
88+
89+
// Start the query-frontend.
90+
queryFrontend := e2ecortex.NewQueryFrontend("query-frontend", flags, "")
91+
require.NoError(t, s.Start(queryFrontend))
92+
93+
distributor := e2ecortex.NewDistributor("distributor", e2ecortex.RingStoreConsul, consul.NetworkHTTPEndpoint(), flags, "")
94+
ingester := e2ecortex.NewIngester("ingester", e2ecortex.RingStoreConsul, consul.NetworkHTTPEndpoint(), flags, "")
95+
require.NoError(t, s.StartAndWaitReady(distributor, ingester))
96+
97+
// Wait until both the distributor updated the ring.
98+
require.NoError(t, distributor.WaitSumMetrics(e2e.Equals(512), "cortex_ring_tokens_total"))
99+
100+
querier := e2ecortex.NewQuerier("querierWithFrontend", e2ecortex.RingStoreConsul, consul.NetworkHTTPEndpoint(), mergeFlags(flags, map[string]string{
101+
"-querier.frontend-address": queryFrontend.NetworkGRPCEndpoint(),
102+
}), "")
103+
104+
require.NoError(t, s.StartAndWaitReady(querier))
105+
require.NoError(t, s.WaitReady(queryFrontend))
106+
107+
require.NoError(t, querier.WaitSumMetrics(e2e.Equals(512), "cortex_ring_tokens_total"))
108+
109+
endpoint := fmt.Sprintf("http://%s/api/prom/api/v1/parse_query?query=foo/bar", queryFrontend.HTTPEndpoint())
110+
111+
req, err := http.NewRequest("GET", endpoint, nil)
112+
require.NoError(t, err)
113+
req.Header.Set("X-Scope-OrgID", "user-1")
114+
115+
resp, err := http.DefaultClient.Do(req)
116+
require.NoError(t, err)
117+
defer resp.Body.Close()
118+
119+
body, err := io.ReadAll(resp.Body)
120+
require.NoError(t, err)
121+
122+
require.Equal(t, http.StatusOK, resp.StatusCode)
123+
124+
var parsed struct {
125+
Status string `json:"status"`
126+
Data json.RawMessage `json:"data"`
127+
}
128+
require.NoError(t, json.Unmarshal(body, &parsed))
129+
require.Equal(t, "success", parsed.Status)
130+
131+
// check for AST contents.
132+
require.Contains(t, string(parsed.Data), "\"op\":\"/\"")
133+
require.Contains(t, string(parsed.Data), `"lhs":{"matchers":[{"name":"__name__","type":"=","value":"foo"}]`)
134+
require.Contains(t, string(parsed.Data), `"rhs":{"matchers":[{"name":"__name__","type":"=","value":"bar"}]`)
135+
}

pkg/api/api.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,7 @@ func (a *API) RegisterQueryAPI(handler http.Handler) {
437437
a.RegisterRoute(path.Join(a.cfg.PrometheusHTTPPrefix, "/api/v1/query_range"), hf, true, "GET", "POST")
438438
a.RegisterRoute(path.Join(a.cfg.PrometheusHTTPPrefix, "/api/v1/query_exemplars"), hf, true, "GET", "POST")
439439
a.RegisterRoute(path.Join(a.cfg.PrometheusHTTPPrefix, "/api/v1/format_query"), hf, true, "GET", "POST")
440+
a.RegisterRoute(path.Join(a.cfg.PrometheusHTTPPrefix, "/api/v1/parse_query"), hf, true, "GET", "POST")
440441
a.RegisterRoute(path.Join(a.cfg.PrometheusHTTPPrefix, "/api/v1/labels"), hf, true, "GET", "POST")
441442
a.RegisterRoute(path.Join(a.cfg.PrometheusHTTPPrefix, "/api/v1/label/{name}/values"), hf, true, "GET")
442443
a.RegisterRoute(path.Join(a.cfg.PrometheusHTTPPrefix, "/api/v1/series"), hf, true, "GET", "POST", "DELETE")
@@ -448,6 +449,7 @@ func (a *API) RegisterQueryAPI(handler http.Handler) {
448449
a.RegisterRoute(path.Join(a.cfg.LegacyHTTPPrefix, "/api/v1/query_range"), hf, true, "GET", "POST")
449450
a.RegisterRoute(path.Join(a.cfg.LegacyHTTPPrefix, "/api/v1/query_exemplars"), hf, true, "GET", "POST")
450451
a.RegisterRoute(path.Join(a.cfg.LegacyHTTPPrefix, "/api/v1/format_query"), hf, true, "GET", "POST")
452+
a.RegisterRoute(path.Join(a.cfg.LegacyHTTPPrefix, "/api/v1/parse_query"), hf, true, "GET", "POST")
451453
a.RegisterRoute(path.Join(a.cfg.LegacyHTTPPrefix, "/api/v1/labels"), hf, true, "GET", "POST")
452454
a.RegisterRoute(path.Join(a.cfg.LegacyHTTPPrefix, "/api/v1/label/{name}/values"), hf, true, "GET")
453455
a.RegisterRoute(path.Join(a.cfg.LegacyHTTPPrefix, "/api/v1/series"), hf, true, "GET", "POST", "DELETE")

pkg/api/handlers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ func NewQuerierHandler(
295295
router.Path(path.Join(prefix, "/api/v1/query_range")).Methods("GET", "POST").Handler(queryAPI.Wrap(queryAPI.RangeQueryHandler))
296296
router.Path(path.Join(prefix, "/api/v1/query_exemplars")).Methods("GET", "POST").Handler(promRouter)
297297
router.Path(path.Join(prefix, "/api/v1/format_query")).Methods("GET", "POST").Handler(promRouter)
298+
router.Path(path.Join(prefix, "/api/v1/parse_query")).Methods("GET", "POST").Handler(promRouter)
298299
router.Path(path.Join(prefix, "/api/v1/labels")).Methods("GET", "POST").Handler(promRouter)
299300
router.Path(path.Join(prefix, "/api/v1/label/{name}/values")).Methods("GET").Handler(promRouter)
300301
router.Path(path.Join(prefix, "/api/v1/series")).Methods("GET", "POST", "DELETE").Handler(promRouter)
@@ -309,6 +310,7 @@ func NewQuerierHandler(
309310
router.Path(path.Join(legacyPrefix, "/api/v1/query_range")).Methods("GET", "POST").Handler(queryAPI.Wrap(queryAPI.RangeQueryHandler))
310311
router.Path(path.Join(legacyPrefix, "/api/v1/query_exemplars")).Methods("GET", "POST").Handler(legacyPromRouter)
311312
router.Path(path.Join(legacyPrefix, "/api/v1/format_query")).Methods("GET", "POST").Handler(legacyPromRouter)
313+
router.Path(path.Join(legacyPrefix, "/api/v1/parse_query")).Methods("GET", "POST").Handler(legacyPromRouter)
312314
router.Path(path.Join(legacyPrefix, "/api/v1/labels")).Methods("GET", "POST").Handler(legacyPromRouter)
313315
router.Path(path.Join(legacyPrefix, "/api/v1/label/{name}/values")).Methods("GET").Handler(legacyPromRouter)
314316
router.Path(path.Join(legacyPrefix, "/api/v1/series")).Methods("GET", "POST", "DELETE").Handler(legacyPromRouter)

0 commit comments

Comments
 (0)