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

mlbiscoc pushed a commit to branch feature/SOLR-17458-rebased
in repository https://gitbox.apache.org/repos/asf/solr.git

commit e9ee7836d52f4c8bd7f400471d41ac239f872576
Author: bjacobowitz <[email protected]>
AuthorDate: Tue Sep 16 15:11:13 2025 -0400

    SOLR-17806 Migrate AuthenticationPlugin metrics to OTEL (#3636)
    
    * SOLR-17806 Migrate AuthenticationPlugin metrics to OTEL
    
    * SOLR-17806 Code review comments
---
 .../otel/instruments/AttributedLongTimer.java      |  16 +-
 .../apache/solr/security/AuthenticationPlugin.java |  83 +++++---
 .../org/apache/solr/security/BasicAuthPlugin.java  |   4 +-
 .../org/apache/solr/security/MultiAuthPlugin.java  |   1 +
 .../solr/security/PKIAuthenticationPlugin.java     |   2 +-
 .../core/src/java/org/apache/solr/util/RTimer.java |  15 +-
 .../solr/security/BasicAuthIntegrationTest.java    |  29 +--
 .../apache/solr/security/CertAuthPluginTest.java   |  53 ++++-
 .../solr/security/TestPKIAuthenticationPlugin.java |  17 ++
 solr/modules/jwt-auth/build.gradle                 |   2 +
 solr/modules/jwt-auth/gradle.lockfile              |   6 +-
 .../apache/solr/security/jwt/JWTAuthPlugin.java    |   6 +-
 .../security/jwt/JWTAuthPluginIntegrationTest.java |  18 ++
 .../apache/solr/cloud/SolrCloudAuthTestCase.java   | 218 ++++++++++++++++-----
 .../org/apache/solr/util/SolrMetricTestUtils.java  |  20 +-
 15 files changed, 393 insertions(+), 97 deletions(-)

diff --git 
a/solr/core/src/java/org/apache/solr/metrics/otel/instruments/AttributedLongTimer.java
 
b/solr/core/src/java/org/apache/solr/metrics/otel/instruments/AttributedLongTimer.java
index 588a81507db..cda4daa4ba5 100644
--- 
a/solr/core/src/java/org/apache/solr/metrics/otel/instruments/AttributedLongTimer.java
+++ 
b/solr/core/src/java/org/apache/solr/metrics/otel/instruments/AttributedLongTimer.java
@@ -18,6 +18,7 @@ package org.apache.solr.metrics.otel.instruments;
 
 import io.opentelemetry.api.common.Attributes;
 import io.opentelemetry.api.metrics.LongHistogram;
+import java.util.concurrent.TimeUnit;
 import org.apache.solr.util.RTimer;
 
 /**
@@ -32,16 +33,29 @@ public class AttributedLongTimer extends 
AttributedLongHistogram {
 
   /**
    * Return a {@link MetricTimer} and starts the timer. When the timer calls 
{@link
-   * MetricTimer#stop()}, the elapsed time is recorded
+   * MetricTimer#stop()}, the elapsed time is recorded, in milliseconds
    */
   public MetricTimer start() {
     return new MetricTimer(this);
   }
 
+  /**
+   * Return a {@link MetricTimer} and starts the timer. When the timer calls 
{@link
+   * MetricTimer#stop()}, the elapsed time is recorded, in the specified 
TimeUnit
+   */
+  public MetricTimer start(TimeUnit unit) {
+    return new MetricTimer(this, unit);
+  }
+
   public static class MetricTimer extends RTimer {
     private final AttributedLongTimer bound;
 
     private MetricTimer(AttributedLongTimer bound) {
+      this(bound, TimeUnit.MILLISECONDS);
+    }
+
+    private MetricTimer(AttributedLongTimer bound, TimeUnit unit) {
+      super(unit);
       this.bound = bound;
     }
 
diff --git 
a/solr/core/src/java/org/apache/solr/security/AuthenticationPlugin.java 
b/solr/core/src/java/org/apache/solr/security/AuthenticationPlugin.java
index 8d588aada46..ed7646ad7b4 100644
--- a/solr/core/src/java/org/apache/solr/security/AuthenticationPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/AuthenticationPlugin.java
@@ -16,10 +16,8 @@
  */
 package org.apache.solr.security;
 
-import com.codahale.metrics.Counter;
-import com.codahale.metrics.Meter;
-import com.codahale.metrics.Timer;
 import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.LongCounter;
 import jakarta.servlet.FilterChain;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletRequestWrapper;
@@ -28,8 +26,12 @@ import java.security.Principal;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
 import org.apache.solr.core.SolrInfoBean;
 import org.apache.solr.metrics.SolrMetricsContext;
+import org.apache.solr.metrics.otel.OtelUnit;
+import org.apache.solr.metrics.otel.instruments.AttributedLongCounter;
+import org.apache.solr.metrics.otel.instruments.AttributedLongTimer;
 import org.eclipse.jetty.client.Request;
 
 /**
@@ -44,14 +46,13 @@ public abstract class AuthenticationPlugin implements 
SolrInfoBean {
   private Set<String> metricNames = ConcurrentHashMap.newKeySet();
   protected SolrMetricsContext solrMetricsContext;
 
-  protected Meter numErrors = new Meter();
-  protected Counter requests = new Counter();
-  protected Timer requestTimes = new Timer();
-  protected Counter totalTime = new Counter();
-  protected Counter numAuthenticated = new Counter();
-  protected Counter numPassThrough = new Counter();
-  protected Counter numWrongCredentials = new Counter();
-  protected Counter numMissingCredentials = new Counter();
+  protected AttributedLongCounter numErrors;
+  protected AttributedLongCounter requests;
+  protected AttributedLongTimer requestTimes;
+  protected AttributedLongCounter numAuthenticated;
+  protected AttributedLongCounter numPassThrough;
+  protected AttributedLongCounter numWrongCredentials;
+  protected AttributedLongCounter numMissingCredentials;
 
   /**
    * This is called upon loading up of a plugin, used for setting it up.
@@ -84,16 +85,16 @@ public abstract class AuthenticationPlugin implements 
SolrInfoBean {
   public final boolean authenticate(
       HttpServletRequest request, HttpServletResponse response, FilterChain 
filterChain)
       throws Exception {
-    Timer.Context timer = requestTimes.time();
+    AttributedLongTimer.MetricTimer timer = 
requestTimes.start(TimeUnit.NANOSECONDS);
     requests.inc();
     try {
       return doAuthenticate(request, response, filterChain);
     } catch (Exception e) {
-      numErrors.mark();
+      numErrors.inc();
       throw e;
     } finally {
-      long elapsed = timer.stop();
-      totalTime.inc(elapsed);
+      // Record the timing metric
+      timer.stop();
     }
   }
 
@@ -142,24 +143,58 @@ public abstract class AuthenticationPlugin implements 
SolrInfoBean {
     return solrMetricsContext;
   }
 
-  // TODO SOLR-17458: Migrate to Otel
   @Override
   public void initializeMetrics(
       SolrMetricsContext parentContext, Attributes attributes, String scope) {
     this.solrMetricsContext = parentContext.getChildContext(this);
+    Attributes attrsWithCategory =
+        Attributes.builder()
+            .putAll(attributes)
+            .put("category", getCategory().toString())
+            .put("plugin_name", this.getClass().getSimpleName())
+            .build();
     // Metrics
-    numErrors = this.solrMetricsContext.meter("errors", 
getCategory().toString(), scope);
-    requests = this.solrMetricsContext.counter("requests", 
getCategory().toString(), scope);
+    numErrors =
+        new AttributedLongCounter(
+            this.solrMetricsContext.longCounter(
+                "solr_authentication_errors", "Count of errors during 
authentication"),
+            attrsWithCategory);
+    requests =
+        new AttributedLongCounter(
+            this.solrMetricsContext.longCounter(
+                "solr_authentication_requests", "Count of requests for 
authentication"),
+            attrsWithCategory);
     numAuthenticated =
-        this.solrMetricsContext.counter("authenticated", 
getCategory().toString(), scope);
+        new AttributedLongCounter(
+            this.solrMetricsContext.longCounter(
+                "solr_authentication_num_authenticated",
+                "Count of successful requests for authentication"),
+            attrsWithCategory);
     numPassThrough =
-        this.solrMetricsContext.counter("passThrough", 
getCategory().toString(), scope);
+        new AttributedLongCounter(
+            this.solrMetricsContext.longCounter(
+                "solr_authentication_num_pass_through",
+                "Count of requests allowed to pass through without 
authentication credentials (as enabled with configuration \"blockUnknown\": 
false)"),
+            attrsWithCategory);
+    LongCounter solrAuthenticationPluginFail =
+        this.solrMetricsContext.longCounter(
+            "solr_authentication_failures",
+            "Count of authentication failures (unsuccessful, but processed 
correctly)");
     numWrongCredentials =
-        this.solrMetricsContext.counter("failWrongCredentials", 
getCategory().toString(), scope);
+        new AttributedLongCounter(
+            solrAuthenticationPluginFail,
+            attrsWithCategory.toBuilder().put(TYPE_ATTR, 
"wrong_credentials").build());
     numMissingCredentials =
-        this.solrMetricsContext.counter("failMissingCredentials", 
getCategory().toString(), scope);
-    requestTimes = this.solrMetricsContext.timer("requestTimes", 
getCategory().toString(), scope);
-    totalTime = this.solrMetricsContext.counter("totalTime", 
getCategory().toString(), scope);
+        new AttributedLongCounter(
+            solrAuthenticationPluginFail,
+            attrsWithCategory.toBuilder().put(TYPE_ATTR, 
"missing_credentials").build());
+    requestTimes =
+        new AttributedLongTimer(
+            this.solrMetricsContext.longHistogram(
+                "solr_authentication_request_times",
+                "Distribution of authentication request durations",
+                OtelUnit.NANOSECONDS),
+            attrsWithCategory);
   }
 
   @Override
diff --git a/solr/core/src/java/org/apache/solr/security/BasicAuthPlugin.java 
b/solr/core/src/java/org/apache/solr/security/BasicAuthPlugin.java
index 753a19ad0d1..be9b4e8deba 100644
--- a/solr/core/src/java/org/apache/solr/security/BasicAuthPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/BasicAuthPlugin.java
@@ -148,7 +148,7 @@ public class BasicAuthPlugin extends AuthenticationPlugin
                   return true;
                 }
               } else {
-                numErrors.mark();
+                numErrors.inc();
                 authenticationFailure(response, isAjaxRequest, "Invalid 
authentication token");
                 return false;
               }
@@ -156,7 +156,7 @@ public class BasicAuthPlugin extends AuthenticationPlugin
               throw new Error("Couldn't retrieve authentication", e);
             }
           } else {
-            numErrors.mark();
+            numErrors.inc();
             authenticationFailure(response, isAjaxRequest, "Malformed Basic 
Auth header");
             return false;
           }
diff --git a/solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java 
b/solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java
index 903218eed56..7c7e5a4b5df 100644
--- a/solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java
@@ -202,6 +202,7 @@ public class MultiAuthPlugin extends AuthenticationPlugin
       // TODO SOLR-17458: Add Otel
       plugin.initializeMetrics(parentContext, Attributes.empty(), scope);
     }
+    super.initializeMetrics(parentContext, attributes, scope);
   }
 
   private String getSchemeFromAuthHeader(final String authHeader) {
diff --git 
a/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java 
b/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java
index 3a7009682fd..44251bb324d 100644
--- a/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java
@@ -224,7 +224,7 @@ public class PKIAuthenticationPlugin extends 
AuthenticationPlugin
    */
   private boolean sendError(HttpServletResponse response, boolean v2, String 
message)
       throws IOException {
-    numErrors.mark();
+    numErrors.inc();
     log.error(message);
     response.setHeader(HttpHeader.WWW_AUTHENTICATE.asString(), v2 ? HEADER_V2 
: HEADER);
     response.sendError(HttpServletResponse.SC_UNAUTHORIZED, message);
diff --git a/solr/core/src/java/org/apache/solr/util/RTimer.java 
b/solr/core/src/java/org/apache/solr/util/RTimer.java
index e4a46bffb71..40934cb2868 100644
--- a/solr/core/src/java/org/apache/solr/util/RTimer.java
+++ b/solr/core/src/java/org/apache/solr/util/RTimer.java
@@ -35,6 +35,7 @@ public class RTimer {
   private TimerImpl timerImpl;
   private double time;
   private double culmTime;
+  private TimeUnit timeUnit;
 
   protected interface TimerImpl {
     void start();
@@ -43,8 +44,13 @@ public class RTimer {
   }
 
   private static class NanoTimeTimerImpl implements TimerImpl {
+    private final TimeUnit timeUnit;
     private long start;
 
+    public NanoTimeTimerImpl(TimeUnit timeUnit) {
+      this.timeUnit = timeUnit;
+    }
+
     @Override
     public void start() {
       start = System.nanoTime();
@@ -52,15 +58,20 @@ public class RTimer {
 
     @Override
     public double elapsed() {
-      return TimeUnit.MILLISECONDS.convert(System.nanoTime() - start, 
TimeUnit.NANOSECONDS);
+      return timeUnit.convert(System.nanoTime() - start, TimeUnit.NANOSECONDS);
     }
   }
 
   protected TimerImpl newTimerImpl() {
-    return new NanoTimeTimerImpl();
+    return new NanoTimeTimerImpl(timeUnit);
   }
 
   public RTimer() {
+    this(TimeUnit.MILLISECONDS);
+  }
+
+  public RTimer(TimeUnit timeUnit) {
+    this.timeUnit = timeUnit;
     time = 0;
     culmTime = 0;
     timerImpl = newTimerImpl();
diff --git 
a/solr/core/src/test/org/apache/solr/security/BasicAuthIntegrationTest.java 
b/solr/core/src/test/org/apache/solr/security/BasicAuthIntegrationTest.java
index 4ac0ca904a7..c4035ea0380 100644
--- a/solr/core/src/test/org/apache/solr/security/BasicAuthIntegrationTest.java
+++ b/solr/core/src/test/org/apache/solr/security/BasicAuthIntegrationTest.java
@@ -19,7 +19,6 @@ package org.apache.solr.security;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Collections.singletonMap;
 
-import com.codahale.metrics.MetricRegistry;
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.nio.charset.StandardCharsets;
@@ -128,7 +127,7 @@ public class BasicAuthIntegrationTest extends 
SolrCloudAuthTestCase {
       baseUrl = randomJetty.getBaseUrl().toString();
       verifySecurityStatus(
           cl, baseUrl + authcPrefix, "authentication/class", 
"solr.BasicAuthPlugin", 20);
-      assertNumberOfMetrics(16); // Basic auth metrics available
+
       assertAuthMetricsMinimums(1, 0, 1, 0, 0, 0);
       assertPkiAuthMetricsMinimums(0, 0, 0, 0, 0, 0);
 
@@ -399,16 +398,22 @@ public class BasicAuthIntegrationTest extends 
SolrCloudAuthTestCase {
     }
   }
 
-  private void assertNumberOfMetrics(int num) {
-    MetricRegistry registry0 =
-        
cluster.getJettySolrRunner(0).getCoreContainer().getMetricManager().registry("solr.node");
-    assertNotNull(registry0);
-
-    assertEquals(
-        num,
-        registry0.getMetrics().entrySet().stream()
-            .filter(e -> e.getKey().startsWith("SECURITY"))
-            .count());
+  private void assertAuthMetricsMinimums(
+      int requests,
+      int authenticated,
+      int passThrough,
+      int failWrongCredentials,
+      int failMissingCredentials,
+      int errors)
+      throws InterruptedException {
+    super.assertAuthMetricsMinimums(
+        BasicAuthPlugin.class,
+        requests,
+        authenticated,
+        passThrough,
+        failWrongCredentials,
+        failMissingCredentials,
+        errors);
   }
 
   private QueryResponse executeQuery(ModifiableSolrParams params, String user, 
String pass)
diff --git 
a/solr/core/src/test/org/apache/solr/security/CertAuthPluginTest.java 
b/solr/core/src/test/org/apache/solr/security/CertAuthPluginTest.java
index e724ad7ab6e..db37c071ec5 100644
--- a/solr/core/src/test/org/apache/solr/security/CertAuthPluginTest.java
+++ b/solr/core/src/test/org/apache/solr/security/CertAuthPluginTest.java
@@ -23,6 +23,9 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import io.opentelemetry.api.common.Attributes;
+import io.prometheus.metrics.model.snapshots.CounterSnapshot;
+import io.prometheus.metrics.model.snapshots.Labels;
 import jakarta.servlet.FilterChain;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
@@ -30,6 +33,11 @@ import java.security.cert.X509Certificate;
 import java.util.Collections;
 import javax.security.auth.x500.X500Principal;
 import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.metrics.SolrMetricsContext;
+import org.apache.solr.util.SolrMetricTestUtils;
+import org.junit.After;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -46,10 +54,23 @@ public class CertAuthPluginTest extends SolrTestCaseJ4 {
   @Before
   public void setUp() throws Exception {
     super.setUp();
+    CoreContainer coreContainer =
+        SolrTestCaseJ4.createCoreContainer(
+            "collection1", "data", "solrconfig-basic.xml", "schema.xml");
+    String registryName = "solr.core.collection1";
     plugin = new CertAuthPlugin();
+    SolrMetricsContext solrMetricsContext =
+        new SolrMetricsContext(coreContainer.getMetricManager(), registryName, 
"foo");
+    plugin.initializeMetrics(solrMetricsContext, Attributes.empty(), null);
     plugin.init(Collections.emptyMap());
   }
 
+  @After
+  public void tearDown() throws Exception {
+    deleteCore();
+    super.tearDown();
+  }
+
   @Test
   public void testAuthenticateOk() throws Exception {
     X500Principal principal = new X500Principal("CN=NAME");
@@ -63,7 +84,17 @@ public class CertAuthPluginTest extends SolrTestCaseJ4 {
         (req, rsp) -> assertEquals(principal, ((HttpServletRequest) 
req).getUserPrincipal());
     assertTrue(plugin.doAuthenticate(request, null, chain));
 
-    assertEquals(1, plugin.numAuthenticated.getCount());
+    Labels labels =
+        Labels.of(
+            "otel_scope_name",
+            "org.apache.solr",
+            "category",
+            "SECURITY",
+            "plugin_name",
+            "CertAuthPlugin");
+    long missingCredentialsCount =
+        getLongMetricValue("solr_authentication_num_authenticated", labels);
+    assertEquals(1L, missingCredentialsCount);
   }
 
   @Test
@@ -76,6 +107,24 @@ public class CertAuthPluginTest extends SolrTestCaseJ4 {
     assertFalse(plugin.doAuthenticate(request, response, null));
     verify(response).sendError(eq(401), anyString());
 
-    assertEquals(1, plugin.numMissingCredentials.getCount());
+    Labels labels =
+        Labels.of(
+            "otel_scope_name",
+            "org.apache.solr",
+            "category",
+            "SECURITY",
+            "type",
+            "missing_credentials",
+            "plugin_name",
+            "CertAuthPlugin");
+    long missingCredentialsCount = 
getLongMetricValue("solr_authentication_failures", labels);
+    assertEquals(1L, missingCredentialsCount);
+  }
+
+  private long getLongMetricValue(String metricName, Labels labels) {
+    SolrCore core = h.getCore();
+    CounterSnapshot.CounterDataPointSnapshot metric =
+        SolrMetricTestUtils.getCounterDatapoint(core, metricName, labels);
+    return (metric != null) ? (long) metric.getValue() : 0L;
   }
 }
diff --git 
a/solr/core/src/test/org/apache/solr/security/TestPKIAuthenticationPlugin.java 
b/solr/core/src/test/org/apache/solr/security/TestPKIAuthenticationPlugin.java
index 699f6249dc3..42e1800274f 100644
--- 
a/solr/core/src/test/org/apache/solr/security/TestPKIAuthenticationPlugin.java
+++ 
b/solr/core/src/test/org/apache/solr/security/TestPKIAuthenticationPlugin.java
@@ -23,6 +23,9 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.LongCounter;
+import io.opentelemetry.api.metrics.LongHistogram;
 import jakarta.servlet.FilterChain;
 import jakarta.servlet.ServletRequest;
 import jakarta.servlet.http.HttpServletRequest;
@@ -39,6 +42,7 @@ import org.apache.http.auth.BasicUserPrincipal;
 import org.apache.http.message.BasicHttpRequest;
 import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.metrics.SolrMetricsContext;
 import org.apache.solr.request.LocalSolrQueryRequest;
 import org.apache.solr.request.SolrRequestInfo;
 import org.apache.solr.response.SolrQueryResponse;
@@ -118,9 +122,20 @@ public class TestPKIAuthenticationPlugin extends 
SolrTestCaseJ4 {
 
     mockReq = createMockRequest(header);
     mock = new MockPKIAuthenticationPlugin(nodeName);
+    mockMetrics(mock);
     request = new BasicHttpRequest("GET", "http://localhost:56565";);
   }
 
+  private static void mockMetrics(MockPKIAuthenticationPlugin mock) {
+    SolrMetricsContext smcMock = mock(SolrMetricsContext.class);
+    when(smcMock.getChildContext(any())).thenReturn(smcMock);
+    LongCounter longCounterMock = mock(LongCounter.class);
+    LongHistogram longHistogramMock = mock(LongHistogram.class);
+    when(smcMock.longCounter(any(), any())).thenReturn(longCounterMock);
+    when(smcMock.longHistogram(any(), any(), 
any())).thenReturn(longHistogramMock);
+    mock.initializeMetrics(smcMock, Attributes.empty(), "");
+  }
+
   @Override
   public void tearDown() throws Exception {
     if (mock != null) {
@@ -165,6 +180,7 @@ public class TestPKIAuthenticationPlugin extends 
SolrTestCaseJ4 {
             }
           }
         };
+    mockMetrics(mock1);
 
     // Setup regular superuser request
     mock.solrRequestInfo = null;
@@ -191,6 +207,7 @@ public class TestPKIAuthenticationPlugin extends 
SolrTestCaseJ4 {
     System.setProperty(PKIAuthenticationPlugin.SEND_VERSION, "v1");
     System.setProperty(PKIAuthenticationPlugin.ACCEPT_VERSIONS, "v2");
     mock = new MockPKIAuthenticationPlugin(nodeName);
+    mockMetrics(mock);
 
     principal.set(new BasicUserPrincipal("solr"));
     mock.solrRequestInfo = new SolrRequestInfo(localSolrQueryRequest, new 
SolrQueryResponse());
diff --git a/solr/modules/jwt-auth/build.gradle 
b/solr/modules/jwt-auth/build.gradle
index 9f4078f0478..be4ae8f38bc 100644
--- a/solr/modules/jwt-auth/build.gradle
+++ b/solr/modules/jwt-auth/build.gradle
@@ -46,6 +46,8 @@ dependencies {
   testImplementation project(':solr:test-framework')
   testImplementation libs.apache.lucene.testframework
   testImplementation libs.junit.junit
+  testImplementation libs.prometheus.metrics.model
+  testImplementation libs.opentelemetry.exporter.prometheus
 
   testImplementation(libs.mockito.core, {
     exclude group: "net.bytebuddy", module: "byte-buddy-agent"
diff --git a/solr/modules/jwt-auth/gradle.lockfile 
b/solr/modules/jwt-auth/gradle.lockfile
index 88772eaf17e..bf36af6c0b0 100644
--- a/solr/modules/jwt-auth/gradle.lockfile
+++ b/solr/modules/jwt-auth/gradle.lockfile
@@ -77,10 +77,12 @@ 
io.opentelemetry:opentelemetry-api:1.53.0=compileClasspath,jarValidation,runtime
 
io.opentelemetry:opentelemetry-common:1.53.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath
 
io.opentelemetry:opentelemetry-context:1.53.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath
 
io.opentelemetry:opentelemetry-exporter-common:1.50.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath
-io.opentelemetry:opentelemetry-exporter-prometheus:1.50.0-alpha=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath
+io.opentelemetry:opentelemetry-exporter-prometheus:1.50.0-alpha=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath
+io.opentelemetry:opentelemetry-sdk-common:1.50.0=testCompileClasspath
 
io.opentelemetry:opentelemetry-sdk-common:1.53.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath
 
io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:1.50.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath
 
io.opentelemetry:opentelemetry-sdk-logs:1.53.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath
+io.opentelemetry:opentelemetry-sdk-metrics:1.50.0=testCompileClasspath
 
io.opentelemetry:opentelemetry-sdk-metrics:1.53.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath
 
io.opentelemetry:opentelemetry-sdk-trace:1.53.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath
 
io.opentelemetry:opentelemetry-sdk:1.53.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath
@@ -90,7 +92,7 @@ 
io.prometheus:prometheus-metrics-exporter-httpserver:1.3.6=jarValidation,testRun
 
io.prometheus:prometheus-metrics-exposition-formats:1.1.0=runtimeClasspath,runtimeLibs,solrPlatformLibs
 
io.prometheus:prometheus-metrics-exposition-formats:1.3.6=jarValidation,testRuntimeClasspath
 
io.prometheus:prometheus-metrics-exposition-textformats:1.3.6=jarValidation,testRuntimeClasspath
-io.prometheus:prometheus-metrics-model:1.1.0=runtimeClasspath,runtimeLibs,solrPlatformLibs
+io.prometheus:prometheus-metrics-model:1.1.0=runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath
 io.prometheus:prometheus-metrics-model:1.3.6=jarValidation,testRuntimeClasspath
 
io.sgr:s2-geometry-library-java:1.0.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath
 
io.swagger.core.v3:swagger-annotations-jakarta:2.2.22=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath
diff --git 
a/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java
 
b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java
index bf7068588db..323e4a6646f 100644
--- 
a/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java
+++ 
b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java
@@ -443,7 +443,7 @@ public class JWTAuthPlugin extends AuthenticationPlugin
       }
       if (jwtConsumer == null) {
         log.warn("JWTAuth not configured");
-        numErrors.mark();
+        numErrors.inc();
         throw new SolrException(
             SolrException.ErrorCode.SERVER_ERROR, "JWTAuth plugin not 
correctly configured");
       }
@@ -484,7 +484,7 @@ public class JWTAuthPlugin extends AuthenticationPlugin
         final Principal principal = authResponse.getPrincipal();
         request = wrapWithPrincipal(request, principal);
         if (!(principal instanceof JWTPrincipal)) {
-          numErrors.mark();
+          numErrors.inc();
           throw new SolrException(
               SolrException.ErrorCode.SERVER_ERROR,
               "JWTAuth plugin says AUTHENTICATED but no token extracted");
@@ -510,7 +510,7 @@ public class JWTAuthPlugin extends AuthenticationPlugin
             "Authentication failed. {}, {}",
             authResponse.getAuthCode(),
             authResponse.getAuthCode().getMsg());
-        numErrors.mark();
+        numErrors.inc();
         authenticationFailure(
             response,
             authResponse.getAuthCode().getMsg(),
diff --git 
a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java
 
b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java
index 45e35d753fa..f0ca70fe55c 100644
--- 
a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java
+++ 
b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java
@@ -342,6 +342,24 @@ public class JWTAuthPluginIntegrationTest extends 
SolrCloudAuthTestCase {
     return "Bearer " + jws.getCompactSerialization();
   }
 
+  private void assertAuthMetricsMinimums(
+      int requests,
+      int authenticated,
+      int passThrough,
+      int failWrongCredentials,
+      int failMissingCredentials,
+      int errors)
+      throws InterruptedException {
+    super.assertAuthMetricsMinimums(
+        JWTAuthPlugin.class,
+        requests,
+        authenticated,
+        passThrough,
+        failWrongCredentials,
+        failMissingCredentials,
+        errors);
+  }
+
   /**
    * Configure solr cluster with a security.json talking to MockOAuth2 server
    *
diff --git 
a/solr/test-framework/src/java/org/apache/solr/cloud/SolrCloudAuthTestCase.java 
b/solr/test-framework/src/java/org/apache/solr/cloud/SolrCloudAuthTestCase.java
index 99085837700..c9ed378aa27 100644
--- 
a/solr/test-framework/src/java/org/apache/solr/cloud/SolrCloudAuthTestCase.java
+++ 
b/solr/test-framework/src/java/org/apache/solr/cloud/SolrCloudAuthTestCase.java
@@ -24,6 +24,11 @@ import com.codahale.metrics.Meter;
 import com.codahale.metrics.Metric;
 import com.codahale.metrics.MetricRegistry;
 import com.codahale.metrics.Timer;
+import io.opentelemetry.exporter.prometheus.PrometheusMetricReader;
+import io.prometheus.metrics.model.snapshots.CounterSnapshot;
+import io.prometheus.metrics.model.snapshots.DataPointSnapshot;
+import io.prometheus.metrics.model.snapshots.HistogramSnapshot;
+import io.prometheus.metrics.model.snapshots.Labels;
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.util.ArrayList;
@@ -44,7 +49,10 @@ import org.apache.http.util.EntityUtils;
 import org.apache.solr.client.solrj.impl.HttpClientUtil;
 import org.apache.solr.common.util.StrUtils;
 import org.apache.solr.common.util.Utils;
+import org.apache.solr.core.CoreContainer;
 import org.apache.solr.embedded.JettySolrRunner;
+import org.apache.solr.security.AuthenticationPlugin;
+import org.apache.solr.util.SolrMetricTestUtils;
 import org.apache.solr.util.TimeOut;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
@@ -60,32 +68,22 @@ public class SolrCloudAuthTestCase extends 
SolrCloudTestCase {
   private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
   private static final List<String> AUTH_METRICS_KEYS =
       Arrays.asList(
-          "errors",
-          "requests",
-          "authenticated",
-          "passThrough",
-          "failWrongCredentials",
-          "failMissingCredentials",
-          "requestTimes",
-          "totalTime");
-  private static final List<String> AUTH_METRICS_METER_KEYS = 
Arrays.asList("errors", "count");
+          "solr_authentication_errors",
+          "solr_authentication_requests",
+          "solr_authentication_num_authenticated",
+          "solr_authentication_num_pass_through",
+          "solr_authentication_failures/wrong_credentials",
+          "solr_authentication_failures/missing_credentials",
+          "solr_authentication_request_times_nanoseconds");
+  private static final List<String> AUTH_METRICS_METER_KEYS =
+      Arrays.asList("solr_authentication_errors", "count");
   private static final List<String> AUTH_METRICS_TIMER_KEYS =
-      Collections.singletonList("requestTimes");
-  private static final String METRICS_PREFIX_PKI = 
"SECURITY./authentication/pki.";
-  private static final String METRICS_PREFIX = "SECURITY./authentication.";
+      
Collections.singletonList("solr_authentication_request_times_nanoseconds");
 
   @SuppressWarnings({"rawtypes"})
   public static final Predicate NOT_NULL_PREDICATE = o -> o != null;
 
   private static final List<String> AUDIT_METRICS_KEYS = 
Arrays.asList("count");
-  private static final List<String> AUTH_METRICS_TO_COMPARE =
-      Arrays.asList(
-          "requests",
-          "authenticated",
-          "passThrough",
-          "failWrongCredentials",
-          "failMissingCredentials",
-          "errors");
   private static final List<String> AUDIT_METRICS_TO_COMPARE = 
Arrays.asList("count");
 
   @BeforeClass
@@ -107,8 +105,22 @@ public class SolrCloudAuthTestCase extends 
SolrCloudTestCase {
       int failMissingCredentials,
       int errors)
       throws InterruptedException {
-    assertAuthMetricsMinimums(
-        METRICS_PREFIX_PKI,
+    String handler = "/authentication/pki";
+    String registryName = "solr.node";
+    Labels labels =
+        Labels.of(
+            "otel_scope_name",
+            "org.apache.solr",
+            "category",
+            "SECURITY",
+            "handler",
+            handler,
+            "plugin_name",
+            
org.apache.solr.security.PKIAuthenticationPlugin.class.getSimpleName());
+    assertAuthMetricsMinimumsPrometheus(
+        handler,
+        registryName,
+        labels,
         requests,
         authenticated,
         passThrough,
@@ -124,6 +136,7 @@ public class SolrCloudAuthTestCase extends 
SolrCloudTestCase {
    * desired params and timeout
    */
   protected void assertAuthMetricsMinimums(
+      Class<? extends AuthenticationPlugin> authPluginClass,
       int requests,
       int authenticated,
       int passThrough,
@@ -131,8 +144,22 @@ public class SolrCloudAuthTestCase extends 
SolrCloudTestCase {
       int failMissingCredentials,
       int errors)
       throws InterruptedException {
-    assertAuthMetricsMinimums(
-        METRICS_PREFIX,
+    String handler = "/authentication";
+    String registryName = "solr.node";
+    Labels labels =
+        Labels.of(
+            "otel_scope_name",
+            "org.apache.solr",
+            "category",
+            "SECURITY",
+            "handler",
+            handler,
+            "plugin_name",
+            authPluginClass.getSimpleName());
+    assertAuthMetricsMinimumsPrometheus(
+        handler,
+        registryName,
+        labels,
         requests,
         authenticated,
         passThrough,
@@ -170,35 +197,35 @@ public class SolrCloudAuthTestCase extends 
SolrCloudTestCase {
     return counts;
   }
 
-  /**
-   * Common test method to be able to check auth metrics from any 
authentication plugin
-   *
-   * @param prefix the metrics key prefix, currently 
"SECURITY./authentication." for basic auth and
-   *     "SECURITY./authentication/pki." for PKI
-   */
-  private void assertAuthMetricsMinimums(
-      String prefix,
+  /** Common test method to be able to check auth metrics from any 
authentication plugin */
+  void assertAuthMetricsMinimumsPrometheus(
+      String handler,
+      String registryName,
+      Labels labels,
       int requests,
       int authenticated,
       int passThrough,
       int failWrongCredentials,
       int failMissingCredentials,
-      int errors)
-      throws InterruptedException {
+      int errors) {
     Map<String, Long> expectedCounts = new HashMap<>();
-    expectedCounts.put("requests", (long) requests);
-    expectedCounts.put("authenticated", (long) authenticated);
-    expectedCounts.put("passThrough", (long) passThrough);
-    expectedCounts.put("failWrongCredentials", (long) failWrongCredentials);
-    expectedCounts.put("failMissingCredentials", (long) 
failMissingCredentials);
-    expectedCounts.put("errors", (long) errors);
+    expectedCounts.put("solr_authentication_requests", (long) requests);
+    expectedCounts.put("solr_authentication_num_authenticated", (long) 
authenticated);
+    expectedCounts.put("solr_authentication_num_pass_through", (long) 
passThrough);
+    expectedCounts.put(
+        "solr_authentication_failures/wrong_credentials", (long) 
failWrongCredentials);
+    expectedCounts.put(
+        "solr_authentication_failures/missing_credentials", (long) 
failMissingCredentials);
+    expectedCounts.put("solr_authentication_errors", (long) errors);
 
-    final Map<String, Long> counts = countSecurityMetrics(cluster, prefix, 
AUTH_METRICS_KEYS);
-    final boolean success = isMetricsEqualOrLarger(AUTH_METRICS_TO_COMPARE, 
expectedCounts, counts);
+    final Map<String, Long> counts =
+        countSecurityMetricsPrometheus(cluster, AUTH_METRICS_KEYS, 
registryName, labels);
+    final boolean success =
+        expectedCounts.keySet().stream().allMatch(k -> counts.get(k) >= 
expectedCounts.get(k));
 
     assertTrue(
-        "Expected metric minimums for prefix "
-            + prefix
+        "Expected metric minimums for handler "
+            + handler
             + ": "
             + expectedCounts
             + ", but got: "
@@ -207,10 +234,107 @@ public class SolrCloudAuthTestCase extends 
SolrCloudTestCase {
             + "security.json; see SOLR-13464 for test work around)",
         success);
 
-    if (counts.get("requests") > 0) {
-      assertTrue("requestTimes count not > 1", counts.get("requestTimes") > 1);
-      assertTrue("totalTime not > 0", counts.get("totalTime") > 0);
+    if (counts.get("solr_authentication_requests") > 0) {
+      assertTrue(
+          "requestTimes count not > 0",
+          counts.get("solr_authentication_request_times_nanoseconds") > 0);
+    }
+  }
+
+  /**
+   * Common test method to sum the prometheus metrics from any authentication 
plugin from all solr
+   * core containers
+   */
+  Map<String, Long> countSecurityMetricsPrometheus(
+      MiniSolrCloudCluster cluster, List<String> keys, String registryName, 
Labels labels) {
+    List<Map<String, DataPointSnapshot>> metrics = new ArrayList<>();
+    cluster
+        .getJettySolrRunners()
+        .forEach(
+            r -> {
+              metrics.add(getMetricValues(r.getCoreContainer(), keys, 
registryName, labels));
+            });
+
+    Map<String, Long> counts = new HashMap<>();
+    keys.forEach(
+        k -> {
+          counts.put(k, sumCountPrometheus(k, metrics));
+        });
+    return counts;
+  }
+
+  private long counterToLong(CounterSnapshot.CounterDataPointSnapshot metric) {
+    if (metric == null) {
+      return 0L;
+    }
+    return (long) metric.getValue();
+  }
+
+  private long 
histogramToLongCount(HistogramSnapshot.HistogramDataPointSnapshot metric) {
+    if (metric == null) {
+      return 0;
+    }
+    return metric.getCount();
+  }
+
+  // Have to sum the metrics from all three shards/nodes
+  private long sumCountPrometheus(String key, List<Map<String, 
DataPointSnapshot>> metricsPerNode) {
+    assertTrue("Metric " + key + " does not exist", 
metricsPerNode.get(0).containsKey(key));
+    if (AUTH_METRICS_METER_KEYS.contains(key)) {
+      return metricsPerNode.stream()
+          .mapToLong(
+              nodeMap -> 
counterToLong((CounterSnapshot.CounterDataPointSnapshot) nodeMap.get(key)))
+          .sum();
+    } else if (AUTH_METRICS_TIMER_KEYS.contains(key)) {
+      // Sum of the count of timer metrics (NOT the sum of their values)
+      return metricsPerNode.stream()
+          .mapToLong(
+              nodeMap ->
+                  histogramToLongCount(
+                      (HistogramSnapshot.HistogramDataPointSnapshot) 
nodeMap.get(key)))
+          .sum();
+    } else {
+      return metricsPerNode.stream()
+          .mapToLong(
+              nodeMap -> 
counterToLong((CounterSnapshot.CounterDataPointSnapshot) nodeMap.get(key)))
+          .sum();
+    }
+  }
+
+  private static Map<String, DataPointSnapshot> getMetricValues(
+      CoreContainer coreContainer, List<String> metricNames, String 
registryName, Labels labels) {
+    Map<String, DataPointSnapshot> metrics = new HashMap<>();
+    PrometheusMetricReader prometheusMetricReader =
+        SolrMetricTestUtils.getPrometheusMetricReader(coreContainer, 
registryName);
+    for (String metricName : metricNames) {
+      if ("solr_authentication_request_times_nanoseconds".equals(metricName)) {
+        HistogramSnapshot.HistogramDataPointSnapshot metric =
+            SolrMetricTestUtils.getHistogramDatapoint(prometheusMetricReader, 
metricName, labels);
+        metrics.put(metricName, metric);
+      } else if 
("solr_authentication_failures/wrong_credentials".equals(metricName)) {
+        // Fake metric name, actual metric will be in 
solr_authentication_failures with label type:
+        // wrong_credentials
+        Labels wrongCredsLabels = Labels.of("type", 
"wrong_credentials").merge(labels);
+        CounterSnapshot.CounterDataPointSnapshot wrongCredsMetric =
+            SolrMetricTestUtils.getCounterDatapoint(
+                prometheusMetricReader, "solr_authentication_failures", 
wrongCredsLabels);
+
+        metrics.put("solr_authentication_failures/wrong_credentials", 
wrongCredsMetric);
+      } else if 
("solr_authentication_failures/missing_credentials".equals(metricName)) {
+        // Fake metric name, actual metric will be in 
solr_authentication_failures with label type:
+        // missing_credentials
+        Labels missingCredsLabels = Labels.of("type", 
"missing_credentials").merge(labels);
+        CounterSnapshot.CounterDataPointSnapshot missingCredsMetric =
+            SolrMetricTestUtils.getCounterDatapoint(
+                prometheusMetricReader, "solr_authentication_failures", 
missingCredsLabels);
+        metrics.put("solr_authentication_failures/missing_credentials", 
missingCredsMetric);
+      } else {
+        CounterSnapshot.CounterDataPointSnapshot metric =
+            SolrMetricTestUtils.getCounterDatapoint(prometheusMetricReader, 
metricName, labels);
+        metrics.put(metricName, metric);
+      }
     }
+    return metrics;
   }
 
   /**
diff --git 
a/solr/test-framework/src/java/org/apache/solr/util/SolrMetricTestUtils.java 
b/solr/test-framework/src/java/org/apache/solr/util/SolrMetricTestUtils.java
index 3de85bf3018..8fcafc125c9 100644
--- a/solr/test-framework/src/java/org/apache/solr/util/SolrMetricTestUtils.java
+++ b/solr/test-framework/src/java/org/apache/solr/util/SolrMetricTestUtils.java
@@ -25,6 +25,7 @@ import 
io.prometheus.metrics.model.snapshots.DataPointSnapshot;
 import io.prometheus.metrics.model.snapshots.GaugeSnapshot;
 import io.prometheus.metrics.model.snapshots.HistogramSnapshot;
 import io.prometheus.metrics.model.snapshots.Labels;
+import io.prometheus.metrics.model.snapshots.MetricSnapshots;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -169,7 +170,8 @@ public final class SolrMetricTestUtils {
 
   public static DataPointSnapshot getDataPointSnapshot(
       PrometheusMetricReader reader, String metricName, Labels labels) {
-    return reader.collect().stream()
+    MetricSnapshots metricSnapshots = reader.collect();
+    return metricSnapshots.stream()
         .filter(ms -> ms.getMetadata().getPrometheusName().equals(metricName))
         .findFirst()
         .flatMap(
@@ -211,6 +213,11 @@ public final class SolrMetricTestUtils {
   private static <T> T getDatapoint(
       SolrCore core, String metricName, Labels labels, Class<T> snapshotType) {
     var reader = getPrometheusMetricReader(core);
+    return getDataPoint(reader, metricName, labels, snapshotType);
+  }
+
+  private static <T> T getDataPoint(
+      PrometheusMetricReader reader, String metricName, Labels labels, 
Class<T> snapshotType) {
     return snapshotType.cast(SolrMetricTestUtils.getDataPointSnapshot(reader, 
metricName, labels));
   }
 
@@ -224,6 +231,17 @@ public final class SolrMetricTestUtils {
     return getDatapoint(core, metricName, labels, 
CounterSnapshot.CounterDataPointSnapshot.class);
   }
 
+  public static CounterSnapshot.CounterDataPointSnapshot getCounterDatapoint(
+      PrometheusMetricReader reader, String metricName, Labels labels) {
+    return getDataPoint(reader, metricName, labels, 
CounterSnapshot.CounterDataPointSnapshot.class);
+  }
+
+  public static HistogramSnapshot.HistogramDataPointSnapshot 
getHistogramDatapoint(
+      PrometheusMetricReader reader, String metricName, Labels labels) {
+    return getDataPoint(
+        reader, metricName, labels, 
HistogramSnapshot.HistogramDataPointSnapshot.class);
+  }
+
   public static HistogramSnapshot.HistogramDataPointSnapshot 
getHistogramDatapoint(
       SolrCore core, String metricName, Labels labels) {
     return getDatapoint(


Reply via email to