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

Reply via email to