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

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


The following commit(s) were added to refs/heads/feature/SOLR-17458 by this 
push:
     new 99b547d69af SOLR-17806: Migrate AuditLoggerPlugin to OTEL (#3665)
99b547d69af is described below

commit 99b547d69af5bdf82f133d3d8ff92326fa401ae0
Author: Adam Quigley <[email protected]>
AuthorDate: Thu Sep 25 13:33:21 2025 -0400

    SOLR-17806: Migrate AuditLoggerPlugin to OTEL (#3665)
    
    * SOLR-17806: Migrate AuditLoggerPlugin to OTEL
    
    * Address PR comment on InterruptedException in test
    
    * Fix AuditLoggerPlugin description for solr_auditlogger_request_times 
metric
    
    * Register Nanosecond Histogram view in SolrMetricManager
    
    * Run gradlew tidy
    
    * Refactor plugin_name as a shared metric category attribute
    
    * Address part of PR feedback
    
    * Improve description for solr_auditlogger_lost OTEL metric
    
    * Add TODO note about making histogram buckets configurable
    
    * Use synchronous gauge for solr_auditlogger_async_enabled metric
    
    * Run gradlew tidy
---
 .../org/apache/solr/metrics/SolrMetricManager.java |  35 +++++-
 .../apache/solr/metrics/SolrMetricProducer.java    |   1 +
 .../apache/solr/security/AuditLoggerPlugin.java    | 126 +++++++++++++--------
 .../apache/solr/security/AuthenticationPlugin.java |   4 +-
 .../solr/security/MultiDestinationAuditLogger.java |   3 +-
 .../solr/security/AuditLoggerIntegrationTest.java  | 126 ++++++++++++++++++---
 .../solr/security/AuditLoggerPluginTest.java       |   4 +
 .../security/MockSolrMetricsContextFactory.java    |  49 ++++++++
 .../security/MultiDestinationAuditLoggerTest.java  |   5 +
 .../security/SolrLogAuditLoggerPluginTest.java     |   4 +
 .../deployment-guide/pages/audit-logging.adoc      |  25 ++--
 .../apache/solr/cloud/SolrCloudAuthTestCase.java   |  40 -------
 12 files changed, 299 insertions(+), 123 deletions(-)

diff --git a/solr/core/src/java/org/apache/solr/metrics/SolrMetricManager.java 
b/solr/core/src/java/org/apache/solr/metrics/SolrMetricManager.java
index e179306eaae..98d18cea32f 100644
--- a/solr/core/src/java/org/apache/solr/metrics/SolrMetricManager.java
+++ b/solr/core/src/java/org/apache/solr/metrics/SolrMetricManager.java
@@ -52,7 +52,11 @@ import io.opentelemetry.api.metrics.ObservableLongGauge;
 import io.opentelemetry.api.metrics.ObservableLongMeasurement;
 import io.opentelemetry.api.metrics.ObservableLongUpDownCounter;
 import io.opentelemetry.api.metrics.ObservableMeasurement;
+import io.opentelemetry.sdk.metrics.Aggregation;
+import io.opentelemetry.sdk.metrics.InstrumentSelector;
+import io.opentelemetry.sdk.metrics.InstrumentType;
 import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+import io.opentelemetry.sdk.metrics.View;
 import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil;
 import io.opentelemetry.sdk.metrics.internal.exemplar.ExemplarFilter;
 import java.io.IOException;
@@ -157,6 +161,23 @@ public class SolrMetricManager {
   private final ConcurrentMap<String, MeterProviderAndReaders> 
meterProviderAndReaders =
       new ConcurrentHashMap<>();
 
+  private static final List<Double> SOLR_NANOSECOND_HISTOGRAM_BOUNDARIES =
+      List.of(
+          0.0,
+          5_000.0,
+          10_000.0,
+          25_000.0,
+          50_000.0,
+          100_000.0,
+          250_000.0,
+          500_000.0,
+          1_000_000.0,
+          2_500_000.0,
+          5_000_000.0,
+          25_000_000.0,
+          100_000_000.0,
+          1_000_000_000.0);
+
   public SolrMetricManager() {
     metricsConfig = new MetricsConfig.MetricsConfigBuilder().build();
     counterSupplier = MetricSuppliers.counterSupplier(null, null);
@@ -773,7 +794,19 @@ public class SolrMetricManager {
               var reader = new FilterablePrometheusMetricReader(true, null);
               // NOCOMMIT: We need to add a Periodic Metric Reader here if we 
want to push with OTLP
               // with an exporter
-              var provider = 
SdkMeterProvider.builder().registerMetricReader(reader);
+              var provider =
+                  SdkMeterProvider.builder()
+                      .registerMetricReader(reader)
+                      .registerView(
+                          InstrumentSelector.builder()
+                              .setType(InstrumentType.HISTOGRAM)
+                              .setUnit(OtelUnit.NANOSECONDS.getSymbol())
+                              .build(),
+                          View.builder()
+                              .setAggregation(
+                                  Aggregation.explicitBucketHistogram(
+                                      SOLR_NANOSECOND_HISTOGRAM_BOUNDARIES))
+                              .build()); // TODO: Make histogram bucket 
boundaries configurable
               SdkMeterProviderUtil.setExemplarFilter(provider, 
ExemplarFilter.traceBased());
               return new MeterProviderAndReaders(provider.build(), reader);
             })
diff --git a/solr/core/src/java/org/apache/solr/metrics/SolrMetricProducer.java 
b/solr/core/src/java/org/apache/solr/metrics/SolrMetricProducer.java
index 94b740540af..435b7c0051c 100644
--- a/solr/core/src/java/org/apache/solr/metrics/SolrMetricProducer.java
+++ b/solr/core/src/java/org/apache/solr/metrics/SolrMetricProducer.java
@@ -27,6 +27,7 @@ public interface SolrMetricProducer extends AutoCloseable {
   public static final AttributeKey<String> CATEGORY_ATTR = 
AttributeKey.stringKey("category");
   public static final AttributeKey<String> HANDLER_ATTR = 
AttributeKey.stringKey("handler");
   public static final AttributeKey<String> OPERATION_ATTR = 
AttributeKey.stringKey("ops");
+  public static final AttributeKey<String> PLUGIN_NAME_ATTR = 
AttributeKey.stringKey("plugin_name");
 
   /**
    * Unique metric tag identifies components with the same life-cycle, which 
should be registered /
diff --git a/solr/core/src/java/org/apache/solr/security/AuditLoggerPlugin.java 
b/solr/core/src/java/org/apache/solr/security/AuditLoggerPlugin.java
index 78dfdd4d572..89a6c923f7a 100644
--- a/solr/core/src/java/org/apache/solr/security/AuditLoggerPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/AuditLoggerPlugin.java
@@ -16,9 +16,6 @@
  */
 package org.apache.solr.security;
 
-import com.codahale.metrics.Counter;
-import com.codahale.metrics.Meter;
-import com.codahale.metrics.Timer;
 import com.fasterxml.jackson.annotation.JsonInclude.Include;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.SerializationFeature;
@@ -33,10 +30,8 @@ import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -46,6 +41,10 @@ import org.apache.solr.common.util.SolrNamedThreadFactory;
 import org.apache.solr.common.util.TimeSource;
 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.AttributedLongGauge;
+import org.apache.solr.metrics.otel.instruments.AttributedLongTimer;
 import org.apache.solr.security.AuditEvent.EventType;
 import org.apache.solr.util.TimeOut;
 import org.slf4j.Logger;
@@ -77,18 +76,16 @@ public abstract class AuditLoggerPlugin implements 
Closeable, Runnable, SolrInfo
   int blockingQueueSize;
 
   protected AuditEventFormatter formatter;
-  private Set<String> metricNames = ConcurrentHashMap.newKeySet();
   private ExecutorService executorService;
   private boolean closed;
   private MuteRules muteRules;
 
   protected SolrMetricsContext solrMetricsContext;
-  protected Meter numErrors = new Meter();
-  protected Meter numLost = new Meter();
-  protected Meter numLogged = new Meter();
-  protected Timer requestTimes = new Timer();
-  protected Timer queuedTime = new Timer();
-  protected Counter totalTime = new Counter();
+  protected AttributedLongCounter numLogged;
+  protected AttributedLongCounter numErrors;
+  protected AttributedLongCounter numLost;
+  protected AttributedLongTimer requestTimes;
+  private AttributedLongTimer queuedTime;
 
   // Event types to be logged by default
   protected List<String> eventTypes =
@@ -163,15 +160,16 @@ public abstract class AuditLoggerPlugin implements 
Closeable, Runnable, SolrInfo
     if (async) {
       auditAsync(event);
     } else {
-      Timer.Context timer = requestTimes.time();
-      numLogged.mark();
+      AttributedLongTimer.MetricTimer timer = 
requestTimes.start(TimeUnit.NANOSECONDS);
+      numLogged.inc();
       try {
         audit(event);
       } catch (Exception e) {
-        numErrors.mark();
+        numErrors.inc();
         log.error("Exception when attempting to audit log", e);
       } finally {
-        totalTime.inc(timer.stop());
+        // Record the timing metric
+        timer.stop();
       }
     }
   }
@@ -207,7 +205,7 @@ public abstract class AuditLoggerPlugin implements 
Closeable, Runnable, SolrInfo
             "Audit log async queue is full (size={}), not blocking since 
{}==false",
             blockingQueueSize,
             PARAM_BLOCKASYNC);
-        numLost.mark();
+        numLost.inc();
       }
     }
   }
