Skip to content

Commit

Permalink
feat: implement parse_query api (#4860)
Browse files Browse the repository at this point in the history
* feat: implement parse_query api

* chore: switch to upstream

* fix: add post method for parse_query

* chore: bump promql-parser

* test: use latest promql ast serialization
  • Loading branch information
sunng87 authored Oct 30, 2024
1 parent 0da112b commit 6942079
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 5 deletions.
6 changes: 4 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ parquet = { version = "51.0.0", default-features = false, features = ["arrow", "
paste = "1.0"
pin-project = "1.0"
prometheus = { version = "0.13.3", features = ["process"] }
promql-parser = { version = "0.4.1" }
promql-parser = { version = "0.4.3", features = ["ser"] }
prost = "0.12"
raft-engine = { version = "0.4.1", default-features = false }
rand = "0.8"
Expand Down
5 changes: 3 additions & 2 deletions src/servers/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ use crate::http::influxdb::{influxdb_health, influxdb_ping, influxdb_write_v1, i
use crate::http::influxdb_result_v1::InfluxdbV1Response;
use crate::http::json_result::JsonResponse;
use crate::http::prometheus::{
build_info_query, format_query, instant_query, label_values_query, labels_query, range_query,
series_query,
build_info_query, format_query, instant_query, label_values_query, labels_query, parse_query,
range_query, series_query,
};
use crate::interceptor::LogIngestInterceptorRef;
use crate::metrics::http_metrics_layer;
Expand Down Expand Up @@ -819,6 +819,7 @@ impl HttpServer {
.route("/query_range", routing::post(range_query).get(range_query))
.route("/labels", routing::post(labels_query).get(labels_query))
.route("/series", routing::post(series_query).get(series_query))
.route("/parse_query", routing::post(parse_query).get(parse_query))
.route(
"/label/:label_name/values",
routing::get(label_values_query),
Expand Down
33 changes: 33 additions & 0 deletions src/servers/src/http/prometheus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ pub enum PrometheusResponse {
LabelValues(Vec<String>),
FormatQuery(String),
BuildInfo(OwnedBuildInfo),
#[schemars(skip)]
#[serde(skip_deserializing)]
ParseResult(promql_parser::parser::Expr),
}

impl Default for PrometheusResponse {
Expand Down Expand Up @@ -1014,3 +1017,33 @@ pub async fn series_query(
resp.resp_metrics = merge_map;
resp
}

#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct ParseQuery {
query: Option<String>,
db: Option<String>,
}

#[axum_macros::debug_handler]
#[tracing::instrument(
skip_all,
fields(protocol = "prometheus", request_type = "parse_query")
)]
pub async fn parse_query(
State(_handler): State<PrometheusHandlerRef>,
Query(params): Query<ParseQuery>,
Extension(_query_ctx): Extension<QueryContext>,
Form(form_params): Form<ParseQuery>,
) -> PrometheusJsonResponse {
if let Some(query) = params.query.or(form_params.query) {
match promql_parser::parser::parse(&query) {
Ok(ast) => PrometheusJsonResponse::success(PrometheusResponse::ParseResult(ast)),
Err(err) => {
let msg = err.to_string();
PrometheusJsonResponse::error(StatusCode::InvalidArguments, msg)
}
}
} else {
PrometheusJsonResponse::error(StatusCode::InvalidArguments, "query is required")
}
}
23 changes: 23 additions & 0 deletions tests-integration/tests/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,29 @@ pub async fn test_prom_http_api(store_type: StorageType) {
let body = serde_json::from_str::<PrometheusJsonResponse>(&res.text().await).unwrap();
assert_eq!(body.status, "success");

// parse_query
let res = client
.get("/v1/prometheus/api/v1/parse_query?query=http_requests")
.send()
.await;
assert_eq!(res.status(), StatusCode::OK);
let data = res.text().await;
// we don't have deserialization for ast so we keep test simple and compare
// the json output directly.
// the correctness should be covered by parser. In this test we only check
// response format.
let expected = "{\"status\":\"success\",\"data\":{\"type\":\"vectorSelector\",\"name\":\"http_requests\",\"matchers\":[],\"offset\":0,\"startOrEnd\":null,\"timestamp\":null}}";
assert_eq!(expected, data);

let res = client
.get("/v1/prometheus/api/v1/parse_query?query=not http_requests")
.send()
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let data = res.text().await;
let expected = "{\"status\":\"error\",\"data\":{\"resultType\":\"\",\"result\":[]},\"error\":\"invalid promql query\",\"errorType\":\"InvalidArguments\"}";
assert_eq!(expected, data);

guard.remove_all().await;
}

Expand Down

0 comments on commit 6942079

Please sign in to comment.