diff --git a/Cargo.lock b/Cargo.lock index 26554d7f81c9..53ab681cd6a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8627,9 +8627,9 @@ dependencies = [ [[package]] name = "promql-parser" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1ad4a4cfa84ec4aa5831c82e57af0a3faf3f0af83bee13fa1390b2d0a32dc9" +checksum = "7fe99e6f80a79abccf1e8fb48dd63473a36057e600cc6ea36147c8318698ae6f" dependencies = [ "cfgrammar", "chrono", @@ -8637,6 +8637,8 @@ dependencies = [ "lrlex", "lrpar", "regex", + "serde", + "serde_json", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2e7f70d2ab94..e45236758f25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/servers/src/http.rs b/src/servers/src/http.rs index de4f2cf5ff44..12ac06db9070 100644 --- a/src/servers/src/http.rs +++ b/src/servers/src/http.rs @@ -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; @@ -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), diff --git a/src/servers/src/http/prometheus.rs b/src/servers/src/http/prometheus.rs index 941cac253972..6f749f259508 100644 --- a/src/servers/src/http/prometheus.rs +++ b/src/servers/src/http/prometheus.rs @@ -101,6 +101,9 @@ pub enum PrometheusResponse { LabelValues(Vec), FormatQuery(String), BuildInfo(OwnedBuildInfo), + #[schemars(skip)] + #[serde(skip_deserializing)] + ParseResult(promql_parser::parser::Expr), } impl Default for PrometheusResponse { @@ -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, + db: Option, +} + +#[axum_macros::debug_handler] +#[tracing::instrument( + skip_all, + fields(protocol = "prometheus", request_type = "parse_query") +)] +pub async fn parse_query( + State(_handler): State, + Query(params): Query, + Extension(_query_ctx): Extension, + Form(form_params): Form, +) -> 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") + } +} diff --git a/tests-integration/tests/http.rs b/tests-integration/tests/http.rs index 823de40d1124..638734faba4f 100644 --- a/tests-integration/tests/http.rs +++ b/tests-integration/tests/http.rs @@ -660,6 +660,29 @@ pub async fn test_prom_http_api(store_type: StorageType) { let body = serde_json::from_str::(&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; }