@@ -222,19 +220,21 @@ public abstract class AuditLoggerPlugin implements 
Closeable, Runnable, SolrInfo
         auditsInFlight.incrementAndGet();
         if (event == null) continue;
         if (event.getDate() != null) {
-          queuedTime.update(
-              new Date().getTime() - event.getDate().getTime(), 
TimeUnit.MILLISECONDS);
+          long queueTimeNanos =
+              TimeUnit.MILLISECONDS.toNanos(new Date().getTime() - 
event.getDate().getTime());
+          queuedTime.record(queueTimeNanos);
         }
-        Timer.Context timer = requestTimes.time();
+        AttributedLongTimer.MetricTimer timer = 
requestTimes.start(TimeUnit.NANOSECONDS);
         audit(event);
-        numLogged.mark();
-        totalTime.inc(timer.stop());
+        numLogged.inc();
+        // Record the timing metric
+        timer.stop();
       } catch (InterruptedException e) {
         log.warn("Interrupted while waiting for next audit log event");
         Thread.currentThread().interrupt();
       } catch (Exception ex) {
         log.error("Exception when attempting to audit log asynchronously", ex);
-        numErrors.mark();
+        numErrors.inc();
       } finally {
         auditsInFlight.decrementAndGet();
       }
@@ -261,39 +261,73 @@ public abstract class AuditLoggerPlugin implements 
Closeable, Runnable, SolrInfo
     this.formatter = formatter;
   }
 
