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 3b93da49910 SOLR-17853: Migrate Plugins / Stats UI tab with Prometheus 
metrics (#3623)
3b93da49910 is described below

commit 3b93da49910b79eb94a7c1a95b56a37dccf1bb4e
Author: Matthew Biscocho <[email protected]>
AuthorDate: Mon Oct 6 19:46:13 2025 -0400

    SOLR-17853: Migrate Plugins / Stats UI tab with Prometheus metrics (#3623)
    
    * Switch plugins/stats tab to use prometheus
    
    * Remove /admin/plugins handler
    
    * Changes from comments
    
    * Add JFR permission to security.policy
    
    * Remove extra space
    
    ---------
    
    Co-authored-by: Matthew Biscocho <[email protected]>
---
 .../solr/handler/admin/PluginInfoHandler.java      |  85 ------
 .../solr/handler/admin/SolrInfoMBeanHandler.java   | 292 ---------------------
 solr/core/src/resources/ImplicitPlugins.json       |   7 -
 .../org/apache/solr/cloud/TestPullReplica.java     |  33 ++-
 .../apache/solr/cloud/TestPullReplicaWithAuth.java |  60 +++--
 .../org/apache/solr/cloud/TestTlogReplica.java     |  42 +--
 .../test/org/apache/solr/core/SolrCoreTest.java    |   4 -
 .../apache/solr/core/TestSolrConfigHandler.java    |   2 -
 .../solr/handler/admin/MBeansHandlerTest.java      | 240 -----------------
 solr/server/etc/security.policy                    |   6 +-
 solr/webapp/web/css/angular/plugins.css            |  15 ++
 solr/webapp/web/js/angular/controllers/plugins.js  | 223 ++++++++++------
 solr/webapp/web/js/angular/services.js             |  31 +--
 solr/webapp/web/partials/plugins.html              |  17 +-
 14 files changed, 252 insertions(+), 805 deletions(-)

diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/PluginInfoHandler.java 
b/solr/core/src/java/org/apache/solr/handler/admin/PluginInfoHandler.java
deleted file mode 100644
index d795d3d2f2f..00000000000
--- a/solr/core/src/java/org/apache/solr/handler/admin/PluginInfoHandler.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * 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.handler.admin;
-
-import static org.apache.solr.common.params.CommonParams.NAME;
-
-import java.util.Map;
-import org.apache.solr.common.params.SolrParams;
-import org.apache.solr.common.util.SimpleOrderedMap;
-import org.apache.solr.core.SolrCore;
-import org.apache.solr.core.SolrInfoBean;
-import org.apache.solr.handler.RequestHandlerBase;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
-import org.apache.solr.security.AuthorizationContext;
-
-/**
- * @since solr 1.2
- */
-public class PluginInfoHandler extends RequestHandlerBase {
-  @Override
-  public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) 
throws Exception {
-    SolrParams params = req.getParams();
-
-    boolean stats = params.getBool("stats", false);
-    rsp.add("plugins", getSolrInfoBeans(req.getCore(), stats));
-    rsp.setHttpCaching(false);
-  }
-
-  private static SimpleOrderedMap<Object> getSolrInfoBeans(SolrCore core, 
boolean stats) {
-    SimpleOrderedMap<Object> list = new SimpleOrderedMap<>();
-    for (SolrInfoBean.Category cat : SolrInfoBean.Category.values()) {
-      SimpleOrderedMap<Object> category = new SimpleOrderedMap<>();
-      list.add(cat.name(), category);
-      Map<String, SolrInfoBean> reg = core.getInfoRegistry();
-      for (Map.Entry<String, SolrInfoBean> entry : reg.entrySet()) {
-        SolrInfoBean m = entry.getValue();
-        if (m.getCategory() != cat) continue;
-
-        String na = "Not Declared";
-        SimpleOrderedMap<Object> info = new SimpleOrderedMap<>();
-        category.add(entry.getKey(), info);
-
-        info.add(NAME, (m.getName() != null ? m.getName() : na));
-        info.add("description", (m.getDescription() != null ? 
m.getDescription() : na));
-
-        if (stats && m.getSolrMetricsContext() != null) {
-          info.add("stats", m.getSolrMetricsContext().getMetricsSnapshot());
-        }
-      }
-    }
-    return list;
-  }
-
-  //////////////////////// SolrInfoMBeans methods //////////////////////
-
-  @Override
-  public String getDescription() {
-    return "Registry";
-  }
-
-  @Override
-  public Category getCategory() {
-    return Category.ADMIN;
-  }
-
-  @Override
-  public Name getPermissionName(AuthorizationContext request) {
-    return Name.METRICS_READ_PERM;
-  }
-}
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/SolrInfoMBeanHandler.java 
b/solr/core/src/java/org/apache/solr/handler/admin/SolrInfoMBeanHandler.java
deleted file mode 100644
index a9ba9d0bea2..00000000000
--- a/solr/core/src/java/org/apache/solr/handler/admin/SolrInfoMBeanHandler.java
+++ /dev/null
@@ -1,292 +0,0 @@
-/*
- * 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.handler.admin;
-
-import java.io.StringReader;
-import java.text.NumberFormat;
-import java.util.HashSet;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import org.apache.solr.client.solrj.impl.XMLResponseParser;
-import org.apache.solr.common.SolrException;
-import org.apache.solr.common.SolrException.ErrorCode;
-import org.apache.solr.common.util.ContentStream;
-import org.apache.solr.common.util.NamedList;
-import org.apache.solr.common.util.SimpleOrderedMap;
-import org.apache.solr.common.util.StrUtils;
-import org.apache.solr.core.SolrInfoBean;
-import org.apache.solr.handler.RequestHandlerBase;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.JavaBinResponseWriter;
-import org.apache.solr.response.SolrQueryResponse;
-import org.apache.solr.security.AuthorizationContext;
-
-/** A request handler that provides info about all registered SolrInfoMBeans. 
*/
-public class SolrInfoMBeanHandler extends RequestHandlerBase {
-
-  /**
-   * Take an array of any type and generate a Set containing the toString. Set 
is guarantee to never
-   * be null (but may be empty)
-   */
-  private Set<String> arrayToSet(Object[] arr) {
-    HashSet<String> r = new HashSet<>();
-    if (null == arr) return r;
-    for (Object o : arr) {
-      if (null != o) r.add(o.toString());
-    }
-    return r;
-  }
-
-  @Override
-  @SuppressWarnings("unchecked")
-  public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) 
throws Exception {
-    NamedList<NamedList<NamedList<Object>>> cats = getMBeanInfo(req);
-    if (req.getParams().getBool("diff", false)) {
-      ContentStream body = null;
-      try {
-        body = req.getContentStreams().iterator().next();
-      } catch (Exception ex) {
-        throw new SolrException(ErrorCode.BAD_REQUEST, "missing content-stream 
for diff");
-      }
-      final String content = StrUtils.stringFromReader(body.getReader());
-
-      NamedList<NamedList<NamedList<Object>>> ref = fromXML(content);
-
-      // Normalize the output
-      SolrQueryResponse wrap = new SolrQueryResponse();
-      wrap.add("solr-mbeans", cats);
-      cats =
-          (NamedList<NamedList<NamedList<Object>>>)
-              JavaBinResponseWriter.getParsedResponse(req, 
wrap).get("solr-mbeans");
-
-      // Get rid of irrelevant things
-      normalize(ref);
-      normalize(cats);
-
-      // Only the changes
-      boolean showAll = req.getParams().getBool("all", false);
-      rsp.add("solr-mbeans", getDiff(ref, cats, showAll));
-    } else {
-      rsp.add("solr-mbeans", cats);
-    }
-    rsp.setHttpCaching(false); // never cache, no matter what init config 
looks like
-  }
-
-  @SuppressWarnings("unchecked")
-  static NamedList<NamedList<NamedList<Object>>> fromXML(String content) {
-    int idx = content.indexOf("<response>");
-    if (idx < 0) {
-      throw new SolrException(ErrorCode.BAD_REQUEST, "Body does not appear to 
be an XML response");
-    }
-
-    try {
-      XMLResponseParser parser = new XMLResponseParser();
-      return (NamedList<NamedList<NamedList<Object>>>)
-          parser.processResponse(new StringReader(content)).get("solr-mbeans");
-    } catch (Exception ex) {
-      throw new SolrException(ErrorCode.BAD_REQUEST, "Unable to read original 
XML", ex);
-    }
-  }
-
-  protected NamedList<NamedList<NamedList<Object>>> 
getMBeanInfo(SolrQueryRequest req) {
-
-    NamedList<NamedList<NamedList<Object>>> cats = new NamedList<>();
-
-    String[] requestedCats = req.getParams().getParams("cat");
-    if (null == requestedCats || 0 == requestedCats.length) {
-      for (SolrInfoBean.Category cat : SolrInfoBean.Category.values()) {
-        cats.add(cat.name(), new SimpleOrderedMap<>());
-      }
-    } else {
-      for (String catName : requestedCats) {
-        cats.add(catName, new SimpleOrderedMap<>());
-      }
-    }
-
-    Set<String> requestedKeys = arrayToSet(req.getParams().getParams("key"));
-
-    Map<String, SolrInfoBean> reg = req.getCore().getInfoRegistry();
-    for (Map.Entry<String, SolrInfoBean> entry : reg.entrySet()) {
-      addMBean(req, cats, requestedKeys, entry.getKey(), entry.getValue());
-    }
-
-    for (SolrInfoBean infoMBean : 
req.getCoreContainer().getResourceLoader().getInfoMBeans()) {
-      addMBean(req, cats, requestedKeys, infoMBean.getName(), infoMBean);
-    }
-    return cats;
-  }
-
-  private void addMBean(
-      SolrQueryRequest req,
-      NamedList<NamedList<NamedList<Object>>> cats,
-      Set<String> requestedKeys,
-      String key,
-      SolrInfoBean m) {
-    if (!(requestedKeys.isEmpty() || requestedKeys.contains(key))) return;
-    NamedList<NamedList<Object>> catInfo = cats.get(m.getCategory().name());
-    if (null == catInfo) return;
-    NamedList<Object> mBeanInfo = new SimpleOrderedMap<>();
-    mBeanInfo.add("class", m.getName());
-    mBeanInfo.add("description", m.getDescription());
-
-    if (req.getParams().getFieldBool(key, "stats", false) && 
m.getSolrMetricsContext() != null)
-      mBeanInfo.add("stats", m.getSolrMetricsContext().getMetricsSnapshot());
-
-    catInfo.add(key, mBeanInfo);
-  }
-
-  protected NamedList<NamedList<NamedList<Object>>> getDiff(
-      NamedList<NamedList<NamedList<Object>>> ref,
-      NamedList<NamedList<NamedList<Object>>> now,
-      boolean includeAll) {
-
-    NamedList<NamedList<NamedList<Object>>> changed = new NamedList<>();
-
-    // Cycle through each category
-    for (Entry<String, NamedList<NamedList<Object>>> catEntry : ref) {
-      String category = catEntry.getKey();
-      NamedList<NamedList<Object>> ref_cat = catEntry.getValue();
-      NamedList<NamedList<Object>> now_cat = now.get(category);
-      if (now_cat != null) {
-        String ref_txt = ref_cat + "";
-        String now_txt = now_cat + "";
-        if (!ref_txt.equals(now_txt)) {
-          // Something in the category changed
-          // Now iterate the real beans
-
-          NamedList<NamedList<Object>> cat = new SimpleOrderedMap<>();
-          for (Entry<String, NamedList<Object>> beanEntry : ref_cat) {
-            String name = beanEntry.getKey();
-            NamedList<Object> ref_bean = beanEntry.getValue();
-            NamedList<Object> now_bean = now_cat.get(name);
-
-            ref_txt = ref_bean + "";
-            now_txt = now_bean + "";
-            if (!ref_txt.equals(now_txt)) {
-              //              System.out.println( "----" );
-              //              System.out.println( category +" : " + name );
-              //              System.out.println( "REF: " + ref_txt );
-              //              System.out.println( "NOW: " + now_txt );
-
-              // Calculate the differences
-              NamedList<Object> diff = diffNamedList(ref_bean, now_bean);
-              diff.add("_changed_", true); // flag the changed thing
-              cat.add(name, diff);
-            } else if (includeAll) {
-              cat.add(name, ref_bean);
-            }
-          }
-          if (cat.size() > 0) {
-            changed.add(category, cat);
-          }
-        } else if (includeAll) {
-          changed.add(category, ref_cat);
-        }
-      }
-    }
-    return changed;
-  }
-
-  public NamedList<Object> diffNamedList(NamedList<?> ref, NamedList<?> now) {
-    NamedList<Object> out = new SimpleOrderedMap<>();
-    ref.forEach(
-        (name, r) -> {
-          Object n = now.get(name);
-          if (n == null) {
-            if (r != null) {
-              out.add("REMOVE " + name, r);
-              now.remove(name);
-            }
-          } else if (r != null) {
-            out.add(name, diffObject(r, n));
-            now.remove(name);
-          }
-        });
-
-    now.forEach(
-        (name, v) -> {
-          if (v != null) {
-            out.add("ADD " + name, v);
-          }
-        });
-
-    return out;
-  }
-
-  @SuppressWarnings("unchecked")
-  public Object diffObject(Object ref, Object now) {
-    if (now instanceof Map) {
-      now = new NamedList<>((Map<String, ?>) now);
-    }
-    if (ref instanceof NamedList) {
-      return diffNamedList((NamedList<?>) ref, (NamedList<?>) now);
-    }
-    if (ref.equals(now)) {
-      return ref;
-    }
-    StringBuilder str = new StringBuilder();
-    str.append("Was: ").append(ref).append(", Now: ").append(now);
-
-    if (ref instanceof Number) {
-      NumberFormat nf = NumberFormat.getIntegerInstance(Locale.ROOT);
-      if ((ref instanceof Double) || (ref instanceof Float)) {
-        nf = NumberFormat.getInstance(Locale.ROOT);
-      }
-      double dref = ((Number) ref).doubleValue();
-      double dnow = ((Number) now).doubleValue();
-      double diff = Double.NaN;
-      if (Double.isNaN(dref)) {
-        diff = dnow;
-      } else if (Double.isNaN(dnow)) {
-        diff = dref;
-      } else {
-        diff = dnow - dref;
-      }
-      str.append(", Delta: ").append(nf.format(diff));
-    }
-    return str.toString();
-  }
-
-  /** The 'avgRequestsPerSecond' field will make everything look like it 
changed */
-  public void normalize(NamedList<?> input) {
-    input.remove("avgRequestsPerSecond");
-    for (Entry<String, ?> entry : input) {
-      Object v = entry.getValue();
-      if (v instanceof NamedList) {
-        // edit in place so we don't need to return it
-        normalize((NamedList<?>) v);
-      }
-    }
-  }
-
-  @Override
-  public String getDescription() {
-    return "Get Info (and statistics) for registered SolrInfoMBeans";
-  }
-
-  @Override
-  public Category getCategory() {
-    return Category.ADMIN;
-  }
-
-  @Override
-  public Name getPermissionName(AuthorizationContext request) {
-    return Name.METRICS_READ_PERM;
-  }
-}
diff --git a/solr/core/src/resources/ImplicitPlugins.json 
b/solr/core/src/resources/ImplicitPlugins.json
index c4da631c17a..4154e70ded9 100644
--- a/solr/core/src/resources/ImplicitPlugins.json
+++ b/solr/core/src/resources/ImplicitPlugins.json
@@ -77,13 +77,6 @@
       "class": "solr.SystemInfoHandler",
       "useParams":"_ADMIN_SYSTEM"
     },
-    "/admin/mbeans": {
-      "class": "solr.SolrInfoMBeanHandler",
-      "useParams":"_ADMIN_MBEANS"
-    },
-    "/admin/plugins": {
-      "class": "solr.PluginInfoHandler"
-    },
     "/admin/file": {
       "class": "solr.ShowFileRequestHandler",
       "useParams":"_ADMIN_FILE"
diff --git a/solr/core/src/test/org/apache/solr/cloud/TestPullReplica.java 
b/solr/core/src/test/org/apache/solr/cloud/TestPullReplica.java
index a5642b81942..09c700cfee1 100644
--- a/solr/core/src/test/org/apache/solr/cloud/TestPullReplica.java
+++ b/solr/core/src/test/org/apache/solr/cloud/TestPullReplica.java
@@ -44,7 +44,6 @@ import 
org.apache.solr.client.solrj.impl.CloudLegacySolrClient;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
 import org.apache.solr.client.solrj.request.QueryRequest;
 import org.apache.solr.client.solrj.response.CollectionAdminResponse;
-import org.apache.solr.client.solrj.response.QueryResponse;
 import org.apache.solr.common.SolrDocument;
 import org.apache.solr.common.SolrDocumentList;
 import org.apache.solr.common.SolrException;
@@ -55,6 +54,7 @@ import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.Slice;
 import org.apache.solr.common.cloud.ZkStateReader;
 import org.apache.solr.common.util.TimeSource;
+import org.apache.solr.core.CoreContainer;
 import org.apache.solr.core.CoreDescriptor;
 import org.apache.solr.core.SolrCore;
 import org.apache.solr.embedded.JettySolrRunner;
@@ -310,17 +310,26 @@ public class TestPullReplica extends SolrCloudTestCase {
               : s.getReplicas(EnumSet.of(Replica.Type.PULL));
       waitForNumDocsInAllReplicas(numDocs, pullReplicas);
 
-      for (Replica r : pullReplicas) {
-        try (SolrClient pullReplicaClient = getHttpSolrClient(r)) {
-          SolrQuery req = new SolrQuery("qt", "/admin/plugins", "stats", 
"true");
-          QueryResponse statsResponse = pullReplicaClient.query(req);
-          // The adds gauge metric should be null for pull replicas since they 
don't process adds
-          assertNull(
-              "Replicas shouldn't process the add document request: " + 
statsResponse,
-              ((Map<String, Object>)
-                      (statsResponse.getResponse())
-                          ._get(List.of("plugins", "UPDATE", "updateHandler", 
"stats"), null))
-                  .get("UPDATE.updateHandler.adds"));
+      for (JettySolrRunner jetty : cluster.getJettySolrRunners()) {
+        CoreContainer cc = jetty.getCoreContainer();
+        for (String coreName : cc.getAllCoreNames()) {
+          try (SolrCore core = cc.getCore(coreName)) {
+            var addOpsDatapoint =
+                org.apache.solr.util.SolrMetricTestUtils.getCounterDatapoint(
+                    core,
+                    "solr_core_update_committed_ops",
+                    
org.apache.solr.util.SolrMetricTestUtils.newCloudLabelsBuilder(core)
+                        .label("category", "UPDATE")
+                        .label("ops", "adds")
+                        .build());
+            // The adds gauge metric should be null for pull replicas since 
they don't process adds
+            if ((core.getCoreDescriptor().getCloudDescriptor().getReplicaType()
+                == Replica.Type.PULL)) {
+              assertNull(addOpsDatapoint);
+            } else {
+              assertNotNull(addOpsDatapoint);
+            }
+          }
         }
       }
 
diff --git 
a/solr/core/src/test/org/apache/solr/cloud/TestPullReplicaWithAuth.java 
b/solr/core/src/test/org/apache/solr/cloud/TestPullReplicaWithAuth.java
index 5e2b2500382..d0e1bb78f20 100644
--- a/solr/core/src/test/org/apache/solr/cloud/TestPullReplicaWithAuth.java
+++ b/solr/core/src/test/org/apache/solr/cloud/TestPullReplicaWithAuth.java
@@ -24,7 +24,6 @@ import static 
org.apache.solr.cloud.TestPullReplica.waitForNumDocsInAllReplicas;
 import java.io.IOException;
 import java.util.EnumSet;
 import java.util.List;
-import java.util.Map;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrQuery;
 import org.apache.solr.client.solrj.SolrRequest;
@@ -40,8 +39,11 @@ import org.apache.solr.common.SolrInputDocument;
 import org.apache.solr.common.cloud.DocCollection;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.Slice;
-import org.apache.solr.common.util.NamedList;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.embedded.JettySolrRunner;
 import org.apache.solr.util.SecurityJson;
+import org.apache.solr.util.SolrMetricTestUtils;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
@@ -68,9 +70,6 @@ public class TestPullReplicaWithAuth extends 
SolrCloudTestCase {
   }
 
   @Test
-  // NOCOMMIT: This test is broken from OTEL migration and the /admin/plugins 
endpoint. Placing
-  // BadApple test but this must be fixed before this feature gets merged to a 
release branch
-  @AwaitsFix(bugUrl = "https://issues.apache.org/jira/browse/SOLR-17458";)
   public void testPKIAuthWorksForPullReplication() throws Exception {
     int numPullReplicas = 2;
     withBasicAuth(
@@ -104,19 +103,36 @@ public class TestPullReplicaWithAuth extends 
SolrCloudTestCase {
       waitForNumDocsInAllReplicas(
           numDocs, pullReplicas, "*:*", SecurityJson.USER, SecurityJson.PASS);
 
-      for (Replica r : pullReplicas) {
-        try (SolrClient pullReplicaClient = getHttpSolrClient(r)) {
-          QueryResponse statsResponse =
-              queryWithBasicAuth(
-                  pullReplicaClient, new SolrQuery("qt", "/admin/plugins", 
"stats", "true"));
-          // the 'adds' metric is a gauge, which is null for PULL replicas
-          assertNull(
-              "Replicas shouldn't process the add document request: " + 
statsResponse,
-              getUpdateHandlerMetric(statsResponse, 
"UPDATE.updateHandler.adds"));
-          assertEquals(
-              "Replicas shouldn't process the add document request: " + 
statsResponse,
-              0L,
-              getUpdateHandlerMetric(statsResponse, 
"UPDATE.updateHandler.cumulativeAdds.count"));
+      for (JettySolrRunner jetty : cluster.getJettySolrRunners()) {
+        CoreContainer cc = jetty.getCoreContainer();
+        for (String coreName : cc.getAllCoreNames()) {
+          try (SolrCore core = cc.getCore(coreName)) {
+            var addOpsDatapoint =
+                org.apache.solr.util.SolrMetricTestUtils.getCounterDatapoint(
+                    core,
+                    "solr_core_update_committed_ops",
+                    
org.apache.solr.util.SolrMetricTestUtils.newCloudLabelsBuilder(core)
+                        .label("category", "UPDATE")
+                        .label("ops", "adds")
+                        .build());
+            var cumulativeAddsDatapoint =
+                SolrMetricTestUtils.getGaugeDatapoint(
+                    core,
+                    "solr_core_update_cumulative_ops",
+                    SolrMetricTestUtils.newCloudLabelsBuilder(core)
+                        .label("category", "UPDATE")
+                        .label("ops", "adds")
+                        .build());
+            // The adds gauge metric should be null for pull replicas since 
they don't process adds
+            if ((core.getCoreDescriptor().getCloudDescriptor().getReplicaType()
+                == Replica.Type.PULL)) {
+              assertNull(addOpsDatapoint);
+              assertNull(cumulativeAddsDatapoint);
+            } else {
+              assertNotNull(addOpsDatapoint);
+              assertNotNull(cumulativeAddsDatapoint);
+            }
+          }
         }
       }
     }
@@ -154,12 +170,4 @@ public class TestPullReplicaWithAuth extends 
SolrCloudTestCase {
         .process(cluster.getSolrClient());
     waitForDeletion(collectionName);
   }
-
-  @SuppressWarnings("unchecked")
-  private Object getUpdateHandlerMetric(QueryResponse statsResponse, String 
metric) {
-    NamedList<Object> entries = statsResponse.getResponse();
-    return ((Map<String, Object>)
-            entries._get(List.of("plugins", "UPDATE", "updateHandler", 
"stats"), null))
-        .get(metric);
-  }
 }
diff --git a/solr/core/src/test/org/apache/solr/cloud/TestTlogReplica.java 
b/solr/core/src/test/org/apache/solr/cloud/TestTlogReplica.java
index f89aa37f90b..eea30196557 100644
--- a/solr/core/src/test/org/apache/solr/cloud/TestTlogReplica.java
+++ b/solr/core/src/test/org/apache/solr/cloud/TestTlogReplica.java
@@ -257,9 +257,6 @@ public class TestTlogReplica extends SolrCloudTestCase {
   }
 
   @SuppressWarnings("unchecked")
-  // NOCOMMIT: This test is broken from OTEL migration and the /admin/plugins 
endpoint. Placing
-  // BadApple test but this must be fixed before this feature gets merged to a 
release branch
-  @AwaitsFix(bugUrl = "https://issues.apache.org/jira/browse/SOLR-17458";)
   public void testAddDocs() throws Exception {
     int numTlogReplicas = 1 + random().nextInt(3);
     DocCollection docCollection = createAndWaitForCollection(1, 0, 
numTlogReplicas, 0);
@@ -283,22 +280,29 @@ public class TestTlogReplica extends SolrCloudTestCase {
                 "Replica " + r.getName() + " not up to date after 10 seconds",
                 1,
                 tlogReplicaClient.query(new 
SolrQuery("*:*")).getResults().getNumFound());
-            // Append replicas process all updates
-            SolrQuery req =
-                new SolrQuery(
-                    "qt", "/admin/plugins",
-                    "stats", "true");
-            QueryResponse statsResponse = tlogReplicaClient.query(req);
-            NamedList<Object> entries = (statsResponse.getResponse());
-            assertEquals(
-                "Append replicas should recive all updates. Replica: "
-                    + r
-                    + ", response: "
-                    + statsResponse,
-                1L,
-                ((Map<String, Object>)
-                        entries._get(List.of("plugins", "UPDATE", 
"updateHandler", "stats"), null))
-                    .get("UPDATE.updateHandler.cumulativeAdds.count"));
+            JettySolrRunner jetty =
+                cluster.getJettySolrRunners().stream()
+                    .filter(j -> 
j.getBaseUrl().toString().equals(r.getBaseUrl()))
+                    .findFirst()
+                    .orElse(null);
+            assertNotNull("Could not find jetty for replica " + r, jetty);
+
+            try (SolrCore core = 
jetty.getCoreContainer().getCore(r.getCoreName())) {
+              var cumulativeAddsDatapoint =
+                  SolrMetricTestUtils.getGaugeDatapoint(
+                      core,
+                      "solr_core_update_cumulative_ops",
+                      SolrMetricTestUtils.newCloudLabelsBuilder(core)
+                          .label("category", "UPDATE")
+                          .label("ops", "adds")
+                          .build());
+              assertNotNull("Could not find cumulative adds metric", 
cumulativeAddsDatapoint);
+              assertEquals(
+                  "Append replicas should receive all updates. Replica: " + r,
+                  1.0,
+                  cumulativeAddsDatapoint.getValue(),
+                  0.0);
+            }
             break;
           } catch (AssertionError e) {
             if (t.hasTimedOut()) {
diff --git a/solr/core/src/test/org/apache/solr/core/SolrCoreTest.java 
b/solr/core/src/test/org/apache/solr/core/SolrCoreTest.java
index 5af9bfe0436..3e2d3589958 100644
--- a/solr/core/src/test/org/apache/solr/core/SolrCoreTest.java
+++ b/solr/core/src/test/org/apache/solr/core/SolrCoreTest.java
@@ -97,12 +97,8 @@ public class SolrCoreTest extends SolrTestCaseJ4 {
       ++ihCount;
       assertEquals(pathToClassMap.get("/admin/luke"), 
"solr.LukeRequestHandler");
       ++ihCount;
-      assertEquals(pathToClassMap.get("/admin/mbeans"), 
"solr.SolrInfoMBeanHandler");
-      ++ihCount;
       assertEquals(pathToClassMap.get("/admin/ping"), 
"solr.PingRequestHandler");
       ++ihCount;
-      assertEquals(pathToClassMap.get("/admin/plugins"), 
"solr.PluginInfoHandler");
-      ++ihCount;
       assertEquals(pathToClassMap.get("/admin/segments"), 
"solr.SegmentsInfoRequestHandler");
       ++ihCount;
       assertEquals(pathToClassMap.get("/admin/system"), 
"solr.SystemInfoHandler");
diff --git a/solr/core/src/test/org/apache/solr/core/TestSolrConfigHandler.java 
b/solr/core/src/test/org/apache/solr/core/TestSolrConfigHandler.java
index 927a40a47a8..bfcef82ee4e 100644
--- a/solr/core/src/test/org/apache/solr/core/TestSolrConfigHandler.java
+++ b/solr/core/src/test/org/apache/solr/core/TestSolrConfigHandler.java
@@ -153,8 +153,6 @@ public class TestSolrConfigHandler extends RestTestBase {
     MapWriter confMap = getRespMap("/config", harness);
     assertNotNull(confMap._get(asList("config", "requestHandler", 
"/admin/luke"), null));
     assertNotNull(confMap._get(asList("config", "requestHandler", 
"/admin/system"), null));
-    assertNotNull(confMap._get(asList("config", "requestHandler", 
"/admin/mbeans"), null));
-    assertNotNull(confMap._get(asList("config", "requestHandler", 
"/admin/plugins"), null));
     assertNotNull(confMap._get(asList("config", "requestHandler", 
"/admin/file"), null));
     assertNotNull(confMap._get(asList("config", "requestHandler", 
"/admin/ping"), null));
 
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/MBeansHandlerTest.java 
b/solr/core/src/test/org/apache/solr/handler/admin/MBeansHandlerTest.java
deleted file mode 100644
index be55062f79f..00000000000
--- a/solr/core/src/test/org/apache/solr/handler/admin/MBeansHandlerTest.java
+++ /dev/null
@@ -1,240 +0,0 @@
-/*
- * 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.handler.admin;
-
-import io.opentelemetry.api.common.Attributes;
-import java.lang.invoke.MethodHandles;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import org.apache.lucene.tests.util.LuceneTestCase;
-import org.apache.solr.SolrTestCaseJ4;
-import org.apache.solr.common.params.CommonParams;
-import org.apache.solr.common.util.ContentStream;
-import org.apache.solr.common.util.ContentStreamBase;
-import org.apache.solr.common.util.NamedList;
-import org.apache.solr.core.SolrInfoBean;
-import org.apache.solr.metrics.SolrMetricsContext;
-import org.apache.solr.request.LocalSolrQueryRequest;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-// NOCOMMIT: Test fails because of the /admin/mbeans endpoint. Need to create 
a bridge or shim after
-// migrating all metrics to support this.
[email protected](bugUrl = 
"https://issues.apache.org/jira/browse/SOLR-17458";)
-public class MBeansHandlerTest extends SolrTestCaseJ4 {
-  private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
-
-  @BeforeClass
-  public static void beforeClass() throws Exception {
-    initCore("solrconfig.xml", "schema.xml");
-  }
-
-  @Test
-  public void testDiff() throws Exception {
-    String xml =
-        h.query(req(CommonParams.QT, "/admin/mbeans", "stats", "true", 
CommonParams.WT, "xml"));
-    List<ContentStream> streams = new ArrayList<>();
-    streams.add(new ContentStreamBase.StringStream(xml));
-
-    LocalSolrQueryRequest req =
-        lrf.makeRequest(
-            CommonParams.QT,
-            "/admin/mbeans",
-            "stats",
-            "true",
-            CommonParams.WT,
-            "xml",
-            "diff",
-            "true");
-    req.setContentStreams(streams);
-
-    xml = h.query(req);
-    NamedList<NamedList<NamedList<Object>>> diff = 
SolrInfoMBeanHandler.fromXML(xml);
-
-    // The stats bean for SolrInfoMBeanHandler
-    NamedList<?> stats = (NamedList<?>) 
diff.get("ADMIN").get("/admin/mbeans").get("stats");
-
-    // System.out.println("stats:"+stats);
-    Pattern p =
-        Pattern.compile("Was: (?<was>[0-9]+), Now: (?<now>[0-9]+), Delta: 
(?<delta>[0-9]+)");
-    String response = stats.get("ADMIN./admin/mbeans.requests").toString();
-    Matcher m = p.matcher(response);
-    if (!m.matches()) {
-      fail("Response did not match pattern: " + response);
-    }
-
-    assertEquals(1, Integer.parseInt(m.group("delta")));
-    int was = Integer.parseInt(m.group("was"));
-    int now = Integer.parseInt(m.group("now"));
-    assertEquals(1, now - was);
-
-    xml =
-        h.query(
-            req(
-                CommonParams.QT,
-                "/admin/mbeans",
-                "stats",
-                "true",
-                "key",
-                "org.apache.solr.handler.admin.CollectionsHandler"));
-    NamedList<NamedList<NamedList<Object>>> nl = 
SolrInfoMBeanHandler.fromXML(xml);
-    
assertNotNull(nl.get("ADMIN").get("org.apache.solr.handler.admin.CollectionsHandler"));
-  }
-
-  @Test
-  public void testAddedMBeanDiff() throws Exception {
-    String xml =
-        h.query(req(CommonParams.QT, "/admin/mbeans", "stats", "true", 
CommonParams.WT, "xml"));
-
-    // Artificially convert a long value to a null, to trigger the ADD case in
-    // SolrInfoMBeanHandler.diffObject()
-    xml =
-        xml.replaceFirst(
-            
"<long\\s+(name\\s*=\\s*\"ADMIN./admin/mbeans.totalTime\"\\s*)>[^<]*</long>",
-            "<null $1/>");
-
-    LocalSolrQueryRequest req =
-        lrf.makeRequest(
-            CommonParams.QT,
-            "/admin/mbeans",
-            "stats",
-            "true",
-            CommonParams.WT,
-            "xml",
-            "diff",
-            "true");
-    req.setContentStreams(Collections.singletonList(new 
ContentStreamBase.StringStream(xml)));
-    xml = h.query(req);
-
-    NamedList<NamedList<NamedList<Object>>> nl = 
SolrInfoMBeanHandler.fromXML(xml);
-    assertNotNull(
-        ((NamedList<?>) nl.get("ADMIN").get("/admin/mbeans").get("stats"))
-            .get("ADD ADMIN./admin/mbeans.totalTime"));
-  }
-
-  @Test
-  public void testXMLDiffWithExternalEntity() {
-    String file = getFile("mailing_lists.pdf").toUri().toASCIIString();
-    String xml =
-        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
-            + "<!DOCTYPE foo [<!ENTITY bar SYSTEM \""
-            + file
-            + "\">]>\n"
-            + "<response>\n"
-            + "&bar;"
-            + "<lst name=\"responseHeader\"><int name=\"status\">0</int><int 
name=\"QTime\">31</int></lst><lst name=\"solr-mbeans\"></lst>\n"
-            + "</response>";
-
-    NamedList<NamedList<NamedList<Object>>> nl = 
SolrInfoMBeanHandler.fromXML(xml);
-
-    assertTrue("external entity ignored properly", true);
-  }
-
-  boolean runSnapshots;
-
-  @Test
-  public void testMetricsSnapshot() throws Exception {
-    final CountDownLatch counter = new CountDownLatch(500);
-    SolrInfoBean bean =
-        new SolrInfoBean() {
-          SolrMetricsContext solrMetricsContext;
-
-          @Override
-          public String getName() {
-            return "foo";
-          }
-
-          @Override
-          public String getDescription() {
-            return "foo";
-          }
-
-          @Override
-          public Category getCategory() {
-            return Category.ADMIN;
-          }
-
-          @Override
-          public void initializeMetrics(
-              SolrMetricsContext parentContext, Attributes attributes, String 
scope) {
-            this.solrMetricsContext = parentContext.getChildContext(this);
-          }
-
-          @Override
-          public SolrMetricsContext getSolrMetricsContext() {
-            return solrMetricsContext;
-          }
-        };
-    bean.initializeMetrics(
-        new SolrMetricsContext(
-            h.getCoreContainer().getMetricManager(), "testMetricsSnapshot", 
"foobar"),
-        Attributes.empty(),
-        "foo");
-    runSnapshots = true;
-    Thread modifier =
-        new Thread(
-            () -> {
-              int i = 0;
-              while (runSnapshots) {
-                bean.getSolrMetricsContext().registerMetricName("name-" + i++);
-                try {
-                  Thread.sleep(31);
-                } catch (InterruptedException e) {
-                  runSnapshots = false;
-                  break;
-                }
-              }
-            });
-    Thread reader =
-        new Thread(
-            () -> {
-              while (runSnapshots) {
-                try {
-                  bean.getSolrMetricsContext().getMetricsSnapshot();
-                } catch (Exception e) {
-                  runSnapshots = false;
-                  log.error("Exception getting metrics snapshot", e);
-                  fail("Exception getting metrics snapshot: " + e);
-                }
-                try {
-                  Thread.sleep(53);
-                } catch (InterruptedException e) {
-                  Thread.currentThread().interrupt();
-                  log.error("interrupted", e);
-                  runSnapshots = false;
-                  break;
-                }
-                counter.countDown();
-              }
-            });
-    modifier.start();
-    reader.start();
-    counter.await(30, TimeUnit.SECONDS);
-    runSnapshots = false;
-    bean.close();
-
-    reader.join();
-    modifier.join();
-  }
-}
diff --git a/solr/server/etc/security.policy b/solr/server/etc/security.policy
index 2e65fc70e07..a779dfbc7a2 100644
--- a/solr/server/etc/security.policy
+++ b/solr/server/etc/security.policy
@@ -132,7 +132,6 @@ grant {
   permission javax.management.MBeanServerPermission "releaseMBeanServer";
   permission javax.management.MBeanTrustPermission "register";
 
-  
   // needed by crossdc
   permission javax.security.auth.AuthPermission "getLoginConfiguration";
   permission javax.security.auth.AuthPermission "setLoginConfiguration";
@@ -218,3 +217,8 @@ grant {
   // expanded to a wildcard if set, allows all networking everywhere
   permission java.net.SocketPermission "${solr.internal.network.permission}", 
"accept,listen,connect,resolve";
 };
+
+// Permissions for OTEL Runtime Java 17 telemetry and metrics
+grant {
+  permission jdk.jfr.FlightRecorderPermission "accessFlightRecorder";
+};
diff --git a/solr/webapp/web/css/angular/plugins.css 
b/solr/webapp/web/css/angular/plugins.css
index e4398bda20c..775287184da 100644
--- a/solr/webapp/web/css/angular/plugins.css
+++ b/solr/webapp/web/css/angular/plugins.css
@@ -218,3 +218,18 @@ limitations under the License.
 {
   background-image: url( ../../img/ico/new-text.png );
 }
+
+.prometheus-metric {
+  font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+  font-size: 11px;
+  color: #0066cc;
+  background-color: #f5f5f5;
+  padding: 2px 4px;
+  border-radius: 3px;
+}
+
+#content #plugins .stats dl dd.prometheus-metric {
+  word-break: break-all;
+  white-space: pre-wrap;
+}
+
diff --git a/solr/webapp/web/js/angular/controllers/plugins.js 
b/solr/webapp/web/js/angular/controllers/plugins.js
index 5bf7ab5edaa..d3abd35a903 100644
--- a/solr/webapp/web/js/angular/controllers/plugins.js
+++ b/solr/webapp/web/js/angular/controllers/plugins.js
@@ -15,10 +15,8 @@
  limitations under the License.
 */
 
-//NOCOMMIT: This plugin seems tied to the Admin UIs plugin management but is 
tied to dropwizard metrics failing some tests.
-// This needs to change how it gets these metrics or we need to make a shim to 
the /admin/plugins handler for this to support it
 solrAdminApp.controller('PluginsController',
-    function($scope, $rootScope, $routeParams, $location, Mbeans, Constants) {
+    function($scope, $rootScope, $routeParams, $location, Metrics, Constants) {
         $scope.resetMenu("plugins", Constants.IS_CORE_PAGE);
 
         if ($routeParams.legacytype) {
@@ -29,9 +27,15 @@ solrAdminApp.controller('PluginsController',
         }
 
         $scope.refresh = function() {
-            Mbeans.stats({core: $routeParams.core}, function (data) {
-                var type = $location.search().type;
-                $scope.types = getPluginTypes(data, type);
+            var params = {};
+            if ($routeParams.core) {
+                params.core = $routeParams.core;
+            }
+
+            var type = $location.search().type;
+
+            Metrics.prometheus(params, function (response) {
+                $scope.types = getPluginTypesFromMetrics(response.data, type);
                 $scope.type = getSelectedType($scope.types, type);
 
                 if ($scope.type && $routeParams.entry) {
@@ -64,98 +68,153 @@ solrAdminApp.controller('PluginsController',
             }
         }
 
-        $scope.startRecording = function() {
-            $scope.isRecording = true;
-            Mbeans.reference({core: $routeParams.core}, function(data) {
-                $scope.reference = data.reference;
-                console.log($scope.reference);
-            })
+        $scope.refresh();
+    });
+
+var getPluginTypesFromMetrics = function(metricsText, selected) {
+    var keys = [];
+
+    // Parse Prometheus format metrics
+    var lines = metricsText.split('\n');
+    var categoriesMap = {};
+    var metricMetadata = {}; // Store HELP and TYPE info for each metric
+
+    for (var i = 0; i < lines.length; i++) {
+        var line = lines[i].trim();
+
+        // Skip empty lines
+        if (line === '') {
+            continue;
         }
 
-        $scope.stopRecording = function() {
-            $scope.isRecording = false;
-            console.log($scope.reference);
-            Mbeans.delta({core: $routeParams.core}, $scope.reference, 
function(data) {
-                parseDelta($scope.types, data);
-            });
+        // Parse HELP comments - format: # HELP metric_name description
+        if (line.startsWith('# HELP ')) {
+            var helpMatch = line.match(/^# HELP\s+([^\s]+)\s+(.*)$/);
+            if (helpMatch) {
+                var metricName = helpMatch[1];
+                var description = helpMatch[2];
+                if (!metricMetadata[metricName]) {
+                    metricMetadata[metricName] = {};
+                }
+                metricMetadata[metricName].description = description;
+            }
+            continue;
         }
 
-        $scope.refresh();
-    });
+        // Parse TYPE comments - format: # TYPE metric_name type
+        if (line.startsWith('# TYPE ')) {
+            var typeMatch = line.match(/^# TYPE\s+([^\s]+)\s+(.*)$/);
+            if (typeMatch) {
+                var metricName = typeMatch[1];
+                var type = typeMatch[2];
+                if (!metricMetadata[metricName]) {
+                    metricMetadata[metricName] = {};
+                }
+                metricMetadata[metricName].type = type;
+            }
+            continue;
+        }
 
-var getPluginTypes = function(data, selected) {
-    var keys = [];
-    var mbeans = data["solr-mbeans"];
-    for (var i=0; i<mbeans.length; i+=2) {
-        var key = mbeans[i];
-        var lower = key.toLowerCase();
-        var plugins = getPlugins(mbeans[i+1]);
-        if (plugins.length == 0) continue;
-        keys.push({name: key,
-                   selected: lower == selected,
-                   changes: 0,
-                   lower: lower,
-                   plugins: plugins
-        });
-    }
-    keys.sort(function(a,b) {return a.name > b.name});
-    return keys;
-};
+        // Skip other comments
+        if (line.startsWith('#')) {
+            continue;
+        }
 
-var getPlugins = function(data) {
-    var plugins = [];
-    for (var key in data) {
-        var pluginProperties = data[key];
-        var stats = pluginProperties.stats;
-        delete pluginProperties.stats;
-        for (var stat in stats) {
-            // add breaking space after a bracket or @ to handle wrap long 
lines:
-            stats[stat] = new String(stats[stat]).replace( /([\(@])/g, 
'$1&#8203;');
+        // Parse metric line - format: metric_name{labels} value timestamp
+        var metricMatch = 
line.match(/^([a-zA-Z_:][a-zA-Z0-9_:]*)\{([^}]*)\}\s+([^\s]+)(\s+[^\s]+)?$/);
+        if (!metricMatch) {
+            // Try without labels - format: metric_name value timestamp
+            metricMatch = 
line.match(/^([a-zA-Z_:][a-zA-Z0-9_:]*)\s+([^\s]+)(\s+[^\s]+)?$/);
+            if (metricMatch) {
+                // Skip metrics without category labels for prometheus format
+                continue;
+            }
+            continue;
         }
-        plugin = {name: key, changed: false, stats: stats, open:false};
-        plugin.properties = pluginProperties;
-        plugins.push(plugin);
-    }
-    plugins.sort(function(a,b) {return a.name > b.name});
-    return plugins;
-};
 
-var getSelectedType = function(types, selected) {
-    if (selected) {
-        for (var i in types) {
-            if (types[i].lower == selected) {
-                return types[i];
+        var metricName = metricMatch[1];
+        var labelsStr = metricMatch[2];
+        var value = metricMatch[3];
+
+        // Parse labels
+        var labels = {};
+        if (labelsStr) {
+            var labelPairs = labelsStr.split(',');
+            for (var j = 0; j < labelPairs.length; j++) {
+                var labelMatch = 
labelPairs[j].trim().match(/^([^=]+)="([^"]*)"$/);
+                if (labelMatch) {
+                    labels[labelMatch[1]] = labelMatch[2];
+                }
             }
         }
-    }
-};
 
-var parseDelta = function(types, data) {
+        // Use category from labels only - don't fall back to metric name 
parsing
+        var category = labels.category;
+
+        // Skip metrics that don't have a category label
+        if (!category) {
+            continue;
+        }
+
+        if (!categoriesMap[category]) {
+            categoriesMap[category] = {};
+        }
 
-    var getByName = function(list, name) {
-        for (var i in list) {
-            if (list[i].name == name) return list[i];
+        if (!categoriesMap[category][metricName]) {
+            categoriesMap[category][metricName] = {};
         }
+
+        // Create a descriptive key for the metric variant
+        var labelParts = [];
+        for (var labelKey in labels) {
+            if (labelKey !== 'category') {
+                labelParts.push(labelKey + '=' + labels[labelKey]);
+            }
+        }
+        var variantKey = labelParts.length > 0 ? labelParts.join(', ') : 
'default';
+
+        categoriesMap[category][metricName][variantKey] = value;
     }
 
-    var mbeans = data["solr-mbeans"]
-    for (var i=0; i<mbeans.length; i+=2) {
-        var typeName = mbeans[i];
-        var type = getByName(types, typeName);
-        var plugins = mbeans[i+1];
-        for (var key in plugins) {
-            var changedPlugin = plugins[key];
-            if (changedPlugin._changed_) {
-                var plugin = getByName(type.plugins, key);
-                var stats = changedPlugin.stats;
-                delete changedPlugin.stats;
-                plugin.properties = changedPlugin;
-                for (var stat in stats) {
-                    // add breaking space after a bracket or @ to handle wrap 
long lines:
-                    plugin.stats[stat] = new String(stats[stat]).replace( 
/([\(@])/g, '$1&#8203;');
+    // Convert to the expected format
+    for (var categoryName in categoriesMap) {
+        var lower = categoryName.toLowerCase();
+        var metrics = [];
+
+        for (var metricName in categoriesMap[categoryName]) {
+            var metricData = categoriesMap[categoryName][metricName];
+            var metadata = metricMetadata[metricName] || {};
+            metrics.push({
+                name: metricName,
+                changed: false,
+                stats: metricData,
+                open: false,
+                properties: {
+                    description: metadata.description,
+                    type: metadata.type
                 }
-                plugin.changed = true;
-                type.changes++;
+            });
+        }
+
+        if (metrics.length > 0) {
+            keys.push({
+                name: categoryName,
+                selected: lower == selected,
+                changes: 0,
+                lower: lower,
+                plugins: metrics
+            });
+        }
+    }
+
+    return keys;
+};
+
+var getSelectedType = function(types, selected) {
+    if (selected) {
+        for (var i in types) {
+            if (types[i].lower == selected) {
+                return types[i];
             }
         }
     }
diff --git a/solr/webapp/web/js/angular/services.js 
b/solr/webapp/web/js/angular/services.js
index 0a2f5d95266..265873ac9fb 100644
--- a/solr/webapp/web/js/angular/services.js
+++ b/solr/webapp/web/js/angular/services.js
@@ -23,7 +23,15 @@ solrAdminServices.factory('System',
   }])
 .factory('Metrics',
     ['$resource', function($resource) {
-      return $resource('admin/metrics', {"wt":"json", "nodes": "@nodes", 
"prefix":"@prefix", "_":Date.now()});
+      return $resource('admin/metrics', {"wt":"json", "nodes": "@nodes", 
"prefix":"@prefix", "core":"@core", "_":Date.now()}, {
+        "prometheus": {
+          method: 'GET',
+          params: {wt: 'prometheus', core: '@core'},
+          transformResponse: function(data) {
+            return {data: data};
+          }
+        }
+      });
     }])
 .factory('CollectionsV2',
     function() {
@@ -200,25 +208,6 @@ solrAdminServices.factory('System',
      "status": {params:{action:"status"}, headers: {doNotIntercept: "true"}
     }});
   }])
-.factory('Mbeans',
-  ['$resource', function($resource) {
-    return $resource(':core/admin/mbeans', {'wt':'json', core: '@core', 
'_':Date.now()}, {
-        stats: {params: {stats: true}},
-        info: {},
-        reference: {
-            params: {wt: "xml", stats: true}, transformResponse: function 
(data) {
-                return {reference: data}
-            }
-        },
-        delta: {method: "POST",
-                params: {stats: true, diff:true},
-                headers: {'Content-type': 'application/x-www-form-urlencoded'},
-                transformRequest: function(data) {
-                    return "stream.body=" + encodeURIComponent(data);
-                }
-        }
-    });
-  }])
 .factory('Files',
   ['$resource', function($resource) {
     return $resource(':core/admin/file', {'wt':'json', core: '@core', 
'_':Date.now()}, {
@@ -424,6 +413,6 @@ solrAdminServices.factory('System',
           }
           return params;
         };
-        
+
         return service;
       }]);
diff --git a/solr/webapp/web/partials/plugins.html 
b/solr/webapp/web/partials/plugins.html
index aaa424d3b81..1e80b6f9b6e 100644
--- a/solr/webapp/web/partials/plugins.html
+++ b/solr/webapp/web/partials/plugins.html
@@ -31,12 +31,12 @@ limitations under the License.
             </dl>
           </li>
           <li class="stats clearfix" ng-show="plugin.stats">
-            <span>stats:</span>
+            <span>metrics:</span>
             <ul>
               <li ng-repeat="(key, value) in plugin.stats" ng-class="{odd: 
$odd}">
                   <dl class="clearfix">
                       <dt>{{key}}:</dt>
-                      <dd>{{value}}</dd>
+                      <dd class="prometheus-metric">{{value}}</dd>
                   </dl>
               </li>
             </ul>
@@ -53,20 +53,9 @@ limitations under the License.
                 <span ng-show="typeObj.changes">{{typeObj.changes}}</span>
             </a>
         </li>
-        <li class="PLUGINCHANGES"><a ng-click="startRecording()">Watch 
Changes</a></li>
         <li class="RELOAD"><a ng-click="refresh()">Refresh Values</a></li>
-        <li class="NOTE"><p>NOTE: Only selected metrics are shown here. Full 
metrics can be accessed via /admin/metrics handler.</p></li>
+        <li class="NOTE"><p>NOTE: Metrics are displayed in Prometheus format. 
Full metrics can be accessed via /admin/metrics handler.</p></li>
     </ul>
   </div>
 
-  <div id="recording" ng-show="isRecording">
-    <div class="wrapper clearfix">
-
-      <p class="loader">Watching for Changes</p>
-      <button class="primary" ng-click="stopRecording()">Stop &amp; Show 
Changes</button>
-
-    </div>
-    <div id="blockUI"></div>
-  </div>
-
 </div>

Reply via email to