From 2e53d51e143540d2a58ccb93c6e3e99d8ed3bce5 Mon Sep 17 00:00:00 2001 From: Hanabi <317387557+Hanabi9248@users.noreply.github.com> Date: Sat, 12 Sep 2026 11:06:29 +0800 Subject: [PATCH 1/2] [bugfix] preserve special characters in promql queries Signed-off-by: Hanabi <317387557+Hanabi9248@users.noreply.github.com> --- .../hertzbeat/warehouse/db/PromqlQueryExecutor.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/db/PromqlQueryExecutor.java b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/db/PromqlQueryExecutor.java index 9e70596fb96..a17d0998f98 100644 --- a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/db/PromqlQueryExecutor.java +++ b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/db/PromqlQueryExecutor.java @@ -57,6 +57,7 @@ public abstract class PromqlQueryExecutor implements QueryExecutor { private static final String QUERY_RANGE_PATH = "/api/v1/query_range"; private static final String QUERY_PATH = "/api/v1/query"; protected static final String HTTP_QUERY_PARAM = "query"; + private static final String QUERY_TEMPLATE = "{query}"; protected static final String HTTP_TIME_PARAM = "time"; protected static final String HTTP_START_PARAM = "start"; protected static final String HTTP_END_PARAM = "end"; @@ -99,8 +100,8 @@ public List> execute(String queryString) { HttpEntity httpEntity = new HttpEntity<>(headers); UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(httpPromqlProperties.url + QUERY_PATH); - uriComponentsBuilder.queryParam(HTTP_QUERY_PARAM, queryString); - URI uri = uriComponentsBuilder.build().toUri(); + uriComponentsBuilder.queryParam(HTTP_QUERY_PARAM, QUERY_TEMPLATE); + URI uri = uriComponentsBuilder.encode().buildAndExpand(queryString).toUri(); ResponseEntity responseEntity = restTemplate.exchange(uri, HttpMethod.GET, httpEntity, PromQlQueryContent.class); if (responseEntity.getStatusCode().is2xxSuccessful()) { @@ -151,15 +152,15 @@ public DatasourceQueryData query(DatasourceQuery datasourceQuery) { URI uri; if (datasourceQuery.getTimeType().equals(RANGE)) { uri = UriComponentsBuilder.fromUriString(httpPromqlProperties.url() + QUERY_RANGE_PATH) - .queryParam(HTTP_QUERY_PARAM, datasourceQuery.getExpr()) + .queryParam(HTTP_QUERY_PARAM, QUERY_TEMPLATE) .queryParam(HTTP_START_PARAM, datasourceQuery.getStart()) .queryParam(HTTP_END_PARAM, datasourceQuery.getEnd()) .queryParam(HTTP_STEP_PARAM, datasourceQuery.getStep()) - .build().toUri(); + .encode().buildAndExpand(datasourceQuery.getExpr()).toUri(); } else if (datasourceQuery.getTimeType().equals(INSTANT)) { uri = UriComponentsBuilder.fromUriString(httpPromqlProperties.url() + QUERY_PATH) - .queryParam(HTTP_QUERY_PARAM, datasourceQuery.getExpr()) - .build().toUri(); + .queryParam(HTTP_QUERY_PARAM, QUERY_TEMPLATE) + .encode().buildAndExpand(datasourceQuery.getExpr()).toUri(); } else { throw new IllegalArgumentException(String.format("no such time type for query id %s.", datasourceQuery.getRefId())); } From 34ccb9f8bdad508619a82310a40ab290bfbad6af Mon Sep 17 00:00:00 2001 From: Hanabi <317387557+Hanabi9248@users.noreply.github.com> Date: Sat, 12 Sep 2026 11:07:02 +0800 Subject: [PATCH 2/2] [test] cover promql query parameter round trips Signed-off-by: Hanabi <317387557+Hanabi9248@users.noreply.github.com> --- .../warehouse/db/PromqlQueryExecutorTest.java | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/db/PromqlQueryExecutorTest.java diff --git a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/db/PromqlQueryExecutorTest.java b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/db/PromqlQueryExecutorTest.java new file mode 100644 index 00000000000..abed07b32bf --- /dev/null +++ b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/db/PromqlQueryExecutorTest.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hertzbeat.warehouse.db; + +import static org.apache.hertzbeat.warehouse.constants.WarehouseConstants.INSTANT; +import static org.apache.hertzbeat.warehouse.constants.WarehouseConstants.RANGE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.hertzbeat.common.entity.dto.query.DatasourceQuery; +import org.apache.hertzbeat.warehouse.store.history.tsdb.vm.PromQlQueryContent; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +class PromqlQueryExecutorTest { + + @ParameterizedTest + @ValueSource(strings = {"execute", INSTANT, RANGE}) + void preservesQueryExpression(String operation) { + String expression = "sum(up{job=~\"api.+\",label=\"a&b=50%\"}) + 1"; + RestTemplate restTemplate = mock(RestTemplate.class); + when(restTemplate.exchange(any(URI.class), eq(HttpMethod.GET), any(HttpEntity.class), + eq(PromQlQueryContent.class))).thenReturn(ResponseEntity.ok(new PromQlQueryContent())); + PromqlQueryExecutor executor = new PromqlQueryExecutor(restTemplate, + new PromqlQueryExecutor.HttpPromqlProperties("http://localhost:8428", null, null)) { + @Override + public String getDatasource() { + return "test"; + } + }; + + if ("execute".equals(operation)) { + executor.execute(expression); + } else { + executor.query(DatasourceQuery.builder().refId("A").timeType(operation) + .expr(expression).start(1712300000L).end(1712300300L).step("60s").build()); + } + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); + verify(restTemplate).exchange(uriCaptor.capture(), eq(HttpMethod.GET), any(HttpEntity.class), + eq(PromQlQueryContent.class)); + URI uri = uriCaptor.getValue(); + Map parameters = Arrays.stream(uri.getRawQuery().split("&")) + .map(pair -> pair.split("=", 2)) + .collect(Collectors.toMap(pair -> pair[0], + pair -> URLDecoder.decode(pair[1], StandardCharsets.UTF_8))); + assertEquals(expression, parameters.get("query")); + assertEquals(RANGE.equals(operation) ? "/api/v1/query_range" : "/api/v1/query", uri.getPath()); + if (RANGE.equals(operation)) { + assertEquals(Map.of("query", expression, "start", "1712300000", "end", "1712300300", "step", "60s"), parameters); + } else { + assertEquals(Map.of("query", expression), parameters); + } + } +}