-  // TODO SOLR-17458: Migrate to Otel
   @Override
   public void initializeMetrics(
       SolrMetricsContext parentContext, Attributes attributes, final String 
scope) {
     solrMetricsContext = parentContext.getChildContext(this);
     String className = this.getClass().getSimpleName();
     log.debug("Initializing metrics for {}", className);
-    numErrors = solrMetricsContext.meter("errors", getCategory().toString(), 
scope, className);
-    numLost = solrMetricsContext.meter("lost", getCategory().toString(), 
scope, className);
-    numLogged = solrMetricsContext.meter("count", getCategory().toString(), 
scope, className);
+
+    Attributes attrsWithCategory =
+        Attributes.builder()
+            .putAll(attributes)
+            .put(CATEGORY_ATTR, Category.SECURITY.toString())
+            .put(PLUGIN_NAME_ATTR, this.getClass().getSimpleName())
+            .build();
+
+    numLogged =
+        new AttributedLongCounter(
+            solrMetricsContext.longCounter(
+                "solr_auditlogger_count",
+                "The number of audit events that were successfully logged."),
+            attrsWithCategory);
+    numErrors =
+        new AttributedLongCounter(
+            solrMetricsContext.longCounter(
+                "solr_auditlogger_errors", "The number of audit events that 
resulted in errors."),
+            attrsWithCategory);
+    numLost =
+        new AttributedLongCounter(
+            solrMetricsContext.longCounter(
+                "solr_auditlogger_lost",
+                "The number of audit events that were lost due to async queue 
being full."),
+            attrsWithCategory);
     requestTimes =
-        solrMetricsContext.timer("requestTimes", getCategory().toString(), 
scope, className);
-    totalTime = solrMetricsContext.counter("totalTime", 
getCategory().toString(), scope, className);
+        new AttributedLongTimer(
+            this.solrMetricsContext.longHistogram(
+                "solr_auditlogger_request_times",
+                "Distribution of audit event request durations",
+                OtelUnit.NANOSECONDS),
+            attrsWithCategory);
+
     if (async) {
-      solrMetricsContext.gauge(
-          () -> blockingQueueSize,
-          true,
-          "queueCapacity",
-          getCategory().toString(),
-          scope,
-          className);
-      solrMetricsContext.gauge(
-          () -> blockingQueueSize - queue.remainingCapacity(),
-          true,
-          "queueSize",
-          getCategory().toString(),
-          scope,
-          className);
+      solrMetricsContext.observableLongGauge(
+          "solr_auditlogger_queue",
+          "Metrics around the audit logger queue when running in async mode",
+          (observableLongMeasurement -> {
+            observableLongMeasurement.record(
+                blockingQueueSize,
+                attrsWithCategory.toBuilder().put(TYPE_ATTR, 
"capacity").build());
+            observableLongMeasurement.record(
+                blockingQueueSize - queue.remainingCapacity(),
+                attrsWithCategory.toBuilder().put(TYPE_ATTR, "size").build());
+          }));
       queuedTime =
-          solrMetricsContext.timer("queuedTime", getCategory().toString(), 
scope, className);
+          new AttributedLongTimer(
+              solrMetricsContext.longHistogram(
+                  "solr_auditlogger_queued_time",
+                  "Distribution of time events spend queued before processing",
+                  OtelUnit.NANOSECONDS),
+              attrsWithCategory);
     }
-    solrMetricsContext.gauge(
-        () -> async, true, "async", getCategory().toString(), scope, 
className);
+
+    AttributedLongGauge asyncEnabledGauge =
+        new AttributedLongGauge(
+            solrMetricsContext.longGauge(
+                "solr_auditlogger_async_enabled",
+                "Whether the audit logger is running in async mode (1) or not 
(0)"),
+            attrsWithCategory);
+    asyncEnabledGauge.set(async ? 1L : 0L);
   }
 
   @Override
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 fd7e4e46ff2..e8a7d1adcd0 100644
--- a/solr/core/src/java/org/apache/solr/security/AuthenticationPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/AuthenticationPlugin.java
@@ -171,8 +171,8 @@ public abstract class AuthenticationPlugin implements 
SolrInfoBean {
     Attributes attrsWithCategory =
         Attributes.builder()
             .putAll(attributes)
-            .put("category", getCategory().toString())
-            .put("plugin_name", this.getClass().getSimpleName())
+            .put(CATEGORY_ATTR, getCategory().toString())
+            .put(PLUGIN_NAME_ATTR, this.getClass().getSimpleName())
             .build();
     // Metrics
     numErrors =
diff --git 
a/solr/core/src/java/org/apache/solr/security/MultiDestinationAuditLogger.java 
b/solr/core/src/java/org/apache/solr/security/MultiDestinationAuditLogger.java
index d834d3b3c3b..13d3ea48cd2 100644
--- 
a/solr/core/src/java/org/apache/solr/security/MultiDestinationAuditLogger.java
+++ 
b/solr/core/src/java/org/apache/solr/security/MultiDestinationAuditLogger.java
@@ -133,8 +133,7 @@ public class MultiDestinationAuditLogger extends 
AuditLoggerPlugin implements Re
   public void initializeMetrics(
       SolrMetricsContext parentContext, Attributes attributes, String scope) {
     super.initializeMetrics(parentContext, attributes, scope);
-    // TODO SOLR-17458: Add Otel
-    plugins.forEach(p -> p.initializeMetrics(solrMetricsContext, 
Attributes.empty(), scope));
+    plugins.forEach(p -> p.initializeMetrics(parentContext, 
Attributes.empty(), scope));
   }
 
   @Override
diff --git 
a/solr/core/src/test/org/apache/solr/security/AuditLoggerIntegrationTest.java 
b/solr/core/src/test/org/apache/solr/security/AuditLoggerIntegrationTest.java
index 6f800b34f54..f774abbbd58 100644
--- 
a/solr/core/src/test/org/apache/solr/security/AuditLoggerIntegrationTest.java
+++ 
b/solr/core/src/test/org/apache/solr/security/AuditLoggerIntegrationTest.java
@@ -28,9 +28,13 @@ import static 
org.apache.solr.security.AuditEvent.RequestType.SEARCH;
 import static 
org.apache.solr.security.Sha256AuthenticationProvider.getSaltedHashedValue;
 
 import com.codahale.metrics.MetricRegistry;
-import com.codahale.metrics.Timer;
 import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import io.opentelemetry.exporter.prometheus.PrometheusMetricReader;
+import io.prometheus.metrics.model.snapshots.CounterSnapshot;
+import io.prometheus.metrics.model.snapshots.GaugeSnapshot;
+import io.prometheus.metrics.model.snapshots.HistogramSnapshot;
+import io.prometheus.metrics.model.snapshots.Labels;
 import java.io.BufferedReader;
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -61,9 +65,11 @@ import org.apache.solr.cloud.SolrCloudAuthTestCase;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.params.MapSolrParams;
 import org.apache.solr.common.util.SolrNamedThreadFactory;
+import org.apache.solr.core.CoreContainer;
 import org.apache.solr.security.AuditEvent.EventType;
 import org.apache.solr.security.AuditEvent.RequestType;
 import org.apache.solr.security.AuditLoggerPlugin.JSONAuditEventFormatter;
+import org.apache.solr.util.SolrMetricTestUtils;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -81,6 +87,8 @@ public class AuditLoggerIntegrationTest extends 
SolrCloudAuthTestCase {
   // Use a harness per thread to be able to beast this test
   private ThreadLocal<AuditTestHarness> testHarness = new ThreadLocal<>();
 
+  private PrometheusMetricReader metricsReader;
+
   @Override
   @Before
   public void setUp() throws Exception {
@@ -103,8 +111,16 @@ public class AuditLoggerIntegrationTest extends 
SolrCloudAuthTestCase {
     setupCluster(false, null, false);
     runThreeTestAdminCommands();
     assertThreeTestAdminEvents();
-    assertAuditMetricsMinimums(
-        testHarness.get().cluster, 
CallbackAuditLoggerPlugin.class.getSimpleName(), 3, 0);
+    assertAuditMetricsMinimums(3, 0);
+    Labels labels = getDefaultAuditLoggerMetricsLabelsBuilder().build();
+
+    assertGaugeMetricValue("solr_auditlogger_async_enabled", labels, 0);
+
+    var snapshot =
+        SolrMetricTestUtils.getHistogramDatapoint(
+            metricsReader, "solr_auditlogger_request_times_nanoseconds", 
labels);
+    assertNotNull(snapshot);
+    assertTrue("Expected at least 3 measurements", snapshot.getCount() >= 3);
   }
 
   @Test
@@ -112,8 +128,26 @@ public class AuditLoggerIntegrationTest extends 
SolrCloudAuthTestCase {
     setupCluster(true, null, false);
     runThreeTestAdminCommands();
     assertThreeTestAdminEvents();
-    assertAuditMetricsMinimums(
-        testHarness.get().cluster, 
CallbackAuditLoggerPlugin.class.getSimpleName(), 3, 0);
+    assertAuditMetricsMinimums(3, 0);
+    Labels labels = getDefaultAuditLoggerMetricsLabelsBuilder().build();
+
+    assertGaugeMetricValue("solr_auditlogger_async_enabled", labels, 1);
+
+    // Verify queue capacity matches default size
+    Labels capacityLabels =
+        getDefaultAuditLoggerMetricsLabelsBuilder().label("type", 
"capacity").build();
+    assertGaugeMetricValue("solr_auditlogger_queue", capacityLabels, 4096);
+
+    // Verify queue is empty after processing events
+    Labels sizeLabels = 
getDefaultAuditLoggerMetricsLabelsBuilder().label("type", "size").build();
+    assertGaugeMetricValue("solr_auditlogger_queue", sizeLabels, 0);
+
+    var snapshot =
+        SolrMetricTestUtils.getHistogramDatapoint(
+            metricsReader, "solr_auditlogger_request_times_nanoseconds", 
labels);
+    assertNotNull(snapshot);
+    assertTrue("Expected at least 3 measurements", snapshot.getCount() >= 3);
+    assertTrue("Expected a positive sum for request times", snapshot.getSum() 
>= 0);
   }
 
   @Test
@@ -133,20 +167,21 @@ public class AuditLoggerIntegrationTest extends 
SolrCloudAuthTestCase {
     gate.release(3);
 
     assertThreeTestAdminEvents();
-    assertAuditMetricsMinimums(
-        testHarness.get().cluster, 
CallbackAuditLoggerPlugin.class.getSimpleName(), 3, 0);
-    ArrayList<MetricRegistry> registries = 
getMetricsRegistries(testHarness.get().cluster);
-    Timer timer =
-        ((Timer)
-            registries
-                .get(0)
-                .getMetrics()
-                
.get("SECURITY./auditlogging.CallbackAuditLoggerPlugin.queuedTime"));
-    double meanTimeOnQueue = timer.getSnapshot().getMean();
-    double meanTimeExpected = (start - end) / 3.0D;
+    assertAuditMetricsMinimums(3, 0);
+
+    Labels labels = getDefaultAuditLoggerMetricsLabelsBuilder().build();
+    HistogramSnapshot.HistogramDataPointSnapshot snapshot =
+        SolrMetricTestUtils.getHistogramDatapoint(
+            metricsReader, "solr_auditlogger_queued_time_nanoseconds", labels);
+    assertNotNull(snapshot);
+    assertTrue("Expected at least 3 measurements", snapshot.getCount() >= 3);
+
+    // Calculate mean from sum and count
+    double mean = snapshot.getSum() / snapshot.getCount();
+    double minExpectedTimeNanos = (end - start) / 3.0;
     assertTrue(
-        "Expecting mean time on queue > " + meanTimeExpected + ", got " + 
meanTimeOnQueue,
-        meanTimeOnQueue > meanTimeExpected);
+        "Expecting mean time on queue > " + minExpectedTimeNanos + " ns, got " 
+ mean + " ns",
+        mean > minExpectedTimeNanos);
   }
 
   @Test
@@ -542,6 +577,61 @@ public class AuditLoggerIntegrationTest extends 
SolrCloudAuthTestCase {
 
     myCluster.waitForAllNodes(10);
     testHarness.get().setCluster(myCluster);
+
+    CoreContainer coreContainer = 
myCluster.getJettySolrRunner(0).getCoreContainer();
+    assertNotNull(coreContainer);
+    metricsReader = 
SolrMetricTestUtils.getPrometheusMetricReader(coreContainer, "solr.node");
+  }
+
+  private Labels.Builder getDefaultAuditLoggerMetricsLabelsBuilder() {
+    return Labels.builder()
+        .label("category", "SECURITY")
+        .label("plugin_name", "CallbackAuditLoggerPlugin")
+        .label("handler", "/auditlogging")
+        .label("otel_scope_name", "org.apache.solr");
+  }
+
+  private Labels getDefaultAuditLoggerMetricsLabels() {
+    return getDefaultAuditLoggerMetricsLabelsBuilder().build();
+  }
+
+  private void assertGaugeMetricValue(String metricName, Labels labels, long 
expectedValue) {
+    GaugeSnapshot.GaugeDataPointSnapshot dataPointSnapshot =
+        (GaugeSnapshot.GaugeDataPointSnapshot)
+            SolrMetricTestUtils.getDataPointSnapshot(metricsReader, 
metricName, labels);
+    assertNotNull(dataPointSnapshot);
+    assertEquals(expectedValue, dataPointSnapshot.getValue(), 0.0);
+  }
+
+  private void assertAuditMetricsMinimums(int count, int errors) throws 
InterruptedException {
+    Labels labels = getDefaultAuditLoggerMetricsLabels();
+    assertCounterMinimumWithRetry("solr_auditlogger_count", labels, count);
+
+    if (errors > 0) {
+      assertCounterMinimumWithRetry("solr_auditlogger_errors", labels, errors);
+    }
+  }
+
+  private void assertCounterMinimumWithRetry(String metricName, Labels labels, 
int expectedMinimum)
+      throws InterruptedException {
+    boolean success = checkCounterMinimum(metricName, labels, expectedMinimum);
+
+    if (!success && expectedMinimum > 0) {
+      log.info("First {} metric check failed, pausing 2s before re-attempt", 
metricName);
+      Thread.sleep(2000);
+      success = checkCounterMinimum(metricName, labels, expectedMinimum);
+    }
+
+    assertTrue(
+        String.format(
+            "Counter metric '%s' did not meet minimum %d after retry", 
metricName, expectedMinimum),
+        success);
+  }
+
+  private boolean checkCounterMinimum(String metricName, Labels labels, int 
expectedMinimum) {
+    CounterSnapshot.CounterDataPointSnapshot snapshot =
+        SolrMetricTestUtils.getCounterDatapoint(metricsReader, metricName, 
labels);
+    return snapshot != null && snapshot.getValue() >= expectedMinimum;
   }
 
   /**
diff --git 
a/solr/core/src/test/org/apache/solr/security/AuditLoggerPluginTest.java 
b/solr/core/src/test/org/apache/solr/security/AuditLoggerPluginTest.java
index 9b82e6d0585..1de35430800 100644
--- a/solr/core/src/test/org/apache/solr/security/AuditLoggerPluginTest.java
+++ b/solr/core/src/test/org/apache/solr/security/AuditLoggerPluginTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.solr.security;
 
+import io.opentelemetry.api.common.Attributes;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -26,6 +27,7 @@ import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.metrics.SolrMetricsContext;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -133,6 +135,8 @@ public class AuditLoggerPluginTest extends SolrTestCaseJ4 {
     config = new HashMap<>();
     config.put("async", false);
     plugin.init(config);
+    SolrMetricsContext mockSolrMetricsContext = 
MockSolrMetricsContextFactory.create();
+    plugin.initializeMetrics(mockSolrMetricsContext, Attributes.empty(), 
"solr.test");
   }
 
   @Override
diff --git 
a/solr/core/src/test/org/apache/solr/security/MockSolrMetricsContextFactory.java
 
b/solr/core/src/test/org/apache/solr/security/MockSolrMetricsContextFactory.java
new file mode 100644
index 00000000000..679e14af67d
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/security/MockSolrMetricsContextFactory.java
@@ -0,0 +1,49 @@
+package org.apache.solr.security;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.Timer;
+import io.opentelemetry.api.metrics.LongCounter;
+import io.opentelemetry.api.metrics.LongGauge;
+import io.opentelemetry.api.metrics.LongHistogram;
+import org.apache.solr.metrics.SolrMetricsContext;
+import org.apache.solr.metrics.otel.OtelUnit;
+
+public final class MockSolrMetricsContextFactory {
+
+  public static SolrMetricsContext create() {
+    SolrMetricsContext mockParentContext = mock(SolrMetricsContext.class);
+    SolrMetricsContext mockChildContext = mock(SolrMetricsContext.class);
+
+    
when(mockParentContext.getChildContext(any())).thenReturn(mockChildContext);
+
+    LongCounter mockOtelLongCounter = mock(LongCounter.class);
+    when(mockChildContext.longCounter(anyString(), 
any())).thenReturn(mockOtelLongCounter);
+
+    Timer mockTimer = mock(Timer.class);
+    Timer.Context mockTimerContext = mock(Timer.Context.class);
+    when(mockTimer.time()).thenReturn(mockTimerContext);
+    when(mockChildContext.timer(anyString(), anyString(), anyString(), 
anyString()))
+        .thenReturn(mockTimer);
+
+    Counter mockCounter = mock(Counter.class);
+    when(mockChildContext.counter(anyString(), anyString(), anyString(), 
anyString()))
+        .thenReturn(mockCounter);
+
+    LongHistogram mockLongHistogram = mock(LongHistogram.class);
+    when(mockChildContext.longHistogram(anyString(), anyString(), 
any(OtelUnit.class)))
+        .thenReturn(mockLongHistogram);
+
+    when(mockChildContext.observableLongGauge(anyString(), anyString(), 
any())).thenReturn(null);
+    when(mockChildContext.observableLongCounter(anyString(), anyString(), 
any())).thenReturn(null);
+
+    LongGauge mockLongGauge = mock(LongGauge.class);
+    when(mockChildContext.longGauge(anyString(), 
anyString())).thenReturn(mockLongGauge);
+
+    return mockParentContext;
+  }
+}
diff --git 
a/solr/core/src/test/org/apache/solr/security/MultiDestinationAuditLoggerTest.java
 
b/solr/core/src/test/org/apache/solr/security/MultiDestinationAuditLoggerTest.java
index 3b1ba2c4827..fe6c30070b0 100644
--- 
a/solr/core/src/test/org/apache/solr/security/MultiDestinationAuditLoggerTest.java
+++ 
b/solr/core/src/test/org/apache/solr/security/MultiDestinationAuditLoggerTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.solr.security;
 
+import io.opentelemetry.api.common.Attributes;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -24,6 +25,7 @@ import java.util.HashMap;
 import java.util.Map;
 import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.core.SolrResourceLoader;
+import org.apache.solr.metrics.SolrMetricsContext;
 import org.junit.Test;
 
 public class MultiDestinationAuditLoggerTest extends SolrTestCaseJ4 {
@@ -52,6 +54,9 @@ public class MultiDestinationAuditLoggerTest extends 
SolrTestCaseJ4 {
     al.inform(loader);
     al.init(config);
 
+    SolrMetricsContext mockSolrMetricsContext = 
MockSolrMetricsContextFactory.create();
+    al.initializeMetrics(mockSolrMetricsContext, Attributes.empty(), 
"solr.test");
+
     al.doAudit(new 
AuditEvent(AuditEvent.EventType.ANONYMOUS).setUsername("me"));
     assertEquals(
         0,
diff --git 
a/solr/core/src/test/org/apache/solr/security/SolrLogAuditLoggerPluginTest.java 
b/solr/core/src/test/org/apache/solr/security/SolrLogAuditLoggerPluginTest.java
index 6685b25f340..15242a166ec 100644
--- 
a/solr/core/src/test/org/apache/solr/security/SolrLogAuditLoggerPluginTest.java
+++ 
b/solr/core/src/test/org/apache/solr/security/SolrLogAuditLoggerPluginTest.java
@@ -20,9 +20,11 @@ package org.apache.solr.security;
 import static org.apache.solr.security.AuditLoggerPluginTest.EVENT_ANONYMOUS;
 import static 
org.apache.solr.security.AuditLoggerPluginTest.EVENT_AUTHENTICATED;
 
+import io.opentelemetry.api.common.Attributes;
 import java.util.HashMap;
 import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.metrics.SolrMetricsContext;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -38,6 +40,8 @@ public class SolrLogAuditLoggerPluginTest extends 
SolrTestCaseJ4 {
     plugin = new SolrLogAuditLoggerPlugin();
     config = new HashMap<>();
     config.put("async", false);
+    SolrMetricsContext mockSolrMetricsContext = 
MockSolrMetricsContextFactory.create();
+    plugin.initializeMetrics(mockSolrMetricsContext, Attributes.empty(), 
"solr.test");
   }
 
   @Test(expected = SolrException.class)
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/audit-logging.adoc 
b/solr/solr-ref-guide/modules/deployment-guide/pages/audit-logging.adoc
index 44efcd7f4c5..20c94bb71a1 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/audit-logging.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/audit-logging.adoc
@@ -230,20 +230,17 @@ See the javadocs for the base class at 
{solr-javadocs}/core/org/apache/solr/secu
 
 == Metrics
 
-Audit logging plugins record metrics about count and timing of log requests, 
as well as queue size for async loggers.
-The metrics keys are all recorded on the `SECURITY` category, and each metric 
name are prefixed with a scope of `/auditlogging` and the class name of the 
logger, e.g., `SolrLogAuditLoggerPlugin`.
+The metrics keys are all recorded on the `SECURITY` category, and each metric 
includes attributes for the handler (e.g., `/auditlogging`) and the class name 
of the logger (e.g., `SolrLogAuditLoggerPlugin`).
 The individual metrics are:
 
-* `count`: (_meter_) Records number and rate of audit logs written.
-* `errors`: (_meter_) Records number and rate of errors.
-* `lost`: (_meter_) Records number and rate of events lost if the queue is 
full and `blockAsync=false`.
-* `requestTimes`: (_timer_) Records latency and percentiles for audit logging 
performance.
-* `totalTime`: (_counter_) Records total time spent logging.
-* `queueCapacity`: (_gauge_) Records the maximum size of the async logging 
queue.
-* `queueSize`: (_gauge_) Records the number of events currently waiting in the 
queue.
-* `queuedTime`: (_timer_) Records the amount of time events waited in queue.
-Adding this with the `requestTimes` metric will show the total time from event 
to logging complete.
-* `async`: (_gauge_) Tells whether this logger is in async mode.
-
-TIP: If you experience a very high request rate and have a slow audit logger 
plugin, you may see the `queueSize` and `queuedTime` metrics increase, and 
possibly start dropping events (shown by an increase in `lost` count).
+* `solr_auditlogger_count_total`: (_counter_) Records number of audit logs 
written.
+* `solr_auditlogger_errors_total`: (_counter_) Records number of errors.
+* `solr_auditlogger_lost_total`: (_counter_) Records number of events lost if 
the queue is full and `blockAsync=false`.
+* `solr_auditlogger_request_times_nanoseconds`: (_histogram_) Records latency 
and percentiles for audit logging performance.
+* `solr_auditlogger_queue`: (_gauge_) Records the maximum size 
(`type=capacity`) and current size (`type=size`) of the async logging queue.
+* `solr_auditlogger_queued_time_nanoseconds`: (_histogram_) Records the amount 
of time events waited in queue.
+Adding this with the `solr_auditlogger_request_times_nanoseconds` metric will 
show the total time from event to logging complete.
+* `solr_auditlogger_async_enabled`: (_gauge_) Tells whether this logger is in 
async mode (1) or not (0).
+
+TIP: If you experience a very high request rate and have a slow audit logger 
plugin, you may see the `solr_auditlogger_queue` size metric increase, and 
possibly start dropping events (shown by an increase in 
`solr_auditlogger_lost_total` count).
 In this case you may want to increase the `numThreads` setting.
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 1bf0f3134f6..dc8d7d7034e 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
@@ -336,46 +336,6 @@ public class SolrCloudAuthTestCase extends 
SolrCloudTestCase {
     return metrics;
   }
 
-  /**
-   * Common test method to be able to check audit metrics
-   *
-   * @param className the class name to be used for composing prefix, e.g.
-   *     "SECURITY./auditlogging/SolrLogAuditLoggerPlugin"
-   */
-  protected void assertAuditMetricsMinimums(
-      MiniSolrCloudCluster cluster, String className, int count, int errors)
-      throws InterruptedException {
-    String prefix = "SECURITY./auditlogging." + className + ".";
-    Map<String, Long> expectedCounts = new HashMap<>();
-    expectedCounts.put("count", (long) count);
-
-    Map<String, Long> counts = countSecurityMetrics(cluster, prefix, 
AUDIT_METRICS_KEYS);
-    boolean success = isMetricsEqualOrLarger(AUDIT_METRICS_TO_COMPARE, 
expectedCounts, counts);
-    if (!success) {
-      log.info("First metrics count assert failed, pausing 2s before 
re-attempt");
-      Thread.sleep(2000);
-      counts = countSecurityMetrics(cluster, prefix, AUDIT_METRICS_KEYS);
-      success = isMetricsEqualOrLarger(AUDIT_METRICS_TO_COMPARE, 
expectedCounts, counts);
-    }
-
-    assertTrue(
-        "Expected metric minimums for prefix "
-            + prefix
-            + ": "
-            + expectedCounts
-            + ", but got: "
-            + counts,
-        success);
-  }
-
-  private boolean isMetricsEqualOrLarger(
-      List<String> metricsToCompare,
-      Map<String, Long> expectedCounts,
-      Map<String, Long> actualCounts) {
-    return metricsToCompare.stream()
-        .allMatch(k -> actualCounts.get(k).intValue() >= 
expectedCounts.get(k).intValue());
-  }
-
   // Have to sum the metrics from all three shards/nodes
   private long sumCount(String prefix, String key, List<Map<String, Metric>> 
metrics) {
     assertTrue(


Reply via email to