This is an automated email from the ASF dual-hosted git repository.
epugh pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/branch_9x by this push:
new 4aa1e1d1649 Update dependency net.thisptr:jackson-jq to v1 (branch_9x)
(#3576)
4aa1e1d1649 is described below
commit 4aa1e1d16495c0bb8ee28759aa95a277cba48ed6
Author: Solr Bot <[email protected]>
AuthorDate: Tue Aug 25 16:13:36 2026 +0200
Update dependency net.thisptr:jackson-jq to v1 (branch_9x) (#3576)
Co-authored-by: Eric Pugh <[email protected]>
---
solr/licenses/jackson-jq-0.0.13.jar.sha1 | 1 -
solr/licenses/jackson-jq-1.6.2.jar.sha1 | 1 +
.../apache/solr/prometheus/exporter/JqSupport.java | 53 +++++++
.../solr/prometheus/exporter/MetricsQuery.java | 2 +-
.../solr/prometheus/scraper/SolrScraper.java | 159 +++++++++++++--------
.../conf/test-config-partial-failure.xml | 54 +++++++
.../exporter/MetricsQueryTemplateTest.java | 6 +-
.../scraper/SolrScraperPartialFailureTest.java | 93 ++++++++++++
versions.lock | 4 +-
versions.props | 2 +-
10 files changed, 309 insertions(+), 66 deletions(-)
diff --git a/solr/licenses/jackson-jq-0.0.13.jar.sha1
b/solr/licenses/jackson-jq-0.0.13.jar.sha1
deleted file mode 100644
index b20dc335fe4..00000000000
--- a/solr/licenses/jackson-jq-0.0.13.jar.sha1
+++ /dev/null
@@ -1 +0,0 @@
-66479e34ce27a86a924bb5f2b1556db3e117f591
diff --git a/solr/licenses/jackson-jq-1.6.2.jar.sha1
b/solr/licenses/jackson-jq-1.6.2.jar.sha1
new file mode 100644
index 00000000000..35896daa8b8
--- /dev/null
+++ b/solr/licenses/jackson-jq-1.6.2.jar.sha1
@@ -0,0 +1 @@
+3644d072842c4b4ac5b083ab6172abfb89df2978
diff --git
a/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/exporter/JqSupport.java
b/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/exporter/JqSupport.java
new file mode 100644
index 00000000000..e8ed1543f71
--- /dev/null
+++
b/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/exporter/JqSupport.java
@@ -0,0 +1,53 @@
+/*
+ * 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.solr.prometheus.exporter;
+
+import net.thisptr.jackson.jq.BuiltinFunctionLoader;
+import net.thisptr.jackson.jq.JsonQuery;
+import net.thisptr.jackson.jq.Scope;
+import net.thisptr.jackson.jq.Version;
+import net.thisptr.jackson.jq.exception.JsonQueryException;
+
+/**
+ * Single shared place for the jq language {@link Version} used to both
compile queries and populate
+ * the {@link Scope} they run against. These two must stay in sync: a query
compiled against one
+ * version referencing builtins loaded for a different version fails at
evaluation time, not compile
+ * time.
+ */
+public final class JqSupport {
+
+ private JqSupport() {}
+
+ private static final Version VERSION = Version.LATEST;
+
+ /**
+ * Root scope with jq's built-in functions (select, to_entries, startswith,
etc.) registered once.
+ * Safe to pass directly to {@link JsonQuery#apply}, including concurrently:
{@code apply}
+ * isolates each evaluation in its own child scope internally, and this
scope's function registry
+ * is populated once here and never mutated afterward.
+ */
+ public static final Scope ROOT_SCOPE = Scope.newEmptyScope();
+
+ static {
+ BuiltinFunctionLoader.getInstance().loadFunctions(VERSION, ROOT_SCOPE);
+ }
+
+ public static JsonQuery compile(String query) throws JsonQueryException {
+ return JsonQuery.compile(query, VERSION);
+ }
+}
diff --git
a/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/exporter/MetricsQuery.java
b/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/exporter/MetricsQuery.java
index 5c3ba1f5303..416e08855b5 100644
---
a/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/exporter/MetricsQuery.java
+++
b/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/exporter/MetricsQuery.java
@@ -135,7 +135,7 @@ public class MetricsQuery {
}
}
- JsonQuery compiledJsonQuery = JsonQuery.compile(jsonQuery);
+ JsonQuery compiledJsonQuery = JqSupport.compile(jsonQuery);
compiledQueries.add(compiledJsonQuery);
}
}
diff --git
a/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/scraper/SolrScraper.java
b/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/scraper/SolrScraper.java
index 5bc4d720b1b..25fe2f30f01 100644
---
a/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/scraper/SolrScraper.java
+++
b/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/scraper/SolrScraper.java
@@ -41,6 +41,7 @@ import org.apache.solr.client.solrj.impl.Http2SolrClient;
import org.apache.solr.client.solrj.request.QueryRequest;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.prometheus.collector.MetricSamples;
+import org.apache.solr.prometheus.exporter.JqSupport;
import org.apache.solr.prometheus.exporter.MetricsQuery;
import org.apache.solr.prometheus.exporter.SolrExporter;
import org.slf4j.Logger;
@@ -122,13 +123,18 @@ public abstract class SolrScraper implements Closeable {
protected MetricSamples request(SolrClient client, MetricsQuery query)
throws IOException {
MetricSamples samples = new MetricSamples();
- String baseUrlLabelValue = "";
- String zkHostLabelValue = "";
+ String initialBaseUrlLabelValue = "";
+ String initialZkHostLabelValue = "";
if (client instanceof Http2SolrClient) {
- baseUrlLabelValue = ((Http2SolrClient) client).getBaseURL();
+ initialBaseUrlLabelValue = ((Http2SolrClient) client).getBaseURL();
} else if (client instanceof CloudSolrClient) {
- zkHostLabelValue = ((CloudSolrClient)
client).getClusterStateProvider().getQuorumHosts();
+ initialZkHostLabelValue =
+ ((CloudSolrClient)
client).getClusterStateProvider().getQuorumHosts();
}
+ // effectively-final copies so the per-result lambda below (see
jsonQuery.apply) can close
+ // over them
+ final String baseUrlLabelValue = initialBaseUrlLabelValue;
+ final String zkHostLabelValue = initialZkHostLabelValue;
QueryRequest queryRequest = new QueryRequest(query.getParameters());
queryRequest.setPath(query.getPath());
@@ -155,68 +161,103 @@ public abstract class SolrScraper implements Closeable {
JsonNode jsonNode = OBJECT_MAPPER.readTree((String)
queryResponse.get("response"));
for (JsonQuery jsonQuery : query.getJsonQueries()) {
+ // Buffer results here rather than adding them to `samples` as they
stream in: if the query
+ // emits some results and then throws partway through, we want to
discard the partial
+ // output for this query rather than leaving it in the returned scrape.
+ List<ParsedResult> parsedResults = new ArrayList<>();
try {
- List<JsonNode> results = jsonQuery.apply(jsonNode);
- for (JsonNode result : results) {
- String type = result.get("type").textValue();
- String name = result.get("name").textValue();
- String help = result.get("help").textValue();
- double value = result.get("value").doubleValue();
-
- List<String> labelNames = new ArrayList<>();
- List<String> labelValues = new ArrayList<>();
-
- /* Labels in response */
- for (JsonNode item : result.get("label_names")) {
- labelNames.add(item.textValue());
- }
-
- for (JsonNode item : result.get("label_values")) {
- labelValues.add(item.textValue());
- }
-
- /* Labels due to client */
- if (!baseUrlLabelValue.isEmpty()) {
- labelNames.add(BASE_URL_LABEL);
- labelValues.add(baseUrlLabelValue);
- } else if (!zkHostLabelValue.isEmpty()) {
- labelNames.add(ZK_HOST_LABEL);
- labelValues.add(zkHostLabelValue);
- }
-
- // Add the unique cluster ID, either as specified on cmdline
--cluster-id or
- // baseUrl/zkHost
- labelNames.add(CLUSTER_ID_LABEL);
- labelValues.add(clusterId);
-
- // Deduce core if not there
- if (labelNames.indexOf("core") < 0
- && labelNames.indexOf("collection") >= 0
- && labelNames.indexOf("shard") >= 0
- && labelNames.indexOf("replica") >= 0) {
- labelNames.add("core");
-
- String collection =
labelValues.get(labelNames.indexOf("collection"));
- String shard = labelValues.get(labelNames.indexOf("shard"));
- String replica = labelValues.get(labelNames.indexOf("replica"));
-
- labelValues.add(collection + "_" + shard + "_" + replica);
- }
-
- samples.addSamplesIfNotPresent(
- name,
- new Collector.MetricFamilySamples(
- name, Collector.Type.valueOf(type), help, new
ArrayList<>()));
-
- samples.addSampleIfMetricExists(
- name, new Collector.MetricFamilySamples.Sample(name, labelNames,
labelValues, value));
- }
+ jsonQuery.apply(
+ JqSupport.ROOT_SCOPE,
+ jsonNode,
+ result -> {
+ String type = result.get("type").textValue();
+ String name = result.get("name").textValue();
+ String help = result.get("help").textValue();
+ double value = result.get("value").doubleValue();
+
+ List<String> labelNames = new ArrayList<>();
+ List<String> labelValues = new ArrayList<>();
+
+ /* Labels in response */
+ for (JsonNode item : result.get("label_names")) {
+ labelNames.add(item.textValue());
+ }
+
+ for (JsonNode item : result.get("label_values")) {
+ labelValues.add(item.textValue());
+ }
+
+ /* Labels due to client */
+ if (!baseUrlLabelValue.isEmpty()) {
+ labelNames.add(BASE_URL_LABEL);
+ labelValues.add(baseUrlLabelValue);
+ } else if (!zkHostLabelValue.isEmpty()) {
+ labelNames.add(ZK_HOST_LABEL);
+ labelValues.add(zkHostLabelValue);
+ }
+
+ // Add the unique cluster ID, either as specified on cmdline
--cluster-id or
+ // baseUrl/zkHost
+ labelNames.add(CLUSTER_ID_LABEL);
+ labelValues.add(clusterId);
+
+ // Deduce core if not there
+ if (labelNames.indexOf("core") < 0
+ && labelNames.indexOf("collection") >= 0
+ && labelNames.indexOf("shard") >= 0
+ && labelNames.indexOf("replica") >= 0) {
+ labelNames.add("core");
+
+ String collection =
labelValues.get(labelNames.indexOf("collection"));
+ String shard = labelValues.get(labelNames.indexOf("shard"));
+ String replica =
labelValues.get(labelNames.indexOf("replica"));
+
+ labelValues.add(collection + "_" + shard + "_" + replica);
+ }
+
+ parsedResults.add(
+ new ParsedResult(
+ name,
+ type,
+ help,
+ new Collector.MetricFamilySamples.Sample(
+ name, labelNames, labelValues, value)));
+ });
} catch (JsonQueryException e) {
log.error("Error apply JSON query={} to result", jsonQuery, e);
scrapeErrorTotal.labels(zkHostLabelValue, baseUrlLabelValue,
clusterId).inc();
+ continue;
+ }
+
+ for (ParsedResult parsedResult : parsedResults) {
+ samples.addSamplesIfNotPresent(
+ parsedResult.name,
+ new Collector.MetricFamilySamples(
+ parsedResult.name,
+ Collector.Type.valueOf(parsedResult.type),
+ parsedResult.help,
+ new ArrayList<>()));
+
+ samples.addSampleIfMetricExists(parsedResult.name,
parsedResult.sample);
}
}
return samples;
}
+
+ /** One jq result, parsed but not yet merged into the scrape's {@link
MetricSamples}. */
+ private static final class ParsedResult {
+ private final String name;
+ private final String type;
+ private final String help;
+ private final Collector.MetricFamilySamples.Sample sample;
+
+ private ParsedResult(
+ String name, String type, String help,
Collector.MetricFamilySamples.Sample sample) {
+ this.name = name;
+ this.type = type;
+ this.help = help;
+ this.sample = sample;
+ }
+ }
}
diff --git
a/solr/prometheus-exporter/src/test-files/conf/test-config-partial-failure.xml
b/solr/prometheus-exporter/src/test-files/conf/test-config-partial-failure.xml
new file mode 100644
index 00000000000..e1b651755f4
--- /dev/null
+++
b/solr/prometheus-exporter/src/test-files/conf/test-config-partial-failure.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ 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.
+-->
+
+<config>
+
+ <rules>
+
+ <!--
+ Used by SolrScraperPartialFailureTest: the single jsonQuery below emits
one valid
+ metric for the first array element, then throws while processing the
second, to verify
+ that a mid-query failure discards the whole query's results rather than
leaking the
+ already-emitted one into the scrape.
+ -->
+ <metrics>
+ <lst name="request">
+ <lst name="query">
+ <str name="path">/admin/metrics</str>
+ </lst>
+ <arr name="jsonQueries">
+ <str>
+ .items[] |
+ if . == "bad" then error("boom") else
+ {
+ name : "solr_test_partial_failure_total",
+ type : "COUNTER",
+ help : "Test metric for partial-failure scrape behavior",
+ label_names : [],
+ label_values : [],
+ value : 1
+ }
+ end
+ </str>
+ </arr>
+ </lst>
+ </metrics>
+
+ </rules>
+
+</config>
diff --git
a/solr/prometheus-exporter/src/test/org/apache/solr/prometheus/exporter/MetricsQueryTemplateTest.java
b/solr/prometheus-exporter/src/test/org/apache/solr/prometheus/exporter/MetricsQueryTemplateTest.java
index 9a23a1f19d8..7ccc69519d9 100644
---
a/solr/prometheus-exporter/src/test/org/apache/solr/prometheus/exporter/MetricsQueryTemplateTest.java
+++
b/solr/prometheus-exporter/src/test/org/apache/solr/prometheus/exporter/MetricsQueryTemplateTest.java
@@ -21,6 +21,7 @@ import static
org.apache.solr.prometheus.exporter.MetricsConfiguration.xpathFact
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
@@ -122,8 +123,9 @@ public class MetricsQueryTemplateTest extends
SolrTestCaseJ4 {
Optional<Matcher> maybe = MetricsQueryTemplate.matches(queryMetrics[m]);
assertTrue(maybe.isPresent());
Matcher matcher = maybe.get();
- JsonQuery jsonQuery =
JsonQuery.compile(coreQueryTemplate.applyTemplate(matcher));
- List<JsonNode> results = jsonQuery.apply(parsedMetrics);
+ JsonQuery jsonQuery =
JqSupport.compile(coreQueryTemplate.applyTemplate(matcher));
+ List<JsonNode> results = new ArrayList<>();
+ jsonQuery.apply(JqSupport.ROOT_SCOPE, parsedMetrics, results::add);
assertNotNull(results);
assertEquals(1, results.size());
double value = results.get(0).get("value").doubleValue();
diff --git
a/solr/prometheus-exporter/src/test/org/apache/solr/prometheus/scraper/SolrScraperPartialFailureTest.java
b/solr/prometheus-exporter/src/test/org/apache/solr/prometheus/scraper/SolrScraperPartialFailureTest.java
new file mode 100644
index 00000000000..1e1ef97a072
--- /dev/null
+++
b/solr/prometheus-exporter/src/test/org/apache/solr/prometheus/scraper/SolrScraperPartialFailureTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.solr.prometheus.scraper;
+
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.common.util.ExecutorUtil;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SolrNamedThreadFactory;
+import org.apache.solr.prometheus.collector.MetricSamples;
+import org.apache.solr.prometheus.exporter.MetricsConfiguration;
+import org.apache.solr.prometheus.exporter.MetricsQuery;
+import org.apache.solr.prometheus.utils.Helpers;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Verifies that {@link SolrScraper#request} discards a jsonQuery's results
entirely when the query
+ * throws partway through, instead of leaking whatever it already emitted
before failing.
+ */
+public class SolrScraperPartialFailureTest extends SolrTestCaseJ4 {
+
+ private static ExecutorService executor;
+
+ @BeforeClass
+ public static void setupExecutor() {
+ executor =
+ ExecutorUtil.newMDCAwareSingleThreadExecutor(
+ new SolrNamedThreadFactory("solr-scraper-partial-failure-tests"));
+ }
+
+ @AfterClass
+ public static void teardownExecutor() {
+ ExecutorUtil.shutdownNowAndAwaitTermination(executor);
+ executor = null;
+ }
+
+ /** Always answers with the same canned JSON response, regardless of what's
requested. */
+ private static final class FixedResponseSolrClient extends SolrClient {
+ private final String json;
+
+ private FixedResponseSolrClient(String json) {
+ this.json = json;
+ }
+
+ @Override
+ public NamedList<Object> request(SolrRequest<?> request, String
collection) {
+ NamedList<Object> response = new NamedList<>();
+ response.add("response", json);
+ return response;
+ }
+
+ @Override
+ public void close() {}
+ }
+
+ @Test
+ public void testPartialResultsAreDiscardedOnQueryFailure() throws Exception {
+ MetricsConfiguration configuration =
+ Helpers.loadConfiguration("conf/test-config-partial-failure.xml");
+ List<MetricsQuery> queries = configuration.getMetricsConfiguration();
+ assertEquals(1, queries.size());
+ MetricsQuery query = queries.get(0);
+
+ SolrClient solrClient = new FixedResponseSolrClient("{\"items\": [\"ok\",
\"bad\"]}");
+
+ SolrStandaloneScraper scraper = new SolrStandaloneScraper(null, executor,
"test-cluster");
+ MetricSamples samples = scraper.request(solrClient, query);
+
+ assertTrue(
+ "the sample emitted before the query threw must not appear in the
scrape",
+ samples.asList().isEmpty());
+ }
+}
diff --git a/versions.lock b/versions.lock
index c1e21d0774b..1b983a22721 100644
--- a/versions.lock
+++ b/versions.lock
@@ -5,7 +5,7 @@ com.carrotsearch:hppc:0.10.0 (2 constraints: d40fecb0)
com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.9.1 (2
constraints: d01534e2)
com.cybozu.labs:langdetect:1.1-20120112 (1 constraints: 5c066d5e)
com.fasterxml.jackson:jackson-bom:2.22.2 (12 constraints: 01002c6d)
-com.fasterxml.jackson.core:jackson-annotations:2.22 (15 constraints: 6e1c6df7)
+com.fasterxml.jackson.core:jackson-annotations:2.22 (16 constraints: 8a2682e7)
com.fasterxml.jackson.core:jackson-core:2.22.2 (16 constraints: 4645aec9)
com.fasterxml.jackson.core:jackson-databind:2.22.2 (36 constraints: 6e8334d7)
com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.22.2 (2
constraints: 611c95f1)
@@ -174,7 +174,7 @@ junit:junit:4.13.2 (7 constraints: 736dcb65)
net.arnx:jsonic:1.2.7 (1 constraints: d00b47eb)
net.sf.jopt-simple:jopt-simple:5.0.4 (3 constraints: 132590bf)
net.sourceforge.argparse4j:argparse4j:0.7.0 (1 constraints: fe0a06e8)
-net.thisptr:jackson-jq:0.0.13 (1 constraints: 3605223b)
+net.thisptr:jackson-jq:1.6.2 (1 constraints: 0b050436)
org.antlr:antlr4-runtime:4.11.1 (1 constraints: f70fbd96)
org.apache.calcite:calcite-core:1.42.0 (1 constraints: 39053b3b)
org.apache.calcite:calcite-linq4j:1.42.0 (2 constraints: c7128a4b)
diff --git a/versions.props b/versions.props
index 9d003cda966..a8f45a8439a 100644
--- a/versions.props
+++ b/versions.props
@@ -39,7 +39,7 @@ jakarta.ws.rs:jakarta.ws.rs-api=3.1.0
junit:junit=4.13.2
net.bytebuddy:*=1.18.11
net.java.dev.jna:jna=5.19.1
-net.thisptr:jackson-jq=0.0.13
+net.thisptr:jackson-jq=1.6.2
no.nav.security:mock-oauth2-server=0.5.10
org.apache.calcite.avatica:avatica-core=1.28.0
org.apache.calcite:*=1.42.0