You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by st...@apache.org on 2023/09/26 13:13:25 UTC

[solr] branch main updated: SOLR-16980 Connect to SOLR standalone with basic authentication (#1957)

This is an automated email from the ASF dual-hosted git repository.

stillalex pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new 61d41e5734f SOLR-16980 Connect to SOLR standalone with basic authentication (#1957)
61d41e5734f is described below

commit 61d41e5734fd8e965b3d7ba9ef5ed86e01d50325
Author: Alex D <st...@apache.org>
AuthorDate: Tue Sep 26 06:13:19 2023 -0700

    SOLR-16980 Connect to SOLR standalone with basic authentication (#1957)
---
 solr/CHANGES.txt                                   |   2 +
 solr/prometheus-exporter/build.gradle              |   2 +-
 .../prometheus/exporter/SolrClientFactory.java     |  41 ++++---
 .../solr/prometheus/exporter/SolrExporter.java     |  25 +++-
 .../exporter/SolrScrapeConfiguration.java          |  16 +++
 .../prometheus/scraper/SolrCloudScraperTest.java   |  25 ++--
 .../SolrStandaloneScraperBasicAuthTest.java        | 127 +++++++++++++++++++++
 .../scraper/SolrStandaloneScraperTest.java         |  69 ++++++-----
 .../monitoring-with-prometheus-and-grafana.adoc    |  12 +-
 .../org/apache/solr/util/SolrClientTestRule.java   |  20 ++++
 10 files changed, 271 insertions(+), 68 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 5849c78767a..ae656b4ad8f 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -213,6 +213,8 @@ Bug Fixes
 
 * SOLR-16997: OTEL configurator NPE when SOLR_HOST not set (janhoy)
 
+* SOLR-16980: Connect to SOLR standalone with basic authentication (Alex Deparvu)
+
 Dependency Upgrades
 ---------------------
 
diff --git a/solr/prometheus-exporter/build.gradle b/solr/prometheus-exporter/build.gradle
index a64fa08147d..66beb70aed7 100644
--- a/solr/prometheus-exporter/build.gradle
+++ b/solr/prometheus-exporter/build.gradle
@@ -56,8 +56,8 @@ dependencies {
   testImplementation project(':solr:test-framework')
   testImplementation 'com.carrotsearch.randomizedtesting:randomizedtesting-runner'
   testImplementation 'junit:junit'
+  testImplementation 'org.apache.lucene:lucene-test-framework'
 
-  testImplementation 'commons-io:commons-io'
   testImplementation 'org.apache.httpcomponents:httpclient'
   testImplementation 'org.apache.httpcomponents:httpcore'
 }
diff --git a/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/exporter/SolrClientFactory.java b/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/exporter/SolrClientFactory.java
index f34aad926e3..c1c4a891593 100644
--- a/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/exporter/SolrClientFactory.java
+++ b/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/exporter/SolrClientFactory.java
@@ -17,6 +17,7 @@
 
 package org.apache.solr.prometheus.exporter;
 
+import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
@@ -28,37 +29,43 @@ import org.apache.zookeeper.client.ConnectStringParser;
 
 public class SolrClientFactory {
 
-  private PrometheusExporterSettings settings;
+  private final PrometheusExporterSettings settings;
+  private final SolrScrapeConfiguration configuration;
 
-  public SolrClientFactory(PrometheusExporterSettings settings) {
+  public SolrClientFactory(
+      PrometheusExporterSettings settings, SolrScrapeConfiguration configuration) {
     this.settings = settings;
+    this.configuration = configuration;
   }
 
-  public Http2SolrClient createStandaloneSolrClient(String solrHost) {
-    Http2SolrClient http2SolrClient =
+  private static Http2SolrClient.Builder newHttp2SolrClientBuilder(
+      String solrHost, PrometheusExporterSettings settings, SolrScrapeConfiguration configuration) {
+    var builder =
         new Http2SolrClient.Builder(solrHost)
             .withIdleTimeout(settings.getHttpReadTimeout(), TimeUnit.MILLISECONDS)
             .withConnectionTimeout(settings.getHttpConnectionTimeout(), TimeUnit.MILLISECONDS)
-            .withResponseParser(new NoOpResponseParser("json"))
-            .build();
+            .withResponseParser(new NoOpResponseParser("json"));
+    if (configuration.getBasicAuthUser() != null) {
+      builder.withBasicAuthCredentials(
+          configuration.getBasicAuthUser(), configuration.getBasicAuthPwd());
+    }
+    return builder;
+  }
 
-    return http2SolrClient;
+  public Http2SolrClient createStandaloneSolrClient(String solrHost) {
+    return newHttp2SolrClientBuilder(solrHost, settings, configuration).build();
   }
 
   public CloudSolrClient createCloudSolrClient(String zookeeperConnectionString) {
     ConnectStringParser parser = new ConnectStringParser(zookeeperConnectionString);
 
+    List<String> zkHosts =
+        parser.getServerAddresses().stream()
+            .map(address -> address.getHostString() + ":" + address.getPort())
+            .collect(Collectors.toList());
     CloudSolrClient client =
-        new CloudHttp2SolrClient.Builder(
-                parser.getServerAddresses().stream()
-                    .map(address -> address.getHostString() + ":" + address.getPort())
-                    .collect(Collectors.toList()),
-                Optional.ofNullable(parser.getChrootPath()))
-            .withInternalClientBuilder(
-                new Http2SolrClient.Builder()
-                    .withIdleTimeout(settings.getHttpReadTimeout(), TimeUnit.MILLISECONDS)
-                    .withConnectionTimeout(
-                        settings.getHttpConnectionTimeout(), TimeUnit.MILLISECONDS))
+        new CloudHttp2SolrClient.Builder(zkHosts, Optional.ofNullable(parser.getChrootPath()))
+            .withInternalClientBuilder(newHttp2SolrClientBuilder(null, settings, configuration))
             .withResponseParser(new NoOpResponseParser("json"))
             .build();
 
diff --git a/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/exporter/SolrExporter.java b/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/exporter/SolrExporter.java
index 2cad046fa44..946df18cd78 100644
--- a/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/exporter/SolrExporter.java
+++ b/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/exporter/SolrExporter.java
@@ -96,6 +96,13 @@ public class SolrExporter {
           + ARG_NUM_THREADS_DEFAULT
           + ".";
 
+  private static final String[] ARG_CREDENTIALS_FLAGS = {"-u", "--credentials"};
+  private static final String ARG_CREDENTIALS_METAVAR = "CREDENTIALS";
+  private static final String ARG_CREDENTIALS_DEST = "credentials";
+  private static final String ARG_CREDENTIALS_DEFAULT = "";
+  private static final String ARG_CREDENTIALS_HELP =
+      "Specify the credentials in the format username:password. Example: --credentials solr:SolrRocks";
+
   public static final CollectorRegistry defaultRegistry = new CollectorRegistry();
 
   private final int port;
@@ -161,7 +168,7 @@ public class SolrExporter {
       SolrScrapeConfiguration configuration,
       PrometheusExporterSettings settings,
       String clusterId) {
-    SolrClientFactory factory = new SolrClientFactory(settings);
+    SolrClientFactory factory = new SolrClientFactory(settings, configuration);
 
     switch (configuration.getType()) {
       case STANDALONE:
@@ -242,6 +249,14 @@ public class SolrExporter {
         .setDefault(ARG_CLUSTER_ID_DEFAULT)
         .help(ARG_CLUSTER_ID_HELP);
 
+    parser
+        .addArgument(ARG_CREDENTIALS_FLAGS)
+        .metavar(ARG_CREDENTIALS_METAVAR)
+        .dest(ARG_CREDENTIALS_DEST)
+        .type(String.class)
+        .setDefault(ARG_CREDENTIALS_DEFAULT)
+        .help(ARG_CREDENTIALS_HELP);
+
     try {
       Namespace res = parser.parseArgs(args);
 
@@ -266,6 +281,14 @@ public class SolrExporter {
         clusterId = defaultClusterId;
       }
 
+      if (!res.getString(ARG_CREDENTIALS_DEST).isEmpty()) {
+        String credentials = res.getString(ARG_CREDENTIALS_DEST);
+        if (credentials.indexOf(':') > 0) {
+          String[] credentialsArray = credentials.split(":", 2);
+          scrapeConfiguration.withBasicAuthCredentials(credentialsArray[0], credentialsArray[1]);
+        }
+      }
+
       SolrExporter solrExporter =
           new SolrExporter(
               port,
diff --git a/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/exporter/SolrScrapeConfiguration.java b/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/exporter/SolrScrapeConfiguration.java
index a1e1fbdf27b..f61447b810d 100644
--- a/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/exporter/SolrScrapeConfiguration.java
+++ b/solr/prometheus-exporter/src/java/org/apache/solr/prometheus/exporter/SolrScrapeConfiguration.java
@@ -29,6 +29,8 @@ public class SolrScrapeConfiguration {
   private final ConnectionType type;
   private final String zookeeperConnectionString;
   private final String solrHost;
+  private String basicAuthUser;
+  private String basicAuthPwd;
 
   private SolrScrapeConfiguration(
       ConnectionType type, String zookeeperConnectionString, String solrHost) {
@@ -57,6 +59,20 @@ public class SolrScrapeConfiguration {
     return new SolrScrapeConfiguration(ConnectionType.STANDALONE, null, solrHost);
   }
 
+  public SolrScrapeConfiguration withBasicAuthCredentials(String user, String password) {
+    this.basicAuthUser = user;
+    this.basicAuthPwd = password;
+    return this;
+  }
+
+  public String getBasicAuthUser() {
+    return basicAuthUser;
+  }
+
+  public String getBasicAuthPwd() {
+    return basicAuthPwd;
+  }
+
   @Override
   public String toString() {
     if (type == ConnectionType.CLOUD) {
diff --git a/solr/prometheus-exporter/src/test/org/apache/solr/prometheus/scraper/SolrCloudScraperTest.java b/solr/prometheus-exporter/src/test/org/apache/solr/prometheus/scraper/SolrCloudScraperTest.java
index f3839a1d8cd..2ebc3752cae 100644
--- a/solr/prometheus-exporter/src/test/org/apache/solr/prometheus/scraper/SolrCloudScraperTest.java
+++ b/solr/prometheus-exporter/src/test/org/apache/solr/prometheus/scraper/SolrCloudScraperTest.java
@@ -21,15 +21,11 @@ import com.carrotsearch.randomizedtesting.annotations.ThreadLeakLingering;
 import io.prometheus.client.Collector;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.stream.Collectors;
-import org.apache.solr.client.solrj.impl.CloudSolrClient;
-import org.apache.solr.client.solrj.impl.NoOpResponseParser;
 import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.DocCollection;
 import org.apache.solr.common.cloud.Replica;
@@ -42,6 +38,7 @@ import org.apache.solr.prometheus.collector.MetricSamples;
 import org.apache.solr.prometheus.exporter.MetricsConfiguration;
 import org.apache.solr.prometheus.exporter.PrometheusExporterSettings;
 import org.apache.solr.prometheus.exporter.SolrClientFactory;
+import org.apache.solr.prometheus.exporter.SolrScrapeConfiguration;
 import org.apache.solr.prometheus.utils.Helpers;
 import org.junit.After;
 import org.junit.Before;
@@ -55,16 +52,11 @@ public class SolrCloudScraperTest extends PrometheusExporterTestBase {
   private ExecutorService executor;
 
   private SolrCloudScraper createSolrCloudScraper() {
-    var solrClient =
-        new CloudSolrClient.Builder(
-                Collections.singletonList(cluster.getZkServer().getZkAddress()), Optional.empty())
-            .withResponseParser(new NoOpResponseParser("json"))
-            .build();
-
-    solrClient.connect();
-
-    SolrClientFactory factory = new SolrClientFactory(PrometheusExporterSettings.builder().build());
-
+    PrometheusExporterSettings settings = PrometheusExporterSettings.builder().build();
+    SolrScrapeConfiguration scrapeConfiguration =
+        SolrScrapeConfiguration.standalone(cluster.getZkServer().getZkAddress());
+    SolrClientFactory factory = new SolrClientFactory(settings, scrapeConfiguration);
+    var solrClient = factory.createCloudSolrClient(cluster.getZkServer().getZkAddress());
     return new SolrCloudScraper(solrClient, executor, factory, "test");
   }
 
@@ -93,10 +85,7 @@ public class SolrCloudScraperTest extends PrometheusExporterTestBase {
   public void tearDown() throws Exception {
     super.tearDown();
     IOUtils.closeQuietly(solrCloudScraper);
-    if (null != executor) {
-      executor.shutdownNow();
-      executor = null;
-    }
+    ExecutorUtil.shutdownNowAndAwaitTermination(executor);
   }
 
   @Test
diff --git a/solr/prometheus-exporter/src/test/org/apache/solr/prometheus/scraper/SolrStandaloneScraperBasicAuthTest.java b/solr/prometheus-exporter/src/test/org/apache/solr/prometheus/scraper/SolrStandaloneScraperBasicAuthTest.java
new file mode 100644
index 00000000000..b08e771e7d3
--- /dev/null
+++ b/solr/prometheus-exporter/src/test/org/apache/solr/prometheus/scraper/SolrStandaloneScraperBasicAuthTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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 io.prometheus.client.Collector;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import org.apache.lucene.tests.util.LuceneTestCase;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.client.solrj.impl.Http2SolrClient;
+import org.apache.solr.common.util.ExecutorUtil;
+import org.apache.solr.common.util.IOUtils;
+import org.apache.solr.common.util.SolrNamedThreadFactory;
+import org.apache.solr.prometheus.PrometheusExporterTestBase;
+import org.apache.solr.prometheus.exporter.MetricsConfiguration;
+import org.apache.solr.prometheus.exporter.PrometheusExporterSettings;
+import org.apache.solr.prometheus.exporter.SolrClientFactory;
+import org.apache.solr.prometheus.exporter.SolrScrapeConfiguration;
+import org.apache.solr.prometheus.utils.Helpers;
+import org.apache.solr.util.SolrJettyTestRule;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+public class SolrStandaloneScraperBasicAuthTest extends SolrTestCaseJ4 {
+
+  @ClassRule public static final SolrJettyTestRule solrRule = new SolrJettyTestRule();
+
+  private static Http2SolrClient solrClient;
+  private static MetricsConfiguration configuration;
+  private static SolrStandaloneScraper solrScraper;
+  private static ExecutorService executor;
+
+  private static String user = "solr";
+  private static String pass = "SolrRocks";
+  private static String securityJson =
+      "{\n"
+          + "\"authentication\":{ \n"
+          + "   \"blockUnknown\": true, \n"
+          + "   \"class\":\"solr.BasicAuthPlugin\",\n"
+          + "   \"credentials\":{\"solr\":\"IV0EHq1OnNrj6gvRCwvFwTrZ1+z1oBbnQdiVC3otuq0= Ndd7LKvVBAaZIF0QAVi1ekCfAJXr1GGfLtRUXhgrF8c=\"}, \n"
+          + "   \"realm\":\"My Solr users\", \n"
+          + "   \"forwardCredentials\": false \n"
+          + "},\n"
+          + "\"authorization\":{\n"
+          + "   \"class\":\"solr.RuleBasedAuthorizationPlugin\",\n"
+          + "   \"permissions\":[{\"name\":\"security-edit\",\n"
+          + "      \"role\":\"admin\"}],\n"
+          + "   \"user-role\":{\"solr\":\"admin\"}\n"
+          + "}}";
+
+  @BeforeClass
+  public static void setupSolrHome() throws Exception {
+    Path solrHome = LuceneTestCase.createTempDir();
+    Files.write(solrHome.resolve("security.json"), securityJson.getBytes(StandardCharsets.UTF_8));
+    solrRule.startSolr(solrHome);
+
+    Path configSet = LuceneTestCase.createTempDir();
+    SolrStandaloneScraperTest.createConf(configSet);
+    solrRule
+        .newCollection()
+        .withConfigSet(configSet.toString())
+        .withBasicAuthCredentials(user, pass)
+        .create();
+
+    configuration =
+        Helpers.loadConfiguration("conf/prometheus-solr-exporter-scraper-test-config.xml");
+
+    PrometheusExporterSettings settings = PrometheusExporterSettings.builder().build();
+    SolrScrapeConfiguration scrapeConfiguration =
+        SolrScrapeConfiguration.standalone(solrRule.getBaseUrl())
+            .withBasicAuthCredentials(user, pass);
+    solrClient =
+        new SolrClientFactory(settings, scrapeConfiguration)
+            .createStandaloneSolrClient(solrRule.getBaseUrl());
+    executor =
+        ExecutorUtil.newMDCAwareFixedThreadPool(
+            25, new SolrNamedThreadFactory("solr-cloud-scraper-tests"));
+    solrScraper = new SolrStandaloneScraper(solrClient, executor, "test");
+
+    Helpers.indexAllDocs(solrClient);
+  }
+
+  @AfterClass
+  public static void cleanup() throws Exception {
+    // scraper also closes the client
+    IOUtils.closeQuietly(solrScraper);
+    ExecutorUtil.shutdownNowAndAwaitTermination(executor);
+  }
+
+  @Test
+  public void search() throws Exception {
+    List<Collector.MetricFamilySamples> samples =
+        solrScraper.search(configuration.getSearchConfiguration().get(0)).asList();
+
+    assertEquals(1, samples.size());
+
+    Collector.MetricFamilySamples sampleFamily = samples.get(0);
+    assertEquals("solr_facets_category", sampleFamily.name);
+    assertEquals(PrometheusExporterTestBase.FACET_VALUES.size(), sampleFamily.samples.size());
+
+    for (Collector.MetricFamilySamples.Sample sample : sampleFamily.samples) {
+      assertEquals(
+          PrometheusExporterTestBase.FACET_VALUES.get(sample.labelValues.get(0)),
+          sample.value,
+          0.001);
+    }
+  }
+}
diff --git a/solr/prometheus-exporter/src/test/org/apache/solr/prometheus/scraper/SolrStandaloneScraperTest.java b/solr/prometheus-exporter/src/test/org/apache/solr/prometheus/scraper/SolrStandaloneScraperTest.java
index 1db0142c2ea..7f756c0fb3e 100644
--- a/solr/prometheus-exporter/src/test/org/apache/solr/prometheus/scraper/SolrStandaloneScraperTest.java
+++ b/solr/prometheus-exporter/src/test/org/apache/solr/prometheus/scraper/SolrStandaloneScraperTest.java
@@ -18,27 +18,34 @@
 package org.apache.solr.prometheus.scraper;
 
 import io.prometheus.client.Collector;
-import java.io.File;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutorService;
-import org.apache.commons.io.FileUtils;
+import org.apache.lucene.tests.util.LuceneTestCase;
+import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.client.solrj.impl.Http2SolrClient;
-import org.apache.solr.client.solrj.impl.NoOpResponseParser;
 import org.apache.solr.common.util.ExecutorUtil;
 import org.apache.solr.common.util.IOUtils;
 import org.apache.solr.common.util.SolrNamedThreadFactory;
 import org.apache.solr.prometheus.PrometheusExporterTestBase;
 import org.apache.solr.prometheus.collector.MetricSamples;
 import org.apache.solr.prometheus.exporter.MetricsConfiguration;
+import org.apache.solr.prometheus.exporter.PrometheusExporterSettings;
+import org.apache.solr.prometheus.exporter.SolrClientFactory;
+import org.apache.solr.prometheus.exporter.SolrScrapeConfiguration;
 import org.apache.solr.prometheus.utils.Helpers;
-import org.apache.solr.util.RestTestBase;
+import org.apache.solr.util.SolrJettyTestRule;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
+import org.junit.ClassRule;
 import org.junit.Test;
 
-public class SolrStandaloneScraperTest extends RestTestBase {
+public class SolrStandaloneScraperTest extends SolrTestCaseJ4 {
+
+  @ClassRule public static final SolrJettyTestRule solrRule = new SolrJettyTestRule();
 
   private static MetricsConfiguration configuration;
   private static SolrStandaloneScraper solrScraper;
@@ -47,42 +54,45 @@ public class SolrStandaloneScraperTest extends RestTestBase {
 
   @BeforeClass
   public static void setupBeforeClass() throws Exception {
-    File tmpSolrHome = createTempDir().toFile();
-    tmpSolrHome.deleteOnExit();
-
-    FileUtils.copyDirectory(new File(TEST_HOME()), tmpSolrHome.getAbsoluteFile());
-
-    initCore("solrconfig.xml", "managed-schema");
+    solrRule.startSolr(LuceneTestCase.createTempDir());
 
-    createJettyAndHarness(
-        tmpSolrHome.getAbsolutePath(), "solrconfig.xml", "managed-schema", "/solr", true, null);
+    Path configSet = LuceneTestCase.createTempDir();
+    createConf(configSet);
+    solrRule.newCollection().withConfigSet(configSet.toString()).create();
 
+    PrometheusExporterSettings settings = PrometheusExporterSettings.builder().build();
+    SolrScrapeConfiguration scrapeConfiguration =
+        SolrScrapeConfiguration.standalone(solrRule.getBaseUrl());
+    solrClient =
+        new SolrClientFactory(settings, scrapeConfiguration)
+            .createStandaloneSolrClient(solrRule.getBaseUrl());
     executor =
         ExecutorUtil.newMDCAwareFixedThreadPool(
             25, new SolrNamedThreadFactory("solr-cloud-scraper-tests"));
     configuration =
         Helpers.loadConfiguration("conf/prometheus-solr-exporter-scraper-test-config.xml");
-
-    solrClient =
-        new Http2SolrClient.Builder(restTestHarness.getAdminURL())
-            .withResponseParser(new NoOpResponseParser("json"))
-            .build();
     solrScraper = new SolrStandaloneScraper(solrClient, executor, "test");
 
     Helpers.indexAllDocs(solrClient);
   }
 
+  public static void createConf(Path configSet) throws IOException {
+    Path subHome = configSet.resolve("conf");
+    Files.createDirectories(subHome);
+
+    Path top = SolrTestCaseJ4.TEST_PATH().resolve("collection1").resolve("conf");
+    Files.copy(top.resolve("managed-schema.xml"), subHome.resolve("schema.xml"));
+    Files.copy(top.resolve("solrconfig.xml"), subHome.resolve("solrconfig.xml"));
+
+    Files.copy(top.resolve("stopwords.txt"), subHome.resolve("stopwords.txt"));
+    Files.copy(top.resolve("synonyms.txt"), subHome.resolve("synonyms.txt"));
+  }
+
   @AfterClass
-  public static void cleanUp() throws Exception {
+  public static void cleanup() throws Exception {
+    // scraper also closes the client
     IOUtils.closeQuietly(solrScraper);
-    IOUtils.closeQuietly(solrClient);
-    cleanUpHarness();
-    if (null != executor) {
-      executor.shutdownNow();
-      executor = null;
-    }
-    solrScraper = null;
-    solrClient = null;
+    ExecutorUtil.shutdownNowAndAwaitTermination(executor);
   }
 
   @Test
@@ -107,8 +117,7 @@ public class SolrStandaloneScraperTest extends RestTestBase {
     assertEquals(1, samples.samples.size());
     assertEquals(1.0, samples.samples.get(0).value, 0.001);
     assertEquals(List.of("base_url", "cluster_id"), samples.samples.get(0).labelNames);
-    assertEquals(
-        List.of(restTestHarness.getAdminURL(), "test"), samples.samples.get(0).labelValues);
+    assertEquals(List.of(solrRule.getBaseUrl(), "test"), samples.samples.get(0).labelValues);
   }
 
   @Test
@@ -127,7 +136,7 @@ public class SolrStandaloneScraperTest extends RestTestBase {
     assertEquals(1, metricsByHost.size());
 
     List<Collector.MetricFamilySamples> replicaSamples =
-        metricsByHost.get(restTestHarness.getAdminURL()).asList();
+        metricsByHost.get(solrRule.getBaseUrl()).asList();
 
     assertEquals(1, replicaSamples.size());
 
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/monitoring-with-prometheus-and-grafana.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/monitoring-with-prometheus-and-grafana.adoc
index b05631f89a6..3e484647c5d 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/monitoring-with-prometheus-and-grafana.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/monitoring-with-prometheus-and-grafana.adoc
@@ -40,6 +40,8 @@ See the section below <<Prometheus Configuration>>
 == Starting the Exporter
 You can start `solr-exporter` by running `./bin/solr-exporter` (Linux) or `.\bin\solr-exporter.cmd` (Windows) from the `prometheus-exporter/` directory.
 
+The metrics exposed by `solr-exporter` can be seen at the metrics endpoint: `\http://localhost:8983/solr/admin/metrics`.
+
 See the commands below depending on your operating system and Solr operating mode:
 
 [.dynamic-tabs]
@@ -171,7 +173,15 @@ The freshness of the metrics can be improved by reducing the scrape interval but
 +
 A unique ID for the cluster to monitor. This ID will be added to all metrics as a label `cluster_id` and can be used as a filter in the Grafana dashboard if you operate multiple Solr clusters reporting to the same Prometheus instance. If this option is omitted, a hash of the `baseUrl` or `zkHost` will be used as ID by default.
 
-The metrics exposed by `solr-exporter` can be seen at the metrics endpoint: `\http://localhost:8983/solr/admin/metrics`.
+`-u`, `--credentials`, `$CREDENTIALS`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: none
+|===
++
+Specify the credentials in the format `username:password`. Example: `--credentials solr:SolrRocks`.
+
 
 === Environment Variable Options
 
diff --git a/solr/test-framework/src/java/org/apache/solr/util/SolrClientTestRule.java b/solr/test-framework/src/java/org/apache/solr/util/SolrClientTestRule.java
index a0407093bb4..cb2d74ff204 100644
--- a/solr/test-framework/src/java/org/apache/solr/util/SolrClientTestRule.java
+++ b/solr/test-framework/src/java/org/apache/solr/util/SolrClientTestRule.java
@@ -61,6 +61,8 @@ public abstract class SolrClientTestRule extends ExternalResource {
     private String configSet;
     private String configFile;
     private String schemaFile;
+    private String basicAuthUser;
+    private String basicAuthPwd;
 
     public NewCollectionBuilder(String name) {
       this.name = name;
@@ -93,6 +95,12 @@ public abstract class SolrClientTestRule extends ExternalResource {
       return this;
     }
 
+    public NewCollectionBuilder withBasicAuthCredentials(String user, String password) {
+      this.basicAuthUser = user;
+      this.basicAuthPwd = password;
+      return this;
+    }
+
     public String getName() {
       return name;
     }
@@ -112,6 +120,14 @@ public abstract class SolrClientTestRule extends ExternalResource {
     public void create() throws SolrServerException, IOException {
       SolrClientTestRule.this.create(this);
     }
+
+    public String getBasicAuthUser() {
+      return basicAuthUser;
+    }
+
+    public String getBasicAuthPwd() {
+      return basicAuthPwd;
+    }
   }
 
   protected void create(NewCollectionBuilder b) throws SolrServerException, IOException {
@@ -132,6 +148,10 @@ public abstract class SolrClientTestRule extends ExternalResource {
       req.setSchemaName(b.getSchemaFile());
     }
 
+    if (b.getBasicAuthUser() != null) {
+      req.setBasicAuthCredentials(b.getBasicAuthUser(), b.getBasicAuthPwd());
+    }
+
     req.process(getAdminClient());
   }