This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
The following commit(s) were added to refs/heads/master by this push:
new b61dbfcacf fix(collector): correct jsonpath alias parsing for rows
missing path (#4265)
b61dbfcacf is described below
commit b61dbfcacf900e55863ff6fc0a8cc8d9075de181
Author: NekoPunch <[email protected]>
AuthorDate: Sun Aug 16 18:18:42 2026 -0700
fix(collector): correct jsonpath alias parsing for rows missing path (#4265)
Co-authored-by: Tomsun28 <[email protected]>
---
.../collector/collect/http/HttpCollectImpl.java | 12 ++--
.../collect/http/HttpCollectImplTest.java | 43 ++++++++++++
.../collector/dispatch/MetricsCollect.java | 12 +++-
.../collector/dispatch/MetricsCollectTest.java | 73 +++++++++++++++++++
.../hertzbeat/collector/util/JsonPathParser.java | 18 +++++
.../collector/util/JsonPathParserTest.java | 81 ++++++++++++++++++++++
6 files changed, 229 insertions(+), 10 deletions(-)
diff --git
a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/HttpCollectImpl.java
b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/HttpCollectImpl.java
index ab9f2dca91..408d32c0ce 100644
---
a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/HttpCollectImpl.java
+++
b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/HttpCollectImpl.java
@@ -704,13 +704,11 @@ public class HttpCollectImpl extends AbstractCollect {
valueRowBuilder.addColumn(String.valueOf(value));
} else {
if (alias.startsWith("$.")) {
- List<Object> subResults =
JsonPathParser.parseContentWithJsonPath(resp, http.getParseScript() +
alias.substring(1));
- if (subResults != null && subResults.size() > i) {
- Object resultValue = subResults.get(i);
- valueRowBuilder.addColumn(resultValue == null
? CommonConstants.NULL_VALUE : String.valueOf(resultValue));
- } else {
-
valueRowBuilder.addColumn(CommonConstants.NULL_VALUE);
- }
+ // per-row evaluation, a global "parseScript +
alias" query would misalign rows missing the path
+ List<Object> aliasValues =
JsonPathParser.parseRowWithJsonPath(objectValue, alias);
+ // a wildcard alias matching multiple values is
kept whole and rendered as "[v1, v2]"
+ Object resultValue = aliasValues.size() == 1 ?
aliasValues.get(0) : (aliasValues.isEmpty() ? null : aliasValues);
+ valueRowBuilder.addColumn(resultValue == null ?
CommonConstants.NULL_VALUE : String.valueOf(resultValue));
} else {
addColumnForSummary(responseTime, valueRowBuilder,
keywordNum, alias);
}
diff --git
a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/http/HttpCollectImplTest.java
b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/http/HttpCollectImplTest.java
index 05199a2feb..f69a95a32e 100644
---
a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/http/HttpCollectImplTest.java
+++
b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/http/HttpCollectImplTest.java
@@ -20,6 +20,7 @@ package org.apache.hertzbeat.collector.collect.http;
import com.google.common.collect.Lists;
import com.sun.net.httpserver.HttpServer;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
+import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
@@ -383,6 +384,48 @@ class HttpCollectImplTest {
assertEquals("0.268751364291017", firstRow.getColumns(0));
}
+ @Test
+ void parseResponseByJsonPathKeepsRowAlignmentWhenAliasPathMissing() throws
Exception {
+ String jsonResponse = "{\"items\": ["
+ + "{\"metadata\": {\"name\": \"pod-a\"}, \"status\":
{\"phase\": \"Running\","
+ + " \"containerStatuses\": [{\"name\": \"c1\", \"ready\":
true, \"restartCount\": 5}]}},"
+ + "{\"metadata\": {\"name\": \"pod-b-pending\"}, \"status\":
{\"phase\": \"Pending\"}},"
+ + "{\"metadata\": {\"name\": \"pod-c\"}, \"status\":
{\"phase\": \"Running\","
+ + " \"containerStatuses\": [{\"name\": \"c3\", \"ready\":
true, \"restartCount\": 2}]}}"
+ + "]}";
+ HttpProtocol http = HttpProtocol.builder()
+ .parseType(DispatchConstants.PARSE_JSON_PATH)
+ .parseScript("$.items.*")
+ .build();
+ List<CollectRep.ValueRow> capturedRows = new ArrayList<>();
+ CollectRep.MetricsData.Builder builder = new
CollectRep.MetricsData.Builder() {
+ @Override
+ public CollectRep.MetricsData.Builder
addValueRow(CollectRep.ValueRow valueRow) {
+ capturedRows.add(valueRow);
+ return super.addValueRow(valueRow);
+ }
+ };
+ Method parseMethod = HttpCollectImpl.class.getDeclaredMethod(
+ "parseResponseByJsonPath",
+ String.class,
+ List.class,
+ HttpProtocol.class,
+ CollectRep.MetricsData.Builder.class,
+ Long.class);
+ parseMethod.setAccessible(true);
+
+ parseMethod.invoke(httpCollectImpl, jsonResponse,
+ Lists.newArrayList("$.metadata.name",
"$.status.containerStatuses[0].restartCount"), http, builder, 100L);
+
+ assertEquals(3, capturedRows.size());
+ assertEquals("pod-a", capturedRows.get(0).getColumns(0));
+ assertEquals("5", capturedRows.get(0).getColumns(1));
+ assertEquals("pod-b-pending", capturedRows.get(1).getColumns(0));
+ assertEquals(CommonConstants.NULL_VALUE,
capturedRows.get(1).getColumns(1));
+ assertEquals("pod-c", capturedRows.get(2).getColumns(0));
+ assertEquals("2", capturedRows.get(2).getColumns(1));
+ }
+
@Test
void testParsePromQlLabelValue() throws Exception {
// Create Prometheus format test data
diff --git
a/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/dispatch/MetricsCollect.java
b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/dispatch/MetricsCollect.java
index 94b2022599..d896e1404b 100644
---
a/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/dispatch/MetricsCollect.java
+++
b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/dispatch/MetricsCollect.java
@@ -252,11 +252,12 @@ public class MetricsCollect implements Runnable,
Comparable<MetricsCollect> {
if (metrics.getCalculates() == null) {
metrics.setCalculates(Collections.emptyList());
}
+ List<String> aliasFields =
Optional.ofNullable(metrics.getAliasFields()).orElseGet(Collections::emptyList);
// eg: database_pages=Database pages unconventional mapping
Map<String, String> fieldAliasMap = new HashMap<>(8);
Map<String, JexlExpression> fieldExpressionMap =
metrics.getCalculates()
.stream()
- .map(cal -> transformCal(cal, fieldAliasMap))
+ .map(cal -> transformCal(cal, fieldAliasMap, aliasFields))
.filter(Objects::nonNull)
.collect(Collectors.toMap(arr -> (String) arr[0], arr ->
(JexlExpression) arr[1], (oldValue, newValue) -> newValue));
@@ -270,7 +271,6 @@ public class MetricsCollect implements Runnable,
Comparable<MetricsCollect> {
.collect(Collectors.toMap(arr -> (String) arr[0], arr ->
(Pair<String, String>) arr[1], (oldValue, newValue) -> newValue));
List<Metrics.Field> fields = metrics.getFields();
- List<String> aliasFields =
Optional.ofNullable(metrics.getAliasFields()).orElseGet(Collections::emptyList);
Map<String, String> aliasFieldValueMap = new HashMap<>(8);
Map<String, Object> fieldValueMap = new HashMap<>(8);
Map<String, Object> stringTypefieldValueMap = new HashMap<>(8);
@@ -420,13 +420,19 @@ public class MetricsCollect implements Runnable,
Comparable<MetricsCollect> {
* @param fieldAliasMap field alias map
* @return expr
*/
- private Object[] transformCal(String cal, Map<String, String>
fieldAliasMap) {
+ private Object[] transformCal(String cal, Map<String, String>
fieldAliasMap, List<String> aliasFields) {
int splitIndex = cal.indexOf("=");
if (splitIndex < 0) {
return null;
}
String field = cal.substring(0, splitIndex).trim();
String expressionStr = cal.substring(splitIndex +
1).trim().replace("\\#", "#");
+ // a direct alias reference (RHS must exactly equal an aliasField, no
whitespace/case tolerance) is not a formula,
+ // JEXL parses "[0]" in such paths as array access and silently
returns null
+ if (aliasFields.contains(expressionStr)) {
+ fieldAliasMap.put(field, expressionStr);
+ return null;
+ }
JexlExpression expression;
try {
expression = JexlExpressionRunner.compile(expressionStr);
diff --git
a/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/dispatch/MetricsCollectTest.java
b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/dispatch/MetricsCollectTest.java
new file mode 100644
index 0000000000..6bc9ec8914
--- /dev/null
+++
b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/dispatch/MetricsCollectTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.collector.dispatch;
+
+import java.util.List;
+import org.apache.hertzbeat.collector.timer.WheelTimerTask;
+import org.apache.hertzbeat.common.constants.CommonConstants;
+import org.apache.hertzbeat.common.entity.job.Job;
+import org.apache.hertzbeat.common.entity.job.Metrics;
+import org.apache.hertzbeat.common.entity.message.CollectRep;
+import org.apache.hertzbeat.common.timer.Timeout;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test case for {@link MetricsCollect}
+ */
+class MetricsCollectTest {
+
+ @Test
+ void calculateFieldsMapsIndexedJsonPathAlias() {
+ Metrics metrics = Metrics.builder()
+ .name("pods")
+ .priority((byte) 0)
+ .fields(List.of(
+
Metrics.Field.builder().field("pod").type(CommonConstants.TYPE_STRING).build(),
+
Metrics.Field.builder().field("rc").type(CommonConstants.TYPE_STRING).build()))
+ .aliasFields(List.of("$.metadata.name",
"$.status.containerStatuses[0].restartCount"))
+ .calculates(List.of(
+ "pod=$.metadata.name",
+ "rc=$.status.containerStatuses[0].restartCount"))
+ .build();
+
+ Timeout timeout = mock(Timeout.class);
+ WheelTimerTask timerTask = mock(WheelTimerTask.class);
+ when(timeout.task()).thenReturn(timerTask);
+ when(timerTask.getJob()).thenReturn(Job.builder().build());
+ MetricsCollect metricsCollect = new MetricsCollect(metrics, timeout,
null, "test", List.of());
+
+ CollectRep.MetricsData.Builder collectData =
CollectRep.MetricsData.newBuilder();
+ collectData.addValueRow(CollectRep.ValueRow.newBuilder()
+ .addColumn("pod-a").addColumn("5").build());
+ collectData.addValueRow(CollectRep.ValueRow.newBuilder()
+
.addColumn("pod-b-pending").addColumn(CommonConstants.NULL_VALUE).build());
+
+ metricsCollect.calculateFields(metrics, collectData);
+
+ List<CollectRep.ValueRow> rows = collectData.getValuesList();
+ assertEquals(2, rows.size());
+ assertEquals("pod-a", rows.get(0).getColumns(0));
+ assertEquals("5", rows.get(0).getColumns(1));
+ assertEquals("pod-b-pending", rows.get(1).getColumns(0));
+ assertEquals(CommonConstants.NULL_VALUE, rows.get(1).getColumns(1));
+ }
+}
diff --git
a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/util/JsonPathParser.java
b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/util/JsonPathParser.java
index 5deb848e58..97a8345b04 100644
---
a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/util/JsonPathParser.java
+++
b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/util/JsonPathParser.java
@@ -36,12 +36,16 @@ public final class JsonPathParser {
private static final ParseContext PARSER;
+ private static final ParseContext ROW_PARSER;
+
static {
Configuration conf = Configuration.defaultConfiguration()
.addOptions(Option.DEFAULT_PATH_LEAF_TO_NULL)
.addOptions(Option.ALWAYS_RETURN_LIST);
CacheProvider.setCache(new LRUCache(128));
PARSER = JsonPath.using(conf);
+ // a single row legitimately may not contain the queried path
+ ROW_PARSER =
JsonPath.using(conf.addOptions(Option.SUPPRESS_EXCEPTIONS));
}
private JsonPathParser() {
@@ -73,4 +77,18 @@ public final class JsonPathParser {
return PARSER.parse(content).read(jsonPath, typeRef);
}
+ /**
+ * use json path to parse one already-parsed row object, missing paths
yield an empty list
+ * @param document parsed json object of a single row
+ * @param jsonPath jsonPath relative to the row root
+ * @return matched values, empty list when the path does not exist in this
row
+ */
+ public static List<Object> parseRowWithJsonPath(Object document, String
jsonPath) {
+ if (document == null || StringUtils.isEmpty(jsonPath)) {
+ return Collections.emptyList();
+ }
+ List<Object> values = ROW_PARSER.parse(document).read(jsonPath);
+ return values == null ? Collections.emptyList() : values;
+ }
+
}
diff --git
a/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/util/JsonPathParserTest.java
b/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/util/JsonPathParserTest.java
new file mode 100644
index 0000000000..b1d4ecc7f2
--- /dev/null
+++
b/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/util/JsonPathParserTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.collector.util;
+
+import com.jayway.jsonpath.PathNotFoundException;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Test case for {@link JsonPathParser}
+ */
+class JsonPathParserTest {
+
+ private static final String ROW_JSON = "{\"metadata\": {\"name\":
\"pod-a\"},"
+ + " \"status\": {\"phase\": \"Running\","
+ + " \"containerStatuses\": [{\"name\": \"c1\", \"ready\": true,
\"restartCount\": 5}]}}";
+
+ private Object row() {
+ return JsonPathParser.parseContentWithJsonPath(ROW_JSON, "$").get(0);
+ }
+
+ @Test
+ void parseRowWithJsonPathReturnsExistingValue() {
+ List<Object> values = JsonPathParser.parseRowWithJsonPath(row(),
"$.status.containerStatuses[0].restartCount");
+
+ assertEquals(1, values.size());
+ assertEquals(5, values.get(0));
+ }
+
+ @Test
+ void parseRowWithJsonPathReturnsEmptyListWhenPathMissing() {
+ Object pendingRow = JsonPathParser
+ .parseContentWithJsonPath("{\"metadata\": {\"name\":
\"pod-b\"}, \"status\": {\"phase\": \"Pending\"}}", "$")
+ .get(0);
+
+ List<Object> values = JsonPathParser.parseRowWithJsonPath(pendingRow,
"$.status.containerStatuses[0].restartCount");
+
+ assertTrue(values.isEmpty());
+ }
+
+ @Test
+ void parseRowWithJsonPathReturnsAllValuesForWildcard() {
+ List<Object> values = JsonPathParser.parseRowWithJsonPath(row(),
"$.status.containerStatuses[0].*");
+
+ assertEquals(3, values.size());
+ assertTrue(values.contains("c1"));
+ assertTrue(values.contains(true));
+ assertTrue(values.contains(5));
+ }
+
+ @Test
+ void parseContentWithJsonPathStillThrowsWhenPathMissing() {
+ assertThrows(PathNotFoundException.class,
+ () -> JsonPathParser.parseContentWithJsonPath(ROW_JSON,
"$.spec.nodeName"));
+ }
+
+ @Test
+ void parseRowWithJsonPathHandlesNullDocumentAndEmptyPath() {
+ assertTrue(JsonPathParser.parseRowWithJsonPath(null,
"$.status").isEmpty());
+ assertTrue(JsonPathParser.parseRowWithJsonPath(row(), "").isEmpty());
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]