Copilot commented on code in PR #3908:
URL: https://github.com/apache/solr/pull/3908#discussion_r2576163348


##########
solr/core/src/java/org/apache/solr/handler/admin/AdminHandlersProxy.java:
##########
@@ -129,9 +134,169 @@ public static CompletableFuture<NamedList<Object>> 
callRemoteNode(
     URI baseUri = 
URI.create(zkController.zkStateReader.getBaseUrlForNodeName(nodeName));
     SolrRequest<?> proxyReq = new GenericSolrRequest(SolrRequest.METHOD.GET, 
uriPath, params);
 
+    // Set response parser based on wt parameter to ensure correct format is 
used
+    String wt = params.get("wt");
+    if ("prometheus".equals(wt) || "openmetrics".equals(wt)) {
+      proxyReq.setResponseParser(new InputStreamResponseParser(wt));
+    }
+
     return zkController
         .getCoreContainer()
         .getDefaultHttpSolrClient()
         .requestWithBaseUrl(baseUri.toString(), c -> c.requestAsync(proxyReq));
   }
+
+  /**
+   * Resolve node names from the "nodes" parameter into a set of live node 
names.
+   *
+   * @param nodeNames the value of the "nodes" parameter ("all" or 
comma-separated node names)
+   * @param container the CoreContainer
+   * @return set of resolved node names
+   * @throws SolrException if node format is invalid or node is not in cluster
+   */
+  private static Set<String> resolveNodes(String nodeNames, CoreContainer 
container) {
+    Set<String> liveNodes =
+        
container.getZkController().zkStateReader.getClusterState().getLiveNodes();
+
+    if (nodeNames.equals("all")) {
+      log.debug("All live nodes requested");
+      return liveNodes;
+    }
+
+    Set<String> nodes = new HashSet<>(Arrays.asList(nodeNames.split(",")));
+    for (String nodeName : nodes) {
+      if (!nodeName.matches("^[^/:]+:\\d+_[\\w/]+$")) {
+        throw new SolrException(
+            SolrException.ErrorCode.BAD_REQUEST, "Parameter " + PARAM_NODES + 
" has wrong format");
+      }
+      if (!liveNodes.contains(nodeName)) {
+        throw new SolrException(
+            SolrException.ErrorCode.BAD_REQUEST,
+            "Requested node " + nodeName + " is not part of cluster");
+      }
+    }
+    log.debug("Nodes requested: {}", nodes);
+    return nodes;
+  }
+
+  /** Handle Prometheus format by fetching from nodes and merging text 
responses. */
+  private static void handlePrometheusFormat(
+      Set<String> nodes,
+      String pathStr,
+      SolrParams params,
+      CoreContainer container,
+      SolrQueryResponse rsp)
+      throws IOException, SolrServerException, InterruptedException {
+
+    ZkController zkController = container.getZkController();
+    Map<String, Future<NamedList<Object>>> responses = new LinkedHashMap<>();
+
+    // Ensure wt=prometheus for all requests
+    ModifiableSolrParams prometheusParams = new ModifiableSolrParams(params);
+    if (!prometheusParams.get("wt", "").equals("prometheus")) {
+      prometheusParams.set("wt", "prometheus");
+    }
+
+    // Submit all requests
+    for (String node : nodes) {
+      responses.put(node, callRemoteNode(node, pathStr, prometheusParams, 
zkController));
+    }
+
+    // Collect all Prometheus text responses
+    StringBuilder mergedText = new StringBuilder();
+    for (Map.Entry<String, Future<NamedList<Object>>> entry : 
responses.entrySet()) {
+      try {
+        NamedList<Object> resp =
+            entry.getValue().get(PROMETHEUS_FETCH_TIMEOUT_SECONDS, 
TimeUnit.SECONDS);
+
+        // Extract text from InputStream response
+        Object streamObj = resp.get("stream");
+        if (streamObj instanceof InputStream) {
+          try (InputStream stream = (InputStream) streamObj) {
+            String prometheusText = new String(stream.readAllBytes(), 
StandardCharsets.UTF_8);
+            if (!prometheusText.isEmpty()) {
+              // Inject node label into each metric line
+              String labeledText = injectNodeLabelIntoText(prometheusText, 
entry.getKey());
+              mergedText.append(labeledText);
+            }
+          } catch (IOException ioe) {
+            log.warn("IOException when reading stream from node {}", 
entry.getKey(), ioe);
+          }
+        } else {
+          log.warn("No stream in response from node {}", entry.getKey());
+        }
+      } catch (ExecutionException ee) {
+        log.warn("Exception when fetching Prometheus result from node {}", 
entry.getKey(), ee);
+      } catch (TimeoutException te) {
+        log.warn("Timeout when fetching Prometheus result from node {}", 
entry.getKey(), te);
+      }
+    }
+
+    // Store the merged text in response - will be written as-is
+    rsp.add("prometheusText", mergedText.toString());
+  }
+
+  /**
+   * Escape special characters in Prometheus label values according to 
Prometheus specification.
+   * Escapes backslash, double quote, and newline characters.
+   */
+  private static String escapePrometheusLabelValue(String value) {
+    return value.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", 
"\\n");
+  }
+
+  /**
+   * Inject node="nodeName" label into Prometheus text format. Each metric 
line gets the node label
+   * added. Uses regex matching to robustly handle metric lines with or 
without labels, values in
+   * scientific notation, and optional timestamps.
+   */
+  private static String injectNodeLabelIntoText(String prometheusText, String 
nodeName) {
+    StringBuilder result = new StringBuilder();
+    String[] lines = prometheusText.split("\n");
+    String escapedNodeName = escapePrometheusLabelValue(nodeName);
+
+    // Regex to match Prometheus metric lines:
+    // Group 1: metric name
+    // Group 2: labels (optional, with braces)
+    // Group 3: value and optional timestamp
+    java.util.regex.Pattern pattern =
+        
java.util.regex.Pattern.compile("^([a-zA-Z_:][a-zA-Z0-9_:]*)(?:(\\{[^}]*\\}))?\\s+(.+)$");

Review Comment:
   The regex pattern `\\{[^}]*\\}` may incorrectly match label sets containing 
escaped closing braces within quoted label values. For example, a metric line 
like `metric{label="value\\}"} 123` would match incorrectly because `[^}]*` 
stops at the first `}` even if it's escaped within a quoted string.
   
   While this is a rare edge case, consider using a more robust pattern that 
accounts for quoted strings:
   ```java
   
java.util.regex.Pattern.compile("^([a-zA-Z_:][a-zA-Z0-9_:]*)(?:(\\{(?:[^{}]|\"(?:[^\"\\\\]|\\\\.)*\")*\\}))?\\s+(.+)$");
   ```
   
   This pattern allows either non-brace characters or quoted strings (which can 
contain escaped characters including braces) within the label set.
   ```suggestion
           
java.util.regex.Pattern.compile("^([a-zA-Z_:][a-zA-Z0-9_:]*)(?:(\\{(?:[^{}]|\"(?:[^\"\\\\]|\\\\.)*\")*\\}))?\\s+(.+)$");
   ```



##########
solr/webapp/web/js/angular/controllers/cloud.js:
##########
@@ -394,118 +394,138 @@ var nodesSubController = function($scope, Collections, 
System, Metrics) {
      Fetch metrics for all selected nodes. Only pull the metrics that we'll 
show to save bandwidth
      Pick the data we want to display and add it to the node-centric data 
structure
       */
-    Metrics.get({
-          "nodes": nodesParam,
-          "prefix": 
"CONTAINER.fs,org.eclipse.jetty.server.handler.DefaultHandler.get-requests,INDEX.sizeInBytes,SEARCHER.searcher.numDocs,SEARCHER.searcher.deletedDocs,SEARCHER.searcher.warmupTime"
-        },
-        function (metricsResponse) {
-          for (var node in metricsResponse) {
-            if (node in nodes) {
-              var m = metricsResponse[node];
-              nodes[node]['metrics'] = m;
-              var diskTotal = 
m.metrics['solr.node']['CONTAINER.fs.totalSpace'];
-              var diskFree = 
m.metrics['solr.node']['CONTAINER.fs.usableSpace'];
-              var diskPercentage = Math.floor((diskTotal - diskFree) / 
diskTotal * 100);
-              nodes[node]['diskUsedPct'] = diskPercentage;
-              nodes[node]['diskUsedPctStyle'] = styleForPct(diskPercentage);
-              nodes[node]['diskTotal'] = bytesToSize(diskTotal);
-              nodes[node]['diskFree'] = bytesToSize(diskFree);
-
-              var r = 
m.metrics['solr.jetty']['org.eclipse.jetty.server.handler.DefaultHandler.get-requests'];
-              nodes[node]['req'] = r.count;
-              nodes[node]['req1minRate'] = Math.floor(r['1minRate'] * 100) / 
100;
-              nodes[node]['req5minRate'] = Math.floor(r['5minRate'] * 100) / 
100;
-              nodes[node]['req15minRate'] = Math.floor(r['15minRate'] * 100) / 
100;
-              nodes[node]['reqp75_ms'] = Math.floor(r['p75_ms']);
-              nodes[node]['reqp95_ms'] = Math.floor(r['p95_ms']);
-              nodes[node]['reqp99_ms'] = Math.floor(r['p99_ms']);
-
-              // These are the cores we _expect_ to find on this node 
according to the CLUSTERSTATUS
-              var cores = nodes[node]['cores'];
-              var indexSizeTotal = 0;
-              var indexSizeMax = 0;
-              var docsTotal = 0;
-              var graphData = [];
-              for (let coreId in cores) {
-                var core = cores[coreId];
-                if (core['shard_state'] !== 'active' || core['state'] !== 
'active') {
-                  // If core state is not active, display the real state, or 
if shard is inactive, display that
-                  var labelState = (core['state'] !== 'active') ? 
core['state'] : core['shard_state'];
-                  core['label'] += "_(" + labelState + ")";
-                }
-                var coreMetricName = "solr.core." + core['collection'] + "." + 
core['shard'] + "." + core['replica'];
-                var coreMetric = m.metrics[coreMetricName];
-                // we may not actually get metrics back for every expected 
core (the core may be down)
-                if (coreMetric) {
-                  var size = coreMetric['INDEX.sizeInBytes'];
-                  size = (typeof size !== 'undefined') ? size : 0;
-                  core['sizeInBytes'] = size;
-                  core['size'] = bytesToSize(size);
-                  indexSizeTotal = indexSizeTotal + size;
-                  indexSizeMax = size > indexSizeMax ? size : indexSizeMax;
-                  var numDocs = coreMetric['SEARCHER.searcher.numDocs'];
-                  numDocs = (typeof numDocs !== 'undefined') ? numDocs : 0;
-                  core['numDocs'] = numDocs;
-                  core['numDocsHuman'] = numDocsHuman(numDocs);
-                  core['avgSizePerDoc'] = bytesToSize(numDocs === 0 ? 0 : size 
/ numDocs);
-                  var deletedDocs = 
coreMetric['SEARCHER.searcher.deletedDocs'];
-                  deletedDocs = (typeof deletedDocs !== 'undefined') ? 
deletedDocs : 0;
-                  core['deletedDocs'] = deletedDocs;
-                  core['deletedDocsHuman'] = numDocsHuman(deletedDocs);
-                  var warmupTime = coreMetric['SEARCHER.searcher.warmupTime'];
-                  warmupTime = (typeof warmupTime !== 'undefined') ? 
warmupTime : 0;
-                  core['warmupTime'] = warmupTime;
-                  docsTotal += core['numDocs'];
-                }
-              }
-              for (let coreId in cores) {
-                var core = cores[coreId];
-                var graphObj = {};
-                graphObj['label'] = core['label'];
-                graphObj['size'] = core['sizeInBytes'];
-                graphObj['sizeHuman'] = core['size'];
-                graphObj['pct'] = (core['sizeInBytes'] / indexSizeMax) * 100;
-                graphData.push(graphObj);
-              }
-              if (cores) {
-                cores.sort(function (a, b) {
-                  return b.sizeInBytes - a.sizeInBytes
-                });
-              }
-              graphData.sort(function (a, b) {
-                return b.size - a.size
-              });
-              nodes[node]['graphData'] = graphData;
-              nodes[node]['numDocs'] = numDocsHuman(docsTotal);
-              nodes[node]['sizeInBytes'] = indexSizeTotal;
-              nodes[node]['size'] = bytesToSize(indexSizeTotal);
-              nodes[node]['sizePerDoc'] = docsTotal === 0 ? '0b' : 
bytesToSize(indexSizeTotal / docsTotal);
-
-              // Build the d3 powered bar chart
-              $('#chart' + nodes[node]['id']).empty();
-              var chart = d3.select('#chart' + 
nodes[node]['id']).append('div').attr('class', 'chart');
-
-              // Add one div per bar which will group together both labels and 
bars
-              var g = chart.selectAll('div')
-                  .data(nodes[node]['graphData']).enter()
-                  .append('div');
-
-              // Add the bars
-              var bars = g.append("div")
-                  .attr("class", "rect")
-                  .text(function (d) {
-                    return d.label + ':\u00A0\u00A0' + d.sizeHuman;
-                  });
-
-              // Execute the transition to show the bars
-              bars.transition()
-                  .ease('elastic')
-                  .style('width', function (d) {
-                    return d.pct + '%';
-                  });
+    Metrics.get(
+      {
+        nodes: nodesParam,
+        name: 
"solr_disk_space_megabytes,solr_core_index_size_megabytes,solr_core_indexsearcher_index_num_docs,solr_core_indexsearcher_index_docs,solr_core_indexsearcher_open_time_milliseconds"
+      },
+      function (response) {
+        // response.metrics contains the parsed merged Prometheus data with 
node labels
+        var parsedMetrics = response.metrics;
+        if (!parsedMetrics) {
+          console.error('No metrics in response');

Review Comment:
   The error handling is incomplete. When `PrometheusParser.parse()` throws an 
exception, the service returns `{metrics: {}, error: e.message}`. However, an 
empty object `{}` is truthy in JavaScript, so the check `if (!parsedMetrics)` 
on line 405 will not detect parsing errors.
   
   The code should also check for the `error` property:
   ```javascript
   var parsedMetrics = response.metrics;
   if (!parsedMetrics || response.error) {
     console.error('No metrics in response', response.error);
     return;
   }
   ```
   ```suggestion
           if (!parsedMetrics || response.error) {
             console.error('No metrics in response', response.error);
   ```



##########
solr/webapp/web/js/angular/prometheus-parser.js:
##########
@@ -0,0 +1,178 @@
+/*
+ * 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.
+ */
+
+/**
+ * Prometheus text format parser for Solr Admin UI
+ *
+ * Parses Prometheus exposition format (text-based format for metrics)
+ * into a structured JavaScript object for consumption by the Admin UI.
+ */
+
+(function() {
+  'use strict';
+
+  angular.module('solrAdminApp').factory('PrometheusParser', function() {
+
+    /**
+     * Parse Prometheus text format into structured JavaScript object
+     * @param {string} prometheusText - Raw Prometheus format text
+     * @returns {Object} Parsed metrics object keyed by metric name
+     */
+    function parsePrometheusFormat(prometheusText) {
+      if (!prometheusText || typeof prometheusText !== 'string') {
+        return {};
+      }
+
+      var metrics = {};
+      var lines = prometheusText.split('\n');
+      var currentMetricName = null;
+      var currentMetricType = null;
+      var currentMetricHelp = null;
+
+      for (var i = 0; i < lines.length; i++) {
+        var line = lines[i].trim();
+
+        // Skip empty lines
+        if (!line) continue;
+
+        // Parse HELP comments - use regex for robust parsing
+        if (line.indexOf('# HELP ') === 0) {
+          var helpMatch = line.match(/^# HELP 
([a-zA-Z_:][a-zA-Z0-9_:]*)\s+(.*)$/);
+          if (helpMatch) {
+            currentMetricName = helpMatch[1];
+            currentMetricHelp = helpMatch[2];
+          }
+        }
+        // Parse TYPE comments
+        else if (line.indexOf('# TYPE ') === 0) {
+          var typeParts = line.substring(7).split(' ');
+          currentMetricName = typeParts[0];
+          currentMetricType = typeParts[1];
+
+          // Initialize metric entry
+          if (!metrics[currentMetricName]) {
+            metrics[currentMetricName] = {
+              type: currentMetricType,
+              help: currentMetricHelp || '',
+              samples: []
+            };
+          }
+        }
+        // Skip other comments
+        else if (line.charAt(0) === '#') {
+          continue;
+        }
+        // Parse metric sample
+        else {
+          var sample = parseMetricLine(line);
+          if (sample && sample.metricName) {
+            var baseMetricName = sample.metricName;
+            var metricSuffix = null;
+
+            // Only strip suffixes for histogram and summary types
+            // Check if metric name has known suffixes
+            if (sample.metricName.indexOf('_sum') === sample.metricName.length 
- 4) {
+              baseMetricName = sample.metricName.substring(0, 
sample.metricName.length - 4);
+              metricSuffix = '_sum';
+            } else if (sample.metricName.indexOf('_count') === 
sample.metricName.length - 6) {
+              baseMetricName = sample.metricName.substring(0, 
sample.metricName.length - 6);
+              metricSuffix = '_count';
+            } else if (sample.metricName.indexOf('_bucket') === 
sample.metricName.length - 7) {
+              baseMetricName = sample.metricName.substring(0, 
sample.metricName.length - 7);
+              metricSuffix = '_bucket';
+            } else if (sample.metricName.indexOf('_total') === 
sample.metricName.length - 6) {
+              // Handle _total suffix for summary metrics
+              baseMetricName = sample.metricName.substring(0, 
sample.metricName.length - 6);
+              metricSuffix = '_total';
+            }
+
+            // Check if base metric exists with histogram/summary type
+            var shouldGroup = false;
+            if (metricSuffix && metrics[baseMetricName]) {
+              var baseType = metrics[baseMetricName].type;
+              shouldGroup = (baseType === 'histogram' || baseType === 
'summary');
+            }
+
+            // Use base name if we should group, otherwise use full name
+            var targetMetricName = (shouldGroup || metricSuffix) ? 
baseMetricName : sample.metricName;
+
+            if (!metrics[targetMetricName]) {
+              metrics[targetMetricName] = {
+                type: currentMetricType || 'unknown',
+                help: currentMetricHelp || '',
+                samples: []
+              };
+            }
+
+            // Add suffix info to sample if present
+            if (metricSuffix) {
+              sample.metricSuffix = metricSuffix;
+            }
+
+            metrics[targetMetricName].samples.push(sample);
+          }
+        }
+      }
+
+      return metrics;
+    }
+
+    /**
+     * Parse a single metric line
+     * @param {string} line - Metric line (e.g., 'metric_name{label1="val1"} 
123.45' or with timestamp)
+     * @returns {Object|null} Parsed sample or null
+     */
+    function parseMetricLine(line) {
+      // Regex to match: metric_name{labels} value [timestamp]
+      // or: metric_name value [timestamp]
+      // The timestamp is optional and is a Unix timestamp in milliseconds
+      // The value pattern [^\s]+ matches scientific notation (e.g., 1.23e-4) 
and special values (NaN, +Inf, -Inf).
+      // The label set is matched as everything between { and }, allowing for 
escaped braces inside quoted label values.
+      var match = 
line.match(/^([a-zA-Z_:][a-zA-Z0-9_:]*)(?:\{((?:"(?:[^"\\]|\\.)*"|[^}])*)\})?\s+([^\s]+)(?:\s+\d+)?$/);
+
+      if (!match) return null;
+
+      var metricName = match[1];
+      var labelsStr = match[2] || '';
+      var value = parseFloat(match[3]);
+
+      // Parse labels
+      var labels = {};
+      if (labelsStr) {
+        // Match label="value" patterns - only allow valid Prometheus escape 
sequences (\\, \", \n)
+        var labelRegex = /([a-zA-Z_][a-zA-Z0-9_]*)="((?:[^"\\]|\\[\\"n])*)"/g;
+        var labelMatch;
+        while ((labelMatch = labelRegex.exec(labelsStr)) !== null) {
+          // Unescape label values - must unescape \\ first to avoid 
double-unescaping
+          var labelValue = labelMatch[2].replace(/\\"/g, '"').replace(/\\n/g, 
'\n').replace(/\\\\/g, '\\');

Review Comment:
   The order of replace operations is incorrect and will cause incorrect 
unescaping. The comment says "must unescape \\\\ first to avoid 
double-unescaping" but the code does the opposite - it unescapes `\\\\` last.
   
   For example, if a label value contains `\\"` (an escaped backslash followed 
by an escaped quote), the current code will:
   1. Replace `\\"` with `"` (wrong - should keep the backslash)
   2. Replace `\\n` with newline (won't match if step 1 consumed the backslash)
   3. Replace `\\\\` with `\\`
   
   The correct order should be:
   ```javascript
   var labelValue = labelMatch[2].replace(/\\\\/g, '\\').replace(/\\"/g, 
'"').replace(/\\n/g, '\n');
   ```
   
   This matches the escape order in the Java code which escapes backslash first.
   ```suggestion
             var labelValue = labelMatch[2].replace(/\\\\/g, 
'\\').replace(/\\"/g, '"').replace(/\\n/g, '\n');
   ```



##########
solr/webapp/web/js/angular/metrics-extractor.js:
##########
@@ -0,0 +1,166 @@
+/*
+ * 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.
+ */
+
+/**
+ * Metrics extraction helper for Solr Admin UI
+ *
+ * Provides helper functions to extract specific metric values from
+ * parsed Prometheus metrics data.
+ */
+
+(function() {
+  'use strict';
+
+  angular.module('solrAdminApp').factory('MetricsExtractor', function() {
+
+    /**
+     * Find a metric sample by label filters
+     * @param {Object} metric - Parsed metric object with samples array
+     * @param {Object} labelFilters - Object with label key-value pairs to 
match
+     * @returns {Object|null} Matching sample or null
+     */
+    function findSample(metric, labelFilters) {
+      if (!metric || !metric.samples) return null;
+
+      for (var i = 0; i < metric.samples.length; i++) {
+        var sample = metric.samples[i];
+        var matches = true;
+
+        for (var key in labelFilters) {
+          if (labelFilters.hasOwnProperty(key)) {
+            if (!sample.labels || sample.labels[key] !== labelFilters[key]) {
+              matches = false;
+              break;
+            }
+          }
+        }
+
+        if (matches) {
+          return sample;
+        }
+      }
+
+      return null;
+    }
+
+    /**
+     * Extract disk metrics (total and usable space)
+     * @param {Object} parsedMetrics - Parsed Prometheus metrics
+     * @param {Object} labelFilters - Optional additional label filters (e.g., 
{node: "nodeName"})
+     * @returns {Object} Object with totalSpace and usableSpace in bytes
+     */
+    function extractDiskMetrics(parsedMetrics, labelFilters) {
+      var diskMetric = parsedMetrics['solr_disk_space_megabytes'];
+      if (!diskMetric) {
+        return { totalSpace: 0, usableSpace: 0 };
+      }
+
+      // Merge standard filters with optional filters
+      var totalFilters = { category: 'CONTAINER', type: 'total_space' };
+      var usableFilters = { category: 'CONTAINER', type: 'usable_space' };
+
+      if (labelFilters) {
+        for (var key in labelFilters) {
+          if (labelFilters.hasOwnProperty(key)) {
+            totalFilters[key] = labelFilters[key];
+            usableFilters[key] = labelFilters[key];
+          }
+        }
+      }
+
+      var totalSample = findSample(diskMetric, totalFilters);
+      var usableSample = findSample(diskMetric, usableFilters);
+
+      return {
+        totalSpace: totalSample ? totalSample.value * 1024 * 1024 : 0,  // MB 
to bytes
+        usableSpace: usableSample ? usableSample.value * 1024 * 1024 : 0  // 
MB to bytes
+      };
+    }
+
+    /**
+     * Extract core index size
+     * @param {Object} parsedMetrics - Parsed Prometheus metrics
+     * @param {Object} coreLabels - Labels to identify the core (must include 
'core')
+     * @returns {number} Index size in bytes
+     */
+    function extractCoreIndexSize(parsedMetrics, coreLabels) {
+      var indexSizeMetric = parsedMetrics['solr_core_index_size_megabytes'];
+      if (!indexSizeMetric) return 0;
+
+      var sample = findSample(indexSizeMetric, coreLabels);
+      return sample ? sample.value * 1024 * 1024 : 0;  // MB to bytes
+    }
+
+    /**
+     * Extract searcher metrics (numDocs, deletedDocs, warmupTime)
+     * @param {Object} parsedMetrics - Parsed Prometheus metrics
+     * @param {Object} coreLabels - Labels to identify the core (must include 
'core')
+     * @returns {Object} Object with numDocs, deletedDocs, warmupTime
+     */
+    function extractSearcherMetrics(parsedMetrics, coreLabels) {
+      var numDocsMetric = 
parsedMetrics['solr_core_indexsearcher_index_num_docs'];
+      var totalDocsMetric = 
parsedMetrics['solr_core_indexsearcher_index_docs'];
+      var openTimeMetric = 
parsedMetrics['solr_core_indexsearcher_open_time_milliseconds'];
+
+      var numDocsSample = findSample(numDocsMetric, coreLabels);
+      var totalDocsSample = findSample(totalDocsMetric, coreLabels);
+
+      var numDocs = numDocsSample ? numDocsSample.value : 0;
+      var totalDocs = totalDocsSample ? totalDocsSample.value : 0;
+      var deletedDocs = totalDocs - numDocs;
+
+      // For warmup time, look for _sum sample from the histogram
+      var warmupTime = 0;
+      if (openTimeMetric) {
+        for (var i = 0; i < openTimeMetric.samples.length; i++) {
+          var sample = openTimeMetric.samples[i];
+
+          // Check if labels match
+          var labelsMatch = true;
+          for (var key in coreLabels) {
+            if (coreLabels.hasOwnProperty(key)) {
+              if (sample.labels[key] !== coreLabels[key]) {
+                labelsMatch = false;
+                break;
+              }
+            }

Review Comment:
   Potential null pointer error: The code accesses `sample.labels[key]` without 
checking if `sample.labels` exists. If a sample doesn't have labels, this will 
cause an error.
   
   Add a null check:
   ```javascript
   if (!sample.labels || sample.labels[key] !== coreLabels[key]) {
     labelsMatch = false;
     break;
   }
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to